diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 52f19e86c..b884aa52a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -5,17 +5,17 @@ Contributions to the library are welcome! Here are some guidelines to follow. ## Suggestions Please make suggestions for additional components or enhancements to the -codebase by opening an [issue](https://github.com/RPi-Distro/python-gpiozero/issues) +codebase by opening an [issue](https://github.com/gpiozero/gpiozero/issues) explaining your reasoning clearly. ## Bugs -Please submit bug reports by opening an [issue](https://github.com/RPi-Distro/python-gpiozero/issues) +Please submit bug reports by opening an [issue](https://github.com/gpiozero/gpiozero/issues) explaining the problem clearly using code examples. ## Documentation -The documentation source lives in the [docs](https://github.com/RPi-Distro/python-gpiozero/tree/master/docs) +The documentation source lives in the [docs](https://github.com/gpiozero/gpiozero/tree/master/docs) folder. Contributions to the documentation are welcome but should be easy to read and understand. diff --git a/.github/ISSUE_TEMPLATE/api-change.md b/.github/ISSUE_TEMPLATE/api-change.md new file mode 100644 index 000000000..f68fae34e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/api-change.md @@ -0,0 +1,17 @@ +--- +name: API change +about: Suggest an API change +--- + +Short description of change: e.g. Add color property to LED class +Where would this change be made: e.g. LED class + +Purpose of change: + + + +Please provide example usage of gpiozero with your suggested change: + +```python +print("code here") +``` diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/bug.md similarity index 58% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/bug.md index aea2bb556..798a4b93f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,11 +1,12 @@ --- -name: GPIO Zero Issue -about: Let us know about an issue with GPIO Zero +name: Bug report +about: Report a bug in the gpiozero library --- -Operating system: e.g. Raspbian Stretch -Pi model: e.g. Pi 3 Model B -GPIO Zero version: e.g. 1.4.1 +Operating system: e.g. Raspbian Buster +Python version: e.g. 3.7 +Pi model: e.g. Pi 4 Model B +GPIO Zero version: e.g. 1.5.1 Pin factory used: e.g. RPiGPIO See http://rpf.io/gpzissue for information on how to find out these details diff --git a/.github/ISSUE_TEMPLATE/new-device.md b/.github/ISSUE_TEMPLATE/new-device.md new file mode 100644 index 000000000..750630a44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/new-device.md @@ -0,0 +1,15 @@ +--- +name: New device +about: Request a new device to be added to the gpiozero library +--- + +Device name: e.g. Temperature sensor +Protocol: e.g. GPIO/SPI/I2C +URL of example product: +Further information e.g. data sheet: + +Please provide example code for using this device with Python, using another library: + +```python +print("code here") +``` diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..998ed68d8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,52 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + python: "3.9" + experimental: false + - os: ubuntu-22.04 + python: "3.10" + experimental: false + - os: ubuntu-22.04 + python: "3.11" + experimental: false + - os: ubuntu-22.04 + python: "3.12" + experimental: false + - os: ubuntu-22.04 + python: "3.13" + experimental: false + + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + + steps: + - name: Install Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Checkout GPIO Zero + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + make develop + + - name: Run tests + run: | + pytest -v -k 'not test_real_pins.py' diff --git a/.gitignore b/.gitignore index 2a97010c1..87e90cf75 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,18 @@ +# Python stuff *.py[cdo] # Editor detritus *.vim *.swp tags -.idea # Packaging detritus *.egg *.egg-info +*.pyc +*.whl dist build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg # Unit test / coverage reports coverage @@ -25,3 +20,4 @@ coverage .tox .cache .pytest_cache +.mypy_cache diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..91ac3ec43 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: rtd_requirements.txt diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index cafdc4a8f..000000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: python -python: - - "3.6" - - "3.5" - - "3.4" - - "3.3" - - "3.2" - - "2.7" - - "pypy" - - "pypy3" -matrix: - include: - - python: 3.7 - dist: xenial - sudo: true -install: "pip install -e .[test]" -script: make test -before_install: - # Coverage 4.0 no longer supports py3.2 and codecov depends on latest coverage - - if [[ $TRAVIS_PYTHON_VERSION == '3.2' ]]; then pip install "coverage<4.0dev"; fi - - pip install codecov -after_success: - - codecov -notifications: - slack: raspberrypifoundation:YoIHtVdg8Hd6gcA09QEmCYXN diff --git a/LICENSE.rst b/LICENSE.rst index 0800d1b53..37188b72e 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -1,3 +1,5 @@ +SPDX-License-Identifier: BSD-3-Clause + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index bdaf0c765..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include README.rst -recursive-include tests *.py -include LICENSE.rst diff --git a/Makefile b/Makefile index 58fda6340..ae3d32631 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,23 @@ # vim: set noet sw=4 ts=4 fileencoding=utf-8: # External utilities -PYTHON=python -PIP=pip -PYTEST=py.test -COVERAGE=coverage -TWINE=twine -PYFLAGS= -DEST_DIR=/ - -# Horrid hack to ensure setuptools is installed in our python environment. This -# is necessary with Python 3.3's venvs which don't install it by default. -ifeq ($(shell python -c "import setuptools" 2>&1),) -SETUPTOOLS:= -else -SETUPTOOLS:=$(shell wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $(PYTHON)) -endif +PYTHON ?= python3 +PIP ?= pip +PYTEST ?= pytest +TWINE ?= twine +PYFLAGS ?= +DEST_DIR ?= / +DOC_HTML=docs/build/html +DOC_REQS=rtd_requirements.txt # Calculate the base names of the distribution, the location of all source, # documentation, packaging, icon, and executable script files NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name) +WHEEL_NAME:=$(subst -,_,$(NAME)) VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version) -ifeq ($(shell lsb_release -si),Ubuntu) -DEB_SUFFIX:=ubuntu1 -else -DEB_SUFFIX:= -endif -DEB_ARCH:=$(shell dpkg --print-architecture) -PYVER:=$(shell $(PYTHON) $(PYFLAGS) -c "import sys; print('py%d.%d' % sys.version_info[:2])") PY_SOURCES:=$(shell \ $(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \ - grep -v "\.egg-info" $(NAME).egg-info/SOURCES.txt) -DEB_SOURCES:=debian/changelog \ - debian/control \ - debian/copyright \ - debian/rules \ - debian/docs \ - $(wildcard debian/*.init) \ - $(wildcard debian/*.default) \ - $(wildcard debian/*.manpages) \ - $(wildcard debian/*.docs) \ - $(wildcard debian/*.doc-base) \ - $(wildcard debian/*.desktop) + cat $(WHEEL_NAME).egg-info/SOURCES.txt | grep -v "\.egg-info" | grep -v "\.mo$$") DOC_SOURCES:=docs/conf.py \ $(wildcard docs/*.png) \ $(wildcard docs/*.svg) \ @@ -53,20 +29,10 @@ DOC_SOURCES:=docs/conf.py \ SUBDIRS:= # Calculate the name of all outputs -DIST_WHEEL=dist/$(NAME)-$(VER)-py2.py3-none-any.whl +DIST_WHEEL=dist/$(WHEEL_NAME)-$(VER)-py3-none-any.whl DIST_TAR=dist/$(NAME)-$(VER).tar.gz DIST_ZIP=dist/$(NAME)-$(VER).zip -DIST_DEB=dist/python-$(NAME)_$(VER)$(DEB_SUFFIX)_all.deb \ - dist/python3-$(NAME)_$(VER)$(DEB_SUFFIX)_all.deb \ - dist/python-$(NAME)-doc_$(VER)$(DEB_SUFFIX)_all.deb \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).build \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).buildinfo \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes -DIST_DSC=dist/$(NAME)_$(VER)$(DEB_SUFFIX).tar.xz \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX).dsc \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.build \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.buildinfo \ - dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes +MAN_PAGES=man/pinout.1 man/pintest.1 man/remote-gpio.7 man/gpiozero-env.7 # Default target @@ -75,11 +41,12 @@ all: @echo "make develop - Install symlinks for development" @echo "make test - Run tests" @echo "make doc - Generate HTML and PDF documentation" + @echo "make doc-serve - Serve the docs locally" + @echo "make doc-reqs - Generate requirements file for RTD" @echo "make source - Create source package" - @echo "make egg - Generate a PyPI egg package" + @echo "make wheel - Generate a PyPI wheel package" @echo "make zip - Generate a source zip package" @echo "make tar - Generate a source tar package" - @echo "make deb - Generate Debian packages" @echo "make dist - Generate all packages" @echo "make clean - Get rid of all generated files" @echo "make release - Create and tag a new release" @@ -93,6 +60,17 @@ doc: $(DOC_SOURCES) $(MAKE) -C docs html $(MAKE) -C docs epub $(MAKE) -C docs latexpdf + $(MAKE) $(MAN_PAGES) + +doc-serve: + python -m http.server -d $(DOC_HTML) + +doc-reqs: + echo "." > $(DOC_REQS) + pip freeze | grep -i sphinx >> $(DOC_REQS) + +preview: + $(MAKE) -C docs preview source: $(DIST_TAR) $(DIST_ZIP) @@ -102,33 +80,41 @@ zip: $(DIST_ZIP) tar: $(DIST_TAR) -deb: $(DIST_DEB) $(DIST_DSC) +dist: $(DIST_WHEEL) $(DIST_TAR) $(DIST_ZIP) -dist: $(DIST_WHEEL) $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_ZIP) - -develop: tags +develop: @# These have to be done separately to avoid a cockup... $(PIP) install -U setuptools $(PIP) install -U pip + $(PIP) install -U twine + $(PIP) install -U tox $(PIP) install -e .[doc,test] test: - $(COVERAGE) run --rcfile coverage.cfg -m $(PYTEST) tests -v -r sx - $(COVERAGE) report --rcfile coverage.cfg + $(PYTEST) clean: - dh_clean - rm -fr dist/ $(NAME).egg-info/ tags + rm -fr dist/ build/ man/ .pytest_cache/ .mypy_cache/ $(WHEEL_NAME).egg-info/ tags .coverage* for dir in $(SUBDIRS); do \ $(MAKE) -C $$dir clean; \ done + find $(CURDIR) -name "*.pyc" -delete + find $(CURDIR) -name "__pycache__" -delete tags: $(PY_SOURCES) - ctags -R --exclude="build/*" --exclude="debian/*" --exclude="docs/*" --languages="Python" + ctags -R --languages="Python" $(PY_SOURCES) + +lint: $(PY_SOURCES) + pylint $(WHEEL_NAME) $(SUBDIRS): $(MAKE) -C $@ +$(MAN_PAGES): $(DOC_SOURCES) + $(MAKE) -C docs man + mkdir -p man/ + cp build/man/*.[0-9] man/ + $(DIST_TAR): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats gztar @@ -136,39 +122,16 @@ $(DIST_ZIP): $(PY_SOURCES) $(SUBDIRS) $(PYTHON) $(PYFLAGS) setup.py sdist --formats zip $(DIST_WHEEL): $(PY_SOURCES) $(SUBDIRS) - $(PYTHON) $(PYFLAGS) setup.py bdist_wheel --universal + $(PYTHON) $(PYFLAGS) setup.py bdist_wheel -$(DIST_DEB): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(DIST_TAR) - cp $(DIST_TAR) ../$(NAME)_$(VER).orig.tar.gz - debuild -b - mkdir -p dist/ - for f in $(DIST_DEB); do cp ../$${f##*/} dist/; done - -$(DIST_DSC): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(DIST_TAR) - cp $(DIST_TAR) ../$(NAME)_$(VER).orig.tar.gz - debuild -S - mkdir -p dist/ - for f in $(DIST_DSC); do cp ../$${f##*/} dist/; done - -copyrights: $(PY_SOURCES) $(DOC_SOURCES) - ./copyrights - -changelog: $(PY_SOURCES) $(DOC_SOURCES) $(DEB_SOURCES) +release: $(MAKE) clean - # ensure there are no current uncommitted changes test -z "$(shell git status --porcelain)" - # update the debian changelog with new release information - dch --newversion $(VER)$(DEB_SUFFIX) - # commit the changes and add a new tag - git commit debian/changelog -m "Updated changelog for release $(VER)" - -release: $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_WHEEL) git tag -s v$(VER) -m "Release v$(VER)" - git push --tags - # build a source archive and upload to PyPI + git push origin v$(VER) + +upload: $(DIST_TAR) $(DIST_WHEEL) + $(TWINE) check $(DIST_TAR) $(DIST_WHEEL) $(TWINE) upload $(DIST_TAR) $(DIST_WHEEL) - # build the deb source archive and upload to Raspbian - dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes - dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes -.PHONY: all install develop test doc source egg wheel zip tar deb dist clean tags release upload $(SUBDIRS) +.PHONY: all install develop test doc doc-serve doc-reqs source wheel zip tar dist clean tags release upload $(SUBDIRS) diff --git a/README.rst b/README.rst index 0361d72b9..52b6dec8e 100644 --- a/README.rst +++ b/README.rst @@ -2,25 +2,11 @@ gpiozero ======== -.. image:: https://badge.fury.io/py/gpiozero.svg - :target: https://badge.fury.io/py/gpiozero - :alt: Latest Version - -.. image:: https://travis-ci.org/RPi-Distro/python-gpiozero.svg?branch=master - :target: https://travis-ci.org/RPi-Distro/python-gpiozero - :alt: Build Tests - -.. image:: https://img.shields.io/codecov/c/github/RPi-Distro/python-gpiozero/master.svg?maxAge=2592000 - :target: https://codecov.io/github/RPi-Distro/python-gpiozero - :alt: Code Coverage - -A simple interface to GPIO devices with Raspberry Pi. - -Created by `Ben Nuttall`_ of the `Raspberry Pi Foundation`_, `Dave Jones`_, and -other contributors. +A simple interface to GPIO devices with `Raspberry Pi`_, developed and +maintained by `Ben Nuttall`_ and `Dave Jones`_. +.. _Raspberry Pi: https://www.raspberrypi.com/ .. _Ben Nuttall: https://github.com/bennuttall -.. _Raspberry Pi Foundation: https://www.raspberrypi.org/ .. _Dave Jones: https://github.com/waveform80 About @@ -59,16 +45,15 @@ together: pause() You can advance to using the declarative paradigm along with provided -:doc:`source tools ` to describe the behaviour of devices and their -interactions: +to describe the behaviour of devices and their interactions: .. code:: python - from gpiozero import LED, MotionSensor, LightSensor + from gpiozero import OutputDevice, MotionSensor, LightSensor from gpiozero.tools import booleanized, all_values from signal import pause - garden = LED(17) + garden = OutputDevice(17) motion = MotionSensor(4) light = LightSensor(5) @@ -76,7 +61,9 @@ interactions: pause() -See the chapter on :doc:`Source/Values ` for more information. +See the chapter on `Source/Values`_ for more information. + +.. _Source/Values: https://gpiozero.readthedocs.io/en/stable/source_values.html The library includes interfaces to many simple everyday components, as well as some more complex things like sensors, analogue-to-digital converters, full @@ -91,23 +78,26 @@ Pin factories GPIO Zero builds on a number of underlying pin libraries, including `RPi.GPIO`_ and `pigpio`_, each with their own benefits. You can select a particular pin library to be used, either for the whole script or per-device, according to your -needs. See the section on :ref:`changing the pin factory -`. +needs. See the section on `changing the pin factory`_. -.. _RPi.GPIO: https://pypi.org/project/RPi.GPIO/ +.. _RPi.GPIO: https://pypi.org/project/RPi.GPIO .. _pigpio: https://pypi.org/project/pigpio +.. _changing the pin factory: https://gpiozero.readthedocs.io/en/stable/api_pins.html#changing-the-pin-factory A "mock pin" interface is also provided for testing purposes. Read more about -this in the section on :ref:`mock pins `. +this in the section on `mock pins`_. + +.. _mock pins: https://gpiozero.readthedocs.io/en/stable/api_pins.html#mock-pins Installation ============ -GPIO Zero is installed by default in the Raspbian desktop image, available from -`raspberrypi.org`_. To install on Raspbian Lite or other operating systems, -including for PCs using remote GPIO, see the `Installing`_ chapter. +GPIO Zero is installed by default in the Raspberry Pi OS desktop image, +available from `raspberrypi.com`_. To install on Raspberry Pi OS Lite or other +operating systems, including for PCs using remote GPIO, see the `Installing`_ +chapter. -.. _raspberrypi.org: https://www.raspberrypi.org/downloads/ +.. _raspberrypi.com: https://www.raspberrypi.com/software/ .. _Installing: https://gpiozero.readthedocs.io/en/stable/installing.html Documentation @@ -120,59 +110,114 @@ documentation for information on contributing to the project. .. _Contributing: https://gpiozero.readthedocs.io/en/stable/contributing.html .. _Development: https://gpiozero.readthedocs.io/en/stable/development.html -Contributors -============ +Issues and questions +==================== -Core developers: +If you have a feature request or bug report, please open an `issue on GitHub`_. +If you have a question or need help, this may be better suited to our `GitHub +discussion board`_, the `Raspberry Pi Stack Exchange`_ or the `Raspberry Pi +Forums`_. -- `Ben Nuttall`_ -- `Dave Jones`_ -- `Andrew Scheller`_ +.. _issue on GitHub: https://github.com/gpiozero/gpiozero/issues/new +.. _GitHub discussion board: https://github.com/gpiozero/gpiozero/discussions +.. _Raspberry Pi Stack Exchange: https://raspberrypi.stackexchange.com/ +.. _Raspberry Pi Forums: https://forums.raspberrypi.com/ -Other contributors: +Contributors +============ -- `Martin O'Hanlon`_ -- `Steve Amor`_ -- `David Glaude`_ -- `Edward Betts`_ - `Alex Chan`_ -- `Thijs Triemstra`_ -- `Schelto van Doorn`_ - `Alex Eames`_ +- `Andrew Scheller`_ - `Barry Byford`_ +- `Cameron Davidson-Pilon`_ +- `Carl Monk`_ +- `Claire Pollard`_ - `Clare Macrae`_ -- `Tim Golden`_ -- `Phil Howard`_ -- `Stewart Adcock`_ -- `Ian Harcombe`_ -- `Russel Winder`_ -- `Mike Kazantsev`_ +- `Dan Jackson`_ +- `Daniele Procida`_ +- `damosurfer`_ +- `David Glaude`_ +- `Delcio Torres`_ +- `Edward Betts`_ - `Fatih Sarhan`_ -- `Rick Ansell`_ +- `Fangchen Li`_ +- `G.S.`_ +- `gnicki`_ +- `Ian Harcombe`_ +- `Jack Wearden`_ - `Jeevan M R`_ -- `Claire Pollard`_ +- `Josh Thorpe`_ +- `Kyle Morgan`_ +- `Linus Groh`_ +- `Mahallon`_ +- `Maksim Levental`_ +- `Martchus`_ +- `Martin O'Hanlon`_ +- `Mike Kazantsev`_ +- `Paulo Mateus`_ +- `Phil Howard`_ - `Philippe Muller`_ - - -.. _Andrew Scheller: https://github.com/lurch -.. _Martin O'Hanlon: https://github.com/martinohanlon -.. _Steve Amor: https://github.com/SteveAmor -.. _David Glaude: https://github.com/dglaude -.. _Edward Betts: https://github.com/edwardbetts -.. _Alex Chan: https://github.com/alexwlchan -.. _Thijs Triemstra: https://github.com/thijstriemstra -.. _Schelto van Doorn: https://github.com/goloplo -.. _Alex Eames: https://github.com/raspitv -.. _Barry Byford: https://github.com/ukBaz -.. _Clare Macrae: https://github.com/claremacrae -.. _Tim Golden: https://github.com/tjguk -.. _Phil Howard: https://github.com/Gadgetoid -.. _Stewart Adcock: https://github.com/stewartadcock -.. _Ian Harcombe: https://github.com/MrHarcombe +- `Rick Ansell`_ +- `Rimas Misevičius`_ +- `Robert Erdin`_ +- `Russel Winder`_ +- `Ryan Walmsley`_ +- `Schelto van Doorn`_ +- `Sofiia Kosovan`_ +- `Steve Amor`_ +- `Stewart Adcock`_ +- `Thijs Triemstra`_ +- `Tim Golden`_ +- `Yisrael Dov Lebow`_ + +See the `contributors page`_ on GitHub for more info. + +.. _Alex Chan: https://github.com/gpiozero/gpiozero/commits?author=alexwlchan +.. _Alex Eames: https://github.com/gpiozero/gpiozero/commits?author=raspitv +.. _Andrew Scheller: https://github.com/gpiozero/gpiozero/commits?author=lurch +.. _Barry Byford: https://github.com/gpiozero/gpiozero/commits?author=ukBaz +.. _Cameron Davidson-Pilon: https://github.com/gpiozero/gpiozero/commits?author=CamDavidsonPilon +.. _Carl Monk: https://github.com/gpiozero/gpiozero/commits?author=ForToffee +.. _Chris R: https://github.com/gpiozero/gpiozero/commits?author=chrisruk +.. _Claire Pollard: https://github.com/gpiozero/gpiozero/commits?author=tuftii +.. _Clare Macrae: https://github.com/gpiozero/gpiozero/commits?author=claremacrae +.. _Dan Jackson: https://github.com/gpiozero/gpiozero/commits?author=e28eta +.. _Daniele Procida: https://github.com/evildmp +.. _Dariusz Kowalczyk: https://github.com/gpiozero/gpiozero/commits?author=darton +.. _damosurfer: https://github.com/gpiozero/gpiozero/commits?author=damosurfer +.. _David Glaude: https://github.com/gpiozero/gpiozero/commits?author=dglaude +.. _Delcio Torres: https://github.com/gpiozero/gpiozero/commits?author=delciotorres +.. _Edward Betts: https://github.com/gpiozero/gpiozero/commits?author=edwardbetts +.. _Fatih Sarhan: https://github.com/gpiozero/gpiozero/commits?author=f9n +.. _Fangchen Li: https://github.com/gpiozero/gpiozero/commits?author=fangchenli +.. _G.S.: https://github.com/gpiozero/gpiozero/commits?author=gszy +.. _gnicki: https://github.com/gpiozero/gpiozero/commits?author=gnicki2000 +.. _Ian Harcombe: https://github.com/gpiozero/gpiozero/commits?author=MrHarcombe +.. _Jack Wearden: https://github.com/gpiozero/gpiozero/commits?author=NotBobTheBuilder +.. _Jeevan M R: https://github.com/gpiozero/gpiozero/commits?author=jee1mr +.. _Josh Thorpe: https://github.com/gpiozero/gpiozero/commits?author=ThorpeJosh +.. _Kyle Morgan: https://github.com/gpiozero/gpiozero/commits?author=knmorgan +.. _Linus Groh: https://github.com/gpiozero/gpiozero/commits?author=linusg +.. _Mahallon: https://github.com/gpiozero/gpiozero/commits?author=Mahallon +.. _Maksim Levental: https://github.com/gpiozero/gpiozero/commits?author=makslevental +.. _Martchus: https://github.com/gpiozero/gpiozero/commits?author=Martchus +.. _Martin O'Hanlon: https://github.com/martinohanlon/commits?author=martinohanlon +.. _Mike Kazantsev: https://github.com/gpiozero/gpiozero/commits?author=mk-fg +.. _Paulo Mateus: https://github.com/gpiozero/gpiozero/commits?author=SrMouraSilva +.. _Phil Howard: https://github.com/gpiozero/gpiozero/commits?author=Gadgetoid +.. _Philippe Muller: https://github.com/gpiozero/gpiozero/commits?author=pmuller +.. _Rick Ansell: https://github.com/gpiozero/gpiozero/commits?author=ricksbt +.. _Rimas Misevičius: https://github.com/gpiozero/gpiozero/commits?author=rmisev +.. _Robert Erdin: https://github.com/gpiozero/gpiozero/commits?author=roberterdin .. _Russel Winder: https://github.com/russel -.. _Mike Kazantsev: https://github.com/mk-fg -.. _Fatih Sarhan: https://github.com/f9n -.. _Rick Ansell: https://github.com/ricksbt -.. _Jeevan M R: https://github.com/jee1mr -.. _Claire Pollard: https://github.com/tuftii -.. _Philippe Muller: https://github.com/pmuller +.. _Ryan Walmsley: https://github.com/gpiozero/gpiozero/commits?author=ryanteck +.. _Schelto van Doorn: https://github.com/gpiozero/gpiozero/commits?author=goloplo +.. _Sofiia Kosovan: https://github.com/gpiozero/gpiozero/commits?author=SofiiaKosovan +.. _Steve Amor: https://github.com/gpiozero/gpiozero/commits?author=SteveAmor +.. _Stewart Adcock: https://github.com/gpiozero/gpiozero/commits?author=stewartadcock +.. _Thijs Triemstra: https://github.com/gpiozero/gpiozero/commits?author=thijstriemstra +.. _Tim Golden: https://github.com/gpiozero/gpiozero/commits?author=tjguk +.. _Yisrael Dov Lebow: https://github.com/gpiozero/gpiozero/commits?author=yisraeldov + +.. _contributors page: https://github.com/gpiozero/gpiozero/graphs/contributors diff --git a/RELEASE.rst b/RELEASE.rst index 1a272442a..1f24c7dd9 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -31,7 +31,7 @@ On your build Pi, perform the following steps: 5. Ensure you have a valid public/private key-pair defined for GNUPG. -6. In the ``python-gpiozero`` directory, run ``make release``. This will launch +6. In the root ``gpiozero`` directory, run ``make release``. This will launch ``dch`` to update the Debian changelog. Fill this out properly (ticket references!) and the release will be generated, tagged, signed, and registered with GitHub and PyPI. @@ -41,7 +41,7 @@ On your build Pi, perform the following steps: Although the release has been registered at this point, no packages have been generated or uploaded to any service. -7. Still in the ``python-gpiozero`` directory, run ``make upload``. This will +7. Still in the root ``gpiozero`` directory, run ``make upload``. This will generate the actual debian packages, upload them to Raspbian, and upload the source package to PyPI. diff --git a/copyrights b/copyrights deleted file mode 100755 index e1789d775..000000000 --- a/copyrights +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python - -import io -import re -import sys -from collections import namedtuple -from operator import attrgetter -from itertools import groupby -from datetime import datetime -from subprocess import Popen, PIPE, DEVNULL -from pathlib import Path -from fnmatch import fnmatch -from functools import lru_cache - - -Contribution = namedtuple('Contribution', ('author', 'email', 'year', 'filename')) -Copyright = namedtuple('Copyright', ('author', 'email', 'years')) - - -def main(): - includes = { - '**/*.py', - '**/*.rst', - } - excludes = { - 'docs/examples/*.py', - 'docs/license.rst', - } - prefixes = { - '.py': '#', - '.rst': '..', - } - if len(sys.argv) > 1: - includes = set(sys.argv[1:]) - contributions = get_contributions(includes, excludes) - for filename, copyrights in contributions.items(): - filename = Path(filename) - update_copyright(filename, copyrights, prefixes[filename.suffix]) - - -def get_contributions(include, exclude): - sorted_blame = sorted( - get_blame(include, exclude), - key=lambda c: (c.filename, c.author, c.email) - ) - blame_by_file = { - filename: list(file_contributions) - for filename, file_contributions in groupby( - sorted_blame, key=attrgetter('filename') - ) - } - contributors_all_years = { - filename: { - Copyright(author, email, frozenset(y.year for y in years)) - for (author, email), years in groupby( - file_contributors, key=lambda c: (c.author, c.email) - ) - } - for filename, file_contributors in blame_by_file.items() - } - contributions = { - filename: { - Copyright( - c.author, c.email, - (min(c.years), max(c.years)) - if len(c.years) > 1 else - min(c.years) - ) - for c in contributors - } - for filename, contributors in contributors_all_years.items() - } - return contributions - - -def get_blame(include, exclude): - for filename in get_source_files(include, exclude): - blame = Popen( - ['git', 'blame', '--line-porcelain', 'HEAD', '--', filename], - stdout=PIPE, - stderr=PIPE, - universal_newlines=True - ) - author = email = year = None - for line in blame.stdout: - if line.startswith('author '): - author = line.split(' ', 1)[1].rstrip() - elif line.startswith('author-mail '): - email = line.split(' ', 1)[1].rstrip().lstrip('<').rstrip('>') - elif line.startswith('author-time '): - # Forget the timezone; we only want the year anyway - year = datetime.fromtimestamp(int(line.split(' ', 1)[1].rstrip())).year - elif line.startswith('filename '): - yield Contribution( - author=author, email=email, year=year, filename=filename) - author = email = year = None - blame.wait() - assert blame.returncode == 0 - - -def get_source_files(include, exclude): - ls_tree = Popen( - ['git', 'ls-tree', '-r', '--name-only', 'HEAD'], - stdout=PIPE, - stderr=DEVNULL, - universal_newlines=True - ) - if not include: - include = {'*'} - for filename in ls_tree.stdout: - filename = filename.strip() - if any(fnmatch(filename, pattern) for pattern in exclude): - continue - if any(fnmatch(filename, pattern) for pattern in include): - yield filename - ls_tree.wait() - assert ls_tree.returncode == 0 - - -insertion_point = object() -def parse_source_file(filename, prefix): - license = get_license() - license_start = license[0] - license_end = license[-1] - with filename.open('r') as source: - state = 'preamble' - for linenum, line in enumerate(source, start=1): - if state == 'preamble': - if linenum == 1 and line.startswith('#!'): - yield line - elif linenum < 10 and 'set fileencoding' in line: - yield line - elif line.rstrip() == prefix: - pass # skip blank comment lines - elif line.startswith(prefix + ' GPIO Zero:'): - pass # skip existing header lines - elif line.startswith(prefix + ' Copyright (c)'): - pass # skip existing copyright lines - elif line.startswith(prefix + ' ' + license_start): - state = 'license' # skip existing license lines - else: - yield insertion_point - state = 'blank' - elif state == 'license': - if line.startswith(prefix + ' ' + license_end): - yield insertion_point - state = 'blank' - continue - if state == 'blank': - # Ensure there's a blank line between license and start of the - # source body - if line.strip(): - yield '\n' - yield line - state = 'body' - elif state == 'body': - yield line - - -def update_copyright(filename, copyrights, prefix): - print('Re-writing {filename}...'.format(filename=filename)) - license = get_license() - copyrights = sorted( - copyrights, reverse=True, key=lambda c: - (c.years[::-1] if isinstance(c.years, tuple) else (c.years, 0), c.author) - ) - content = [] - for line in parse_source_file(filename, prefix): - if line is insertion_point: - if len(content) > 0: - content.append(prefix + '\n') - content.append( - prefix + " GPIO Zero: a library for controlling the " - "Raspberry Pi's GPIO pins\n") - for copyright in copyrights: - try: - start, end = copyright.years - except TypeError: - years = str(copyright.years) - else: - years = '{start}-{end}'.format(start=start, end=end) - content.append( - prefix + " Copyright (c) {years} " - "{copyright.author} <{copyright.email}>\n" - "".format(years=years, copyright=copyright)) - content.append(prefix + '\n') - content.extend( - (prefix + ' ' + l).strip() + '\n' - for l in license - ) - else: - content.append(line) - # Yes, if I was doing this "properly" I'd write to a temporary file and - # rename it over the target. However, I'm assuming you're running this - # under a git clone ... after all, you are ... aren't you? - with filename.open('w') as target: - for line in content: - target.write(line) - - -@lru_cache() -def get_license(): - with io.open('LICENSE.rst', 'r') as text: - return text.read().splitlines() - - -if __name__ == '__main__': - main() diff --git a/coverage.cfg b/coverage.cfg deleted file mode 100644 index 24d6c833a..000000000 --- a/coverage.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[run] -branch = True -include = gpiozero/* -;omit = */bar.py,*/baz.py - -[report] -ignore_errors = True -show_missing = True -exclude_lines = - pragma: no cover - if self\.debug - raise AssertionError - raise NotImplementedError - if 0: - if __name__ == .__main__.: - -[html] -directory = coverage diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index bb2d2946e..000000000 --- a/debian/changelog +++ /dev/null @@ -1,199 +0,0 @@ -gpiozero (1.5.0ubuntu1) xenial; urgency=medium - - * Ubuntu release - - -- Dave Jones Thu, 07 Mar 2019 15:03:34 +0000 - -gpiozero (1.5.0) stable; urgency=medium - - * Introduced pin event timing to increase accuracy of certain devices such - as the HC-SR04 "DistanceSensor". (#664, #665) - * Further improvements to "DistanceSensor" (ignoring missed edges). (#719) - * Allow "source" to take a device object as well as "values" or other - values. See the "Source/Values" chapter in the docs. (#640) - * Added internal device classes "LoadAverage" and "DiskUsage" (thanks to - Jeevan M R for the latter). (#532, #714) - * Added support for colorzero with "RGBLED" (this adds a new dependency). - (#655) - * Added "TonalBuzzer" with "Tone" API for specifying frequencies raw or via - MIDI or musical notes. (#681, #717) - * Added "PiHutXmasTree". (#502) - * Added "PumpkinPi" and "JamHat" (thanks to Claire Pollard). (#680, #681, - #717) - * Ensured gpiozero can be imported without a valid pin factory set. (#591, - #713) - * Reduced import time by not computing default pin factory at the point of - import. (#675, #722) - * Added support for various pin numbering mechanisms. (#470) - * "Motor" instances now use "DigitalOutputDevice" for non-PWM pins. - * Allow non-PWM use of "Robot". (#481) - * Added optional enable init param to "Motor". (#366) - * Added --xyz option to "pinout" command line tool to open - https://pinout.xyz in a web browser. (#604) - * Added 3B+, 3A+ and CM3+ to Pi model data. (#627, #704) - * Minor improvements to "Energenie", thanks to Steve Amor. (#629, #634) - * Allow "SmoothedInputDevice", "LightSensor" and "MotionSensor" to have - pull-up configured. (#652) - * Allow input devices to be pulled up or down externally, thanks to Philippe - Muller. (#593, #658) - * Minor changes to support Python 3.7, thanks to Russel Winder and Rick - Ansell. (#666, #668, #669, #671, #673) - * Added "zip_values" source tool. - * Correct row/col numbering logic in "PinInfo". (#674) - * Many additional tests, and other improvements to the test suite. - * Many documentation corrections, additions and clarifications. - * Automatic documentation class hierarchy diagram generation. - * Automatic copyright attribution in source files. - - -- Dave Jones Tue, 12 Feb 2019 21:31:09 +0000 - -gpiozero (1.4.1) stable; urgency=low - - * Added "curve_left" and "curve_right" parameters to "Robot.forward" and - "Robot.backward" (#306 and #619) - * Fixed "DistanceSensor" returning incorrect readings after a long pause, - and added a lock to ensure multiple distance sensors can operate - simultaneously in a single project (#584, #595, #617, #618) - * Added support for phase/enable motor drivers with "PhaseEnableMotor", - "PhaseEnableRobot", and descendants, thanks to Ian Harcombe! (#386) - * A variety of other minor enhancements, largely thanks to Andrew Scheller! - (#479, #489, #491, #492) - - -- Dave Jones Tue, 20 Feb 2018 21:59:28 +0000 - -gpiozero (1.4.0) stable; urgency=low - - * Pin factory is now configurable from device constructors as well as - command line. NOTE: this is a backwards incompatible change for manual pin - construction but it's hoped this is (currently) a sufficiently rare use - case that this won't affect too many people and the benefits of the new - system warrant such a change, i.e. the ability to use remote pin factories - with HAT classes that don't accept pin assignations (#279) - * Major work on SPI, primarily to support remote hardware SPI (#421, #459, - #465, #468, #575) - * Pin reservation now works properly between GPIO and SPI devices (#459, - #468) - * Lots of work on the documentation: source/values chapter, better charts, - more recipes, remote GPIO configuration, mock pins, better PDF output - (#484, #469, #523, #520, #434, #565, #576) - * Support for "StatusZero" and "StatusBoard" HATs (#558) - * Added pinout command line tool to provide a simple reference to the GPIO - layout and information about the associated Pi (#497, #504) thanks to - Stewart Adcock for the initial work - * "pi_info" made more lenient for new (unknown) Pi models (#529) - * Fixed a variety of packaging issues (#535, #518, #519) - * Improved text in factory fallback warnings (#572) - - -- Dave Jones Wed, 26 Jul 2017 23:02:35 +0100 - -gpiozero (1.3.2) stable; urgency=low - - * Added new Pi models to stop "pi_info" breaking - - -- Ben Nuttall Fri, 03 Mar 2017 13:18:00 +0100 - -gpiozero (1.3.1) stable; urgency=low - - * Fixed hardware SPI support which Dave broke in 1.3.0. Sorry! - * Some minor docs changes - - -- Dave Jones Wed, 31 Aug 2016 23:40:33 +0100 - -gpiozero (1.3.0) stable; urgency=low - - * Added "ButtonBoard" for reading multiple buttons in a single class (#340) - * Added "Servo" and "AngularServo" classes for controlling simple servo - motors (#248) - * Lots of work on supporting easier use of internal and third-part pin - implementations (#359) - * "Robot" now has a proper "value" attribute (#305) - * Added "CPUTemperature" as another demo of "internal" devices (#294) - * A temporary work-around for an issue with "DistanceSensor" was included - but a full fix is in the works (#385) - * More work on the documentation (#320, #295, #289, etc.) - - -- Dave Jones Wed, 31 Aug 2016 00:18:18 +0100 - -gpiozero (1.2.0) stable; urgency=low - - * Added "Energenie" class for controlling Energenie plugs (#69) - * Added "LineSensor" class for single line-sensors (#109) - * Added "DistanceSensor" class for HC-SR04 ultra-sonic sensors (#114) - * Added "SnowPi" class for the Ryanteck Snow-pi board (#130) - * Added "when_held" (and related properties) to "Button" (#115) - * Fixed issues with installing GPIO Zero for python 3 on Raspbian Wheezy - releases (#140) - * Added support for lots of ADC chips (MCP3xxx family) (#162) - many thanks - to pcopa and lurch! - * Added support for pigpiod as a pin implementation with "PiGPIOPin" (#180) - * Many refinements to the base classes mean more consistency in composite - devices and several bugs squashed (#164, #175, #182, #189, #193, #229) - * GPIO Zero is now aware of what sort of Pi it's running on via "pi_info" - and has a fairly extensive database of Pi information which it uses to - determine when users request impossible things (like pull-down on a pin - with a physical pull-up resistor) (#222) - * The source/values system was enhanced to ensure normal usage doesn't - stress the CPU and lots of utilities were added (#181, #251) - - -- Dave Jones Sun, 10 Apr 2016 21:04:42 +0100 - -gpiozero (1.1.0) stable; urgency=low - - * Documentation converted to reST and expanded to include generic classes - and several more recipes (#80, #82, #101, #119, #135, #168) - * New "LEDBarGraph" class (many thanks to Martin O'Hanlon!) (#126, #176) - * New "Pin" implementation abstracts out the concept of a GPIO pin paving - the way for alternate library support and IO extenders in future (#141) - * New "LEDBoard.blink" method which works properly even when background is - set to "False" (#94, #161) - * New "RGBLED.blink" method which implements (rudimentary) color fading - too! (#135, #174) - * New "initial_value" attribute on "OutputDevice" ensures consistent - behaviour on construction (#118) - * New "active_high" attribute on "PWMOutputDevice" and "RGBLED" allows use - of common anode devices (#143, #154) - * Loads of new ADC chips supported (many thanks to GitHub user pcopa!) - (#150) - - -- Dave Jones Mon, 08 Feb 2016 23:55:00 +0000 - -gpiozero (1.0.0) stable; urgency=low - - * Debian packaging added (#44) - * PWMLED class added (#58) - * TemperatureSensor remove pending further work (#93) - * Buzzer.beep alias method added (#75) - * Motor PWM devices exposed, and Robot motor devices exposed (#107) - - -- Dave Jones Mon, 16 Nov 2015 12:16:29 +0000 - -gpiozero (0.9.0) stable; urgency=medium - - * Added source and values properties to all relevant classes (#76) - * Fix names of parameters in Motor constructor (#79) - * Added wrappers for LED groups on add-on boards (#81) - - -- Dave Jones Sun, 25 Oct 2015 18:56:58 +0100 - -gpiozero (0.8.0) stable; urgency=medium - - * Added generic AnalogInputDevice class along with specific classes for the - MCP3008 and MCP3004 (#41) - * Fixed blink (#57) - - -- Dave Jones Fri, 16 Oct 2015 11:40:42 +0100 - -gpiozero (0.7.0) stable; urgency=medium - - * Second public beta - - -- Dave Jones Fri, 09 Oct 2015 12:30:13 +0100 - -gpiozero (0.6.0) stable; urgency=medium - - * Raspbian packaging (#44) - * PWM functionality including variable level RGB LEDs (#40) - * Ability to recreate GPIO device objects (#38) - - -- Dave Jones Mon, 05 Oct 2015 22:21:48 +0100 - diff --git a/debian/clean b/debian/clean deleted file mode 100644 index 0325a6c6c..000000000 --- a/debian/clean +++ /dev/null @@ -1,2 +0,0 @@ -build/ -.pybuild/ diff --git a/debian/compat b/debian/compat deleted file mode 100644 index 45a4fb75d..000000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -8 diff --git a/debian/control b/debian/control deleted file mode 100644 index 42fd08835..000000000 --- a/debian/control +++ /dev/null @@ -1,61 +0,0 @@ -Source: gpiozero -Maintainer: Ben Nuttall -Homepage: http://gpiozero.readthedocs.io/ -Section: python -Priority: extra -Build-Depends: - debhelper (>= 8), - dh-python, - python-all (>= 2.7), - python-setuptools, - python3-all, - python3-setuptools, - python3-sphinx -Standards-Version: 3.9.6 -Vcs-Git: https://github.com/RPi-Distro/python-gpiozero.git -Vcs-Browser: https://github.com/RPi-Distro/python-gpiozero -X-Python-Version: >= 2.7 -X-Python3-Version: >= 3.2 - -Package: python-gpiozero -Architecture: all -Section: python -Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources -Recommends: python-rpi.gpio, python-spidev -Suggests: python-gpiozero-docs -Description: Simple API for controlling devices attached to a Pi's GPIO pins. - gpiozero builds on various pin libraries to provide a set of classes designed - to simplify interaction with devices connected to the GPIO pins, from simple - buttons and LEDs, up to various add-on boards. The API tries to adhere closely - to Python's idioms and naming conventions, and features alternative - programming paradigm approaches. - . - This is the Python 2 version of the package. - -Package: python3-gpiozero -Architecture: all -Section: python -Depends: ${misc:Depends}, ${python3:Depends}, python3-pkg-resources -Recommends: python3-rpi.gpio, python3-spidev -Suggests: python-gpiozero-docs -Description: Simple API for controlling devices attached to a Pi's GPIO pins. - gpiozero builds on various pin libraries to provide a set of classes designed - to simplify interaction with devices connected to the GPIO pins, from simple - buttons and LEDs, up to various add-on boards. The API tries to adhere closely - to Python's idioms and naming conventions, and features alternative - programming paradigm approaches. - . - This is the Python 3 version of the package. - -Package: python-gpiozero-doc -Architecture: all -Section: doc -Depends: ${sphinxdoc:Depends}, ${misc:Depends} -Description: Simple API for controlling devices attached to a Pi's GPIO pins. - gpiozero builds on various pin libraries to provide a set of classes designed - to simplify interaction with devices connected to the GPIO pins, from simple - buttons and LEDs, up to various add-on boards. The API tries to adhere closely - to Python's idioms and naming conventions, and features alternative - programming paradigm approaches. - . - This is the version independent documentation for the package. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 785c7bf96..000000000 --- a/debian/copyright +++ /dev/null @@ -1,32 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: gpiozero -Source: https://github.com/RPi-Distro/python-gpiozero - -Files: * -Copyright: 2015-2018 Ben Nuttall -License: BSD-3-Clause - -License: BSD-3-Clause - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - . - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of the copyright holder nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - . - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/docs b/debian/docs deleted file mode 100644 index f17fb5252..000000000 --- a/debian/docs +++ /dev/null @@ -1,2 +0,0 @@ -README.rst -LICENSE.rst diff --git a/debian/python-gpiozero-doc.doc-base b/debian/python-gpiozero-doc.doc-base deleted file mode 100644 index caf7e2106..000000000 --- a/debian/python-gpiozero-doc.doc-base +++ /dev/null @@ -1,10 +0,0 @@ -Document: python-gpiozero -Title: GPIO Zero Reference -Author: Ben Nuttall -Abstract: Documentation for the gpiozero API -Section: Programming/Python - -Format: HTML -Index: /usr/share/doc/python-gpiozero-doc/html/index.html -Files: /usr/share/doc/python-gpiozero-doc/html/* - diff --git a/debian/python-gpiozero-doc.docs b/debian/python-gpiozero-doc.docs deleted file mode 100644 index 344fcaaf0..000000000 --- a/debian/python-gpiozero-doc.docs +++ /dev/null @@ -1 +0,0 @@ -build/html/ diff --git a/debian/python3-gpiozero.manpages b/debian/python3-gpiozero.manpages deleted file mode 100644 index 65766cff8..000000000 --- a/debian/python3-gpiozero.manpages +++ /dev/null @@ -1,2 +0,0 @@ -build/man/pinout.1 -build/man/remote-gpio.7 diff --git a/debian/rules b/debian/rules deleted file mode 100755 index b32efc9c6..000000000 --- a/debian/rules +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- - -#export DH_VERBOSE=1 -export DH_OPTIONS - -export PYBUILD_NAME=gpiozero - -%: - dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild - -override_dh_auto_install: - dh_auto_install - # Strip out binaries from the py2 package - rm debian/python-gpiozero/usr/bin/pinout - -override_dh_auto_test: - # Don't run the tests! - -override_dh_auto_build: - dh_auto_build - PYTHONPATH=. sphinx-build -N -bhtml docs/ build/html - PYTHONPATH=. sphinx-build -N -bman docs/ build/man diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 89ae9db8f..000000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/debian/source/options b/debian/source/options deleted file mode 100644 index 0c2b6d83d..000000000 --- a/debian/source/options +++ /dev/null @@ -1,12 +0,0 @@ -diff-ignore -tar-ignore -tar-ignore = dist -tar-ignore = tags -tar-ignore = *.egg-info -tar-ignore = .cache -tar-ignore = .pytest_cache -tar-ignore = .coverage* -tar-ignore = .github -tar-ignore = .tox -tar-ignore = *.fzz -tar-ignore = *.xcf diff --git a/docs/Makefile b/docs/Makefile index bac6dd2ab..45ea56710 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -13,6 +13,7 @@ GPI_DIAGRAMS := $(wildcard images/*.gpi images/*/*.gpi) SVG_IMAGES := $(wildcard images/*.svg images/*/*.svg) $(DOT_DIAGRAMS:%.dot=%.svg) $(MSC_DIAGRAMS:%.mscgen=%.svg) PNG_IMAGES := $(wildcard images/*.png images/*/*.png) $(GPI_DIAGRAMS:%.gpi=%.png) $(SVG_IMAGES:%.svg=%.png) PDF_IMAGES := $(SVG_IMAGES:%.svg=%.pdf) $(GPI_DIAGRAMS:%.gpi=%.pdf) $(DOT_DIAGRAMS:%.dot=%.pdf) $(MSC_DIAGRAMS:%.mscgen=%.pdf) +INKSCAPE_VER := $(shell inkscape --version | sed -ne '/^Inkscape/ s/^Inkscape \([0-9]\+\)\..*$$/\1/p') # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) @@ -29,21 +30,14 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" + @echo " preview to start a web-server that watches for file changes" + @echo " and re-builds the docs when required" @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @@ -59,50 +53,11 @@ html: $(SVG_IMAGES) $(PNG_IMAGES) @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." -dirhtml: $(SVG_IMAGES) $(PNG_IMAGES) - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: $(SVG_IMAGES) $(PNG_IMAGES) - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/gpiozero.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/gpiozero.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/gpiozero" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/gpiozero" - @echo "# devhelp" - epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @@ -121,12 +76,6 @@ latexpdf: $(PDF_IMAGES) $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." -latexpdfja: $(PDF_IMAGES) - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @@ -137,19 +86,6 @@ man: @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @@ -181,27 +117,30 @@ pseudoxml: @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +preview: + ../scripts/previewer $(BUILDDIR)/html + images/device_hierarchy.dot: $(PY_SOURCES) - ./images/class_graph \ + ../scripts/class_graph \ -i Device \ -i SharedMixin \ -x SPI -x SPISoftwareBus > $@ images/composite_device_hierarchy.dot: $(PY_SOURCES) - ./images/class_graph \ + ../scripts/class_graph \ -i CompositeDevice \ -i Energenie \ -i LedBord \ -x Motor \ -x PhaseEnableMotor \ -x Servo \ - -o SourceMixin -o HoldMixin > $@ + -o SourceMixin > $@ images/spi_device_hierarchy.dot: $(PY_SOURCES) - ./images/class_graph -i SPIDevice > $@ + ../scripts/class_graph -i SPIDevice > $@ images/output_device_hierarchy.dot: $(PY_SOURCES) - ./images/class_graph \ + ../scripts/class_graph \ -i OutputDevice \ -i RGBLED \ -i Servo \ @@ -212,7 +151,10 @@ images/output_device_hierarchy.dot: $(PY_SOURCES) -x SPI -o SourceMixin > $@ images/input_device_hierarchy.dot: $(PY_SOURCES) - ./images/class_graph -i InputDevice -o EventsMixin -o HoldMixin > $@ + ../scripts/class_graph -i InputDevice -o EventsMixin -o HoldMixin > $@ + +images/internal_device_hierarchy.dot: $(PY_SOURCES) + ../scripts/class_graph -i InternalDevice -o EventsMixin > $@ %.svg: %.mscgen mscgen -T svg -o $@ $< @@ -220,19 +162,27 @@ images/input_device_hierarchy.dot: $(PY_SOURCES) %.svg: %.dot dot -T svg -o $@ $< -%.png: %.gpi common.gp - gnuplot -e "set term pngcairo size 640,480" $< > $@ +%.png: %.gpi + gnuplot -e "set term pngcairo transparent size 400,400" $< > $@ +ifeq ($(INKSCAPE_VER),0) %.png: %.svg - inkscape -e $@ $< + inkscape --export-dpi 150 -e $@ $< %.pdf: %.svg inkscape -A $@ $< +else +%.png: %.svg + inkscape --export-dpi 150 --export-type png -o $@ $< + +%.pdf: %.svg + inkscape --export-type pdf -o $@ $< +endif -%.pdf: %.gpi common.gp - gnuplot -e "set term pdfcairo size 10cm,7.5cm" $< > $@ +%.pdf: %.gpi + gnuplot -e "set term pdfcairo size 5cm,5cm" $< > $@ %.pdf: %.mscgen mscgen -T eps -o - $< | ps2pdf -dEPSCrop - $@ -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext +.PHONY: help clean html preview json epub latex latexpdf text man changes linkcheck doctest gettext diff --git a/docs/api_boards.rst b/docs/api_boards.rst index c8346cec2..74a53dd12 100644 --- a/docs/api_boards.rst +++ b/docs/api_boards.rst @@ -1,35 +1,12 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2016-2019 Ben Nuttall -.. Copyright (c) 2015-2019 Dave Jones +.. +.. Copyright (c) 2015-2021 Dave Jones +.. Copyright (c) 2016-2020 Ben Nuttall .. Copyright (c) 2018 Claire Pollard .. Copyright (c) 2016 Andrew Scheller .. Copyright (c) 2016 Andrew Scheller .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ============================ API - Boards and Accessories @@ -61,168 +38,202 @@ named after. All classes in this section are concrete (not abstract). LEDBoard -------- -.. autoclass:: LEDBoard(\*pins, pwm=False, active_high=True, initial_value=False, pin_factory=None, \*\*named_pins) +.. autoclass:: LEDBoard :members: on, off, blink, pulse, toggle LEDBarGraph ----------- -.. autoclass:: LEDBarGraph(\*pins, pwm=False, active_high=True, initial_value=0, pin_factory=None) +.. autoclass:: LEDBarGraph :members: value, source, values, lit_count +LEDCharDisplay +-------------- + +.. autoclass:: LEDCharDisplay + :members: value, font + + +LEDMultiCharDisplay +------------------- + +.. autoclass:: LEDMultiCharDisplay + :members: value, plex_delay + + +LEDCharFont +----------- + +.. autoclass:: LEDCharFont + + ButtonBoard ----------- -.. autoclass:: ButtonBoard(\*pins, pull_up=True, active_state=None, bounce_time=None, hold_time=1, hold_repeat=False, pin_factory=None, \*\*named_pins) +.. autoclass:: ButtonBoard :members: wait_for_press, wait_for_release, is_pressed, pressed_time, when_pressed, when_released, value TrafficLights ------------- -.. autoclass:: TrafficLights(red, amber, green, \*, yellow=None, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: TrafficLights :members: TrafficLightsBuzzer ------------------- -.. autoclass:: TrafficLightsBuzzer(lights, buzzer, button, \*, pin_factory=None) +.. autoclass:: TrafficLightsBuzzer :members: PiHutXmasTree ------------- -.. autoclass:: PiHutXmasTree(\*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: PiHutXmasTree :members: LedBorg ------- -.. autoclass:: LedBorg(\*, pwm=True, initial_value=(0, 0, 0), pin_factory=None) +.. autoclass:: LedBorg :members: PiLiter ------- -.. autoclass:: PiLiter(\*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: PiLiter :members: PiLiterBarGraph --------------- -.. autoclass:: PiLiterBarGraph(\*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: PiLiterBarGraph :members: PiTraffic --------- -.. autoclass:: PiTraffic(\*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: PiTraffic :members: PiStop ------ -.. autoclass:: PiStop(location, \*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: PiStop :members: FishDish -------- -.. autoclass:: FishDish(\*, pwm=False, pin_factory=None) +.. autoclass:: FishDish :members: TrafficHat ---------- -.. autoclass:: TrafficHat(\*, pwm=False, pin_factory=None) +.. autoclass:: TrafficHat + :members: + + +TrafficpHat +----------- + +.. autoclass:: TrafficpHat :members: JamHat ------ -.. autoclass:: JamHat(\*, pwm=False, pin_factory=None) +.. autoclass:: JamHat + :members: + + +Pibrella +-------- + +.. autoclass:: Pibrella :members: Robot ----- -.. autoclass:: Robot(left, right, \*, pwm=True, pin_factory=None) +.. autoclass:: Robot :members: PhaseEnableRobot ---------------- -.. autoclass:: PhaseEnableRobot(left, right, \*, pwm=True, pin_factory=None) +.. autoclass:: PhaseEnableRobot :members: RyanteckRobot ------------- -.. autoclass:: RyanteckRobot(\*, pwm=True, pin_factory=None) +.. autoclass:: RyanteckRobot :members: CamJamKitRobot -------------- -.. autoclass:: CamJamKitRobot(\*, pwm=True, pin_factory=None) +.. autoclass:: CamJamKitRobot :members: PololuDRV8835Robot ------------------ -.. autoclass:: PololuDRV8835Robot(\*, pwm=True, pin_factory=None) +.. autoclass:: PololuDRV8835Robot :members: Energenie --------- -.. autoclass:: Energenie(socket, \*, initial_value=False, pin_factory=None) +.. autoclass:: Energenie :members: on, off, socket, value StatusZero ---------- -.. autoclass:: StatusZero(\*labels, pwm=False, active_high=True, initial_value=False, pin_factory=None) +.. autoclass:: StatusZero :members: StatusBoard ----------- -.. autoclass:: StatusBoard(\*labels, pwm=False, active_high=True, initial_value=False, pin_factory=None) +.. autoclass:: StatusBoard :members: SnowPi ------ -.. autoclass:: SnowPi(\*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: SnowPi :members: PumpkinPi --------- -.. autoclass:: PumpkinPi(\*, pwm=False, initial_value=False, pin_factory=None) +.. autoclass:: PumpkinPi :members: @@ -247,19 +258,19 @@ to construct classes for their own devices. LEDCollection ------------- -.. autoclass:: LEDCollection(\*pins, pwm=False, active_high=True, initial_value=False, pin_factory=None, \*\*named_pins) +.. autoclass:: LEDCollection :members: CompositeOutputDevice --------------------- -.. autoclass:: CompositeOutputDevice(\*args, _order=None, pin_factory=None, \*\*kwargs) +.. autoclass:: CompositeOutputDevice :members: CompositeDevice --------------- -.. autoclass:: CompositeDevice(\*args, _order=None, pin_factory=None, \*\*kwargs) +.. autoclass:: CompositeDevice :members: diff --git a/docs/api_exc.rst b/docs/api_exc.rst index b949e1372..6813f3469 100644 --- a/docs/api_exc.rst +++ b/docs/api_exc.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2016-2019 Dave Jones -.. Copyright (c) 2019 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2016-2023 Dave Jones +.. Copyright (c) 2019 Ben Nuttall .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ================ API - Exceptions diff --git a/docs/api_fonts.rst b/docs/api_fonts.rst new file mode 100644 index 000000000..24cb2910e --- /dev/null +++ b/docs/api_fonts.rst @@ -0,0 +1,36 @@ +.. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +.. +.. Copyright (c) 2021 Dave Jones +.. +.. SPDX-License-Identifier: BSD-3-Clause + +=========== +API - Fonts +=========== + +.. module:: gpiozero.fonts + +GPIO Zero includes a concept of "fonts" which is somewhat different to that you +may be familiar with. While a typical printing font determines how a particular +character is rendered on a page, a GPIO Zero font determines how a particular +character is rendered by a series of lights, like LED segments (e.g. with +:class:`~gpiozero.LEDCharDisplay` or :class:`~gpiozero.LEDMultiCharDisplay`). + +As a result, GPIO Zero's fonts are quite crude affairs, being little more than +mappings of characters to tuples of LED states. Still, it helps to have a +"friendly" format for creating such fonts, and in this module the library +provides several routines for this purpose. + +The module itself is typically imported as follows:: + + from gpiozero import fonts + + +Font Parsing +============ + +.. autofunction:: load_font_7seg + +.. autofunction:: load_font_14seg + +.. autofunction:: load_segment_font diff --git a/docs/api_generic.rst b/docs/api_generic.rst index f5c1e6a20..6f0009573 100644 --- a/docs/api_generic.rst +++ b/docs/api_generic.rst @@ -1,31 +1,8 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2015-2019 Dave Jones .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2015-2021 Dave Jones .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ===================== API - Generic Classes @@ -65,7 +42,7 @@ are represented in purple, while abstract classes are shaded lighter): Device ====== -.. autoclass:: Device(\*, pin_factory=None) +.. autoclass:: Device :members: close, closed, value, is_active, pin_factory diff --git a/docs/api_info.rst b/docs/api_info.rst index d15c5176c..3a12a2794 100644 --- a/docs/api_info.rst +++ b/docs/api_info.rst @@ -1,31 +1,8 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2017-2023 Dave Jones .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ==================== API - Pi Information diff --git a/docs/api_input.rst b/docs/api_input.rst index 64dec66bc..68435ea7d 100644 --- a/docs/api_input.rst +++ b/docs/api_input.rst @@ -1,31 +1,8 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2015-2019 Dave Jones .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2015-2021 Dave Jones .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause =================== API - Input Devices @@ -54,38 +31,45 @@ represent. All classes in this section are concrete (not abstract). Button ------ -.. autoclass:: Button(pin, \*, pull_up=True, active_state=None, bounce_time=None, hold_time=1, hold_repeat=False, pin_factory=None) +.. autoclass:: Button :members: wait_for_press, wait_for_release, pin, is_pressed, is_held, hold_time, held_time, hold_repeat, pull_up, when_pressed, when_released, when_held, value LineSensor (TRCT5000) --------------------- -.. autoclass:: LineSensor(pin, \*, queue_len=5, sample_rate=100, threshold=0.5, partial=False, pin_factory=None) +.. autoclass:: LineSensor :members: wait_for_line, wait_for_no_line, pin, line_detected, when_line, when_no_line, value MotionSensor (D-SUN PIR) ------------------------ -.. autoclass:: MotionSensor(pin, \*, queue_len=1, sample_rate=10, threshold=0.5, partial=False, pin_factory=None) +.. autoclass:: MotionSensor :members: wait_for_motion, wait_for_no_motion, pin, motion_detected, when_motion, when_no_motion, value LightSensor (LDR) ----------------- -.. autoclass:: LightSensor(pin, \*, queue_len=5, charge_time_limit=0.01, threshold=0.1, partial=False, pin_factory=None) +.. autoclass:: LightSensor :members: wait_for_light, wait_for_dark, pin, light_detected, when_light, when_dark, value DistanceSensor (HC-SR04) ------------------------ -.. autoclass:: DistanceSensor(echo, trigger, \*, queue_len=30, max_distance=1, threshold_distance=0.3, partial=False, pin_factory=None) +.. autoclass:: DistanceSensor :members: wait_for_in_range, wait_for_out_of_range, trigger, echo, when_in_range, when_out_of_range, max_distance, distance, threshold_distance, value +RotaryEncoder +------------- + +.. autoclass:: RotaryEncoder + :members: wait_for_rotate, wait_for_rotate_clockwise, wait_for_rotate_counter_clockwise, when_rotated, when_rotated_clockwise, when_rotated_counter_clockwise, steps, value, max_steps, threshold_steps, wrap + + Base Classes ============ @@ -103,26 +87,26 @@ to construct classes for their own devices. DigitalInputDevice ------------------ -.. autoclass:: DigitalInputDevice(pin, \*, pull_up=False, active_state=None, bounce_time=None, pin_factory=None) +.. autoclass:: DigitalInputDevice :members: wait_for_active, wait_for_inactive, when_activated, when_deactivated, active_time, inactive_time, value SmoothedInputDevice ------------------- -.. autoclass:: SmoothedInputDevice(pin, \*, pull_up=False, active_state=None, threshold=0.5, queue_len=5, sample_wait=0.0, partial=False, pin_factory=None) +.. autoclass:: SmoothedInputDevice :members: is_active, value, threshold, partial, queue_len InputDevice ----------- -.. autoclass:: InputDevice(pin, \*, pull_up=False, active_state=None, pin_factory=None) +.. autoclass:: InputDevice :members: pull_up, is_active, value GPIODevice ---------- -.. autoclass:: GPIODevice(pin, pin_factory=None) +.. autoclass:: GPIODevice :members: diff --git a/docs/api_internal.rst b/docs/api_internal.rst index 16797fc15..b52f9f6c1 100644 --- a/docs/api_internal.rst +++ b/docs/api_internal.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2018-2019 Ben Nuttall -.. Copyright (c) 2016-2019 Dave Jones .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2016-2021 Dave Jones +.. Copyright (c) 2018-2021 Ben Nuttall .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ====================== API - Internal Devices @@ -40,11 +17,56 @@ GPIO Zero also provides several "internal" devices which represent facilities provided by the operating system itself. These can be used to react to things like the time of day, or whether a server is available on the network. -.. warning:: +These devices provide an API similar to and compatible with GPIO devices so that +internal device events can trigger changes to GPIO output devices the way input +devices can. In the same way a :class:`~gpiozero.Button` object is *active* when +it's pressed, and can be used to trigger other devices when its state changes, +a :class:`TimeOfDay` object is *active* during a particular time period. + +Consider the following code in which a :class:`~gpiozero.Button` object is used +to control an :class:`~gpiozero.LED` object:: + + from gpiozero import LED, Button + from signal import pause + + led = LED(2) + btn = Button(3) + + btn.when_pressed = led.on + btn.when_released = led.off + + pause() + +Now consider the following example in which a :class:`TimeOfDay` object is used +to control an :class:`~gpiozero.LED` using the same method:: - These devices are experimental and their API is not yet considered stable. - We welcome any comments from testers, especially regarding new "internal - devices" that you'd find useful! + from gpiozero import LED, TimeOfDay + from datetime import time + from signal import pause + + led = LED(2) + tod = TimeOfDay(time(9), time(10)) + + tod.when_activated = led.on + tod.when_deactivated = led.off + + pause() + +Here, rather than the LED being controlled by the press of a button, it's +controlled by the time. When the time reaches 09:00AM, the LED comes on, and at +10:00AM it goes off. + +Like the :class:`~gpiozero.Button` object, internal devices like the +:class:`TimeOfDay` object has :attr:`~TimeOfDay.value`, +:attr:`~TimeOfDay.values`, :attr:`~TimeOfDay.is_active`, +:attr:`~TimeOfDay.when_activated` and :attr:`~TimeOfDay.when_deactivated` +attributes, so alternative methods using the other paradigms would also work. + +.. note:: + Note that although the constructor parameter ``pin_factory`` is available + for internal devices, and is required to be valid, the pin factory chosen + will not make any practical difference. Reading a remote Pi's CPU + temperature, for example, is not currently possible. Regular Classes @@ -57,36 +79,36 @@ named after. All classes in this section are concrete (not abstract). TimeOfDay --------- -.. autoclass:: TimeOfDay(start_time, end_time, \*, utc=True, pin_factory=None) - :members: start_time, end_time, utc, value +.. autoclass:: TimeOfDay + :members: start_time, end_time, utc, value, is_active, when_activated, when_deactivated PingServer ---------- -.. autoclass:: PingServer(host, \*, pin_factory=None) - :members: host, value +.. autoclass:: PingServer + :members: host, value, is_active, when_activated, when_deactivated CPUTemperature -------------- -.. autoclass:: CPUTemperature(sensor_file='/sys/class/thermal/thermal_zone0/temp', \*, min_temp=0.0, max_temp=100.0, threshold=80.0, pin_factory=None) - :members: temperature, value, is_active +.. autoclass:: CPUTemperature + :members: temperature, value, is_active, when_activated, when_deactivated LoadAverage ----------- -.. autoclass:: LoadAverage(load_average_file='/proc/loadavg', \*, min_load_average=0.0, max_load_average=1.0, threshold=0.8, minutes=5, pin_factory=None) - :members: load_average, value, is_active +.. autoclass:: LoadAverage + :members: load_average, value, is_active, when_activated, when_deactivated DiskUsage --------- -.. autoclass:: DiskUsage(filesystem='/', \*, threshold=90.0, pin_factory=None) - :members: usage, value, is_active +.. autoclass:: DiskUsage + :members: usage, value, is_active, when_activated, when_deactivated Base Classes @@ -103,7 +125,13 @@ The following sections document these base classes for advanced users that wish to construct classes for their own devices. +PolledInternalDevice +-------------------- + +.. autoclass:: PolledInternalDevice + + InternalDevice -------------- -.. autoclass:: InternalDevice(\*, pin_factory=None) +.. autoclass:: InternalDevice diff --git a/docs/api_output.rst b/docs/api_output.rst index 309b482d6..08284c6c9 100644 --- a/docs/api_output.rst +++ b/docs/api_output.rst @@ -1,33 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2015-2019 Dave Jones +.. +.. Copyright (c) 2015-2021 Dave Jones .. Copyright (c) 2019 Ben Nuttall .. Copyright (c) 2016 Andrew Scheller .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ==================== API - Output Devices @@ -56,63 +33,63 @@ represent. All classes in this section are concrete (not abstract). LED --- -.. autoclass:: LED(pin, \*, active_high=True, initial_value=False, pin_factory=None) +.. autoclass:: LED :members: on, off, toggle, blink, pin, is_lit, value PWMLED ------ -.. autoclass:: PWMLED(pin, \*, active_high=True, initial_value=0, frequency=100, pin_factory=None) +.. autoclass:: PWMLED :members: on, off, toggle, blink, pulse, pin, is_lit, value RGBLED ------ -.. autoclass:: RGBLED(red, green, blue, \*, active_high=True, initial_value=(0, 0, 0), pwm=True, pin_factory=None) +.. autoclass:: RGBLED :members: on, off, toggle, blink, pulse, red, green, blue, is_lit, color, value Buzzer ------ -.. autoclass:: Buzzer(pin, \*, active_high=True, initial_value=False, pin_factory=None) +.. autoclass:: Buzzer :members: on, off, toggle, beep, pin, is_active, value TonalBuzzer ----------- -.. autoclass:: TonalBuzzer(pin, \*, initial_value=None, mid_tone=Tone('A4'), octaves=1, pin_factory=None) +.. autoclass:: TonalBuzzer :members: play, stop, octaves, min_tone, mid_tone, max_tone, tone, is_active, value Motor ----- -.. autoclass:: Motor(forward, backward, \*, pwm=True, pin_factory=None) +.. autoclass:: Motor :members: forward, backward, reverse, stop, is_active, value PhaseEnableMotor ---------------- -.. autoclass:: PhaseEnableMotor(phase, enable, \*, pwm=True, pin_factory=None) +.. autoclass:: PhaseEnableMotor :members: forward, backward, reverse, stop, is_active, value Servo ----- -.. autoclass:: Servo(pin, \*, initial_value=0, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000, pin_factory=None) +.. autoclass:: Servo :members: AngularServo ------------ -.. autoclass:: AngularServo(pin, \*, initial_angle=0, min_angle=-90, max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, frame_width=20/1000, pin_factory=None) +.. autoclass:: AngularServo :members: angle, max_angle, min_angle, min, max, mid, is_active, value @@ -133,27 +110,27 @@ to construct classes for their own devices. DigitalOutputDevice ------------------- -.. autoclass:: DigitalOutputDevice(pin, \*, active_high=True, initial_value=False, pin_factory=None) +.. autoclass:: DigitalOutputDevice :members: on, off, blink, value PWMOutputDevice --------------- -.. autoclass:: PWMOutputDevice(pin, \*, active_high=True, initial_value=0, frequency=100, pin_factory=None) +.. autoclass:: PWMOutputDevice :members: on, off, blink, pulse, toggle, frequency, is_active, value OutputDevice ------------ -.. autoclass:: OutputDevice(pin, \*, active_high=True, initial_value=False, pin_factory=None) +.. autoclass:: OutputDevice :members: GPIODevice ---------- -.. autoclass:: GPIODevice(pin, \*, pin_factory=None) +.. autoclass:: GPIODevice :members: :noindex: diff --git a/docs/api_pins.rst b/docs/api_pins.rst index 03234bbe7..65edf55fa 100644 --- a/docs/api_pins.rst +++ b/docs/api_pins.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2015-2019 Dave Jones -.. Copyright (c) 2019 Ben Nuttall .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2015-2023 Dave Jones +.. Copyright (c) 2019-2021 Ben Nuttall .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ========== API - Pins @@ -56,11 +33,50 @@ This is illustrated in the following flow-chart: .. image:: images/device_pin_flowchart.* :align: center -The default factory is constructed when GPIO Zero is first imported; if no +The default factory is constructed when the first device is initialised; if no default factory can be constructed (e.g. because no GPIO implementations are -installed, or all of them fail to load for whatever reason), an -:exc:`ImportError` will be raised. +installed, or all of them fail to load for whatever reason), a +:exc:`BadPinFactory` exception will be raised at construction time. + +After importing gpiozero, until constructing a gpiozero device, the pin factory +is :data:`None`, but at the point of first construction the default pin factory +will come into effect: +.. code-block:: console + + pi@raspberrypi:~ $ python3 + Python 3.7.3 (default, Apr 3 2019, 05:39:12) + [GCC 8.2.0] on linux + Type "help", "copyright", "credits" or "license" for more information. + >>> from gpiozero import Device, LED + >>> print(Device.pin_factory) + None + >>> led = LED(2) + >>> Device.pin_factory + + >>> led.pin_factory + + +As above, on a Raspberry Pi with the RPi.GPIO library installed, (assuming no +environment variables are set), the default pin factory will be +:class:`~gpiozero.pins.rpigpio.RPiGPIOFactory`. + +On a PC (with no pin libraries installed and no environment variables set), +importing will work but attempting to create a device will raise +:exc:`BadPinFactory`: + +.. code-block:: console + + ben@magicman:~ $ python3 + Python 3.6.8 (default, Aug 20 2019, 17:12:48) + [GCC 8.3.0] on linux + Type "help", "copyright", "credits" or "license" for more information. + >>> from gpiozero import Device, LED + >>> print(Device.pin_factory) + None + >>> led = LED(2) + ... + BadPinFactory: Unable to load any default pin factory! .. _changing-pin-factory: @@ -72,12 +88,12 @@ The default pin factory can be replaced by specifying a value for the .. code-block:: console - $ GPIOZERO_PIN_FACTORY=native python - Python 3.4.2 (default, Oct 19 2014, 13:31:11) - [GCC 4.9.1] on linux + pi@raspberrypi:~ $ GPIOZERO_PIN_FACTORY=native python3 + Python 3.7.3 (default, Apr 3 2019, 05:39:12) + [GCC 8.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. - >>> import gpiozero - >>> gpiozero.Device.pin_factory + >>> from gpiozero import Device + >>> Device._default_pin_factory() To set the :envvar:`GPIOZERO_PIN_FACTORY` for the rest of your session you can @@ -85,26 +101,30 @@ To set the :envvar:`GPIOZERO_PIN_FACTORY` for the rest of your session you can .. code-block:: console - $ export GPIOZERO_PIN_FACTORY=native - $ python - Python 3.4.2 (default, Oct 19 2014, 13:31:11) - [GCC 4.9.1] on linux + pi@raspberrypi:~ $ export GPIOZERO_PIN_FACTORY=native + pi@raspberrypi:~ $ python3 + Python 3.7.3 (default, Apr 3 2019, 05:39:12) + [GCC 8.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import gpiozero - >>> gpiozero.Device.pin_factory + >>> Device._default_pin_factory() >>> quit() - $ python - Python 3.4.2 (default, Oct 19 2014, 13:31:11) - [GCC 4.9.1] on linux + pi@raspberrypi:~ $ python3 + Python 3.7.3 (default, Apr 3 2019, 05:39:12) + [GCC 8.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import gpiozero - >>> gpiozero.Device.pin_factory - + >>> Device._default_pin_factory() + If you add the :command:`export` command to your :file:`~/.bashrc` file, you'll set the default pin factory for all future sessions too. +If the environment variable is set, the corresponding pin factory will be used, +otherwise each of the four GPIO pin factories will be attempted to be used in +turn. + The following values, and the corresponding :class:`Factory` and :class:`Pin` classes are listed in the table below. Factories are listed in the order that they are tried by default. @@ -112,9 +132,9 @@ they are tried by default. +---------+-----------------------------------------------+-------------------------------------------+ | Name | Factory class | Pin class | +=========+===============================================+===========================================+ -| rpigpio | :class:`gpiozero.pins.rpigpio.RPiGPIOFactory` | :class:`gpiozero.pins.rpigpio.RPiGPIOPin` | +| lgpio | :class:`gpiozero.pins.lgpio.LGPIOFactory` | :class:`gpiozero.pins.lgpio.LGPIOPin` | +---------+-----------------------------------------------+-------------------------------------------+ -| rpio | :class:`gpiozero.pins.rpio.RPIOFactory` | :class:`gpiozero.pins.rpio.RPIOPin` | +| rpigpio | :class:`gpiozero.pins.rpigpio.RPiGPIOFactory` | :class:`gpiozero.pins.rpigpio.RPiGPIOPin` | +---------+-----------------------------------------------+-------------------------------------------+ | pigpio | :class:`gpiozero.pins.pigpio.PiGPIOFactory` | :class:`gpiozero.pins.pigpio.PiGPIOPin` | +---------+-----------------------------------------------+-------------------------------------------+ @@ -129,8 +149,7 @@ If you need to change the default pin factory from within a script, either set Device.pin_factory = NativeFactory() - # These will now implicitly use NativePin instead of - # RPiGPIOPin + # These will now implicitly use NativePin instead of RPiGPIOPin led1 = LED(16) led2 = LED(17) @@ -176,20 +195,95 @@ Like the :envvar:`GPIOZERO_PIN_FACTORY` value, these can be exported from your Mock pins ========= -There's also a :class:`gpiozero.pins.mock.MockFactory` which generates entirely +There's also a :class:`~gpiozero.pins.mock.MockFactory` which generates entirely fake pins. This was originally intended for GPIO Zero developers who wish to write tests for devices without having to have the physical device wired in to -their Pi. However, they have also proven relatively useful in developing GPIO -Zero scripts without having a Pi to hand. This pin factory will never be loaded -by default; it must be explicitly specified. For example: +their Pi. However, they have also proven useful in developing GPIO Zero scripts +without having a Pi to hand. This pin factory will never be loaded by default; +it must be explicitly specified, either by setting an environment variable or +setting the pin factory within the script. For example: + +.. code-block:: console + + pi@raspberrypi:~ $ GPIOZERO_PIN_FACTORY=mock python3 + +or: + +.. code-block:: python + + from gpiozero import Device, LED + from gpiozero.pins.mock import MockFactory + + Device.pin_factory = MockFactory() + + led = LED(2) -.. literalinclude:: examples/mock_demo.py +You can create device objects and inspect their value changing as you'd expect: + +.. code-block:: pycon + + pi@raspberrypi:~ $ GPIOZERO_PIN_FACTORY=mock python3 + Python 3.7.3 (default, Apr 3 2019, 05:39:12) + [GCC 8.2.0] on linux + Type "help", "copyright", "credits" or "license" for more information. + >>> from gpiozero import LED + >>> led = LED(2) + >>> led.value + 0 + >>> led.on() + >>> led.value + 1 + +You can even control pin state changes to simulate device behaviour: + +.. code-block:: pycon + + >>> from gpiozero import LED, Button + + # Construct a couple of devices attached to mock pins 16 and 17, and link the devices + >>> led = LED(17) + >>> btn = Button(16) + >>> led.source = btn + + # Initailly the button isn't "pressed" so the LED should be off + >>> led.value + 0 + + # Drive the pin low (this is what would happen electrically when the button is pressed) + >>> btn.pin.drive_low() + # The LED is now on + >>> led.value + 1 + + >>> btn.pin.drive_high() + # The button is now "released", so the LED should be off again + >>> led.value + 0 Several sub-classes of mock pins exist for emulating various other things (pins that do/don't support PWM, pins that are connected together, pins that -drive high after a delay, etc). Interested users are invited to read the GPIO -Zero test suite for further examples of usage. +drive high after a delay, etc), for example, you have to use +:class:`~gpiozero.pins.mock.MockPWMPin` to be able to use devices requiring PWM: + +.. code-block:: console + pi@raspberrypi:~ $ GPIOZERO_PIN_FACTORY=mock GPIOZERO_MOCK_PIN_CLASS=mockpwmpin python3 + +or: + +.. code-block:: python + + from gpiozero import Device, LED + from gpiozero.pins.mock import MockFactory, MockPWMPin + + Device.pin_factory = MockFactory(pin_class=MockPWMPin) + + led = LED(2) + +Interested users are invited to read the `GPIO Zero test suite`_ for further +examples of usage. + +.. _`GPIO Zero test suite`: https://github.com/gpiozero/gpiozero/tree/master/tests Base classes ============ @@ -230,14 +324,14 @@ RPi.GPIO .. autoclass:: gpiozero.pins.rpigpio.RPiGPIOPin -RPIO -==== +lgpio +===== -.. module:: gpiozero.pins.rpio +.. module:: gpiozero.pins.lgpio -.. autoclass:: gpiozero.pins.rpio.RPIOFactory +.. autoclass:: gpiozero.pins.lgpio.LGPIOFactory -.. autoclass:: gpiozero.pins.rpio.RPIOPin +.. autoclass:: gpiozero.pins.lgpio.LGPIOPin PiGPIO @@ -259,6 +353,10 @@ Native .. autoclass:: gpiozero.pins.native.NativePin +.. autoclass:: gpiozero.pins.native.Native2835Pin + +.. autoclass:: gpiozero.pins.native.Native2711Pin + Mock ==== @@ -277,3 +375,5 @@ Mock .. autoclass:: gpiozero.pins.mock.MockChargingPin .. autoclass:: gpiozero.pins.mock.MockTriggerPin + +.. autoclass:: gpiozero.pins.mock.MockSPIDevice diff --git a/docs/api_spi.rst b/docs/api_spi.rst index d9b7c2829..9ed766ae2 100644 --- a/docs/api_spi.rst +++ b/docs/api_spi.rst @@ -1,33 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2016-2019 Dave Jones +.. +.. Copyright (c) 2016-2023 Dave Jones .. Copyright (c) 2017 rgm .. Copyright (c) 2016 Andrew Scheller .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ================= API - SPI Devices diff --git a/docs/api_tones.rst b/docs/api_tones.rst index 3d3458e26..9e48751e8 100644 --- a/docs/api_tones.rst +++ b/docs/api_tones.rst @@ -1,31 +1,8 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2019 Dave Jones .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2019-2023 Dave Jones .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause =========== API - Tones diff --git a/docs/api_tools.rst b/docs/api_tools.rst index 0d4951a3f..27e2d2702 100644 --- a/docs/api_tools.rst +++ b/docs/api_tools.rst @@ -1,34 +1,11 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +.. +.. Copyright (c) 2016-2023 Dave Jones .. Copyright (c) 2017-2019 Ben Nuttall -.. Copyright (c) 2016-2019 Dave Jones .. Copyright (c) 2016 Edward Betts .. Copyright (c) 2016 Andrew Scheller .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ========================= API - Device Source Tools diff --git a/docs/changelog.rst b/docs/changelog.rst index 7de9e7cca..0dd429eb1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,33 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2016-2019 Ben Nuttall -.. Copyright (c) 2015-2019 Dave Jones -.. Copyright (c) 2016 Andrew Scheller -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. .. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2015-2024 Dave Jones +.. Copyright (c) 2016-2021 Ben Nuttall +.. Copyright (c) 2016 Andrew Scheller .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ========= Changelog @@ -35,6 +12,122 @@ Changelog .. currentmodule:: gpiozero + +Release 2.0.1 (2024-02-15) +========================== + +* Fixed Python 3.12 compatibility, and clarify that 3.9 is the lowest supported + version in our CI configuration (`#1113`_) + +.. _#1113: https://github.com/gpiozero/gpiozero/issues/1113 + + +Release 2.0 (2023-09-12) +======================== + +* Removed Python 2.x support; many thanks to Fangchen Li for a substantial + amount of work on this! (`#799`_ `#896`_) +* Removed RPIO pin implementation +* Made :class:`gpiozero.pins.lgpio.LGPIOFactory` the default factory; the + former default, :class:`gpiozero.pins.rpigpio.RPiGPIOFactory`, is now the + second place preference +* Added :doc:`compat` chapter +* Added :program:`pintest` utility +* Added Raspberry Pi 5 board data + +.. _#799: https://github.com/gpiozero/gpiozero/issues/799 +.. _#896: https://github.com/gpiozero/gpiozero/issues/896 + + +Release 1.6.2 (2021-03-18) +========================== + +* Correct docs referring to 1.6.0 as the last version supporting Python 2 + +.. warning:: + + This is the last release to support Python 2 + +Release 1.6.1 (2021-03-17) +========================== + +* Fix missing font files for 7-segment displays + +Release 1.6.0 (2021-03-14) +========================== + +* Added :class:`RotaryEncoder` class (thanks to Paulo Mateus) (`#482`_, `#928`_) +* Added support for multi-segment character displays with + :class:`LEDCharDisplay` and :class:`LEDMultiCharDisplay` along with "font" + support using :class:`LEDCharFont` (thanks to Martin O'Hanlon) (`#357`_, + `#485`_, `#488`_, `#493`_, `#930`_) +* Added :class:`Pibrella` class (thanks to Carl Monk) (`#773`_, `#798`_) +* Added :class:`TrafficpHat` class (thanks to Ryan Walmsley) (`#845`_, `#846`_) +* Added support for the `lgpio`_ library as a pin factory + (:class:`~gpiozero.pins.lgpio.LGPIOFactory`) (thanks to Joan for lg) (`#927`_) +* Allow :class:`Motor` to pass :attr:`~Device.pin_factory` to its child + :class:`OutputDevice` objects (thanks to Yisrael Dov Lebow) (`#792`_) +* Small SPI exception fix (thanks to Maksim Levental) (`#762`_) +* Warn users when using default pin factory for Servos and Distance Sensors + (thanks to Sofiia Kosovan and Daniele Procida at the EuroPython sprints) + (`#780`_, `#781`_) +* Added :attr:`~Servo.pulse_width` property to :class:`Servo` (suggested by + Daniele Procida at the PyCon UK sprints) (`#795`_, `#797`_) +* Added event-driven functionality to :doc:`internal devices ` + (`#941`_) +* Allowed :class:`Energenie` sockets preserve their state on construction + (thanks to Jack Wearden) (`#865`_) +* Added source tools :func:`~gpiozero.tools.scaled_half` and + :func:`~gpiozero.tools.scaled_full` +* Added complete Pi 4 support to :class:`~gpiozero.pins.native.NativeFactory` + (thanks to Andrew Scheller) (`#920`_, `#929`_, `#940`_) +* Updated add-on boards to use BOARD numbering (`#349`_, `#860`_) +* Fixed :class:`ButtonBoard` release events (`#761`_) +* Add ASCII art diagrams to :program:`pinout` for Pi 400 and CM4 (`#932`_) +* Cleaned up software SPI (thanks to Andrew Scheller and Kyle Morgan) (`#777`_, + `#895`_, `#900`_) +* Added USB3 and Ethernet speed attributes to :func:`pi_info` +* Various docs updates + +.. _lgpio: http://abyz.me.uk/lg/py_lgpio.html +.. _#482: https://github.com/gpiozero/gpiozero/issues/482 +.. _#928: https://github.com/gpiozero/gpiozero/issues/928 +.. _#357: https://github.com/gpiozero/gpiozero/issues/357 +.. _#485: https://github.com/gpiozero/gpiozero/issues/485 +.. _#488: https://github.com/gpiozero/gpiozero/issues/488 +.. _#493: https://github.com/gpiozero/gpiozero/issues/493 +.. _#930: https://github.com/gpiozero/gpiozero/issues/930 +.. _#773: https://github.com/gpiozero/gpiozero/issues/773 +.. _#798: https://github.com/gpiozero/gpiozero/issues/798 +.. _#845: https://github.com/gpiozero/gpiozero/issues/845 +.. _#846: https://github.com/gpiozero/gpiozero/issues/846 +.. _#927: https://github.com/gpiozero/gpiozero/issues/927 +.. _#792: https://github.com/gpiozero/gpiozero/issues/792 +.. _#762: https://github.com/gpiozero/gpiozero/issues/762 +.. _#780: https://github.com/gpiozero/gpiozero/issues/780 +.. _#781: https://github.com/gpiozero/gpiozero/issues/781 +.. _#795: https://github.com/gpiozero/gpiozero/issues/795 +.. _#797: https://github.com/gpiozero/gpiozero/issues/797 +.. _#941: https://github.com/gpiozero/gpiozero/issues/941 +.. _#865: https://github.com/gpiozero/gpiozero/issues/865 +.. _#920: https://github.com/gpiozero/gpiozero/issues/920 +.. _#929: https://github.com/gpiozero/gpiozero/issues/929 +.. _#940: https://github.com/gpiozero/gpiozero/issues/940 +.. _#939: https://github.com/gpiozero/gpiozero/issues/939 +.. _#349: https://github.com/gpiozero/gpiozero/issues/349 +.. _#860: https://github.com/gpiozero/gpiozero/issues/860 +.. _#761: https://github.com/gpiozero/gpiozero/issues/761 +.. _#932: https://github.com/gpiozero/gpiozero/issues/932 +.. _#777: https://github.com/gpiozero/gpiozero/issues/777 +.. _#895: https://github.com/gpiozero/gpiozero/issues/895 +.. _#900: https://github.com/gpiozero/gpiozero/issues/900 + +Release 1.5.1 (2019-06-24) +========================== + +* Added Raspberry Pi 4 data for :func:`pi_info` and :program:`pinout` +* Minor docs updates + Release 1.5.0 (2019-02-12) ========================== @@ -81,40 +174,40 @@ Release 1.5.0 (2019-02-12) * Automatic documentation class hierarchy diagram generation. * Automatic copyright attribution in source files. -.. _#640: https://github.com/RPi-Distro/python-gpiozero/issues/640 -.. _#664: https://github.com/RPi-Distro/python-gpiozero/issues/664 -.. _#665: https://github.com/RPi-Distro/python-gpiozero/issues/665 -.. _#719: https://github.com/RPi-Distro/python-gpiozero/issues/719 -.. _#532: https://github.com/RPi-Distro/python-gpiozero/issues/532 -.. _#714: https://github.com/RPi-Distro/python-gpiozero/issues/714 +.. _#640: https://github.com/gpiozero/gpiozero/issues/640 +.. _#664: https://github.com/gpiozero/gpiozero/issues/664 +.. _#665: https://github.com/gpiozero/gpiozero/issues/665 +.. _#719: https://github.com/gpiozero/gpiozero/issues/719 +.. _#532: https://github.com/gpiozero/gpiozero/issues/532 +.. _#714: https://github.com/gpiozero/gpiozero/issues/714 .. _colorzero: https://colorzero.readthedocs.io/en/stable -.. _#655: https://github.com/RPi-Distro/python-gpiozero/issues/655 -.. _#681: https://github.com/RPi-Distro/python-gpiozero/issues/681 -.. _#717: https://github.com/RPi-Distro/python-gpiozero/issues/717 -.. _#680: https://github.com/RPi-Distro/python-gpiozero/issues/680 -.. _#502: https://github.com/RPi-Distro/python-gpiozero/issues/502 -.. _#591: https://github.com/RPi-Distro/python-gpiozero/issues/591 -.. _#713: https://github.com/RPi-Distro/python-gpiozero/issues/713 -.. _#675: https://github.com/RPi-Distro/python-gpiozero/issues/675 -.. _#722: https://github.com/RPi-Distro/python-gpiozero/issues/722 -.. _#470: https://github.com/RPi-Distro/python-gpiozero/issues/470 -.. _#481: https://github.com/RPi-Distro/python-gpiozero/issues/481 -.. _#366: https://github.com/RPi-Distro/python-gpiozero/issues/366 +.. _#655: https://github.com/gpiozero/gpiozero/issues/655 +.. _#681: https://github.com/gpiozero/gpiozero/issues/681 +.. _#717: https://github.com/gpiozero/gpiozero/issues/717 +.. _#680: https://github.com/gpiozero/gpiozero/issues/680 +.. _#502: https://github.com/gpiozero/gpiozero/issues/502 +.. _#591: https://github.com/gpiozero/gpiozero/issues/591 +.. _#713: https://github.com/gpiozero/gpiozero/issues/713 +.. _#675: https://github.com/gpiozero/gpiozero/issues/675 +.. _#722: https://github.com/gpiozero/gpiozero/issues/722 +.. _#470: https://github.com/gpiozero/gpiozero/issues/470 +.. _#481: https://github.com/gpiozero/gpiozero/issues/481 +.. _#366: https://github.com/gpiozero/gpiozero/issues/366 .. _pinout.xyz: https://pinout.xyz -.. _#604: https://github.com/RPi-Distro/python-gpiozero/issues/604 -.. _#627: https://github.com/RPi-Distro/python-gpiozero/issues/627 -.. _#704: https://github.com/RPi-Distro/python-gpiozero/issues/704 -.. _#629: https://github.com/RPi-Distro/python-gpiozero/issues/629 -.. _#634: https://github.com/RPi-Distro/python-gpiozero/issues/634 -.. _#652: https://github.com/RPi-Distro/python-gpiozero/issues/652 -.. _#593: https://github.com/RPi-Distro/python-gpiozero/issues/593 -.. _#658: https://github.com/RPi-Distro/python-gpiozero/issues/658 -.. _#666: https://github.com/RPi-Distro/python-gpiozero/issues/666 -.. _#668: https://github.com/RPi-Distro/python-gpiozero/issues/668 -.. _#669: https://github.com/RPi-Distro/python-gpiozero/issues/669 -.. _#671: https://github.com/RPi-Distro/python-gpiozero/issues/671 -.. _#673: https://github.com/RPi-Distro/python-gpiozero/issues/673 -.. _#674: https://github.com/RPi-Distro/python-gpiozero/issues/674 +.. _#604: https://github.com/gpiozero/gpiozero/issues/604 +.. _#627: https://github.com/gpiozero/gpiozero/issues/627 +.. _#704: https://github.com/gpiozero/gpiozero/issues/704 +.. _#629: https://github.com/gpiozero/gpiozero/issues/629 +.. _#634: https://github.com/gpiozero/gpiozero/issues/634 +.. _#652: https://github.com/gpiozero/gpiozero/issues/652 +.. _#593: https://github.com/gpiozero/gpiozero/issues/593 +.. _#658: https://github.com/gpiozero/gpiozero/issues/658 +.. _#666: https://github.com/gpiozero/gpiozero/issues/666 +.. _#668: https://github.com/gpiozero/gpiozero/issues/668 +.. _#669: https://github.com/gpiozero/gpiozero/issues/669 +.. _#671: https://github.com/gpiozero/gpiozero/issues/671 +.. _#673: https://github.com/gpiozero/gpiozero/issues/673 +.. _#674: https://github.com/gpiozero/gpiozero/issues/674 Release 1.4.1 (2018-02-20) ========================== @@ -132,17 +225,17 @@ This release is mostly bug-fixes, but a few enhancements have made it in too: * A variety of other minor enhancements, largely thanks to Andrew Scheller! (`#479`_, `#489`_, `#491`_, `#492`_) -.. _#306: https://github.com/RPi-Distro/python-gpiozero/issues/306 -.. _#386: https://github.com/RPi-Distro/python-gpiozero/issues/386 -.. _#479: https://github.com/RPi-Distro/python-gpiozero/issues/479 -.. _#489: https://github.com/RPi-Distro/python-gpiozero/issues/489 -.. _#491: https://github.com/RPi-Distro/python-gpiozero/issues/491 -.. _#492: https://github.com/RPi-Distro/python-gpiozero/issues/492 -.. _#584: https://github.com/RPi-Distro/python-gpiozero/issues/584 -.. _#595: https://github.com/RPi-Distro/python-gpiozero/issues/595 -.. _#617: https://github.com/RPi-Distro/python-gpiozero/issues/617 -.. _#618: https://github.com/RPi-Distro/python-gpiozero/issues/618 -.. _#619: https://github.com/RPi-Distro/python-gpiozero/issues/619 +.. _#306: https://github.com/gpiozero/gpiozero/issues/306 +.. _#386: https://github.com/gpiozero/gpiozero/issues/386 +.. _#479: https://github.com/gpiozero/gpiozero/issues/479 +.. _#489: https://github.com/gpiozero/gpiozero/issues/489 +.. _#491: https://github.com/gpiozero/gpiozero/issues/491 +.. _#492: https://github.com/gpiozero/gpiozero/issues/492 +.. _#584: https://github.com/gpiozero/gpiozero/issues/584 +.. _#595: https://github.com/gpiozero/gpiozero/issues/595 +.. _#617: https://github.com/gpiozero/gpiozero/issues/617 +.. _#618: https://github.com/gpiozero/gpiozero/issues/618 +.. _#619: https://github.com/gpiozero/gpiozero/issues/619 Release 1.4.0 (2017-07-26) ========================== @@ -170,27 +263,27 @@ Release 1.4.0 (2017-07-26) * Fixed a variety of packaging issues (`#535`_, `#518`_, `#519`_) * Improved text in factory fallback warnings (`#572`_) -.. _#279: https://github.com/RPi-Distro/python-gpiozero/issues/279 -.. _#421: https://github.com/RPi-Distro/python-gpiozero/issues/421 -.. _#434: https://github.com/RPi-Distro/python-gpiozero/issues/434 -.. _#459: https://github.com/RPi-Distro/python-gpiozero/issues/459 -.. _#465: https://github.com/RPi-Distro/python-gpiozero/issues/465 -.. _#468: https://github.com/RPi-Distro/python-gpiozero/issues/468 -.. _#469: https://github.com/RPi-Distro/python-gpiozero/issues/469 -.. _#484: https://github.com/RPi-Distro/python-gpiozero/issues/484 -.. _#497: https://github.com/RPi-Distro/python-gpiozero/issues/497 -.. _#504: https://github.com/RPi-Distro/python-gpiozero/issues/504 -.. _#518: https://github.com/RPi-Distro/python-gpiozero/issues/518 -.. _#519: https://github.com/RPi-Distro/python-gpiozero/issues/519 -.. _#520: https://github.com/RPi-Distro/python-gpiozero/issues/520 -.. _#523: https://github.com/RPi-Distro/python-gpiozero/issues/523 -.. _#529: https://github.com/RPi-Distro/python-gpiozero/issues/529 -.. _#535: https://github.com/RPi-Distro/python-gpiozero/issues/535 -.. _#558: https://github.com/RPi-Distro/python-gpiozero/issues/558 -.. _#565: https://github.com/RPi-Distro/python-gpiozero/issues/565 -.. _#572: https://github.com/RPi-Distro/python-gpiozero/issues/572 -.. _#575: https://github.com/RPi-Distro/python-gpiozero/issues/575 -.. _#576: https://github.com/RPi-Distro/python-gpiozero/issues/576 +.. _#279: https://github.com/gpiozero/gpiozero/issues/279 +.. _#421: https://github.com/gpiozero/gpiozero/issues/421 +.. _#434: https://github.com/gpiozero/gpiozero/issues/434 +.. _#459: https://github.com/gpiozero/gpiozero/issues/459 +.. _#465: https://github.com/gpiozero/gpiozero/issues/465 +.. _#468: https://github.com/gpiozero/gpiozero/issues/468 +.. _#469: https://github.com/gpiozero/gpiozero/issues/469 +.. _#484: https://github.com/gpiozero/gpiozero/issues/484 +.. _#497: https://github.com/gpiozero/gpiozero/issues/497 +.. _#504: https://github.com/gpiozero/gpiozero/issues/504 +.. _#518: https://github.com/gpiozero/gpiozero/issues/518 +.. _#519: https://github.com/gpiozero/gpiozero/issues/519 +.. _#520: https://github.com/gpiozero/gpiozero/issues/520 +.. _#523: https://github.com/gpiozero/gpiozero/issues/523 +.. _#529: https://github.com/gpiozero/gpiozero/issues/529 +.. _#535: https://github.com/gpiozero/gpiozero/issues/535 +.. _#558: https://github.com/gpiozero/gpiozero/issues/558 +.. _#565: https://github.com/gpiozero/gpiozero/issues/565 +.. _#572: https://github.com/gpiozero/gpiozero/issues/572 +.. _#575: https://github.com/gpiozero/gpiozero/issues/575 +.. _#576: https://github.com/gpiozero/gpiozero/issues/576 Release 1.3.2 (2017-03-03) ========================== @@ -224,15 +317,15 @@ make a Raspbian freeze. As always, thanks to the community - your suggestions and PRs have been brilliant and even if we don't take stuff exactly as is, it's always great to see your ideas. Onto 1.4! -.. _#248: https://github.com/RPi-Distro/python-gpiozero/issues/248 -.. _#289: https://github.com/RPi-Distro/python-gpiozero/issues/289 -.. _#294: https://github.com/RPi-Distro/python-gpiozero/issues/294 -.. _#295: https://github.com/RPi-Distro/python-gpiozero/issues/295 -.. _#305: https://github.com/RPi-Distro/python-gpiozero/issues/305 -.. _#320: https://github.com/RPi-Distro/python-gpiozero/issues/320 -.. _#340: https://github.com/RPi-Distro/python-gpiozero/issues/340 -.. _#359: https://github.com/RPi-Distro/python-gpiozero/issues/359 -.. _#385: https://github.com/RPi-Distro/python-gpiozero/issues/385 +.. _#248: https://github.com/gpiozero/gpiozero/issues/248 +.. _#289: https://github.com/gpiozero/gpiozero/issues/289 +.. _#294: https://github.com/gpiozero/gpiozero/issues/294 +.. _#295: https://github.com/gpiozero/gpiozero/issues/295 +.. _#305: https://github.com/gpiozero/gpiozero/issues/305 +.. _#320: https://github.com/gpiozero/gpiozero/issues/320 +.. _#340: https://github.com/gpiozero/gpiozero/issues/340 +.. _#359: https://github.com/gpiozero/gpiozero/issues/359 +.. _#385: https://github.com/gpiozero/gpiozero/issues/385 Release 1.2.0 (2016-04-10) ========================== @@ -272,23 +365,23 @@ reports in this version. Of particular note: As always, many thanks to the whole community - we look forward to hearing from you more in 1.3! -.. _#69: https://github.com/RPi-Distro/python-gpiozero/issues/69 -.. _#109: https://github.com/RPi-Distro/python-gpiozero/issues/109 -.. _#114: https://github.com/RPi-Distro/python-gpiozero/issues/114 -.. _#115: https://github.com/RPi-Distro/python-gpiozero/issues/115 -.. _#130: https://github.com/RPi-Distro/python-gpiozero/issues/130 -.. _#140: https://github.com/RPi-Distro/python-gpiozero/issues/140 -.. _#162: https://github.com/RPi-Distro/python-gpiozero/issues/162 -.. _#164: https://github.com/RPi-Distro/python-gpiozero/issues/164 -.. _#175: https://github.com/RPi-Distro/python-gpiozero/issues/175 -.. _#180: https://github.com/RPi-Distro/python-gpiozero/issues/180 -.. _#181: https://github.com/RPi-Distro/python-gpiozero/issues/181 -.. _#182: https://github.com/RPi-Distro/python-gpiozero/issues/182 -.. _#189: https://github.com/RPi-Distro/python-gpiozero/issues/189 -.. _#193: https://github.com/RPi-Distro/python-gpiozero/issues/193 -.. _#222: https://github.com/RPi-Distro/python-gpiozero/issues/222 -.. _#229: https://github.com/RPi-Distro/python-gpiozero/issues/229 -.. _#251: https://github.com/RPi-Distro/python-gpiozero/issues/251 +.. _#69: https://github.com/gpiozero/gpiozero/issues/69 +.. _#109: https://github.com/gpiozero/gpiozero/issues/109 +.. _#114: https://github.com/gpiozero/gpiozero/issues/114 +.. _#115: https://github.com/gpiozero/gpiozero/issues/115 +.. _#130: https://github.com/gpiozero/gpiozero/issues/130 +.. _#140: https://github.com/gpiozero/gpiozero/issues/140 +.. _#162: https://github.com/gpiozero/gpiozero/issues/162 +.. _#164: https://github.com/gpiozero/gpiozero/issues/164 +.. _#175: https://github.com/gpiozero/gpiozero/issues/175 +.. _#180: https://github.com/gpiozero/gpiozero/issues/180 +.. _#181: https://github.com/gpiozero/gpiozero/issues/181 +.. _#182: https://github.com/gpiozero/gpiozero/issues/182 +.. _#189: https://github.com/gpiozero/gpiozero/issues/189 +.. _#193: https://github.com/gpiozero/gpiozero/issues/193 +.. _#222: https://github.com/gpiozero/gpiozero/issues/222 +.. _#229: https://github.com/gpiozero/gpiozero/issues/229 +.. _#251: https://github.com/gpiozero/gpiozero/issues/251 Release 1.1.0 (2016-02-08) ========================== @@ -313,22 +406,22 @@ Release 1.1.0 (2016-02-08) * Loads of new ADC chips supported (many thanks to GitHub user pcopa!) (`#150`_) -.. _#80: https://github.com/RPi-Distro/python-gpiozero/issues/80 -.. _#82: https://github.com/RPi-Distro/python-gpiozero/issues/82 -.. _#94: https://github.com/RPi-Distro/python-gpiozero/issues/94 -.. _#101: https://github.com/RPi-Distro/python-gpiozero/issues/101 -.. _#118: https://github.com/RPi-Distro/python-gpiozero/issues/118 -.. _#119: https://github.com/RPi-Distro/python-gpiozero/issues/119 -.. _#126: https://github.com/RPi-Distro/python-gpiozero/issues/126 -.. _#135: https://github.com/RPi-Distro/python-gpiozero/issues/135 -.. _#141: https://github.com/RPi-Distro/python-gpiozero/issues/141 -.. _#143: https://github.com/RPi-Distro/python-gpiozero/issues/143 -.. _#150: https://github.com/RPi-Distro/python-gpiozero/issues/150 -.. _#154: https://github.com/RPi-Distro/python-gpiozero/issues/154 -.. _#161: https://github.com/RPi-Distro/python-gpiozero/issues/161 -.. _#168: https://github.com/RPi-Distro/python-gpiozero/issues/168 -.. _#174: https://github.com/RPi-Distro/python-gpiozero/issues/174 -.. _#176: https://github.com/RPi-Distro/python-gpiozero/issues/176 +.. _#80: https://github.com/gpiozero/gpiozero/issues/80 +.. _#82: https://github.com/gpiozero/gpiozero/issues/82 +.. _#94: https://github.com/gpiozero/gpiozero/issues/94 +.. _#101: https://github.com/gpiozero/gpiozero/issues/101 +.. _#118: https://github.com/gpiozero/gpiozero/issues/118 +.. _#119: https://github.com/gpiozero/gpiozero/issues/119 +.. _#126: https://github.com/gpiozero/gpiozero/issues/126 +.. _#135: https://github.com/gpiozero/gpiozero/issues/135 +.. _#141: https://github.com/gpiozero/gpiozero/issues/141 +.. _#143: https://github.com/gpiozero/gpiozero/issues/143 +.. _#150: https://github.com/gpiozero/gpiozero/issues/150 +.. _#154: https://github.com/gpiozero/gpiozero/issues/154 +.. _#161: https://github.com/gpiozero/gpiozero/issues/161 +.. _#168: https://github.com/gpiozero/gpiozero/issues/168 +.. _#174: https://github.com/gpiozero/gpiozero/issues/174 +.. _#176: https://github.com/gpiozero/gpiozero/issues/176 Release 1.0.0 (2015-11-16) ========================== @@ -340,11 +433,11 @@ Release 1.0.0 (2015-11-16) * :class:`Motor` PWM devices exposed, and :class:`Robot` motor devices exposed (`#107`_) -.. _#44: https://github.com/RPi-Distro/python-gpiozero/issues/44 -.. _#58: https://github.com/RPi-Distro/python-gpiozero/issues/58 -.. _#93: https://github.com/RPi-Distro/python-gpiozero/issues/93 -.. _#75: https://github.com/RPi-Distro/python-gpiozero/issues/75 -.. _#107: https://github.com/RPi-Distro/python-gpiozero/issues/107 +.. _#44: https://github.com/gpiozero/gpiozero/issues/44 +.. _#58: https://github.com/gpiozero/gpiozero/issues/58 +.. _#93: https://github.com/gpiozero/gpiozero/issues/93 +.. _#75: https://github.com/gpiozero/gpiozero/issues/75 +.. _#107: https://github.com/gpiozero/gpiozero/issues/107 Release 0.9.0 (2015-10-25) ========================== @@ -355,9 +448,9 @@ Fourth public beta * Fix names of parameters in :class:`Motor` constructor (`#79`_) * Added wrappers for LED groups on add-on boards (`#81`_) -.. _#76: https://github.com/RPi-Distro/python-gpiozero/issues/76 -.. _#79: https://github.com/RPi-Distro/python-gpiozero/issues/79 -.. _#81: https://github.com/RPi-Distro/python-gpiozero/issues/81 +.. _#76: https://github.com/gpiozero/gpiozero/issues/76 +.. _#79: https://github.com/gpiozero/gpiozero/issues/79 +.. _#81: https://github.com/gpiozero/gpiozero/issues/81 Release 0.8.0 (2015-10-16) ========================== @@ -368,8 +461,8 @@ Third public beta for the :class:`MCP3008` and :class:`MCP3004` (`#41`_) * Fixed :meth:`DigitalOutputDevice.blink` (`#57`_) -.. _#41: https://github.com/RPi-Distro/python-gpiozero/issues/41 -.. _#57: https://github.com/RPi-Distro/python-gpiozero/issues/57 +.. _#41: https://github.com/gpiozero/gpiozero/issues/41 +.. _#57: https://github.com/gpiozero/gpiozero/issues/57 Release 0.7.0 (2015-10-09) ========================== diff --git a/docs/cli_env.rst b/docs/cli_env.rst new file mode 100644 index 000000000..4cfec6ed5 --- /dev/null +++ b/docs/cli_env.rst @@ -0,0 +1,38 @@ +.. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +.. +.. Copyright (c) 2023 Dave Jones +.. +.. SPDX-License-Identifier: BSD-3-Clause + +===================== +Environment Variables +===================== + +All utilities provided by GPIO Zero accept the following environment variables: + +.. envvar:: GPIOZERO_PIN_FACTORY + + The library to use when communicating with the GPIO pins. Defaults to + attempting to load lgpio, then RPi.GPIO, then pigpio, and finally uses a + native Python implementation. Valid values include "lgpio", "rpigpio", + "pigpio", "native", and "mock". The latter is most useful on non-Pi + platforms as it emulates a Raspberry Pi model 3B (by default). + +.. envvar:: PIGPIO_ADDR + + The hostname of the Raspberry Pi the pigpio library should attempt to + connect to (if the pigpio pin factory is being used). Defaults to + ``localhost``. + +.. envvar:: PIGPIO_PORT + + The port number the pigpio library should attempt to connect to (if the + pigpio pin factory is being used). Defaults to ``8888``. + + +.. only:: builder_man + + See Also + -------- + + :manpage:`pinout(1)`, :manpage:`pintest(1)` diff --git a/docs/cli_pinout.rst b/docs/cli_pinout.rst index 04f4c8887..0484e23eb 100644 --- a/docs/cli_pinout.rst +++ b/docs/cli_pinout.rst @@ -1,39 +1,22 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2017-2018 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. .. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2017-2018 Ben Nuttall .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause pinout ====== -.. image:: images/pinout_pi3.png - :align: center - :width: 537px +A utility for querying GPIO pin-out information. + +.. only:: not builder_man + + .. image:: images/pinout_pi3.png + :alt: A screenshot of the output from pinout. In a terminal window, a + description of the board is shown at the top, followed by a + colorful ASCII art rendition of the board, and finally a + color-matched list of pins on the GPIO header. Synopsis @@ -61,20 +44,21 @@ Options .. option:: -h, --help - show this help message and exit + Show a help message and exit .. option:: -r REVISION, --revision REVISION - RPi revision. Default is to autodetect revision of current device + Specifies a particular Raspberry Pi board revision code. The default is to + autodetect revision of current device by reading :file:`/proc/cpuinfo` .. option:: -c, --color Force colored output (by default, the output will include ANSI color codes - if run in a color-capable terminal). See also :option:`--monochrome` + if run in a color-capable terminal). See also :option:`pinout --monochrome` .. option:: -m, --monochrome - Force monochrome output. See also :option:`--color` + Force monochrome output. See also :option:`pinout --color` .. option:: -x, --xyz @@ -94,31 +78,32 @@ For a Raspberry Pi model 3B, this will output something like the following: .. code-block:: none - ,--------------------------------. - | oooooooooooooooooooo J8 +==== - | 1ooooooooooooooooooo | USB - | +==== - | Pi Model 3B V1.1 | - | +----+ +==== - | |D| |SoC | | USB - | |S| | | +==== - | |I| +----+ | - | |C| +====== - | |S| | Net - | pwr |HDMI| |I||A| +====== - `-| |--------| |----|V|-------' - + Description : Raspberry Pi 3B rev 1.2 Revision : a02082 SoC : BCM2837 - RAM : 1024Mb + RAM : 1GB Storage : MicroSD - USB ports : 4 (excluding power) - Ethernet ports : 1 + USB ports : 4 (of which 0 USB3) + Ethernet ports : 1 (100Mbps max. speed) Wi-fi : True Bluetooth : True Camera ports (CSI) : 1 Display ports (DSI): 1 + ,--------------------------------. + | oooooooooooooooooooo J8 +==== + | 1ooooooooooooooooooo | USB + | +==== + | Pi Model 3B V1.2 | + | |D +---+ +==== + | |S |SoC| | USB + | |I +---+ +==== + | |0 C| | + | S| +====== + | I| |A| | Net + | pwr |HDMI| 0| |u| +====== + `-| |------| |-----|x|--------' + J8: 3V3 (1) (2) 5V GPIO2 (3) (4) 5V @@ -141,6 +126,8 @@ For a Raspberry Pi model 3B, this will output something like the following: GPIO26 (37) (38) GPIO20 GND (39) (40) GPIO21 + For further information, please refer to https://pinout.xyz/ + By default, if stdout is a console that supports color, ANSI codes will be used to produce color output. Output can be forced to be :option:`--monochrome`: @@ -188,35 +175,13 @@ this case you'll almost certainly want to specify the Pi revision manually): $ GPIOZERO_PIN_FACTORY=mock pinout -r a22042 -Environment Variables ---------------------- - -.. envvar:: GPIOZERO_PIN_FACTORY - - The library to use when communicating with the GPIO pins. Defaults to - attempting to load RPi.GPIO, then RPIO, then pigpio, and finally uses a - native Python implementation. Valid values include "rpigpio", "rpio", - "pigpio", "native", and "mock". The latter is most useful on non-Pi - platforms as it emulates a Raspberry Pi model 3B (by default). - -.. envvar:: PIGPIO_ADDR - - The hostname of the Raspberry Pi the pigpio library should attempt to - connect to (if the pigpio pin factory is being used). Defaults to - ``localhost``. - -.. envvar:: PIGPIO_PORT - - The port number the pigpio library should attempt to connect to (if the - pigpio pin factory is being used). Defaults to ``8888``. - - .. only:: builder_man See Also -------- - :manpage:`remote-gpio(7)` + :manpage:`pintest(1)`, :manpage:`remote-gpio(7)`, + :manpage:`gpiozero-env(7)` .. _pinout.xyz: https://pinout.xyz/ -.. _revision codes: https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md +.. _revision codes: https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-revision-codes diff --git a/docs/cli_pintest.rst b/docs/cli_pintest.rst new file mode 100644 index 000000000..f8b539b92 --- /dev/null +++ b/docs/cli_pintest.rst @@ -0,0 +1,124 @@ +.. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +.. +.. Copyright (c) 2023 Dave Jones +.. +.. SPDX-License-Identifier: BSD-3-Clause + +pintest +======= + +A utility for testing the GPIO pins on a Raspberry Pi, inspired by pigpio's +gpiotest example script, and wiringPi's pintest utility. + +.. only:: not builder_man + + .. versionadded:: 2.0 + The :program:`pintest` utility. + + .. image:: images/pintest.png + :alt: A screenshot of the output from pintest. In a terminal window, + pintest has prompted the user with the list of GPIOs it intends + to test and asked for confirmation to proceed. Having received + this confirmation, it's printed out each GPIO in turn with "ok" + after it, indicating success. + + +Synopsis +-------- + +:: + + pintest [-h] [--version] [-p PINS] [-s SKIP] [-y] [-r REVISION] + + +Description +----------- + +A utility for testing the function of GPIOs on a Raspberry Pi. It is possible +to damage the GPIOs on a Pi by passing too much current (or voltage in the case +of inputs) through them. The :program:`pintest` utility can be used to +determine if any of the GPIOs on a Pi are broken. + +The utility will test all physically exposed GPIOs (those on the main GPIO +header) by default, but you may wish to only test a subset, or to exclude +certain GPIOs which can be accomplished with the :option:`pintest --pins` or +:option:`pintest --skip` options. + +.. note:: + + You must ensure that nothing is connected to the GPIOs that you intend to + test. By default, the utility will prompt you before proceeding, repeating + this warning. + +In the event that any GPIO is found to be faulty, it will be reported in the +output and the utility will exit with a return code of 1. If all specified +GPIOs test fine, the return code is zero. + + +Options +------- + +.. program:: pintest + +.. option:: -h, --help + + show this help message and exit + +.. option:: --version + + Show the program's version number and exit + +.. option:: -p PINS, --pins PINS + + The pin(s) to test. Can be specified as a comma-separated list of pins. Pin + numbers can be given in any form accepted by gpiozero, e.g. 14, GPIO14, + BOARD8. The default is to test all pins + +.. option:: -s SKIP, --skip SKIP + + The pin(s) to skip testing. Can be specified as comma-separated list of + pins. Pin numbers can be given in any form accepted by gpiozero, e.g. 14, + GPIO14, BOARD8. The default is to skip no pins + +.. option:: -y, --yes + + Proceed without prompting + +.. option:: -r REVISION, --revision REVISION + + Force board revision. Default is to autodetect revision of current device. + You should avoid this option unless you are very sure the detection is + incorrect + + +Examples +-------- + +Test all physically exposed GPIOs on the board: + +.. code-block:: console + + $ pintest + +Test just the I2C GPIOs without prompting: + +.. code-block:: console + + $ pintest --pins 2,3 --yes + +Exclude the SPI GPIOs from testing: + +.. code-block:: console + + $ pintest --exclude GPIO7,GPIO8,GPIO9,GPIO10,GPIO11 + +Note that pin numbers can be given in any form accepted by GPIO Zero, e.g. 14, +GPIO14, or BOARD8. + + +.. only:: builder_man + + See Also + -------- + + :manpage:`pinout(1)`, :manpage:`gpiozero-env(7)` diff --git a/docs/cli_tools.rst b/docs/cli_tools.rst index b71246311..bb01e37d1 100644 --- a/docs/cli_tools.rst +++ b/docs/cli_tools.rst @@ -1,32 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2016 Stewart -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2021 Ben Nuttall +.. Copyright (c) 2016 Stewart .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ================== Command-line Tools @@ -34,6 +12,13 @@ Command-line Tools The gpiozero package contains a database of information about the various revisions of Raspberry Pi. This is queried by the :program:`pinout` -command-line tool to output details of the GPIO pins available. +command-line tool to output details of the GPIO pins available. The +:program:`pintest` tool is also provided to test the operation of GPIO pins on +the board. + +.. toctree:: + :maxdepth: 1 -.. include:: cli_pinout.rst + cli_pinout + cli_pintest + cli_env diff --git a/docs/compat.rst b/docs/compat.rst new file mode 100644 index 000000000..9773941d1 --- /dev/null +++ b/docs/compat.rst @@ -0,0 +1,301 @@ +.. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +.. +.. Copyright (c) 2021-2023 Dave Jones +.. Copyright (c) 2023 Andrew Scheller +.. +.. SPDX-License-Identifier: BSD-3-Clause + +======================= +Backwards Compatibility +======================= + + +.. currentmodule:: gpiozero + +GPIO Zero 2.x is a new major version and thus backwards incompatible changes +can be expected. We have attempted to keep these as minimal as reasonably +possible while taking advantage of the opportunity to clean up things. This +chapter documents breaking changes from version 1.x of the library to 2.x, and +all deprecated functionality which will still work in release 2.0 but is +scheduled for removal in a future 2.x release. + + +Finding and fixing deprecated usage +=================================== + +As of release 2.0, all deprecated functionality will raise +:exc:`DeprecationWarning` when used. By default, the Python interpreter +suppresses these warnings (as they're only of interest to developers, not +users) but you can easily configure different behaviour. + +The following example script uses a number of deprecated functions:: + + import gpiozero + + board = gpiozero.pi_info() + for header in board.headers.values(): + for pin in header.pins.values(): + if pin.pull_up: + print(pin.function, 'is pulled up') + +Despite using deprecated functionality the script runs happily (and silently) +with gpiozero 2.0. To discover what deprecated functions are being used, we add +a couple of lines to tell the warnings module that we want "default" handling +of :exc:`DeprecationWarning`; "default" handling means that the first time an +attempt is made to raise this warning at a particular location, the warning's +details will be printed to the console. All future invocations from the same +location will be ignored. This saves flooding the console with warning details +from tight loops. With this change, the script looks like this:: + + import gpiozero + + import warnings + warnings.filterwarnings('default', category=DeprecationWarning) + + board = gpiozero.pi_info() + for header in board.headers.values(): + for pin in header.pins.values(): + if pin.pull_up: + print(pin.function, 'is pulled up') + +And produces the following output on the console when run: + +.. code-block:: text + + /home/dave/projects/home/gpiozero/gpio-zero/gpiozero/pins/__init__.py:899: + DeprecationWarning: PinInfo.pull_up is deprecated; please use PinInfo.pull + warnings.warn( + /home/dave/projects/home/gpiozero/gpio-zero/gpiozero/pins/__init__.py:889: + DeprecationWarning: PinInfo.function is deprecated; please use PinInfo.name + warnings.warn( + GPIO2 is pulled up + GPIO3 is pulled up + +This tells us which pieces of deprecated functionality are being used in our +script, but it doesn't tell us where in the script they were used. For this, +it is more useful to have warnings converted into full blown exceptions. With +this change, each time a :exc:`DeprecationWarning` would have been printed, it +will instead cause the script to terminate with an unhandled exception and a +full stack trace:: + + import gpiozero + + import warnings + warnings.filterwarnings('error', category=DeprecationWarning) + + board = gpiozero.pi_info() + for header in board.headers.values(): + for pin in header.pins.values(): + if pin.pull_up: + print(pin.function, 'is pulled up') + +Now when we run the script it produces the following: + +.. code-block:: pycon + + Traceback (most recent call last): + File "/home/dave/projects/home/gpiozero/gpio-zero/foo.py", line 9, in + if pin.pull_up: + File "/home/dave/projects/home/gpiozero/gpio-zero/gpiozero/pins/__init__.py", line 899, in pull_up + warnings.warn( + DeprecationWarning: PinInfo.pull_up is deprecated; please use PinInfo.pull + +This tells us that line 9 of our script is using deprecated functionality, and +provides a hint of how to fix it. We change line 9 to use the "pull" attribute +instead. Now we run again, and this time get the following: + +.. code-block:: pycon + + Traceback (most recent call last): + File "/home/dave/projects/home/gpiozero/gpio-zero/foo.py", line 10, in + print(pin.function, 'is pulled up') + File "/home/dave/projects/home/gpiozero/gpio-zero/gpiozero/pins/__init__.py", line 889, in function + warnings.warn( + DeprecationWarning: PinInfo.function is deprecated; please use PinInfo.name + +Now we can tell line 10 has a problem, and once again the exception tells us +how to fix it. We continue in this fashion until the script looks like this:: + + import gpiozero + + import warnings + warnings.filterwarnings('error', category=DeprecationWarning) + + board = gpiozero.pi_info() + for header in board.headers.values(): + for pin in header.pins.values(): + if pin.pull == 'up': + print(pin.name, 'is pulled up') + +The script now runs to completion, so we can be confident it's no longer using +any deprecated functionality and will run happily even when this functionality +is removed in a future 2.x release. At this point, you may wish to remove the +``filterwarnings`` line as well (or at least comment it out). + + +Python 2.x support dropped +========================== + +By far the biggest and most important change is that the Python 2.x series is +no longer supported (in practice, this means Python 2.7 is no longer +supported). If your code is not compatible with Python 3, you should follow the +`porting guide`_ in the `Python documentation`_. + +As of GPIO Zero 2.0, the lowest supported Python version will be 3.5. This base +version may advance with minor releases, but we will make a reasonable best +effort not to break compatibility with old Python 3.x versions, and to ensure +that GPIO Zero can run on the version of Python in Debian oldstable at the +time of its release. + + +RPIO pin factory removed +======================== + +The RPIO pin implementation is unsupported on the Raspberry Pi 2 onwards and +hence of little practical use these days. Anybody still relying on RPIO's +stable PWM implementation is advised to try the pigpio pin implementation +instead (also supported by GPIO Zero). + + +Deprecated pin-factory aliases removed +====================================== + +Several deprecated aliases for pin factories, which could be specified by the +:envvar:`GPIOZERO_PIN_FACTORY` environment variable, have been removed: + +* "PiGPIOPin" is removed in favour of "pigpio" + +* "RPiGPIOPin" is removed in favour of "rpigpio" + +* "NativePin" is removed in favour of "native" + +In other words, you can no longer use the following when invoking your +script: + +.. code-block:: console + + $ GPIOZERO_PIN_FACTORY=PiGPIOPin python3 my_script.py + +Instead, you should use the following: + +.. code-block:: console + + $ GPIOZERO_PIN_FACTORY=pigpio python3 my_script.py + + +Keyword arguments +================= + +Many classes in GPIO Zero 1.x were documented as having keyword-only arguments +in their constructors and methods. For example, the :class:`PiLiter` was +documented as having the constructor: ``PiLiter(*, pwm=False, +initial_value=False, pin_factory=None)`` implying that all its arguments were +keyword only. + +However, despite being documented in this manner, this was rarely enforced as +it was extremely difficult to do so under Python 2.x without complicating the +code-base considerably (Python 2.x lacked the "*" syntax to declare +keyword-only arguments; they could only be implemented via "\*\*kwargs" +arguments and dictionary manipulation). + +In GPIO Zero 2.0, all such arguments are now *actually* keyword arguments. If +your code complied with the 1.x documentation you shouldn't notice any +difference. In other words, the following is still fine:: + + from gpiozero import PiLiter + + l = PiLiter(pwm=True) + +However, if you had omitted the keyword you will need to modify your code:: + + from gpiozero import PiLiter + + l = PiLiter(True) # this will no longer work + + +Robots take Motors, and PhaseEnableRobot is deprecated +====================================================== + +The GPIO Zero 1.x API specified that a :class:`Robot` was constructed with two +tuples that were in turn used to construct two :class:`Motor` instances. The +2.x API replaces this with simply passing in the :class:`Motor`, or +:func:`PhaseEnableMotor` instances you wish to use as the left and right +wheels. + +If your current code uses pins 4 and 14 for the left wheel, and 17 and 18 for +the right wheel, it may look like this:: + + from gpiozero import Robot + + r = Robot(left=(4, 14), right=(17, 18)) + +This should be changed to the following:: + + from gpiozero import Robot, Motor + + r = Robot(left=Motor(4, 14), right=Motor(17, 18)) + +Likewise, if you are currently using :func:`PhaseEnableRobot` your code may +look like this:: + + from gpiozero import PhaseEnableRobot + + r = PhaseEnableRobot(left=(4, 14), right=(17, 18)) + +This should be changed to the following:: + + from gpiozero import Robot, PhaseEnableMotor + + r = Robot(left=PhaseEnableMotor(4, 14), + right=PhaseEnableMotor(17, 18)) + +This change came about because the :class:`Motor` class was also documented as +having two mandatory parameters (*forward* and *backward*) and several +keyword-only parameters, including the *enable* pin. However, *enable* was +treated as a positional argument for the sake of constructing :class:`Robot` +which was inconsistent. Furthermore, :func:`PhaseEnableRobot` was more or less +a redundant duplicate of :class:`Robot` but was lacking a couple of features +added to :class:`Robot` later (notable "curved" turning). + +Although the new API requires a little more typing, it does mean that phase +enable robot boards now inherit all the functionality of :class:`Robot` because +that's all they use. Theoretically you could also mix and match regular motors +and phase-enable motors although there's little sense in doing so. + +The former functionality (passing tuples to the :class:`Robot` constructor) +will remain as deprecated functionality for gpiozero 2.0, but will be removed +in a future 2.x release. :func:`PhaseEnableRobot` remains as a stub function +which simply returns a :class:`Robot` instance, but this will be removed in a +future 2.x release. + + +PiBoardInfo, HeaderInfo, PinInfo +================================ + +The :class:`PiBoardInfo` class, and the associated :class:`HeaderInfo` and +:class:`PinInfo` classes have undergone a major re-structuring. This is partly +because some of the prior terminology was confusing (e.g. the meaning of +:attr:`PinInfo.function` and :attr:`Pin.function` clashed), and partly because +with the addition of the "lgpio" factory it's entirely possible to use gpiozero +on non-Pi boards (although at present the :class:`pins.lgpio.LGPIOFactory` is +still written assuming it is only ever used on a Pi). + +As a result the following classes, methods, and attributes are deprecated +(not yet removed, but will be in a future release within the 2.x series): + +* :attr:`Factory.pi_info` is deprecated in favour of :attr:`Factory.board_info` + which returns a :class:`BoardInfo` instead of :class:`PiBoardInfo` (which is + now a subclass of the former). + +* :attr:`PinInfo.pull_up` is deprecated in favour of :attr:`PinInfo.pull`. + +* :attr:`PinInfo.function` is deprecated in favour of :attr:`PinInfo.name`. + +* :meth:`BoardInfo.physical_pins`, :meth:`BoardInfo.physical_pin`, and + :meth:`BoardInfo.pulled_up`, are all deprecated in favour of a combination of + the new :meth:`BoardInfo.find_pin` and the attributes mentioned above. + +* :attr:`PiPin.number` is deprecated in favour of :attr:`Pin.info.name`. + +.. _Python documentation: https://docs.python.org/3/ +.. _porting guide: https://docs.python.org/3/howto/pyporting.html diff --git a/docs/conf.py b/docs/conf.py index 3c692230e..4c823b96d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,153 +1,81 @@ #!/usr/bin/env python3 +# vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2016 Thijs Triemstra -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2015-2023 Dave Jones +# Copyright (c) 2016 Thijs Triemstra # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import sys +# SPDX-License-Identifier: BSD-3-Clause + import os +import configparser +from pathlib import Path from datetime import datetime -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -import setup as _setup - -# Mock out certain modules while building documentation -class Mock(object): - __all__ = [] - - def __init__(self, *args, **kw): - pass - - def __call__(self, *args, **kw): - return Mock() - - def __mul__(self, other): - return Mock() - def __and__(self, other): - return Mock() +on_rtd = os.environ.get('READTHEDOCS', '').lower() == 'true' +config = configparser.ConfigParser() +config.read([Path(__file__).parent / '..' / 'setup.cfg']) +info = config['metadata'] - def __bool__(self): - return False +# -- Project information ----------------------------------------------------- - def __nonzero__(self): - return False - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - else: - return Mock() - -sys.modules['RPi'] = Mock() -sys.modules['RPi.GPIO'] = sys.modules['RPi'].GPIO -sys.modules['RPIO'] = Mock() -sys.modules['RPIO.PWM'] = sys.modules['RPIO'].PWM -sys.modules['RPIO.Exceptions'] = sys.modules['RPIO'].Exceptions -sys.modules['pigpio'] = Mock() -sys.modules['w1thermsensor'] = Mock() -sys.modules['spidev'] = Mock() -sys.modules['colorzero'] = Mock() +project = info['name'] +author = info['author'] +copyright = f'2015-{datetime.now():%Y} {author}' +release = info['version'] +version = release # -- General configuration ------------------------------------------------ -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] +needs_sphinx = '4.0' +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.imgmath', +] +if on_rtd: + tags.add('rtd') + +root_doc = 'index' templates_path = ['_templates'] -source_suffix = '.rst' -#source_encoding = 'utf-8-sig' -master_doc = 'index' -project = _setup.__project__.title() -copyright = '2015-%s %s' % (datetime.now().year, _setup.__author__) -version = _setup.__version__ -release = _setup.__version__ -#language = None -#today_fmt = '%B %d, %Y' exclude_patterns = ['_build'] -highlight_language='python3' -#default_role = None -#add_function_parentheses = True -#add_module_names = True -#show_authors = False +highlight_language = 'python3' pygments_style = 'sphinx' -#modindex_common_prefix = [] -#keep_warnings = False # -- Autodoc configuration ------------------------------------------------ autodoc_member_order = 'groupwise' +autodoc_mock_imports = [ + 'RPi', + 'lgpio', + 'RPIO', + 'pigpio', + 'w1thermsensor', + 'spidev', + 'colorzero', +] # -- Intersphinx configuration -------------------------------------------- intersphinx_mapping = { - 'python': ('https://docs.python.org/3.5', None), + 'python': ('https://docs.python.org/3.9', None), 'picamera': ('https://picamera.readthedocs.io/en/latest', None), 'colorzero': ('https://colorzero.readthedocs.io/en/latest', None), - } +} # -- Options for HTML output ---------------------------------------------- -if on_rtd: - html_theme = 'sphinx_rtd_theme' - #html_theme_options = {} - #html_sidebars = {} -else: - html_theme = 'default' - #html_theme_options = {} - #html_sidebars = {} -html_title = '%s %s Documentation' % (project, version) -#html_theme_path = [] -#html_short_title = None -#html_logo = None -#html_favicon = None +html_theme = 'sphinx_rtd_theme' +html_title = f'{project} {version} Documentation' html_static_path = ['_static'] -#html_extra_path = [] -#html_last_updated_fmt = '%b %d, %Y' -#html_use_smartypants = True -#html_additional_pages = {} -#html_domain_indices = True -#html_use_index = True -#html_split_index = False -#html_show_sourcelink = True -#html_show_sphinx = True -#html_show_copyright = True -#html_use_opensearch = '' -#html_file_suffix = None -htmlhelp_basename = '%sdoc' % _setup.__project__ - -# Hack to make wide tables work properly in RTD -# See https://github.com/snide/sphinx_rtd_theme/issues/117 for details -#def setup(app): -# app.add_stylesheet('style_override.css') +manpages_url = 'https://manpages.debian.org/bookworm/{page}.{section}.en.html' # -- Options for LaTeX output --------------------------------------------- +latex_engine = 'xelatex' + latex_elements = { 'papersize': 'a4paper', 'pointsize': '10pt', @@ -156,47 +84,38 @@ def __getattr__(cls, name): latex_documents = [ ( - 'index', # source start file - '%s.tex' % _setup.__project__, # target filename - '%s Documentation' % project, # title - _setup.__author__, # author - 'manual', # documentclass - True, # documents ref'd from toctree only - ), + 'index', # source start file + project + '.tex', # target filename + html_title, # title + author, # author + 'manual', # documentclass + True, # documents ref'd from toctree only + ), ] -#latex_logo = None -#latex_use_parts = False latex_show_pagerefs = True latex_show_urls = 'footnote' -#latex_appendices = [] -#latex_domain_indices = True # -- Options for epub output ---------------------------------------------- -epub_basename = _setup.__project__ -#epub_theme = 'epub' -#epub_title = html_title -epub_author = _setup.__author__ -epub_identifier = 'https://gpiozero.readthedocs.io/' -#epub_tocdepth = 3 +epub_basename = project +epub_author = author +epub_identifier = f'https://{info["name"]}.readthedocs.io/' epub_show_urls = 'no' -#epub_use_index = True # -- Options for manual page output --------------------------------------- man_pages = [ - ('cli_pinout', 'pinout', 'GPIO Zero pinout tool', [_setup.__author__], 1), - ('remote_gpio', 'remote-gpio', 'GPIO Zero remote GPIO guide', [_setup.__author__], 7), + ('cli_pinout', 'pinout', 'GPIO Zero pinout tool', [info['author']], 1), + ('cli_pintest', 'pintest', 'GPIO Zero pintest tool', [info['author']], 1), + ('remote_gpio', 'remote-gpio', 'GPIO Zero remote GPIO guide', [info['author']], 7), + ('cli_env', 'gpiozero-env', 'GPIO Zero environment vars', [info['author']], 7), ] man_show_urls = True -# -- Options for Texinfo output ------------------------------------------- - -texinfo_documents = [] +# -- Options for linkcheck builder ---------------------------------------- -#texinfo_appendices = [] -#texinfo_domain_indices = True -#texinfo_show_urls = 'footnote' -#texinfo_no_detailmenu = False +linkcheck_retries = 3 +linkcheck_workers = 20 +linkcheck_anchors = True diff --git a/docs/contributing.rst b/docs/contributing.rst index b28f228bd..c055c76e4 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -1,33 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2016-2019 Dave Jones -.. Copyright (c) 2017 rgm -.. Copyright (c) 2016 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2016-2023 Dave Jones +.. Copyright (c) 2016-2021 Ben Nuttall +.. Copyright (c) 2017 rgm .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause .. _contributing: @@ -92,10 +69,10 @@ Python 2/3 ========== The library is 100% compatible with both Python 2.7 and Python 3 from version -3.2 onwards. We intend to drop Python 2 support in 2020 when Python 2 reaches -`end-of-life`_. +3.2 onwards. Since Python 2 is now past its `end-of-life`_, the 1.6.2 release +(2021-03-18) is the last to support Python 2. -.. _docs: https://github.com/RPi-Distro/python-gpiozero/tree/master/docs -.. _issue: https://github.com/RPi-Distro/python-gpiozero/issues +.. _docs: https://github.com/gpiozero/gpiozero/tree/master/docs +.. _issue: https://github.com/gpiozero/gpiozero/issues/new .. _end-of-life: http://legacy.python.org/dev/peps/pep-0373/ diff --git a/docs/development.rst b/docs/development.rst index 336b27e0c..8ec26f20c 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,33 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2018 Steveis -.. Copyright (c) 2018 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. .. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2018-2019 Ben Nuttall +.. Copyright (c) 2018 Steveis .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause =========== Development @@ -37,7 +14,7 @@ Development The main GitHub repository for the project can be found at: - https://github.com/RPi-Distro/python-gpiozero + https://github.com/gpiozero/gpiozero For anybody wishing to hack on the project, we recommend starting off by getting to grips with some simple device classes. Pick something like @@ -63,9 +40,8 @@ within a virtual Python environment: .. code-block:: console - $ sudo apt install lsb-release build-essential git git-core \ - exuberant-ctags virtualenvwrapper python-virtualenv python3-virtualenv \ - python-dev python3-dev + $ sudo apt install lsb-release build-essential git exuberant-ctags \ + virtualenvwrapper python3-virtualenv python3-dev After installing ``virtualenvwrapper`` you'll need to restart your shell before commands like :command:`mkvirtualenv` will operate correctly. Once you've @@ -74,11 +50,11 @@ restarted your shell, continue: .. code-block:: console $ cd - $ mkvirtualenv -p /usr/bin/python3 python-gpiozero - $ workon python-gpiozero - (python-gpiozero) $ git clone https://github.com/RPi-Distro/python-gpiozero.git - (python-gpiozero) $ cd python-gpiozero - (python-gpiozero) $ make develop + $ mkvirtualenv -p /usr/bin/python3 gpiozero + $ workon gpiozero + (gpiozero) $ git clone https://github.com/gpiozero/gpiozero.git + (gpiozero) $ cd gpiozero + (gpiozero) $ make develop You will likely wish to install one or more pin implementations within the virtual environment (if you don't, GPIO Zero will use the "native" pin @@ -87,7 +63,7 @@ like PWM): .. code-block:: console - (python-gpiozero) $ pip install rpi.gpio pigpio + (gpiozero) $ pip install rpi.gpio pigpio If you are working on SPI devices you may also wish to install the ``spidev`` package to provide hardware SPI capabilities (again, GPIO Zero will work @@ -96,25 +72,25 @@ instead which limits bandwidth): .. code-block:: console - (python-gpiozero) $ pip install spidev + (gpiozero) $ pip install spidev To pull the latest changes from git into your clone and update your installation: .. code-block:: console - $ workon python-gpiozero - (python-gpiozero) $ cd ~/python-gpiozero - (python-gpiozero) $ git pull - (python-gpiozero) $ make develop + $ workon gpiozero + (gpiozero) $ cd ~/gpiozero + (gpiozero) $ git pull + (gpiozero) $ make develop To remove your installation, destroy the sandbox and the clone: .. code-block:: console - (python-gpiozero) $ deactivate - $ rmvirtualenv python-gpiozero - $ rm -fr ~/python-gpiozero + (gpiozero) $ deactivate + $ rmvirtualenv gpiozero + $ rm -rf ~/gpiozero Building the docs @@ -128,16 +104,17 @@ command should install all required dependencies: .. code-block:: console $ sudo apt install texlive-latex-recommended texlive-latex-extra \ - texlive-fonts-recommended graphviz inkscape python-sphinx latexmk + texlive-fonts-recommended texlive-xetex graphviz inkscape \ + python3-sphinx python3-sphinx-rtd-theme latexmk xindy Once these are installed, you can use the "doc" target to build the documentation: .. code-block:: console - $ workon python-gpiozero - (python-gpiozero) $ cd ~/python-gpiozero - (python-gpiozero) $ make doc + $ workon gpiozero + (gpiozero) $ cd ~/gpiozero + (gpiozero) $ make doc The HTML output is written to :file:`build/html` while the PDF output goes to :file:`build/latex`. @@ -152,10 +129,10 @@ You'll also need to install some pip packages: .. code-block:: console - $ workon python-gpiozero - (python-gpiozero) $ pip install coverage mock pytest - (python-gpiozero) $ cd ~/python-gpiozero - (python-gpiozero) $ make test + $ workon gpiozero + (gpiozero) $ pip install coverage mock pytest + (gpiozero) $ cd ~/gpiozero + (gpiozero) $ make test The test suite expects pins 22 and 27 (by default) to be wired together in order to run the "real" pin tests. The pins used by the test suite can be @@ -200,3 +177,12 @@ report of coverage from all environments: .. _Dead Snakes PPA: https://launchpad.net/~deadsnakes/%2Barchive/ubuntu/ppa + + +Mock pins +========= + +The test suite largely depends on the existence of the mock pin factory +:class:`~gpiozero.pins.mock.MockFactory`, which is also useful for manual +testing, for example in the Python shell or another REPL. See the section on +:ref:`mock-pins` in the :doc:`api_pins` chapter for more information. diff --git a/docs/examples/bluedot_robot_1.py b/docs/examples/bluedot_robot_1.py index 1eaea5705..bc02eb1b4 100644 --- a/docs/examples/bluedot_robot_1.py +++ b/docs/examples/bluedot_robot_1.py @@ -1,9 +1,9 @@ from bluedot import BlueDot -from gpiozero import Robot +from gpiozero import Robot, Motor from signal import pause bd = BlueDot() -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) def move(pos): if pos.top: diff --git a/docs/examples/bluedot_robot_2.py b/docs/examples/bluedot_robot_2.py index 9052b1fe0..c55f6c754 100644 --- a/docs/examples/bluedot_robot_2.py +++ b/docs/examples/bluedot_robot_2.py @@ -1,4 +1,4 @@ -from gpiozero import Robot +from gpiozero import Robot, Motor from bluedot import BlueDot from signal import pause @@ -18,7 +18,7 @@ def drive(): else: yield (0, 0) -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) bd = BlueDot() robot.source = drive() diff --git a/docs/examples/button_camera_1.py b/docs/examples/button_camera_1.py index caa456944..a7bf9535c 100644 --- a/docs/examples/button_camera_1.py +++ b/docs/examples/button_camera_1.py @@ -7,8 +7,7 @@ camera = PiCamera() def capture(): - timestamp = datetime.now().isoformat() - camera.capture('/home/pi/%s.jpg' % timestamp) + camera.capture(f'/home/pi/{datetime.now():%Y-%m-%d-%H-%M-%S}.jpg') button.when_pressed = capture diff --git a/docs/examples/button_camera_2.py b/docs/examples/button_camera_2.py index beb5efd32..fa1f70d35 100644 --- a/docs/examples/button_camera_2.py +++ b/docs/examples/button_camera_2.py @@ -8,8 +8,7 @@ camera = PiCamera() def capture(): - timestamp = datetime.now().isoformat() - camera.capture('/home/pi/%s.jpg' % timestamp) + camera.capture(f'/home/pi/{datetime.now():%Y-%m-%d-%H-%M-%S}.jpg') left_button.when_pressed = camera.start_preview left_button.when_released = camera.stop_preview diff --git a/docs/examples/button_stop_motion.py b/docs/examples/button_stop_motion.py index 8111b482f..2d738a4f7 100644 --- a/docs/examples/button_stop_motion.py +++ b/docs/examples/button_stop_motion.py @@ -8,5 +8,5 @@ frame = 1 while True: button.wait_for_press() - camera.capture('/home/pi/frame%03d.jpg' % frame) + camera.capture(f'/home/pi/frame{frame:03d}.jpg') frame += 1 diff --git a/docs/examples/color_picker.py b/docs/examples/color_picker.py new file mode 100644 index 000000000..2d9748da5 --- /dev/null +++ b/docs/examples/color_picker.py @@ -0,0 +1,30 @@ +from threading import Event +from colorzero import Color +from gpiozero import RotaryEncoder, RGBLED, Button + +rotor = RotaryEncoder(16, 20, wrap=True, max_steps=180) +rotor.steps = -180 +led = RGBLED(22, 23, 24, active_high=False) +btn = Button(21, pull_up=False) +led.color = Color('#f00') +done = Event() + +def change_hue(): + # Scale the rotor steps (-180..180) to 0..1 + hue = (rotor.steps + 180) / 360 + led.color = Color(h=hue, s=1, v=1) + +def show_color(): + print(f'Hue {led.color.hue.deg:.1f}° = {led.color.html}') + +def stop_script(): + print('Exiting') + done.set() + +print('Select a color by turning the knob') +rotor.when_rotated = change_hue +print('Push the button to see the HTML code for the color') +btn.when_released = show_color +print('Hold the button to exit') +btn.when_held = stop_script +done.wait() diff --git a/docs/examples/internet_status_indicator.py b/docs/examples/internet_status_indicator.py index b85c8dbd2..608dcf7d5 100644 --- a/docs/examples/internet_status_indicator.py +++ b/docs/examples/internet_status_indicator.py @@ -7,8 +7,8 @@ google = PingServer('google.com') -green.source = google -green.source_delay = 60 +google.when_activated = green.on +google.when_deactivated = green.off red.source = negated(green) pause() diff --git a/docs/examples/led_bargraph_1.py b/docs/examples/led_bargraph_1.py index f43f322c6..96534b6a6 100644 --- a/docs/examples/led_bargraph_1.py +++ b/docs/examples/led_bargraph_1.py @@ -1,6 +1,5 @@ from gpiozero import LEDBarGraph from time import sleep -from __future__ import division # required for python 2 graph = LEDBarGraph(5, 6, 13, 19, 26, 20) diff --git a/docs/examples/led_bargraph_2.py b/docs/examples/led_bargraph_2.py index b3bf28a3b..954499064 100644 --- a/docs/examples/led_bargraph_2.py +++ b/docs/examples/led_bargraph_2.py @@ -1,6 +1,5 @@ from gpiozero import LEDBarGraph from time import sleep -from __future__ import division # required for python 2 graph = LEDBarGraph(5, 6, 13, 19, 26, pwm=True) diff --git a/docs/examples/led_button_remote_1.py b/docs/examples/led_button_remote_1.py index f905b8c04..6563f2776 100644 --- a/docs/examples/led_button_remote_1.py +++ b/docs/examples/led_button_remote_1.py @@ -1,4 +1,4 @@ -from gpiozero import LED +from gpiozero import Button, LED from gpiozero.pins.pigpio import PiGPIOFactory from signal import pause diff --git a/docs/examples/led_button_remote_2.py b/docs/examples/led_button_remote_2.py index 6c183dd1e..58e92c6ef 100644 --- a/docs/examples/led_button_remote_2.py +++ b/docs/examples/led_button_remote_2.py @@ -1,4 +1,4 @@ -from gpiozero import LED +from gpiozero import Button, LED from gpiozero.pins.pigpio import PiGPIOFactory from gpiozero.tools import all_values from signal import pause diff --git a/docs/examples/led_char_display.py b/docs/examples/led_char_display.py new file mode 100644 index 000000000..c07ab673a --- /dev/null +++ b/docs/examples/led_char_display.py @@ -0,0 +1,10 @@ +from gpiozero import LEDCharDisplay +from time import sleep + +display = LEDCharDisplay(21, 20, 16, 22, 23, 24, 12, dp=25) + +for char in '321GO': + display.value = char + sleep(1) + +display.off() diff --git a/docs/examples/led_char_source.py b/docs/examples/led_char_source.py new file mode 100644 index 000000000..254854fd0 --- /dev/null +++ b/docs/examples/led_char_source.py @@ -0,0 +1,8 @@ +from gpiozero import LEDCharDisplay +from signal import pause + +display = LEDCharDisplay(21, 20, 16, 22, 23, 24, 12, dp=25) +display.source_delay = 1 +display.source = '321GO ' + +pause() diff --git a/docs/examples/led_travis.py b/docs/examples/led_travis.py index 9251854b0..fc292d887 100644 --- a/docs/examples/led_travis.py +++ b/docs/examples/led_travis.py @@ -13,7 +13,7 @@ def build_passed(repo): red = LED(12) green = LED(16) -green.source = build_passed('RPi-Distro/python-gpiozero') +green.source = build_passed('gpiozero/gpiozero') green.source_delay = 60 * 5 # check every 5 minutes red.source = negated(green) diff --git a/docs/examples/mock_demo.py b/docs/examples/mock_demo.py deleted file mode 100644 index f2f1ba701..000000000 --- a/docs/examples/mock_demo.py +++ /dev/null @@ -1,28 +0,0 @@ -from gpiozero.pins.mock import MockFactory -from gpiozero import Device, Button, LED -from time import sleep - -# Set the default pin factory to a mock factory -Device.pin_factory = MockFactory() - -# Construct a couple of devices attached to mock pins 16 and 17, and link the -# devices -led = LED(17) -btn = Button(16) -led.source = btn - -# Here the button isn't "pushed" so the LED's value should be False -print(led.value) - -# Get a reference to mock pin 16 (used by the button) -btn_pin = Device.pin_factory.pin(16) - -# Drive the pin low (this is what would happen electrically when the button is -# pushed) -btn_pin.drive_low() -sleep(0.1) # give source some time to re-read the button state -print(led.value) - -btn_pin.drive_high() -sleep(0.1) -print(led.value) diff --git a/docs/examples/multi_room_doorbell.py b/docs/examples/multi_room_doorbell.py index 2437635c2..33a11696c 100644 --- a/docs/examples/multi_room_doorbell.py +++ b/docs/examples/multi_room_doorbell.py @@ -1,11 +1,12 @@ -from gpiozero import LEDBoard, MotionSensor +from gpiozero import Button, Buzzer from gpiozero.pins.pigpio import PiGPIOFactory from signal import pause ips = ['192.168.1.3', '192.168.1.4', '192.168.1.5', '192.168.1.6'] remotes = [PiGPIOFactory(host=ip) for ip in ips] -button = Button(17) # button on this pi +pin = 17 +button = Button(pin) # button on this pi buzzers = [Buzzer(pin, pin_factory=r) for r in remotes] # buzzers on remote pins for buzzer in buzzers: diff --git a/docs/examples/multichar_scroll.py b/docs/examples/multichar_scroll.py new file mode 100644 index 000000000..dd0d32d44 --- /dev/null +++ b/docs/examples/multichar_scroll.py @@ -0,0 +1,18 @@ +from itertools import cycle +from collections import deque +from gpiozero import LEDMultiCharDisplay +from signal import pause + +display = LEDMultiCharDisplay( + LEDCharDisplay(22, 23, 24, 25, 21, 20, 16, dp=12), 26, 19, 13, 6) + +def scroller(message, chars=4): + d = deque(maxlen=chars) + for c in cycle(message): + d.append(c) + if len(d) == chars: + yield ''.join(d) + +display.source_delay = 0.2 +display.source = scroller('GPIO 2ER0 ') +pause() diff --git a/docs/examples/remote_button_robot.py b/docs/examples/remote_button_robot.py index e16e726fd..00582a057 100644 --- a/docs/examples/remote_button_robot.py +++ b/docs/examples/remote_button_robot.py @@ -1,9 +1,10 @@ -from gpiozero import Button, Robot +from gpiozero import Button, Robot, Motor from gpiozero.pins.pigpio import PiGPIOFactory from signal import pause factory = PiGPIOFactory(host='192.168.1.17') -robot = Robot(left=(4, 14), right=(17, 18), pin_factory=factory) # remote pins +robot = Robot(left=Motor(4, 14), right=Motor(17, 18), + pin_factory=factory) # remote pins # local buttons left = Button(26) diff --git a/docs/examples/rgbled.py b/docs/examples/rgbled.py index 6455602f6..b80c9dc9d 100644 --- a/docs/examples/rgbled.py +++ b/docs/examples/rgbled.py @@ -1,6 +1,5 @@ from gpiozero import RGBLED from time import sleep -from __future__ import division # required for python 2 led = RGBLED(red=9, green=10, blue=11) diff --git a/docs/examples/robot_1.py b/docs/examples/robot_1.py index 8c0a79179..c61db1492 100644 --- a/docs/examples/robot_1.py +++ b/docs/examples/robot_1.py @@ -1,7 +1,7 @@ -from gpiozero import Robot +from gpiozero import Robot, Motor from time import sleep -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) for i in range(4): robot.forward() diff --git a/docs/examples/robot_2.py b/docs/examples/robot_2.py index 55107be19..ce6fd647a 100644 --- a/docs/examples/robot_2.py +++ b/docs/examples/robot_2.py @@ -1,8 +1,8 @@ -from gpiozero import Robot, DistanceSensor +from gpiozero import Robot, Motor, DistanceSensor from signal import pause sensor = DistanceSensor(23, 24, max_distance=1, threshold_distance=0.2) -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) sensor.when_in_range = robot.backward sensor.when_out_of_range = robot.stop diff --git a/docs/examples/robot_buttons_1.py b/docs/examples/robot_buttons_1.py index 58a285cb8..bf86e42df 100644 --- a/docs/examples/robot_buttons_1.py +++ b/docs/examples/robot_buttons_1.py @@ -1,7 +1,7 @@ -from gpiozero import Robot, Button +from gpiozero import Robot, Motor, Button from signal import pause -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) left = Button(26) right = Button(16) diff --git a/docs/examples/robot_buttons_2.py b/docs/examples/robot_buttons_2.py index 56241f395..c49ce911b 100644 --- a/docs/examples/robot_buttons_2.py +++ b/docs/examples/robot_buttons_2.py @@ -1,8 +1,8 @@ -from gpiozero import Button, Robot +from gpiozero import Button, Robot, Motor from time import sleep from signal import pause -robot = Robot((17, 18), (22, 23)) +robot = Robot(Motor(17, 18), Motor(22, 23)) left = Button(2) right = Button(3) diff --git a/docs/examples/robot_keyboard_1.py b/docs/examples/robot_keyboard_1.py index 3b03f6e17..186d40570 100644 --- a/docs/examples/robot_keyboard_1.py +++ b/docs/examples/robot_keyboard_1.py @@ -1,7 +1,7 @@ import curses -from gpiozero import Robot +from gpiozero import Robot, Motor -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) actions = { curses.KEY_UP: robot.forward, diff --git a/docs/examples/robot_keyboard_2.py b/docs/examples/robot_keyboard_2.py index 261dceb42..cb40c50a4 100644 --- a/docs/examples/robot_keyboard_2.py +++ b/docs/examples/robot_keyboard_2.py @@ -1,7 +1,7 @@ -from gpiozero import Robot +from gpiozero import Robot, Motor from evdev import InputDevice, list_devices, ecodes -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) # Get the list of available input devices devices = [InputDevice(device) for device in list_devices()] diff --git a/docs/examples/robot_motion_1.py b/docs/examples/robot_motion_1.py index 11ae2a416..e63dc017a 100644 --- a/docs/examples/robot_motion_1.py +++ b/docs/examples/robot_motion_1.py @@ -1,7 +1,7 @@ -from gpiozero import Robot, MotionSensor +from gpiozero import Robot, Motor, MotionSensor from signal import pause -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) pir = MotionSensor(5) pir.when_motion = robot.forward diff --git a/docs/examples/robot_motion_2.py b/docs/examples/robot_motion_2.py index e8182e3cb..2021a9a36 100644 --- a/docs/examples/robot_motion_2.py +++ b/docs/examples/robot_motion_2.py @@ -1,8 +1,8 @@ -from gpiozero import Robot, MotionSensor +from gpiozero import Robot, Motor, MotionSensor from gpiozero.tools import zip_values from signal import pause -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) pir = MotionSensor(5) robot.source = zip_values(pir, pir) diff --git a/docs/examples/robot_pots_1.py b/docs/examples/robot_pots_1.py index a37ab421e..7c5e0daa1 100644 --- a/docs/examples/robot_pots_1.py +++ b/docs/examples/robot_pots_1.py @@ -1,8 +1,8 @@ -from gpiozero import Robot, MCP3008 +from gpiozero import Robot, Motor, MCP3008 from gpiozero.tools import zip_values from signal import pause -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) left_pot = MCP3008(0) right_pot = MCP3008(1) diff --git a/docs/examples/robot_pots_2.py b/docs/examples/robot_pots_2.py index cd3bbd13b..2118ec933 100644 --- a/docs/examples/robot_pots_2.py +++ b/docs/examples/robot_pots_2.py @@ -1,8 +1,8 @@ -from gpiozero import Robot, MCP3008 +from gpiozero import Robot, Motor, MCP3008 from gpiozero.tools import scaled from signal import pause -robot = Robot(left=(4, 14), right=(17, 18)) +robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) left_pot = MCP3008(0) right_pot = MCP3008(1) diff --git a/docs/examples/servo_sweep.py b/docs/examples/servo_sweep.py index 16619a0d2..a93875b56 100644 --- a/docs/examples/servo_sweep.py +++ b/docs/examples/servo_sweep.py @@ -1,7 +1,10 @@ from gpiozero import Servo from gpiozero.tools import sin_values +from signal import pause servo = Servo(17) servo.source = sin_values() servo.source_delay = 0.1 + +pause() diff --git a/docs/examples/timed_heat_lamp.py b/docs/examples/timed_heat_lamp.py index 220fd4164..1591e671c 100644 --- a/docs/examples/timed_heat_lamp.py +++ b/docs/examples/timed_heat_lamp.py @@ -5,7 +5,7 @@ lamp = Energenie(1) daytime = TimeOfDay(time(8), time(20)) -lamp.source = daytime -lamp.source_delay = 60 +daytime.when_activated = lamp.on +daytime.when_deactivated = lamp.off pause() diff --git a/docs/faq.rst b/docs/faq.rst index ee5df5830..d65bd8802 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2017-2019 Ben Nuttall .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2017-2022 Ben Nuttall .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause .. _faq: @@ -49,9 +26,9 @@ The following script looks like it should turn an :class:`LED` on:: led = LED(17) led.on() -And it does, if you're using the Python (or IPython or IDLE) shell. However, -if you saved this script as a Python file and ran it, it would flash on -briefly, then the script would end and it would turn off. +And it does, if you're using the Python or IPython shell, or the IDLE, Thonny or +Mu editors. However, if you saved this script as a Python file and ran it, it +would flash on briefly, then the script would end and it would turn off. The following file includes an intentional :func:`~signal.pause` to keep the script alive:: @@ -81,6 +58,61 @@ events to be detected:: pause() +What's the difference between when_pressed, is_pressed and wait_for_press? +========================================================================== + +gpiozero provides a range of different approaches to reading input devices. +Sometimes you want to ask if a button's pressed, sometimes you want to do +something until it's pressed, and sometimes you want something to happen *when* +it's been pressed, regardless of what else is going on. + +In a simple example where the button is the only device in play, all of the +options would be equally effective. But as soon as you introduce an extra +element, like another GPIO device, you might need to choose the right approach +depending on your use case. + +* :attr:`~gpiozero.Button.is_pressed` is an attribute which reveals whether the + button is currently pressed by returning ``True`` or ``False``:: + + while True: + if btn.is_pressed: + print("Pressed") + else: + print("Not pressed") + +* :meth:`~gpiozero.Button.wait_for_press()` is a method which blocks the code from + continuing until the button is pressed. Also see + :meth:`~gpiozero.Button.wait_for_release()`:: + + while True: + print("Released. Waiting for press..") + btn.wait_for_press() + print("Pressed. Waiting for release...") + btn.wait_for_release() + +* :attr:`~gpiozero.Button.when_pressed` is an attribute which assigns a callback + function to the event of the button being pressed. Every time the button is + pressed, the callback function is executed in a separate thread. Also see + :attr:`~gpiozero.Button.when_released`:: + + def pressed(): + print("Pressed") + + def released(): + print("Released") + + btn.when_pressed = pressed + btn.when_released = released + +This pattern of options is common among many devices. All :doc:`input devices +` and :doc:`internal devices ` have ``is_active``, +``when_activated``, ``when_deactivated``, ``wait_for_active`` and +``wait_for_inactive``, and many provide aliases (such as "pressed" for +"activated"). + +Also see a more advanced approach in the :doc:`source_values` page. + + My event handler isn't being called =================================== @@ -114,11 +146,11 @@ and ``my_function()`` (the result of calling a function). .. note:: - Note that as of v1.5, setting a callback to ``None`` when it was previously - ``None`` will raise a :class:`CallbackSetToNone` warning, with the intention - of alerting users when callbacks are set to ``None`` accidentally. However, - if this is intentional, the warning can be suppressed. See the - :mod:`warnings` module for reference. + Note that as of v1.5, setting a callback to :data:`None` when it was + previously :data:`None` will raise a :class:`CallbackSetToNone` warning, + with the intention of alerting users when callbacks are set to :data:`None` + accidentally. However, if this is intentional, the warning can be + suppressed. See the :mod:`warnings` module for reference. .. _pinfactoryfallback-warnings: @@ -181,9 +213,9 @@ version of gpiozero is available in your Python environment like so: >>> from pkg_resources import require >>> require('gpiozero') - [gpiozero 1.5.0 (/usr/lib/python3/dist-packages)] + [gpiozero 1.6.2 (/usr/lib/python3/dist-packages)] >>> require('gpiozero')[0].version - '1.5.0' + '1.6.2' If you have multiple versions installed (e.g. from :command:`pip` and :command:`apt`) they will not show up in the list returned by the @@ -191,7 +223,7 @@ If you have multiple versions installed (e.g. from :command:`pip` and be the version that ``import gpiozero`` will import. If you receive the error "No module named pkg_resources", you need to install -:command:`pip`. This can be done with the following command in Raspbian: +:command:`pip`. This can be done with the following command in Raspberry Pi OS: .. code-block:: console @@ -268,6 +300,8 @@ This means that you can reuse the pin for another device, and that despite turning the LED on (and hence, the pin high), after calling :meth:`~Device.close` it is restored to its previous state (LED off, pin low). +Read more about :ref:`migrating_from_rpigpio`. + How do I use button.when_pressed and button.when_held together? =============================================================== @@ -392,8 +426,8 @@ more advanced projects. .. _get-pip: https://pip.pypa.io/en/stable/installing/ -.. _GitHub issues: https://github.com/RPi-Distro/python-gpiozero/issues -.. _commits: https://github.com/RPi-Distro/python-gpiozero/commits/master +.. _GitHub issues: https://github.com/gpiozero/gpiozero/issues +.. _commits: https://github.com/gpiozero/gpiozero/commits/master .. _Pygame Zero: https://pygame-zero.readthedocs.io/en/stable/ .. _NetworkZero: https://networkzero.readthedocs.io/en/latest/ .. _guizero: https://lawsie.github.io/guizero/ diff --git a/docs/images/7seg_multi.fzz b/docs/images/7seg_multi.fzz new file mode 100644 index 000000000..1de6500a4 Binary files /dev/null and b/docs/images/7seg_multi.fzz differ diff --git a/docs/images/7seg_multi_bb.pdf b/docs/images/7seg_multi_bb.pdf new file mode 100644 index 000000000..da52165a6 Binary files /dev/null and b/docs/images/7seg_multi_bb.pdf differ diff --git a/docs/images/7seg_multi_bb.png b/docs/images/7seg_multi_bb.png new file mode 100644 index 000000000..97b6f56a4 Binary files /dev/null and b/docs/images/7seg_multi_bb.png differ diff --git a/docs/images/7seg_multi_bb.svg b/docs/images/7seg_multi_bb.svg new file mode 100644 index 000000000..a4ddc35bd --- /dev/null +++ b/docs/images/7seg_multi_bb.svg @@ -0,0 +1,6283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + 5 + + + 5 + + + 10 + + + 10 + + + 15 + + + 15 + + + 20 + + + 20 + + + 25 + + + 25 + + + 30 + + + 30 + + + 35 + + + 35 + + + 40 + + + 40 + + + 45 + + + 45 + + + 50 + + + 50 + + + 55 + + + 55 + + + 60 + + + 60 + + + A + + + A + + + B + + + B + + + C + + + C + + + D + + + D + + + E + + + E + + + F + + + F + + + G + + + G + + + H + + + H + + + I + + + I + + + J + + + J + + + + + + + + + + + + + +Raspberry Pi Model 2 v1.1 + + +© Raspberry Pi 2014 + + + + + + + + + + +h + + +t + + +t + + +p + + +: + + +/ + + +/ + + +w + + +w + + +w + + +. + + +r + + +a + + +s + + +p + + +b + + +e + + +r + + +r + + +y + + +p + + +i + + +. + + +o + + +r + + +g + + + + + + + + + + + + + +Power + + + + + + + + + + + + + + + + +HDMI + + + + + + + + + + + + + + + + +Audio + + + + + + + + + + + + + + + + +USB 2x + + + + + + + + + + + + + + + + +USB 2x + + + + + + + + + + + + + + + + +ETHERNET + + + + + + + + + + + + + + + + +DSI (DISPLAY) + + + + + + + + + + + + + + + + +CSI (CAMERA) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GPIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/7seg_multi_schem.pdf b/docs/images/7seg_multi_schem.pdf new file mode 100644 index 000000000..e8aaad0ee Binary files /dev/null and b/docs/images/7seg_multi_schem.pdf differ diff --git a/docs/images/7seg_multi_schem.png b/docs/images/7seg_multi_schem.png new file mode 100644 index 000000000..87552038d Binary files /dev/null and b/docs/images/7seg_multi_schem.png differ diff --git a/docs/images/7seg_multi_schem.svg b/docs/images/7seg_multi_schem.svg new file mode 100644 index 000000000..d2d7c9fc7 --- /dev/null +++ b/docs/images/7seg_multi_schem.svg @@ -0,0 +1,1951 @@ + + + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + +7 SEGMENT + + +4DIGIT + + +YOUNGSUN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +A + + +B + + +C + + +D + + +E + + +F + + +G + + +DP + + +A + + +B + + +C + + +D + + +E + + +F + + +G + + +DP + + +A + + +B + + +C + + +D + + +E + + +F + + +G + + +DP + + +A + + +B + + +C + + +D + + +E + + +F + + +G + + +DP + + +COL + + +APOS + + + + + + + + +2 + + + + + + + + + + +DIG2 + + + + + + + + + + + + +6 + + + + + + + + + + +DIG3 + + + + + + + + + + + + +8 + + + + + + + + + + +DIG4 + + + + + + + + + + + + +4 + + + + + + + + + + +COL-A + + + + + + + + + + + + +10 + + + + + + + + + + +APOS-A + + + + + + + + + + + + +14 + + + + + + + + + + +A + + + + + + + + + + + + +16 + + + + + + + + + + +B + + + + + + + + + + + + +13 + + + + + + + + + + +C + + + + + + + + + + + + +3 + + + + + + + + + + +D + + + + + + + + + + + + +5 + + + + + + + + + + +E + + + + + + + + + + + + +11 + + + + + + + + + + +F + + + + + + + + + + + + +15 + + + + + + + + + + +G + + + + + + + + + + + + +7 + + + + + + + + + + +DP + + + + + + + + + + + + +12 + + + + + + + + + + +COL-C + + + + + + + + + + + + +9 + + + + + + + + + + +APOS-C + + + + + + + + + + + + +1 + + + + + + + + + + +DIG1 + + + + + + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + + + +1 + + + + + + +2 + + + + + + + + + + +3 + + + + + + + + + + + + + + + + + + + +1 + + + + + + +2 + + + + + + + + + + +3 + + + + + + + + + + + + + + + + + + + +1 + + + + + + +2 + + + + + + + + + + +3 + + + + + + + + + + + + + + + + + + + +1 + + + + + + +2 + + + + + + + + + + +3 + + + + + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + +2 + + +1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +G + + +P + + +I + + +O + + +1 + + +4 + + + + + +U + + +A + + +R + + +T + + +0 + + +_ + + +T + + +X + + +D + + + + + + + +G + + +P + + +I + + +O + + +1 + + +5 + + + + + +U + + +A + + +R + + +T + + +0 + + +_ + + +R + + +X + + +D + + + + +G + + +P + + +I + + +O + + +1 + + +8 + + + + + +P + + +C + + +M + + +_ + + +C + + +L + + +K + + + + +G + + +P + + +I + + +O + + +2 + + +3 + + + + +G + + +P + + +I + + +O + + +2 + + +4 + + + + +G + + +P + + +I + + +O + + +2 + + +5 + + + + +G + + +P + + +I + + +O + + +8 + + + + + +S + + +P + + +I + + +0 + + +_ + + +C + + +E + + +0 + + +_ + + +N + + + + +G + + +P + + +I + + +O + + +7 + + + + + +S + + +P + + +I + + +0 + + +_ + + +C + + +E + + +1 + + +_ + + +N + + + + +ID_SC I2C ID EEPROM + + + + +G + + +P + + +I + + +O + + +1 + + +2 + + + + +G + + +P + + +I + + +O + + +1 + + +6 + + + + +G + + +P + + +I + + +O + + +2 + + +0 + + + + +G + + +P + + +I + + +O + + +2 + + +1 + + + + +G + + +P + + +I + + +O + + +2 + + + + + +S + + +D + + +A + + +1 + + + + + +I + + +2 + + +C + + +G + + +P + + +I + + +O + + +3 + + + + + +S + + +C + + +L + + +1 + + + + + +I + + +2 + + +C + + +G + + +P + + +I + + +O + + +4 + + +G + + +P + + +I + + +O + + +1 + + +7 + + +G + + +P + + +I + + +O + + +2 + + +7 + + +G + + +P + + +I + + +O + + +2 + + +2 + + +GIPO10 SPI0_MOSI + + +G + + +P + + +I + + +O + + +9 + + + + + +S + + +P + + +I + + +0 + + +_ + + +M + + +I + + +S + + +O + + +G + + +P + + +I + + +O + + +1 + + +1 + + + + + +S + + +P + + +I + + +0 + + +_ + + +S + + +C + + +L + + +K + + +ID_SD I2C ID EEPROM + + +G + + +P + + +I + + +O + + +5 + + +G + + +P + + +I + + +O + + +5 + + +G + + +P + + +I + + +O + + +6 + + +G + + +P + + +I + + +O + + +1 + + +3 + + +G + + +P + + +I + + +O + + +1 + + +9 + + +G + + +P + + +I + + +O + + +2 + + +6 + + +GND + + +3V3 + + +5V + + + +RaspberryPi + + +Model 2 v1.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +R1-R8330ΩQ1-Q42N 3904R9-R1233kΩ \ No newline at end of file diff --git a/docs/images/button_bb.pdf b/docs/images/button_bb.pdf index 22b5a2474..ef43a6a86 100644 Binary files a/docs/images/button_bb.pdf and b/docs/images/button_bb.pdf differ diff --git a/docs/images/button_bb.png b/docs/images/button_bb.png index db2607e27..0cdb58a06 100644 Binary files a/docs/images/button_bb.png and b/docs/images/button_bb.png differ diff --git a/docs/images/button_robot_bb.pdf b/docs/images/button_robot_bb.pdf index 67945367d..45c0e3eb9 100644 Binary files a/docs/images/button_robot_bb.pdf and b/docs/images/button_robot_bb.pdf differ diff --git a/docs/images/button_robot_bb.png b/docs/images/button_robot_bb.png index db99f2361..72e486b3a 100644 Binary files a/docs/images/button_robot_bb.png and b/docs/images/button_robot_bb.png differ diff --git a/docs/images/class_graph b/docs/images/class_graph deleted file mode 100755 index a38dd1a81..000000000 --- a/docs/images/class_graph +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/python3 - -import re -import sys -import argparse -from pathlib import Path - - -ABSTRACT_CLASSES = { - 'Device', - 'GPIODevice', - 'SmoothedInputDevice', - 'AnalogInputDevice', - 'MCP3xxx', - 'MCP3xx2', - 'MCP30xx', - 'MCP32xx', - 'MCP33xx', - 'SPIDevice', - 'CompositeDevice', - 'CompositeOutputDevice', - 'LEDCollection', - 'InternalDevice', -} - -OMIT_CLASSES = { - 'object', - 'GPIOBase', - 'GPIOMeta', - 'frozendict', - 'WeakMethod', - '_EnergenieMaster', -} - - -def main(args=None): - """ - A simple application for generating GPIO Zero's charts. Specify the root - class to generate with -i (multiple roots can be specified). Specify parts - of the hierarchy to exclude with -x. Output is in a format suitable for - feeding to graphviz's dot application. - """ - if args is None: - args = sys.argv[1:] - my_path = Path(__file__).parent - default_path = my_path / '..' / '..' / 'gpiozero' - default_path = default_path.resolve() - parser = argparse.ArgumentParser(description=main.__doc__) - parser.add_argument('-p', '--path', action='append', metavar='PATH', - default=[], help= - "search under PATH for Python source files; can be " - "specified multiple times, defaults to %s" % default_path) - parser.add_argument('-i', '--include', action='append', metavar='CLASS', - default=[], help= - "only include classes which have BASE somewhere in " - "their ancestry; can be specified multiple times") - parser.add_argument('-x', '--exclude', action='append', metavar='CLASS', - default=[], help= - "exclude any classes which have BASE somewhere in " - "their ancestry; can be specified multiple times") - parser.add_argument('-o', '--omit', action='append', metavar='CLASS', - default=[], help="omit the specified class, but not " - "its descendents from the chart; can be specified " - "multiple times") - parser.add_argument('output', nargs='?', type=argparse.FileType('w'), - default=sys.stdout, help= - "the file to write the output to; defaults to stdout") - args = parser.parse_args(args) - if not args.path: - args.path = [str(default_path)] - - m = make_class_map(set(args.path), OMIT_CLASSES | set(args.omit)) - if args.include or args.exclude: - m = filter_map(m, include_roots=set(args.include), exclude_roots=set(args.exclude)) - args.output.write(render_map(m, ABSTRACT_CLASSES)) - - -def make_class_map(search_paths, omit): - """ - Find all Python source files under *search_paths*, extract (via a crude - regex) all class definitions and return a mapping of class-name to the list - of base classes. - - All classes listed in *omit* will be excluded from the result, but not - their descendents (useful for excluding "object" etc.) - """ - def find_classes(): - class_re = re.compile( - r'^class\s+(?P\w+)\s*(?:\((?P.*)\))?:', re.MULTILINE) - for path in search_paths: - for py_file in Path(path).rglob('*.py'): - with py_file.open() as f: - for match in class_re.finditer(f.read()): - if match.group('name') not in omit: - yield match.group('name'), { - base.strip() - for base in (match.group('bases') or '').split(',') - if base.strip() not in omit - } - return { - name: bases - for name, bases in find_classes() - } - - -def filter_map(class_map, include_roots, exclude_roots): - """ - Returns *class_map* (which is a mapping such as that returned by - :func:`make_class_map`), with only those classes which have at least one - of the *include_roots* in their ancestry, and none of the *exclude_roots*. - """ - def has_parent(cls, parent): - return cls == parent or any( - has_parent(base, parent) for base in class_map.get(cls, ())) - - filtered = { - name: bases - for name, bases in class_map.items() - if (not include_roots or any(has_parent(name, root) for root in include_roots)) - and not any(has_parent(name, root) for root in exclude_roots) - } - pure_bases = { - base for name, bases in filtered.items() for base in bases - } - set(filtered) - # Make a second pass to fill in missing links between classes that are - # only included as bases of other classes - for base in pure_bases: - filtered[base] = pure_bases & class_map[base] - return filtered - - -def render_map(class_map, abstract): - """ - Renders *class_map* (which is a mapping such as that returned by - :func:`make_class_map`) to graphviz's dot language. - - The *abstract* sequence determines which classes will be rendered lighter - to indicate their abstract nature. All classes with names ending "Mixin" - will be implicitly rendered in a different style. - """ - def all_names(class_map): - for name, bases in class_map.items(): - yield name - for base in bases: - yield base - - template = """\ -digraph classes {{ - graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Sans, fontsize=10]; - edge []; - - /* Mixin classes */ - node [color="#c69ee0", fontcolor="#000000"] - - {mixin_nodes} - - /* Abstract classes */ - node [color="#9ec6e0", fontcolor="#000000"] - - {abstract_nodes} - - /* Concrete classes */ - node [color="#2980b9", fontcolor="#ffffff"]; - - {edges} -}} -""" - - return template.format( - mixin_nodes='\n '.join( - '{name};'.format(name=name) - for name in sorted(set(all_names(class_map))) - if name.endswith('Mixin') - ), - abstract_nodes='\n '.join( - '{name};'.format(name=name) - for name in sorted(abstract & set(all_names(class_map))) - ), - edges='\n '.join( - '{name}->{base};'.format(name=name, base=base) - for name, bases in sorted(class_map.items()) - for base in sorted(bases) - ), - ) - - -if __name__ == '__main__': - main() diff --git a/docs/images/color_picker.fzz b/docs/images/color_picker.fzz new file mode 100644 index 000000000..66438efd4 Binary files /dev/null and b/docs/images/color_picker.fzz differ diff --git a/docs/images/color_picker_bb.pdf b/docs/images/color_picker_bb.pdf new file mode 100644 index 000000000..f7a85dd31 Binary files /dev/null and b/docs/images/color_picker_bb.pdf differ diff --git a/docs/images/color_picker_bb.png b/docs/images/color_picker_bb.png new file mode 100644 index 000000000..bca4e1419 Binary files /dev/null and b/docs/images/color_picker_bb.png differ diff --git a/docs/images/color_picker_bb.svg b/docs/images/color_picker_bb.svg new file mode 100644 index 000000000..77f669965 --- /dev/null +++ b/docs/images/color_picker_bb.svg @@ -0,0 +1,4632 @@ + + + + + + + + + + + + + + + + + + + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + 5 + + + 5 + + + 10 + + + 10 + + + 15 + + + 15 + + + 20 + + + 20 + + + 25 + + + 25 + + + 30 + + + 30 + + + A + + + A + + + B + + + B + + + C + + + C + + + D + + + D + + + E + + + E + + + F + + + F + + + G + + + G + + + H + + + H + + + I + + + I + + + J + + + J + + + + + + + + + + + + + +Raspberry Pi Model 2 v1.1 + + +© Raspberry Pi 2014 + + + + + + + + + + +h + + +t + + +t + + +p + + +: + + +/ + + +/ + + +w + + +w + + +w + + +. + + +r + + +a + + +s + + +p + + +b + + +e + + +r + + +r + + +y + + +p + + +i + + +. + + +o + + +r + + +g + + + + + + + + + + + + + +Power + + + + + + + + + + + + + + + + +HDMI + + + + + + + + + + + + + + + + +Audio + + + + + + + + + + + + + + + + +USB 2x + + + + + + + + + + + + + + + + +USB 2x + + + + + + + + + + + + + + + + +ETHERNET + + + + + + + + + + + + + + + + +DSI (DISPLAY) + + + + + + + + + + + + + + + + +CSI (CAMERA) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GPIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/composed_devices.dot b/docs/images/composed_devices.dot index 3c56d7d97..1e41552ad 100644 --- a/docs/images/composed_devices.dot +++ b/docs/images/composed_devices.dot @@ -11,6 +11,11 @@ digraph classes { LEDBoard->PWMLED; LEDBarGraph->LED; LEDBarGraph->PWMLED; + LEDCharDisplay->LED; + LEDCharDisplay->PWMLED; + + LEDMultiCharDisplay->LEDCharDisplay; + LEDMultiCharDisplay->DigitalOutputDevice; ButtonBoard->Button; @@ -34,4 +39,6 @@ digraph classes { PhaseEnableMotor->PWMOutputDevice; Servo->PWMOutputDevice; + + RotaryEncoder->InputDevice; } diff --git a/docs/images/composed_devices.pdf b/docs/images/composed_devices.pdf index 605df6268..016de0211 100644 Binary files a/docs/images/composed_devices.pdf and b/docs/images/composed_devices.pdf differ diff --git a/docs/images/composed_devices.png b/docs/images/composed_devices.png index 8ef1a88e7..3cd11fa7a 100644 Binary files a/docs/images/composed_devices.png and b/docs/images/composed_devices.png differ diff --git a/docs/images/composed_devices.svg b/docs/images/composed_devices.svg index cfb137653..56a7d3468 100644 --- a/docs/images/composed_devices.svg +++ b/docs/images/composed_devices.svg @@ -1,223 +1,319 @@ - - - + + classes - + -RGBLED - -RGBLED + +RGBLED + +RGBLED -LED - -LED + +LED + +LED -RGBLED->LED - - + +RGBLED->LED + + -PWMLED - -PWMLED + +PWMLED + +PWMLED -RGBLED->PWMLED - - + +RGBLED->PWMLED + + -LEDBoard - -LEDBoard + +LEDBoard + +LEDBoard -LEDBoard->LED - - + +LEDBoard->LED + + -LEDBoard->PWMLED - - + +LEDBoard->PWMLED + + -LEDBarGraph - -LEDBarGraph + +LEDBarGraph + +LEDBarGraph -LEDBarGraph->LED - - + +LEDBarGraph->LED + + -LEDBarGraph->PWMLED - - + +LEDBarGraph->PWMLED + + + + + +LEDCharDisplay + +LEDCharDisplay + + + +LEDCharDisplay->LED + + + + + +LEDCharDisplay->PWMLED + + + + + +LEDMultiCharDisplay + +LEDMultiCharDisplay + + + +LEDMultiCharDisplay->LEDCharDisplay + + + + + +DigitalOutputDevice + +DigitalOutputDevice + + + +LEDMultiCharDisplay->DigitalOutputDevice + + -ButtonBoard - -ButtonBoard + +ButtonBoard + +ButtonBoard -Button - -Button + +Button + +Button -ButtonBoard->Button - - + +ButtonBoard->Button + + -TrafficLightsBuzzer - -TrafficLightsBuzzer + +TrafficLightsBuzzer + +TrafficLightsBuzzer -TrafficLightsBuzzer->Button - - + +TrafficLightsBuzzer->Button + + -TrafficLights - -TrafficLights + +TrafficLights + +TrafficLights -TrafficLightsBuzzer->TrafficLights - - + +TrafficLightsBuzzer->TrafficLights + + -Buzzer - -Buzzer + +Buzzer + +Buzzer -TrafficLightsBuzzer->Buzzer - - + +TrafficLightsBuzzer->Buzzer + + -StatusBoard - -StatusBoard + +StatusBoard + +StatusBoard -StatusBoard->LEDBoard - - + +StatusBoard->LEDBoard + + -StatusBoard->Button - - + +StatusBoard->Button + + -JamHat - -JamHat + +JamHat + +JamHat -JamHat->LEDBoard - - + +JamHat->LEDBoard + + -JamHat->Button - - + +JamHat->Button + + -TonalBuzzer - -TonalBuzzer + +TonalBuzzer + +TonalBuzzer -JamHat->TonalBuzzer - - + +JamHat->TonalBuzzer + + -Robot - -Robot + +Robot + +Robot -Motor - -Motor + +Motor + +Motor -Robot->Motor - - - - -DigitalOutputDevice - -DigitalOutputDevice + +Robot->Motor + + -Motor->DigitalOutputDevice - - + +Motor->DigitalOutputDevice + + -PWMOutputDevice - -PWMOutputDevice + +PWMOutputDevice + +PWMOutputDevice -Motor->PWMOutputDevice - - + +Motor->PWMOutputDevice + + -PhaseEnableRobot - -PhaseEnableRobot + +PhaseEnableRobot + +PhaseEnableRobot -PhaseEnableMotor - -PhaseEnableMotor + +PhaseEnableMotor + +PhaseEnableMotor -PhaseEnableRobot->PhaseEnableMotor - - + +PhaseEnableRobot->PhaseEnableMotor + + -PhaseEnableMotor->DigitalOutputDevice - - + +PhaseEnableMotor->DigitalOutputDevice + + -PhaseEnableMotor->PWMOutputDevice - - + +PhaseEnableMotor->PWMOutputDevice + + -Servo - -Servo + +Servo + +Servo -Servo->PWMOutputDevice - - + +Servo->PWMOutputDevice + + + + + +RotaryEncoder + +RotaryEncoder + + + +InputDevice + +InputDevice + + + +RotaryEncoder->InputDevice + + diff --git a/docs/images/composite_device_hierarchy.dot b/docs/images/composite_device_hierarchy.dot index 7e2baa48c..5b8e3fdb6 100644 --- a/docs/images/composite_device_hierarchy.dot +++ b/docs/images/composite_device_hierarchy.dot @@ -1,11 +1,14 @@ digraph classes { graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Arial, fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; /* Mixin classes */ node [color="#c69ee0", fontcolor="#000000"] + EventsMixin; + HoldMixin; + /* Abstract classes */ node [color="#9ec6e0", fontcolor="#000000"] @@ -18,30 +21,38 @@ digraph classes { node [color="#2980b9", fontcolor="#ffffff"]; ButtonBoard->CompositeDevice; + ButtonBoard->HoldMixin; CamJamKitRobot->Robot; CompositeDevice->Device; CompositeOutputDevice->CompositeDevice; Energenie->Device; - FishDish->TrafficLightsBuzzer; + FishDish->CompositeOutputDevice; + HoldMixin->EventsMixin; JamHat->CompositeOutputDevice; LEDBarGraph->LEDCollection; LEDBoard->LEDCollection; + LEDCharDisplay->LEDCollection; LEDCollection->CompositeOutputDevice; + LEDMultiCharDisplay->CompositeOutputDevice; PhaseEnableRobot->CompositeDevice; PiHutXmasTree->LEDBoard; PiLiter->LEDBoard; PiLiterBarGraph->LEDBarGraph; PiStop->TrafficLights; PiTraffic->TrafficLights; + Pibrella->CompositeOutputDevice; PololuDRV8835Robot->PhaseEnableRobot; PumpkinPi->LEDBoard; Robot->CompositeDevice; + RotaryEncoder->CompositeDevice; + RotaryEncoder->EventsMixin; RyanteckRobot->Robot; SnowPi->LEDBoard; StatusBoard->CompositeOutputDevice; StatusZero->LEDBoard; TonalBuzzer->CompositeDevice; - TrafficHat->TrafficLightsBuzzer; + TrafficHat->CompositeOutputDevice; TrafficLights->LEDBoard; TrafficLightsBuzzer->CompositeOutputDevice; + TrafficpHat->TrafficLights; } diff --git a/docs/images/composite_device_hierarchy.pdf b/docs/images/composite_device_hierarchy.pdf index 40a57afb6..7c09537eb 100644 Binary files a/docs/images/composite_device_hierarchy.pdf and b/docs/images/composite_device_hierarchy.pdf differ diff --git a/docs/images/composite_device_hierarchy.png b/docs/images/composite_device_hierarchy.png index 0a9312653..0f06f59b1 100644 Binary files a/docs/images/composite_device_hierarchy.png and b/docs/images/composite_device_hierarchy.png differ diff --git a/docs/images/composite_device_hierarchy.svg b/docs/images/composite_device_hierarchy.svg index cc9d78356..8f31d6721 100644 --- a/docs/images/composite_device_hierarchy.svg +++ b/docs/images/composite_device_hierarchy.svg @@ -1,288 +1,433 @@ - - - + + classes - + + + +EventsMixin + +EventsMixin + + + +HoldMixin + +HoldMixin + + + +HoldMixin->EventsMixin + + + -CompositeDevice - -CompositeDevice + +CompositeDevice + +CompositeDevice -Device - -Device + +Device + +Device -CompositeDevice->Device - - + +CompositeDevice->Device + + -CompositeOutputDevice - -CompositeOutputDevice + +CompositeOutputDevice + +CompositeOutputDevice -CompositeOutputDevice->CompositeDevice - - + +CompositeOutputDevice->CompositeDevice + + -LEDCollection - -LEDCollection + +LEDCollection + +LEDCollection -LEDCollection->CompositeOutputDevice - - + +LEDCollection->CompositeOutputDevice + + -ButtonBoard - -ButtonBoard + +ButtonBoard + +ButtonBoard + + + +ButtonBoard->HoldMixin + + -ButtonBoard->CompositeDevice - - + +ButtonBoard->CompositeDevice + + -CamJamKitRobot - -CamJamKitRobot + +CamJamKitRobot + +CamJamKitRobot -Robot - -Robot + +Robot + +Robot -CamJamKitRobot->Robot - - + +CamJamKitRobot->Robot + + -Robot->CompositeDevice - - + +Robot->CompositeDevice + + -Energenie - -Energenie + +Energenie + +Energenie -Energenie->Device - - + +Energenie->Device + + -FishDish - -FishDish - - -TrafficLightsBuzzer - -TrafficLightsBuzzer - - -FishDish->TrafficLightsBuzzer - - + +FishDish + +FishDish - -TrafficLightsBuzzer->CompositeOutputDevice - - + + +FishDish->CompositeOutputDevice + + -JamHat - -JamHat + +JamHat + +JamHat -JamHat->CompositeOutputDevice - - + +JamHat->CompositeOutputDevice + + -LEDBarGraph - -LEDBarGraph + +LEDBarGraph + +LEDBarGraph -LEDBarGraph->LEDCollection - - + +LEDBarGraph->LEDCollection + + -LEDBoard - -LEDBoard + +LEDBoard + +LEDBoard -LEDBoard->LEDCollection - - + +LEDBoard->LEDCollection + + + + + +LEDCharDisplay + +LEDCharDisplay + + + +LEDCharDisplay->LEDCollection + + + + + +LEDMultiCharDisplay + +LEDMultiCharDisplay + + + +LEDMultiCharDisplay->CompositeOutputDevice + + -PhaseEnableRobot - -PhaseEnableRobot + +PhaseEnableRobot + +PhaseEnableRobot -PhaseEnableRobot->CompositeDevice - - + +PhaseEnableRobot->CompositeDevice + + -PiHutXmasTree - -PiHutXmasTree + +PiHutXmasTree + +PiHutXmasTree -PiHutXmasTree->LEDBoard - - + +PiHutXmasTree->LEDBoard + + -PiLiter - -PiLiter + +PiLiter + +PiLiter -PiLiter->LEDBoard - - + +PiLiter->LEDBoard + + -PiLiterBarGraph - -PiLiterBarGraph + +PiLiterBarGraph + +PiLiterBarGraph -PiLiterBarGraph->LEDBarGraph - - + +PiLiterBarGraph->LEDBarGraph + + -PiStop - -PiStop + +PiStop + +PiStop -TrafficLights - -TrafficLights + +TrafficLights + +TrafficLights -PiStop->TrafficLights - - + +PiStop->TrafficLights + + -TrafficLights->LEDBoard - - + +TrafficLights->LEDBoard + + -PiTraffic - -PiTraffic + +PiTraffic + +PiTraffic -PiTraffic->TrafficLights - - + +PiTraffic->TrafficLights + + + + + +Pibrella + +Pibrella + + + +Pibrella->CompositeOutputDevice + + -PololuDRV8835Robot - -PololuDRV8835Robot + +PololuDRV8835Robot + +PololuDRV8835Robot -PololuDRV8835Robot->PhaseEnableRobot - - + +PololuDRV8835Robot->PhaseEnableRobot + + -PumpkinPi - -PumpkinPi + +PumpkinPi + +PumpkinPi -PumpkinPi->LEDBoard - - + +PumpkinPi->LEDBoard + + + + + +RotaryEncoder + +RotaryEncoder + + + +RotaryEncoder->EventsMixin + + + + + +RotaryEncoder->CompositeDevice + + -RyanteckRobot - -RyanteckRobot + +RyanteckRobot + +RyanteckRobot -RyanteckRobot->Robot - - + +RyanteckRobot->Robot + + -SnowPi - -SnowPi + +SnowPi + +SnowPi -SnowPi->LEDBoard - - + +SnowPi->LEDBoard + + -StatusBoard - -StatusBoard + +StatusBoard + +StatusBoard -StatusBoard->CompositeOutputDevice - - + +StatusBoard->CompositeOutputDevice + + -StatusZero - -StatusZero + +StatusZero + +StatusZero -StatusZero->LEDBoard - - + +StatusZero->LEDBoard + + -TonalBuzzer - -TonalBuzzer + +TonalBuzzer + +TonalBuzzer -TonalBuzzer->CompositeDevice - - + +TonalBuzzer->CompositeDevice + + -TrafficHat - -TrafficHat - - -TrafficHat->TrafficLightsBuzzer - - + +TrafficHat + +TrafficHat + + + +TrafficHat->CompositeOutputDevice + + + + + +TrafficLightsBuzzer + +TrafficLightsBuzzer + + + +TrafficLightsBuzzer->CompositeOutputDevice + + + + + +TrafficpHat + +TrafficpHat + + + +TrafficpHat->TrafficLights + + diff --git a/docs/images/device_hierarchy.dot b/docs/images/device_hierarchy.dot index 14c48bd01..82d80c47c 100644 --- a/docs/images/device_hierarchy.dot +++ b/docs/images/device_hierarchy.dot @@ -1,6 +1,6 @@ digraph classes { graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Arial, fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; /* Mixin classes */ @@ -40,7 +40,7 @@ digraph classes { ButtonBoard->CompositeDevice; ButtonBoard->HoldMixin; Buzzer->DigitalOutputDevice; - CPUTemperature->InternalDevice; + CPUTemperature->PolledInternalDevice; CamJamKitRobot->Robot; CompositeDevice->Device; CompositeOutputDevice->CompositeDevice; @@ -49,11 +49,11 @@ digraph classes { DigitalInputDevice->EventsMixin; DigitalInputDevice->InputDevice; DigitalOutputDevice->OutputDevice; - DiskUsage->InternalDevice; + DiskUsage->PolledInternalDevice; DistanceSensor->SmoothedInputDevice; Energenie->Device; Energenie->SourceMixin; - FishDish->TrafficLightsBuzzer; + FishDish->CompositeOutputDevice; GPIODevice->Device; HoldMixin->EventsMixin; InputDevice->GPIODevice; @@ -63,11 +63,13 @@ digraph classes { LED->DigitalOutputDevice; LEDBarGraph->LEDCollection; LEDBoard->LEDCollection; + LEDCharDisplay->LEDCollection; LEDCollection->CompositeOutputDevice; + LEDMultiCharDisplay->CompositeOutputDevice; LedBorg->RGBLED; LightSensor->SmoothedInputDevice; LineSensor->SmoothedInputDevice; - LoadAverage->InternalDevice; + LoadAverage->PolledInternalDevice; MCP3001->MCP30xx; MCP3002->MCP30xx; MCP3002->MCP3xx2; @@ -102,13 +104,17 @@ digraph classes { PiLiterBarGraph->LEDBarGraph; PiStop->TrafficLights; PiTraffic->TrafficLights; - PingServer->InternalDevice; + Pibrella->CompositeOutputDevice; + PingServer->PolledInternalDevice; + PolledInternalDevice->InternalDevice; PololuDRV8835Robot->PhaseEnableRobot; PumpkinPi->LEDBoard; RGBLED->Device; RGBLED->SourceMixin; Robot->CompositeDevice; Robot->SourceMixin; + RotaryEncoder->CompositeDevice; + RotaryEncoder->EventsMixin; RyanteckRobot->Robot; SPIDevice->Device; Servo->CompositeDevice; @@ -118,10 +124,11 @@ digraph classes { SnowPi->LEDBoard; StatusBoard->CompositeOutputDevice; StatusZero->LEDBoard; - TimeOfDay->InternalDevice; + TimeOfDay->PolledInternalDevice; TonalBuzzer->CompositeDevice; TonalBuzzer->SourceMixin; - TrafficHat->TrafficLightsBuzzer; + TrafficHat->CompositeOutputDevice; TrafficLights->LEDBoard; TrafficLightsBuzzer->CompositeOutputDevice; + TrafficpHat->TrafficLights; } diff --git a/docs/images/device_hierarchy.pdf b/docs/images/device_hierarchy.pdf index 8908bac4e..c79445edc 100644 Binary files a/docs/images/device_hierarchy.pdf and b/docs/images/device_hierarchy.pdf differ diff --git a/docs/images/device_hierarchy.png b/docs/images/device_hierarchy.png index 3d4c85836..92a446457 100644 Binary files a/docs/images/device_hierarchy.png and b/docs/images/device_hierarchy.png differ diff --git a/docs/images/device_hierarchy.svg b/docs/images/device_hierarchy.svg index 64e68ff0c..d86643bfe 100644 --- a/docs/images/device_hierarchy.svg +++ b/docs/images/device_hierarchy.svg @@ -1,858 +1,1105 @@ - - - + + classes - + -EventsMixin - -EventsMixin + +EventsMixin + +EventsMixin -HoldMixin - -HoldMixin + +HoldMixin + +HoldMixin -HoldMixin->EventsMixin - - + +HoldMixin->EventsMixin + + -SharedMixin - -SharedMixin + +SharedMixin + +SharedMixin -SourceMixin - -SourceMixin + +SourceMixin + +SourceMixin -ValuesMixin - -ValuesMixin + +ValuesMixin + +ValuesMixin -AnalogInputDevice - -AnalogInputDevice + +AnalogInputDevice + +AnalogInputDevice -SPIDevice - -SPIDevice + +SPIDevice + +SPIDevice -AnalogInputDevice->SPIDevice - - + +AnalogInputDevice->SPIDevice + + -CompositeDevice - -CompositeDevice + +CompositeDevice + +CompositeDevice -Device - -Device + +Device + +Device -CompositeDevice->Device - - + +CompositeDevice->Device + + -CompositeOutputDevice - -CompositeOutputDevice + +CompositeOutputDevice + +CompositeOutputDevice -CompositeOutputDevice->SourceMixin - - + +CompositeOutputDevice->SourceMixin + + -CompositeOutputDevice->CompositeDevice - - + +CompositeOutputDevice->CompositeDevice + + -Device->ValuesMixin - - + +Device->ValuesMixin + + -GPIODevice - -GPIODevice + +GPIODevice + +GPIODevice -GPIODevice->Device - - + +GPIODevice->Device + + -InternalDevice - -InternalDevice + +InternalDevice + +InternalDevice -InternalDevice->EventsMixin - - + +InternalDevice->EventsMixin + + -InternalDevice->Device - - + +InternalDevice->Device + + -LEDCollection - -LEDCollection + +LEDCollection + +LEDCollection -LEDCollection->CompositeOutputDevice - - + +LEDCollection->CompositeOutputDevice + + -MCP30xx - -MCP30xx + +MCP30xx + +MCP30xx -MCP3xxx - -MCP3xxx + +MCP3xxx + +MCP3xxx -MCP30xx->MCP3xxx - - + +MCP30xx->MCP3xxx + + -MCP32xx - -MCP32xx + +MCP32xx + +MCP32xx -MCP32xx->MCP3xxx - - + +MCP32xx->MCP3xxx + + -MCP33xx - -MCP33xx + +MCP33xx + +MCP33xx -MCP33xx->MCP3xxx - - + +MCP33xx->MCP3xxx + + -MCP3xx2 - -MCP3xx2 + +MCP3xx2 + +MCP3xx2 -MCP3xx2->MCP3xxx - - + +MCP3xx2->MCP3xxx + + -MCP3xxx->AnalogInputDevice - - + +MCP3xxx->AnalogInputDevice + + -SPIDevice->Device - - + +SPIDevice->Device + + -SmoothedInputDevice - -SmoothedInputDevice + +SmoothedInputDevice + +SmoothedInputDevice -SmoothedInputDevice->EventsMixin - - + +SmoothedInputDevice->EventsMixin + + -InputDevice - -InputDevice + +InputDevice + +InputDevice -SmoothedInputDevice->InputDevice - - + +SmoothedInputDevice->InputDevice + + -AngularServo - -AngularServo + +AngularServo + +AngularServo -Servo - -Servo + +Servo + +Servo -AngularServo->Servo - - + +AngularServo->Servo + + -Servo->SourceMixin - - + +Servo->SourceMixin + + -Servo->CompositeDevice - - + +Servo->CompositeDevice + + -Button - -Button + +Button + +Button -Button->HoldMixin - - + +Button->HoldMixin + + -DigitalInputDevice - -DigitalInputDevice + +DigitalInputDevice + +DigitalInputDevice -Button->DigitalInputDevice - - + +Button->DigitalInputDevice + + -DigitalInputDevice->EventsMixin - - + +DigitalInputDevice->EventsMixin + + -DigitalInputDevice->InputDevice - - + +DigitalInputDevice->InputDevice + + -ButtonBoard - -ButtonBoard + +ButtonBoard + +ButtonBoard -ButtonBoard->HoldMixin - - + +ButtonBoard->HoldMixin + + -ButtonBoard->CompositeDevice - - + +ButtonBoard->CompositeDevice + + -Buzzer - -Buzzer + +Buzzer + +Buzzer -DigitalOutputDevice - -DigitalOutputDevice + +DigitalOutputDevice + +DigitalOutputDevice -Buzzer->DigitalOutputDevice - - + +Buzzer->DigitalOutputDevice + + -OutputDevice - -OutputDevice + +OutputDevice + +OutputDevice -DigitalOutputDevice->OutputDevice - - + +DigitalOutputDevice->OutputDevice + + -CPUTemperature - -CPUTemperature - - -CPUTemperature->InternalDevice - - + +CPUTemperature + +CPUTemperature + + + +PolledInternalDevice + +PolledInternalDevice + + + +CPUTemperature->PolledInternalDevice + + + + + +PolledInternalDevice->InternalDevice + + -CamJamKitRobot - -CamJamKitRobot + +CamJamKitRobot + +CamJamKitRobot -Robot - -Robot + +Robot + +Robot -CamJamKitRobot->Robot - - + +CamJamKitRobot->Robot + + -Robot->SourceMixin - - + +Robot->SourceMixin + + -Robot->CompositeDevice - - + +Robot->CompositeDevice + + -InputDevice->GPIODevice - - + +InputDevice->GPIODevice + + -OutputDevice->SourceMixin - - + +OutputDevice->SourceMixin + + -OutputDevice->GPIODevice - - + +OutputDevice->GPIODevice + + -DiskUsage - -DiskUsage + +DiskUsage + +DiskUsage - -DiskUsage->InternalDevice - - + + +DiskUsage->PolledInternalDevice + + -DistanceSensor - -DistanceSensor + +DistanceSensor + +DistanceSensor -DistanceSensor->SmoothedInputDevice - - + +DistanceSensor->SmoothedInputDevice + + -Energenie - -Energenie + +Energenie + +Energenie -Energenie->SourceMixin - - + +Energenie->SourceMixin + + -Energenie->Device - - + +Energenie->Device + + -FishDish - -FishDish - - -TrafficLightsBuzzer - -TrafficLightsBuzzer + +FishDish + +FishDish - -FishDish->TrafficLightsBuzzer - - - - -TrafficLightsBuzzer->CompositeOutputDevice - - + + +FishDish->CompositeOutputDevice + + -JamHat - -JamHat + +JamHat + +JamHat -JamHat->CompositeOutputDevice - - + +JamHat->CompositeOutputDevice + + -LED - -LED + +LED + +LED -LED->DigitalOutputDevice - - + +LED->DigitalOutputDevice + + -LEDBarGraph - -LEDBarGraph + +LEDBarGraph + +LEDBarGraph -LEDBarGraph->LEDCollection - - + +LEDBarGraph->LEDCollection + + -LEDBoard - -LEDBoard + +LEDBoard + +LEDBoard -LEDBoard->LEDCollection - - + +LEDBoard->LEDCollection + + + + + +LEDCharDisplay + +LEDCharDisplay + + + +LEDCharDisplay->LEDCollection + + + + + +LEDMultiCharDisplay + +LEDMultiCharDisplay + + + +LEDMultiCharDisplay->CompositeOutputDevice + + -LedBorg - -LedBorg + +LedBorg + +LedBorg -RGBLED - -RGBLED + +RGBLED + +RGBLED -LedBorg->RGBLED - - + +LedBorg->RGBLED + + -RGBLED->SourceMixin - - + +RGBLED->SourceMixin + + -RGBLED->Device - - + +RGBLED->Device + + -LightSensor - -LightSensor + +LightSensor + +LightSensor -LightSensor->SmoothedInputDevice - - + +LightSensor->SmoothedInputDevice + + -LineSensor - -LineSensor + +LineSensor + +LineSensor -LineSensor->SmoothedInputDevice - - + +LineSensor->SmoothedInputDevice + + -LoadAverage - -LoadAverage + +LoadAverage + +LoadAverage - -LoadAverage->InternalDevice - - + + +LoadAverage->PolledInternalDevice + + -MCP3001 - -MCP3001 + +MCP3001 + +MCP3001 -MCP3001->MCP30xx - - + +MCP3001->MCP30xx + + -MCP3002 - -MCP3002 + +MCP3002 + +MCP3002 -MCP3002->MCP30xx - - + +MCP3002->MCP30xx + + -MCP3002->MCP3xx2 - - + +MCP3002->MCP3xx2 + + -MCP3004 - -MCP3004 + +MCP3004 + +MCP3004 -MCP3004->MCP30xx - - + +MCP3004->MCP30xx + + -MCP3008 - -MCP3008 + +MCP3008 + +MCP3008 -MCP3008->MCP30xx - - + +MCP3008->MCP30xx + + -MCP3201 - -MCP3201 + +MCP3201 + +MCP3201 -MCP3201->MCP32xx - - + +MCP3201->MCP32xx + + -MCP3202 - -MCP3202 + +MCP3202 + +MCP3202 -MCP3202->MCP32xx - - + +MCP3202->MCP32xx + + -MCP3202->MCP3xx2 - - + +MCP3202->MCP3xx2 + + -MCP3204 - -MCP3204 + +MCP3204 + +MCP3204 -MCP3204->MCP32xx - - + +MCP3204->MCP32xx + + -MCP3208 - -MCP3208 + +MCP3208 + +MCP3208 -MCP3208->MCP32xx - - + +MCP3208->MCP32xx + + -MCP3301 - -MCP3301 + +MCP3301 + +MCP3301 -MCP3301->MCP33xx - - + +MCP3301->MCP33xx + + -MCP3302 - -MCP3302 + +MCP3302 + +MCP3302 -MCP3302->MCP33xx - - + +MCP3302->MCP33xx + + -MCP3304 - -MCP3304 + +MCP3304 + +MCP3304 -MCP3304->MCP33xx - - + +MCP3304->MCP33xx + + -MotionSensor - -MotionSensor + +MotionSensor + +MotionSensor -MotionSensor->SmoothedInputDevice - - + +MotionSensor->SmoothedInputDevice + + -Motor - -Motor + +Motor + +Motor -Motor->SourceMixin - - + +Motor->SourceMixin + + -Motor->CompositeDevice - - + +Motor->CompositeDevice + + -PWMLED - -PWMLED + +PWMLED + +PWMLED -PWMOutputDevice - -PWMOutputDevice + +PWMOutputDevice + +PWMOutputDevice -PWMLED->PWMOutputDevice - - + +PWMLED->PWMOutputDevice + + -PWMOutputDevice->OutputDevice - - + +PWMOutputDevice->OutputDevice + + -PhaseEnableMotor - -PhaseEnableMotor + +PhaseEnableMotor + +PhaseEnableMotor -PhaseEnableMotor->SourceMixin - - + +PhaseEnableMotor->SourceMixin + + -PhaseEnableMotor->CompositeDevice - - + +PhaseEnableMotor->CompositeDevice + + -PhaseEnableRobot - -PhaseEnableRobot + +PhaseEnableRobot + +PhaseEnableRobot -PhaseEnableRobot->SourceMixin - - + +PhaseEnableRobot->SourceMixin + + -PhaseEnableRobot->CompositeDevice - - + +PhaseEnableRobot->CompositeDevice + + -PiHutXmasTree - -PiHutXmasTree + +PiHutXmasTree + +PiHutXmasTree -PiHutXmasTree->LEDBoard - - + +PiHutXmasTree->LEDBoard + + -PiLiter - -PiLiter + +PiLiter + +PiLiter -PiLiter->LEDBoard - - + +PiLiter->LEDBoard + + -PiLiterBarGraph - -PiLiterBarGraph + +PiLiterBarGraph + +PiLiterBarGraph -PiLiterBarGraph->LEDBarGraph - - + +PiLiterBarGraph->LEDBarGraph + + -PiStop - -PiStop + +PiStop + +PiStop -TrafficLights - -TrafficLights + +TrafficLights + +TrafficLights -PiStop->TrafficLights - - + +PiStop->TrafficLights + + -TrafficLights->LEDBoard - - + +TrafficLights->LEDBoard + + -PiTraffic - -PiTraffic + +PiTraffic + +PiTraffic -PiTraffic->TrafficLights - - + +PiTraffic->TrafficLights + + + + + +Pibrella + +Pibrella + + + +Pibrella->CompositeOutputDevice + + -PingServer - -PingServer + +PingServer + +PingServer - -PingServer->InternalDevice - - + + +PingServer->PolledInternalDevice + + -PololuDRV8835Robot - -PololuDRV8835Robot + +PololuDRV8835Robot + +PololuDRV8835Robot -PololuDRV8835Robot->PhaseEnableRobot - - + +PololuDRV8835Robot->PhaseEnableRobot + + -PumpkinPi - -PumpkinPi + +PumpkinPi + +PumpkinPi -PumpkinPi->LEDBoard - - + +PumpkinPi->LEDBoard + + + + + +RotaryEncoder + +RotaryEncoder + + + +RotaryEncoder->EventsMixin + + + + + +RotaryEncoder->CompositeDevice + + -RyanteckRobot - -RyanteckRobot + +RyanteckRobot + +RyanteckRobot -RyanteckRobot->Robot - - + +RyanteckRobot->Robot + + -SnowPi - -SnowPi + +SnowPi + +SnowPi -SnowPi->LEDBoard - - + +SnowPi->LEDBoard + + -StatusBoard - -StatusBoard + +StatusBoard + +StatusBoard -StatusBoard->CompositeOutputDevice - - + +StatusBoard->CompositeOutputDevice + + -StatusZero - -StatusZero + +StatusZero + +StatusZero -StatusZero->LEDBoard - - + +StatusZero->LEDBoard + + -TimeOfDay - -TimeOfDay + +TimeOfDay + +TimeOfDay - -TimeOfDay->InternalDevice - - + + +TimeOfDay->PolledInternalDevice + + -TonalBuzzer - -TonalBuzzer + +TonalBuzzer + +TonalBuzzer -TonalBuzzer->SourceMixin - - + +TonalBuzzer->SourceMixin + + -TonalBuzzer->CompositeDevice - - + +TonalBuzzer->CompositeDevice + + -TrafficHat - -TrafficHat - - -TrafficHat->TrafficLightsBuzzer - - + +TrafficHat + +TrafficHat + + + +TrafficHat->CompositeOutputDevice + + + + + +TrafficLightsBuzzer + +TrafficLightsBuzzer + + + +TrafficLightsBuzzer->CompositeOutputDevice + + + + + +TrafficpHat + +TrafficpHat + + + +TrafficpHat->TrafficLights + + diff --git a/docs/images/device_pin_flowchart.dot b/docs/images/device_pin_flowchart.dot index 54bc2c59c..230e0d23f 100644 --- a/docs/images/device_pin_flowchart.dot +++ b/docs/images/device_pin_flowchart.dot @@ -7,13 +7,18 @@ digraph device_pins { constructor [label="LED(pin_spec, ...,\npin_factory=None)"]; pin_factory_kwarg [shape=diamond,label="pin_factory\nis None?"]; - default_factory [label="self.pin_factory =\nDevice.pin_factory"]; + device_pin_factory [shape=diamond,label="Device.pin_factory\nis None?"]; + set_default_factory [label="Device.pin_factory =\nDevice._default_pin_factory()"]; + get_default_factory [label="self.pin_factory =\nDevice.pin_factory"]; override_factory [label="self.pin_factory =\npin_factory"]; factory_pin [label="self.pin =\nself.pin_factory.pin(pin_spec)"]; constructor->pin_factory_kwarg; - pin_factory_kwarg->default_factory [label="yes"]; + pin_factory_kwarg->device_pin_factory [label="yes"]; pin_factory_kwarg->override_factory [label="no"]; - default_factory->factory_pin; + device_pin_factory->set_default_factory [label="yes"]; + device_pin_factory->get_default_factory [label="no"]; + set_default_factory->get_default_factory; + get_default_factory->factory_pin; override_factory->factory_pin; } diff --git a/docs/images/device_pin_flowchart.pdf b/docs/images/device_pin_flowchart.pdf index 586766b2e..2e2197e52 100644 Binary files a/docs/images/device_pin_flowchart.pdf and b/docs/images/device_pin_flowchart.pdf differ diff --git a/docs/images/device_pin_flowchart.png b/docs/images/device_pin_flowchart.png index 4d4aa865d..4c36e8f60 100644 Binary files a/docs/images/device_pin_flowchart.png and b/docs/images/device_pin_flowchart.png differ diff --git a/docs/images/device_pin_flowchart.svg b/docs/images/device_pin_flowchart.svg index 05d00bdb6..7d121dca7 100644 --- a/docs/images/device_pin_flowchart.svg +++ b/docs/images/device_pin_flowchart.svg @@ -1,70 +1,114 @@ - - - + + device_pins - + -constructor - -LED(pin_spec, ..., -pin_factory=None) + +constructor + +LED(pin_spec, ..., +pin_factory=None) -pin_factory_kwarg - -pin_factory -is None? + +pin_factory_kwarg + +pin_factory +is None? -constructor->pin_factory_kwarg - - - - -default_factory - -self.pin_factory = -Device.pin_factory - - -pin_factory_kwarg->default_factory - - -yes + +constructor->pin_factory_kwarg + + + + + +device_pin_factory + +Device.pin_factory +is None? + + + +pin_factory_kwarg->device_pin_factory + + +yes -override_factory - -self.pin_factory = -pin_factory + +override_factory + +self.pin_factory = +pin_factory -pin_factory_kwarg->override_factory - - -no + +pin_factory_kwarg->override_factory + + +no + + + +set_default_factory + +Device.pin_factory = +Device._default_pin_factory() + + + +device_pin_factory->set_default_factory + + +yes + + + +get_default_factory + +self.pin_factory = +Device.pin_factory + + + +device_pin_factory->get_default_factory + + +no + + + +set_default_factory->get_default_factory + + -factory_pin - -self.pin = -self.pin_factory.pin(pin_spec) + +factory_pin + +self.pin = +self.pin_factory.pin(pin_spec) - -default_factory->factory_pin - - + + +get_default_factory->factory_pin + + -override_factory->factory_pin - - + +override_factory->factory_pin + + diff --git a/docs/images/distance_sensor_bb.pdf b/docs/images/distance_sensor_bb.pdf index 9ba23ae28..cf3637e48 100644 Binary files a/docs/images/distance_sensor_bb.pdf and b/docs/images/distance_sensor_bb.pdf differ diff --git a/docs/images/distance_sensor_bb.png b/docs/images/distance_sensor_bb.png index 3a50b30a5..8f408f1ca 100644 Binary files a/docs/images/distance_sensor_bb.png and b/docs/images/distance_sensor_bb.png differ diff --git a/docs/images/input_device_hierarchy.dot b/docs/images/input_device_hierarchy.dot index a1cbe3f97..576c8385d 100644 --- a/docs/images/input_device_hierarchy.dot +++ b/docs/images/input_device_hierarchy.dot @@ -1,11 +1,13 @@ digraph classes { graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Arial, fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; /* Mixin classes */ node [color="#c69ee0", fontcolor="#000000"] + + /* Abstract classes */ node [color="#9ec6e0", fontcolor="#000000"] diff --git a/docs/images/input_device_hierarchy.pdf b/docs/images/input_device_hierarchy.pdf index 97935adcd..bc5517e28 100644 Binary files a/docs/images/input_device_hierarchy.pdf and b/docs/images/input_device_hierarchy.pdf differ diff --git a/docs/images/input_device_hierarchy.png b/docs/images/input_device_hierarchy.png index 7b8a5ee1d..a432f0009 100644 Binary files a/docs/images/input_device_hierarchy.png and b/docs/images/input_device_hierarchy.png differ diff --git a/docs/images/input_device_hierarchy.svg b/docs/images/input_device_hierarchy.svg index 43e96a7c6..43dd9645e 100644 --- a/docs/images/input_device_hierarchy.svg +++ b/docs/images/input_device_hierarchy.svg @@ -1,98 +1,115 @@ - - + classes - + -GPIODevice - -GPIODevice + +GPIODevice + +GPIODevice -SmoothedInputDevice - -SmoothedInputDevice + +SmoothedInputDevice + +SmoothedInputDevice -InputDevice - -InputDevice + +InputDevice + +InputDevice -SmoothedInputDevice->InputDevice - - + +SmoothedInputDevice->InputDevice + + -Button - -Button + +Button + +Button -DigitalInputDevice - -DigitalInputDevice + +DigitalInputDevice + +DigitalInputDevice -Button->DigitalInputDevice - - + +Button->DigitalInputDevice + + -DigitalInputDevice->InputDevice - - + +DigitalInputDevice->InputDevice + + -InputDevice->GPIODevice - - + +InputDevice->GPIODevice + + -DistanceSensor - -DistanceSensor + +DistanceSensor + +DistanceSensor -DistanceSensor->SmoothedInputDevice - - + +DistanceSensor->SmoothedInputDevice + + -LightSensor - -LightSensor + +LightSensor + +LightSensor -LightSensor->SmoothedInputDevice - - + +LightSensor->SmoothedInputDevice + + -LineSensor - -LineSensor + +LineSensor + +LineSensor -LineSensor->SmoothedInputDevice - - + +LineSensor->SmoothedInputDevice + + -MotionSensor - -MotionSensor + +MotionSensor + +MotionSensor -MotionSensor->SmoothedInputDevice - - + +MotionSensor->SmoothedInputDevice + + diff --git a/docs/images/internal_device_hierarchy.dot b/docs/images/internal_device_hierarchy.dot index 36a5a87f0..9c321ddfe 100644 --- a/docs/images/internal_device_hierarchy.dot +++ b/docs/images/internal_device_hierarchy.dot @@ -1,22 +1,27 @@ -/* vim: set et sw=4 sts=4: */ - digraph classes { graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Arial, fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; + /* Mixin classes */ + node [color="#c69ee0", fontcolor="#000000"] + + + /* Abstract classes */ node [color="#9ec6e0", fontcolor="#000000"] + Device; InternalDevice; /* Concrete classes */ node [color="#2980b9", fontcolor="#ffffff"]; + CPUTemperature->PolledInternalDevice; + DiskUsage->PolledInternalDevice; InternalDevice->Device; - TimeOfDay->InternalDevice; - PingServer->InternalDevice; - CPUTemperature->InternalDevice; - LoadAverage->InternalDevice; - DiskUsage->InternalDevice; + LoadAverage->PolledInternalDevice; + PingServer->PolledInternalDevice; + PolledInternalDevice->InternalDevice; + TimeOfDay->PolledInternalDevice; } diff --git a/docs/images/internal_device_hierarchy.pdf b/docs/images/internal_device_hierarchy.pdf index 3e59e2906..f33793bb6 100644 Binary files a/docs/images/internal_device_hierarchy.pdf and b/docs/images/internal_device_hierarchy.pdf differ diff --git a/docs/images/internal_device_hierarchy.png b/docs/images/internal_device_hierarchy.png index 1e0907420..bf12954ad 100644 Binary files a/docs/images/internal_device_hierarchy.png and b/docs/images/internal_device_hierarchy.png differ diff --git a/docs/images/internal_device_hierarchy.svg b/docs/images/internal_device_hierarchy.svg index 8e0abba92..a1b705c7d 100644 --- a/docs/images/internal_device_hierarchy.svg +++ b/docs/images/internal_device_hierarchy.svg @@ -1,78 +1,103 @@ - - + classes - + -Device + +Device -Device +Device -InternalDevice - -InternalDevice + +InternalDevice + +InternalDevice -InternalDevice->Device - - + +InternalDevice->Device + + - -TimeOfDay - -TimeOfDay + + +CPUTemperature + +CPUTemperature - -TimeOfDay->InternalDevice - - + + +PolledInternalDevice + +PolledInternalDevice - -PingServer - -PingServer + + +CPUTemperature->PolledInternalDevice + + - -PingServer->InternalDevice - - + + +PolledInternalDevice->InternalDevice + + - -CPUTemperature - -CPUTemperature + + +DiskUsage + +DiskUsage - -CPUTemperature->InternalDevice - - + + +DiskUsage->PolledInternalDevice + + -LoadAverage - -LoadAverage + +LoadAverage + +LoadAverage - -LoadAverage->InternalDevice - - + + +LoadAverage->PolledInternalDevice + + - -DiskUsage - -DiskUsage - - -DiskUsage->InternalDevice - - + + +PingServer + +PingServer + + + +PingServer->PolledInternalDevice + + + + + +TimeOfDay + +TimeOfDay + + + +TimeOfDay->PolledInternalDevice + + diff --git a/docs/images/led_bb.pdf b/docs/images/led_bb.pdf index ccb75d98d..531d838f9 100644 Binary files a/docs/images/led_bb.pdf and b/docs/images/led_bb.pdf differ diff --git a/docs/images/led_bb.png b/docs/images/led_bb.png index 464858355..6163dff55 100644 Binary files a/docs/images/led_bb.png and b/docs/images/led_bb.png differ diff --git a/docs/images/led_button_bb.pdf b/docs/images/led_button_bb.pdf index 518e03795..145e2a185 100644 Binary files a/docs/images/led_button_bb.pdf and b/docs/images/led_button_bb.pdf differ diff --git a/docs/images/led_button_bb.png b/docs/images/led_button_bb.png index c1da0fe51..f47938267 100644 Binary files a/docs/images/led_button_bb.png and b/docs/images/led_button_bb.png differ diff --git a/docs/images/led_char_display.fzz b/docs/images/led_char_display.fzz new file mode 100644 index 000000000..b41649ad1 Binary files /dev/null and b/docs/images/led_char_display.fzz differ diff --git a/docs/images/led_char_display_bb.pdf b/docs/images/led_char_display_bb.pdf new file mode 100644 index 000000000..ff5266f1b Binary files /dev/null and b/docs/images/led_char_display_bb.pdf differ diff --git a/docs/images/led_char_display_bb.png b/docs/images/led_char_display_bb.png new file mode 100644 index 000000000..3916850dc Binary files /dev/null and b/docs/images/led_char_display_bb.png differ diff --git a/docs/images/led_char_display_bb.svg b/docs/images/led_char_display_bb.svg new file mode 100644 index 000000000..2b3fbeeac --- /dev/null +++ b/docs/images/led_char_display_bb.svg @@ -0,0 +1,4682 @@ + + + + + + + + + + + + + + + + + + + + 7 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 1 + + + 5 + + + 5 + + + 10 + + + 10 + + + 15 + + + 15 + + + 20 + + + 20 + + + 25 + + + 25 + + + 30 + + + 30 + + + A + + + A + + + B + + + B + + + C + + + C + + + D + + + D + + + E + + + E + + + F + + + F + + + G + + + G + + + H + + + H + + + I + + + I + + + J + + + J + + + + + + + + + + + + + +Raspberry Pi Model 2 v1.1 + + +© Raspberry Pi 2014 + + + + + + + + + + +h + + +t + + +t + + +p + + +: + + +/ + + +/ + + +w + + +w + + +w + + +. + + +r + + +a + + +s + + +p + + +b + + +e + + +r + + +r + + +y + + +p + + +i + + +. + + +o + + +r + + +g + + + + + + + + + + + + + +Power + + + + + + + + + + + + + + + + +HDMI + + + + + + + + + + + + + + + + +Audio + + + + + + + + + + + + + + + + +USB 2x + + + + + + + + + + + + + + + + +USB 2x + + + + + + + + + + + + + + + + +ETHERNET + + + + + + + + + + + + + + + + +DSI (DISPLAY) + + + + + + + + + + + + + + + + +CSI (CAMERA) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GPIO + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/ledbargraph_bb.pdf b/docs/images/ledbargraph_bb.pdf index 2c9012ba2..c34f9cfe6 100644 Binary files a/docs/images/ledbargraph_bb.pdf and b/docs/images/ledbargraph_bb.pdf differ diff --git a/docs/images/ledbargraph_bb.png b/docs/images/ledbargraph_bb.png index f2cdbb00c..5570c96cd 100644 Binary files a/docs/images/ledbargraph_bb.png and b/docs/images/ledbargraph_bb.png differ diff --git a/docs/images/ledboard_bb.pdf b/docs/images/ledboard_bb.pdf index 98fda51ab..ce8e1019f 100644 Binary files a/docs/images/ledboard_bb.pdf and b/docs/images/ledboard_bb.pdf differ diff --git a/docs/images/ledboard_bb.png b/docs/images/ledboard_bb.png index bae1abb24..46026861d 100644 Binary files a/docs/images/ledboard_bb.png and b/docs/images/ledboard_bb.png differ diff --git a/docs/images/light_sensor_bb.pdf b/docs/images/light_sensor_bb.pdf index 35709222a..9735c72f8 100644 Binary files a/docs/images/light_sensor_bb.pdf and b/docs/images/light_sensor_bb.pdf differ diff --git a/docs/images/light_sensor_bb.png b/docs/images/light_sensor_bb.png index ffd2d0a05..9d154d458 100644 Binary files a/docs/images/light_sensor_bb.png and b/docs/images/light_sensor_bb.png differ diff --git a/docs/images/motion_robot_bb.pdf b/docs/images/motion_robot_bb.pdf index cc215c692..13a99324f 100644 Binary files a/docs/images/motion_robot_bb.pdf and b/docs/images/motion_robot_bb.pdf differ diff --git a/docs/images/motion_robot_bb.png b/docs/images/motion_robot_bb.png index 29234cf2c..f30e784d4 100644 Binary files a/docs/images/motion_robot_bb.png and b/docs/images/motion_robot_bb.png differ diff --git a/docs/images/motion_sensor_bb.pdf b/docs/images/motion_sensor_bb.pdf index 21f7342a5..47344e420 100644 Binary files a/docs/images/motion_sensor_bb.pdf and b/docs/images/motion_sensor_bb.pdf differ diff --git a/docs/images/motion_sensor_bb.png b/docs/images/motion_sensor_bb.png index df659b08f..5d070d65f 100644 Binary files a/docs/images/motion_sensor_bb.png and b/docs/images/motion_sensor_bb.png differ diff --git a/docs/images/motor_bb.pdf b/docs/images/motor_bb.pdf index a9b5a57ff..2b443e986 100644 Binary files a/docs/images/motor_bb.pdf and b/docs/images/motor_bb.pdf differ diff --git a/docs/images/motor_bb.png b/docs/images/motor_bb.png index 6eda1e636..195cbc053 100644 Binary files a/docs/images/motor_bb.png and b/docs/images/motor_bb.png differ diff --git a/docs/images/music_box_bb.pdf b/docs/images/music_box_bb.pdf index 5525a7848..a3bec0052 100644 Binary files a/docs/images/music_box_bb.pdf and b/docs/images/music_box_bb.pdf differ diff --git a/docs/images/music_box_bb.png b/docs/images/music_box_bb.png index 881ded04c..a3f225359 100644 Binary files a/docs/images/music_box_bb.png and b/docs/images/music_box_bb.png differ diff --git a/docs/images/output_device_hierarchy.dot b/docs/images/output_device_hierarchy.dot index 281b91dda..d84d4afd3 100644 --- a/docs/images/output_device_hierarchy.dot +++ b/docs/images/output_device_hierarchy.dot @@ -1,11 +1,13 @@ digraph classes { graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Arial, fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; /* Mixin classes */ node [color="#c69ee0", fontcolor="#000000"] + + /* Abstract classes */ node [color="#9ec6e0", fontcolor="#000000"] diff --git a/docs/images/output_device_hierarchy.pdf b/docs/images/output_device_hierarchy.pdf index 84f4899de..2cc69958e 100644 Binary files a/docs/images/output_device_hierarchy.pdf and b/docs/images/output_device_hierarchy.pdf differ diff --git a/docs/images/output_device_hierarchy.png b/docs/images/output_device_hierarchy.png index 20d4a6791..8bed262a8 100644 Binary files a/docs/images/output_device_hierarchy.png and b/docs/images/output_device_hierarchy.png differ diff --git a/docs/images/output_device_hierarchy.svg b/docs/images/output_device_hierarchy.svg index 605799565..148bb38ed 100644 --- a/docs/images/output_device_hierarchy.svg +++ b/docs/images/output_device_hierarchy.svg @@ -1,158 +1,187 @@ - - + classes - + -CompositeDevice - -CompositeDevice + +CompositeDevice + +CompositeDevice -Device + +Device -Device +Device -CompositeDevice->Device - - + +CompositeDevice->Device + + -GPIODevice - -GPIODevice + +GPIODevice + +GPIODevice -GPIODevice->Device - - + +GPIODevice->Device + + -AngularServo - -AngularServo + +AngularServo + +AngularServo -Servo - -Servo + +Servo + +Servo -AngularServo->Servo - - + +AngularServo->Servo + + -Servo->CompositeDevice - - + +Servo->CompositeDevice + + -Buzzer - -Buzzer + +Buzzer + +Buzzer -DigitalOutputDevice - -DigitalOutputDevice + +DigitalOutputDevice + +DigitalOutputDevice -Buzzer->DigitalOutputDevice - - + +Buzzer->DigitalOutputDevice + + -OutputDevice - -OutputDevice + +OutputDevice + +OutputDevice -DigitalOutputDevice->OutputDevice - - + +DigitalOutputDevice->OutputDevice + + -OutputDevice->GPIODevice - - + +OutputDevice->GPIODevice + + -LED - -LED + +LED + +LED -LED->DigitalOutputDevice - - + +LED->DigitalOutputDevice + + -Motor - -Motor + +Motor + +Motor -Motor->CompositeDevice - - + +Motor->CompositeDevice + + -PWMLED - -PWMLED + +PWMLED + +PWMLED -PWMOutputDevice - -PWMOutputDevice + +PWMOutputDevice + +PWMOutputDevice -PWMLED->PWMOutputDevice - - + +PWMLED->PWMOutputDevice + + -PWMOutputDevice->OutputDevice - - + +PWMOutputDevice->OutputDevice + + -PhaseEnableMotor - -PhaseEnableMotor + +PhaseEnableMotor + +PhaseEnableMotor -PhaseEnableMotor->CompositeDevice - - + +PhaseEnableMotor->CompositeDevice + + -RGBLED - -RGBLED + +RGBLED + +RGBLED -RGBLED->Device - - + +RGBLED->Device + + -TonalBuzzer - -TonalBuzzer + +TonalBuzzer + +TonalBuzzer -TonalBuzzer->CompositeDevice - - + +TonalBuzzer->CompositeDevice + + diff --git a/docs/images/pin_layout.pdf b/docs/images/pin_layout.pdf index 9271475cc..18aa5d008 100644 Binary files a/docs/images/pin_layout.pdf and b/docs/images/pin_layout.pdf differ diff --git a/docs/images/pin_layout.png b/docs/images/pin_layout.png index ab8b27cc6..96dc06ffc 100644 Binary files a/docs/images/pin_layout.png and b/docs/images/pin_layout.png differ diff --git a/docs/images/pinout_pi3.png b/docs/images/pinout_pi3.png index 67b36d4cf..6ed73bda3 100644 Binary files a/docs/images/pinout_pi3.png and b/docs/images/pinout_pi3.png differ diff --git a/docs/images/pinout_pizero_w.png b/docs/images/pinout_pizero_w.png index f04d9d2c0..5c76128ed 100644 Binary files a/docs/images/pinout_pizero_w.png and b/docs/images/pinout_pizero_w.png differ diff --git a/docs/images/pintest.png b/docs/images/pintest.png new file mode 100644 index 000000000..ee2319070 Binary files /dev/null and b/docs/images/pintest.png differ diff --git a/docs/images/potentiometer_bb.pdf b/docs/images/potentiometer_bb.pdf index 33d5c5e15..a4bf38c14 100644 Binary files a/docs/images/potentiometer_bb.pdf and b/docs/images/potentiometer_bb.pdf differ diff --git a/docs/images/potentiometer_bb.png b/docs/images/potentiometer_bb.png index 7cc42e91d..baf0b893c 100644 Binary files a/docs/images/potentiometer_bb.png and b/docs/images/potentiometer_bb.png differ diff --git a/docs/images/reaction_game_bb.pdf b/docs/images/reaction_game_bb.pdf index bd8200508..957862d8d 100644 Binary files a/docs/images/reaction_game_bb.pdf and b/docs/images/reaction_game_bb.pdf differ diff --git a/docs/images/reaction_game_bb.png b/docs/images/reaction_game_bb.png index bc9c4475e..faa1a0321 100644 Binary files a/docs/images/reaction_game_bb.png and b/docs/images/reaction_game_bb.png differ diff --git a/docs/images/rgb_led_bb.pdf b/docs/images/rgb_led_bb.pdf index ee83a65b2..d2b4ca5fa 100644 Binary files a/docs/images/rgb_led_bb.pdf and b/docs/images/rgb_led_bb.pdf differ diff --git a/docs/images/rgb_led_bb.png b/docs/images/rgb_led_bb.png index bd8bc8d56..13fd43826 100644 Binary files a/docs/images/rgb_led_bb.png and b/docs/images/rgb_led_bb.png differ diff --git a/docs/images/rgbled_pot_bb.pdf b/docs/images/rgbled_pot_bb.pdf index 7ee369128..fe7cb6b60 100644 Binary files a/docs/images/rgbled_pot_bb.pdf and b/docs/images/rgbled_pot_bb.pdf differ diff --git a/docs/images/rgbled_pot_bb.png b/docs/images/rgbled_pot_bb.png index d79f7635b..e09aecba6 100644 Binary files a/docs/images/rgbled_pot_bb.png and b/docs/images/rgbled_pot_bb.png differ diff --git a/docs/images/robot_bb.pdf b/docs/images/robot_bb.pdf index 5a73870bf..49a321472 100644 Binary files a/docs/images/robot_bb.pdf and b/docs/images/robot_bb.pdf differ diff --git a/docs/images/robot_bb.png b/docs/images/robot_bb.png index fc1c1d2d9..d2c4bed29 100644 Binary files a/docs/images/robot_bb.png and b/docs/images/robot_bb.png differ diff --git a/docs/images/source_values/custom_generator.pdf b/docs/images/source_values/custom_generator.pdf index 43748680d..22042d4ea 100644 Binary files a/docs/images/source_values/custom_generator.pdf and b/docs/images/source_values/custom_generator.pdf differ diff --git a/docs/images/spi_device_hierarchy.dot b/docs/images/spi_device_hierarchy.dot index d62c69a81..475ed97b6 100644 --- a/docs/images/spi_device_hierarchy.dot +++ b/docs/images/spi_device_hierarchy.dot @@ -1,11 +1,13 @@ digraph classes { graph [rankdir=RL]; - node [shape=rect, style=filled, fontname=Arial, fontsize=10]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; edge []; /* Mixin classes */ node [color="#c69ee0", fontcolor="#000000"] + + /* Abstract classes */ node [color="#9ec6e0", fontcolor="#000000"] diff --git a/docs/images/spi_device_hierarchy.pdf b/docs/images/spi_device_hierarchy.pdf index a52ab99ba..599afd37a 100644 Binary files a/docs/images/spi_device_hierarchy.pdf and b/docs/images/spi_device_hierarchy.pdf differ diff --git a/docs/images/spi_device_hierarchy.png b/docs/images/spi_device_hierarchy.png index 0a9f5e773..f200833c3 100644 Binary files a/docs/images/spi_device_hierarchy.png and b/docs/images/spi_device_hierarchy.png differ diff --git a/docs/images/spi_device_hierarchy.svg b/docs/images/spi_device_hierarchy.svg index 5520cd7e2..b4e08ce39 100644 --- a/docs/images/spi_device_hierarchy.svg +++ b/docs/images/spi_device_hierarchy.svg @@ -1,208 +1,247 @@ - - + classes - + -AnalogInputDevice - -AnalogInputDevice + +AnalogInputDevice + +AnalogInputDevice -SPIDevice - -SPIDevice + +SPIDevice + +SPIDevice -AnalogInputDevice->SPIDevice - - + +AnalogInputDevice->SPIDevice + + -Device + +Device -Device +Device -MCP30xx - -MCP30xx + +MCP30xx + +MCP30xx -MCP3xxx - -MCP3xxx + +MCP3xxx + +MCP3xxx -MCP30xx->MCP3xxx - - + +MCP30xx->MCP3xxx + + -MCP32xx - -MCP32xx + +MCP32xx + +MCP32xx -MCP32xx->MCP3xxx - - + +MCP32xx->MCP3xxx + + -MCP33xx - -MCP33xx + +MCP33xx + +MCP33xx -MCP33xx->MCP3xxx - - + +MCP33xx->MCP3xxx + + -MCP3xx2 - -MCP3xx2 + +MCP3xx2 + +MCP3xx2 -MCP3xx2->MCP3xxx - - + +MCP3xx2->MCP3xxx + + -MCP3xxx->AnalogInputDevice - - + +MCP3xxx->AnalogInputDevice + + -SPIDevice->Device - - + +SPIDevice->Device + + -MCP3001 - -MCP3001 + +MCP3001 + +MCP3001 -MCP3001->MCP30xx - - + +MCP3001->MCP30xx + + -MCP3002 - -MCP3002 + +MCP3002 + +MCP3002 -MCP3002->MCP30xx - - + +MCP3002->MCP30xx + + -MCP3002->MCP3xx2 - - + +MCP3002->MCP3xx2 + + -MCP3004 - -MCP3004 + +MCP3004 + +MCP3004 -MCP3004->MCP30xx - - + +MCP3004->MCP30xx + + -MCP3008 - -MCP3008 + +MCP3008 + +MCP3008 -MCP3008->MCP30xx - - + +MCP3008->MCP30xx + + -MCP3201 - -MCP3201 + +MCP3201 + +MCP3201 -MCP3201->MCP32xx - - + +MCP3201->MCP32xx + + -MCP3202 - -MCP3202 + +MCP3202 + +MCP3202 -MCP3202->MCP32xx - - + +MCP3202->MCP32xx + + -MCP3202->MCP3xx2 - - + +MCP3202->MCP3xx2 + + -MCP3204 - -MCP3204 + +MCP3204 + +MCP3204 -MCP3204->MCP32xx - - + +MCP3204->MCP32xx + + -MCP3208 - -MCP3208 + +MCP3208 + +MCP3208 -MCP3208->MCP32xx - - + +MCP3208->MCP32xx + + -MCP3301 - -MCP3301 + +MCP3301 + +MCP3301 -MCP3301->MCP33xx - - + +MCP3301->MCP33xx + + -MCP3302 - -MCP3302 + +MCP3302 + +MCP3302 -MCP3302->MCP33xx - - + +MCP3302->MCP33xx + + -MCP3304 - -MCP3304 + +MCP3304 + +MCP3304 -MCP3304->MCP33xx - - + +MCP3304->MCP33xx + + diff --git a/docs/images/traffic_lights_bb.pdf b/docs/images/traffic_lights_bb.pdf index 2f507f3f6..18518c24f 100644 Binary files a/docs/images/traffic_lights_bb.pdf and b/docs/images/traffic_lights_bb.pdf differ diff --git a/docs/images/traffic_lights_bb.png b/docs/images/traffic_lights_bb.png index 425eb3f17..d82de87c2 100644 Binary files a/docs/images/traffic_lights_bb.png and b/docs/images/traffic_lights_bb.png differ diff --git a/docs/index.rst b/docs/index.rst index b02cf4df0..a4cdd51ca 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Ben Nuttall -.. Copyright (c) 2015-2019 Dave Jones -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2015-2021 Dave Jones +.. Copyright (c) 2017-2019 Ben Nuttall .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause .. include:: ../README.rst @@ -46,6 +23,7 @@ Table of Contents source_values cli_tools faq + compat migrating_from_rpigpio contributing development @@ -56,6 +34,7 @@ Table of Contents api_internal api_generic api_tools + api_fonts api_tones api_info api_pins diff --git a/docs/installing.rst b/docs/installing.rst index f014843be..d4acd603c 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -1,46 +1,37 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2017 Ben Nuttall .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2021 Cameron Davidson-Pilon +.. Copyright (c) 2017-2021 Ben Nuttall .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ==================== Installing GPIO Zero ==================== -GPIO Zero is installed by default in the `Raspbian`_ image, and the -`Raspberry Pi Desktop`_ image for PC/Mac, both available from -`raspberrypi.org`_. Follow these guides to installing on Raspbian Lite -and other operating systems, including for PCs using the -:doc:`remote GPIO ` feature. +GPIO Zero is installed by default in the `Raspberry Pi OS`_ desktop image, `Raspberry Pi OS`_ Lite image, and +the `Raspberry Pi Desktop`_ image for PC/Mac, all available from +`raspberrypi.com`_. Follow these guides to installing on other operating systems, including for PCs using the :doc:`remote GPIO +` feature. + +.. _Raspberry Pi OS: https://www.raspberrypi.com/software/operating-systems/ +.. _Raspberry Pi Desktop: https://www.raspberrypi.com/software/raspberry-pi-desktop/ +.. _raspberrypi.com: https://www.raspberrypi.com/software/ Raspberry Pi ============ +GPIO Zero is packaged in the apt repositories of Raspberry Pi OS, `Debian`_ and +`Ubuntu`_. It is also available on `PyPI`_. + +.. _Debian: https://packages.debian.org/bookworm/python3-gpiozero +.. _Ubuntu: https://packages.ubuntu.com/noble/python3-gpiozero +.. _PyPI: https://pypi.org/project/gpiozero/ + +apt +--- + First, update your repositories list: .. code-block:: console @@ -59,6 +50,9 @@ or Python 2: pi@raspberrypi:~$ sudo apt install python-gpiozero +pip +--- + If you're using another operating system on your Raspberry Pi, you may need to use pip to install GPIO Zero instead. Install pip using `get-pip`_ and then type: @@ -75,6 +69,8 @@ or for Python 2: To install GPIO Zero in a virtual environment, see the :doc:`development` page. +.. _get-pip: https://pip.pypa.io/en/stable/installing/ + PC/Mac ====== @@ -82,8 +78,40 @@ In order to use GPIO Zero's remote GPIO feature from a PC or Mac, you'll need to install GPIO Zero on that computer using pip. See the :doc:`remote_gpio` page for more information. +Documentation +============= -.. _Raspbian: https://www.raspberrypi.org/downloads/raspbian/ -.. _Raspberry Pi Desktop: https://www.raspberrypi.org/downloads/raspberry-pi-desktop/ -.. _raspberrypi.org: https://www.raspberrypi.org/downloads/ -.. _get-pip: https://pip.pypa.io/en/stable/installing/ +This documentation is also available for offline installation like so: + +.. code-block:: console + + pi@raspberrypi:~$ sudo apt install python-gpiozero-doc + +This will install the HTML version of the documentation under the +:file:`/usr/share/doc/python-gpiozero-doc/html` path. To view the offline +documentation you have several options: + +You can open the documentation directly by visiting +file:///usr/share/doc/python-gpiozero-doc/html/index.html in your browser. +However, be aware that using ``file://`` URLs sometimes breaks certain elements. +To avoid this, you can view the docs from an ``http://`` style URL by starting +a trivial HTTP server with Python, like so: + +.. code-block:: console + + $ python3 -m http.server -d /usr/share/doc/python-gpiozero-doc/html + +Then visit http://localhost:8000/ in your browser. + +Alternatively, the package also integrates into Debian's `doc-base`_ system, so +you can install one of the doc-base clients (dochelp, dwww, dhelp, doc-central, +etc.) and use its interface to locate this document. + +If you want to view the documentation offline on a different device, such as an +eReader, there are Epub and PDF versions of the documentation available for +download from the `ReadTheDocs site`_. Simply click on the "Read the Docs" box +at the bottom-left corner of the page (under the table of contents) and select +"PDF" or "Epub" from the "Downloads" section. + +.. _doc-base: https://wiki.debian.org/doc-base +.. _ReadTheDocs site: https://gpiozero.readthedocs.io/ diff --git a/docs/license.rst b/docs/license.rst index 3b3e9e50f..57da34af4 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -4,7 +4,7 @@ License ======= -Copyright © 2015-2019 Ben Nuttall and contributors; see +Copyright © 2015-2020 Ben Nuttall and contributors; see :doc:`index` for current list .. include:: ../LICENSE.rst diff --git a/docs/migrating_from_rpigpio.rst b/docs/migrating_from_rpigpio.rst index e137d5963..7a575aa46 100644 --- a/docs/migrating_from_rpigpio.rst +++ b/docs/migrating_from_rpigpio.rst @@ -1,33 +1,13 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2019 Steveis -.. Copyright (c) 2019 Dave Jones -.. Copyright (c) 2019 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. .. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2019-2023 Dave Jones +.. Copyright (c) 2021 Rimas Misevičius +.. Copyright (c) 2019-2021 Ben Nuttall +.. Copyright (c) 2020 damosurfer <35042619+damosurfer@users.noreply.github.com> +.. Copyright (c) 2020 Andrew Scheller +.. Copyright (c) 2019 Steveis .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause .. _migrating_from_rpigpio: @@ -164,7 +144,7 @@ Wait for a pull-up button to be pressed in RPi.GPIO:: The equivalent in GPIO Zero:: - from gpiozero import Buttons + from gpiozero import Button btn = Button(4) @@ -186,7 +166,7 @@ for a falling edge, we're waiting for a rising edge:: Again, in GPIO Zero, the only difference is in the initialization:: - from gpiozero import Buttons + from gpiozero import Button btn = Button(4, pull_up=False) @@ -216,7 +196,7 @@ and the edge direction:: In GPIO Zero, you assign the :attr:`~Button.when_pressed` and :attr:`~Button.when_released` properties to set up callbacks on those actions:: - from gpiozero import Buttons + from gpiozero import Button def pressed(): print("button was pressed") @@ -293,7 +273,7 @@ In `RPi.GPIO`_:: pwm = GPIO.PWM(2, 100) pwm.start(0) - for dc in range(100): + for dc in range(101): pwm.changeDutyCycle(dc) sleep(0.01) @@ -304,8 +284,8 @@ In GPIO Zero:: led = PWMLED(2) - for b in range(100): - led.value = b / 100 + for b in range(101): + led.value = b / 100.0 sleep(0.01) :class:`PWMLED` has a :meth:`~PWMLED.blink` method which can be used the same @@ -331,6 +311,22 @@ Pin state cleanup is explicit in RPi.GPIO, and is done manually with pin used, at the end of the script. Manual cleanup is possible by use of the :meth:`~Device.close` method on the device. +Note that cleanup only occurs at the point of normal termination of the script. +If the script exits due to a program error, cleanup will not be performed. To +ensure that cleanup is performed after an exception is raised, the exception +must be handled, for example:: + + from gpiozero import Button + + btn = Button(4) + + while True: + try: + if btn.is_pressed: + print("Pressed") + except KeyboardInterrupt: + print("Ending program") + Read more in the relevant FAQ: :ref:`gpio-cleanup` diff --git a/docs/pi_zero_otg.rst b/docs/pi_zero_otg.rst index d17cc8636..fc6ba1e8f 100644 --- a/docs/pi_zero_otg.rst +++ b/docs/pi_zero_otg.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2019 Dave Jones -.. Copyright (c) 2018 Ben Nuttall .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: +.. Copyright (c) 2019-2023 Dave Jones +.. Copyright (c) 2018-2021 Ben Nuttall .. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause =============== Pi Zero USB OTG @@ -43,8 +20,9 @@ GPIO expander method - no SD card required The GPIO expander method allows you to boot the Pi Zero over USB from the PC, without an SD card. Your PC sends the required boot firmware to the Pi over the -USB cable, launching a mini version of Raspbian and booting it in RAM. The OS -then starts the pigpio daemon, allowing "remote" access over the USB cable. +USB cable, launching a mini version of Raspberry Pi OS and booting it in RAM. +The OS then starts the pigpio daemon, allowing "remote" access over the USB +cable. At the time of writing, this is only possible using either the Raspberry Pi Desktop x86 OS, or Ubuntu (or a derivative), or from another Raspberry Pi. @@ -53,16 +31,15 @@ Usage from Windows and Mac OS is not supported at present. Raspberry Pi Desktop x86 setup ------------------------------ -1. Download an ISO of the `Raspberry Pi Desktop OS`_ from raspberrypi.org (this - must be the Stretch release, not the older Jessie image). +1. Download an ISO of the `Raspberry Pi Desktop OS`_ from raspberrypi.com 2. Write the image to a USB stick or burn to a DVD. 3. Live boot your PC or Mac into the OS (select "Run with persistence" and your computer will be back to normal afterwards). -Raspberry Pi (Raspbian) setup ------------------------------ +Raspberry Pi setup (using Raspberry Pi OS) +------------------------------------------ 1. Update your package list and install the ``usbbootgui`` package: @@ -106,10 +83,9 @@ will automatically launch a prompt to select a role for the device. Select It will take 30 seconds or so to flash it, then the dialogue will disappear. -Raspberry Pi Desktop and Raspbian will name your Pi Zero connection ``usb0``. -On Ubuntu, this will likely be something else. You can ping it using the -address ``fe80::1%`` followed by the connection string. You can look this up -using ``ifconfig``. +Raspberry Pi OS will name your Pi Zero connection ``usb0``. On Ubuntu, this will +likely be something else. You can ping it using the address ``fe80::1%`` +followed by the connection string. You can look this up using ``ifconfig``. Set the :envvar:`GPIOZERO_PIN_FACTORY` and :envvar:`PIGPIO_ADDR` environment variables on your PC so GPIO Zero connects to the "remote" Pi Zero: @@ -129,16 +105,17 @@ Zero: Alternatively, you can set the pin factory in-line, as explained in :doc:`remote_gpio`. -Read more on the GPIO expander in blog posts on `raspberrypi.org`_ and +Read more on the GPIO expander in blog posts on `raspberrypi.com`_ and `bennuttall.com`_. Legacy method - SD card required ================================ -The legacy method requires the Pi Zero to have a Raspbian SD card inserted. +The legacy method requires the Pi Zero to have an SD card with Raspberry Pi OS +inserted. -Start by creating a Raspbian (desktop or lite) SD card, and then configure the -boot partition like so: +Start by creating a Raspberry Pi OS (desktop or lite) SD card, and then +configure the boot partition like so: 1. Edit :file:`config.txt` and add ``dtoverlay=dwc2`` on a new line, then save the file. @@ -167,10 +144,10 @@ IP address if you know it), for example: $ GPIOZERO_PIN_FACTORY=pigpio PIGPIO_ADDR=raspberrypi.local python3 led.py -.. _Raspberry Pi Zero: https://www.raspberrypi.org/products/raspberry-pi-zero/ -.. _Pi Zero W: https://www.raspberrypi.org/products/raspberry-pi-zero-w/ -.. _Raspberry Pi Desktop OS: https://www.raspberrypi.org/downloads/raspberry-pi-desktop/ -.. _raspberrypi.org: https://www.raspberrypi.org/blog/gpio-expander/ +.. _Raspberry Pi Zero: https://www.raspberrypi.com/products/raspberry-pi-zero/ +.. _Pi Zero W: https://www.raspberrypi.com/products/raspberry-pi-zero-w/ +.. _Raspberry Pi Desktop OS: https://www.raspberrypi.com/software/raspberry-pi-desktop/ +.. _raspberrypi.com: https://www.raspberrypi.com/news/gpio-expander/ .. _bennuttall.com: http://bennuttall.com/raspberry-pi-zero-gpio-expander/ .. _blog.gbaman.info: http://blog.gbaman.info/?p=791 .. _learn.adafruit.com: https://learn.adafruit.com/turning-your-raspberry-pi-zero-into-a-usb-gadget/ethernet-gadget diff --git a/docs/recipes.rst b/docs/recipes.rst index 5ee3934d6..701ef9797 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -1,34 +1,11 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2015-2019 Dave Jones -.. Copyright (c) 2016-2018 Ben Nuttall +.. +.. Copyright (c) 2015-2023 Dave Jones +.. Copyright (c) 2016-2019 Ben Nuttall .. Copyright (c) 2016 Barry Byford .. Copyright (c) 2016 Andrew Scheller .. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. -.. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. -.. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ============= Basic Recipes @@ -254,6 +231,24 @@ values using LED brightness: .. literalinclude:: examples/led_bargraph_2.py +LEDCharDisplay +============== + +.. image:: images/led_char_display_bb.* + +A common `7-segment display`_ can be used to represent a variety of characters +using :class:`LEDCharDisplay` (which actually supports an arbitrary number of +segments): + +.. literalinclude:: examples/led_char_display.py + +Alternatively: + +.. literalinclude:: examples/led_char_source.py + +See a multi-character example in the :ref:`advanced recipes +` chapter. + Traffic Lights ============== @@ -377,19 +372,43 @@ Run a function when something gets near the sensor: .. literalinclude:: examples/distance_sensor_2.py +Rotary encoder +============== + +.. image:: images/color_picker_bb.* + +.. note:: + + In this recipe, I've used a common *anode* RGB LED. Often, Pi projects use + common *cathode* RGB LEDs because they're slightly easier to think about + electrically. However, in this case all three components can be found in an + illuminated rotary encoder which incorporates a common anode RGB LED, and a + momentary push button. This is also the reason for the button being wired + active-low, contrary to most other examples in this documentation. + + For the sake of clarity, the diagram shows the three separate components, + but this same circuit will work equally well with this commonly available + `illuminated rotary encoder`_ instead. + +Have a :class:`RotaryEncoder`, an :class:`RGBLED`, and :class:`Button` act as a +color picker: + +.. literalinclude:: examples/color_picker.py + Servo ===== -Control a servo between its minimum, mid-point and maximum positions in -sequence: +Control a :class:`Servo` between its minimum, mid-point and maximum positions +in sequence: .. literalinclude:: examples/servo_1.py -Use a button to control the servo between its minimum and maximum positions: +Use a button to control the :class:`Servo` between its minimum and maximum +positions: .. literalinclude:: examples/servo_2.py -Automate the servo to continuously slowly sweep: +Automate the :class:`Servo` to continuously slowly sweep: .. literalinclude:: examples/servo_sweep.py @@ -415,8 +434,8 @@ Make a :class:`Robot` drive around in (roughly) a square: .. literalinclude:: examples/robot_1.py -Make a robot with a distance sensor that runs away when things get within -20cm of it: +Make a :class:`Robot` with a :class:`DistanceSensor` that runs away when things +get within 20cm of it: .. literalinclude:: examples/robot_2.py @@ -425,7 +444,7 @@ Button controlled robot .. image:: images/button_robot_bb.* -Use four GPIO buttons as forward/back/left/right controls for a robot: +Use four GPIO buttons as forward/back/left/right controls for a :class:`Robot`: .. literalinclude:: examples/robot_buttons_1.py @@ -434,7 +453,7 @@ Keyboard controlled robot .. image:: images/robot_bb.* -Use up/down/left/right keys to control a robot: +Use up/down/left/right keys to control a :class:`Robot`: .. literalinclude:: examples/robot_keyboard_1.py @@ -549,3 +568,5 @@ Continue to: .. _Quick Reaction Game: https://projects.raspberrypi.org/en/projects/python-quick-reaction-game .. _GPIO Music Box: https://projects.raspberrypi.org/en/projects/gpio-music-box .. _Energenie Pi-mote: https://energenie4u.co.uk/catalogue/product/ENER002-2PI +.. _illuminated rotary encoder: https://shop.pimoroni.com/products/rotary-encoder-illuminated-rgb +.. _7-segment display: https://en.wikipedia.org/wiki/Seven-segment_display diff --git a/docs/recipes_advanced.rst b/docs/recipes_advanced.rst index 4e128c496..0cdc3b50c 100644 --- a/docs/recipes_advanced.rst +++ b/docs/recipes_advanced.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2017-2019 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2017-2021 Dave Jones +.. Copyright (c) 2017-2019 Ben Nuttall .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ================ Advanced Recipes @@ -67,6 +44,47 @@ objects: .. literalinclude:: examples/led_board_7.py +.. _multichar-display: + +Multi-character 7-segment display +================================= + +The 7-segment display demonstrated in the previous chapter is often available +in multi-character variants (typically 4 characters long). Such displays are +multiplexed meaning that the LED pins are typically the same as for the single +character display but are shared across all characters. Each character in turn +then has its own common line which can be tied to ground (in the case of a +common cathode display) to enable that particular character. By activating each +character in turn very quickly, the eye can be fooled into thinking four +different characters are being displayed simultaneously. + +In such circuits you should not attempt to sink all the current from a single +character (which may have up to 8 LEDs, in the case of a decimal-point, active) +into a single GPIO. Rather, use some appropriate transistor (or similar +component, e.g. an opto-coupler) to tie the digit's cathode to ground, and +control that component from a GPIO. + +.. image:: images/7seg_multi_bb.* + +This circuit demonstrates a 4-character 7-segment (actually 8-segment, with +decimal-point) display, controlled by the Pi's GPIOs with 4 2N-3904 NPN +transistors to control the digits. + +.. warning:: + + You are strongly advised to check the data-sheet for your particular + multi-character 7-segment display. The pin-outs of these displays vary + significantly and are very likely to be different to that shown on the + breadboard above. For this reason, the schematic for this circuit is + provided below; adapt it to your particular display. + +.. image:: images/7seg_multi_schem.* + +The following code can be used to scroll a message across the display: + +.. literalinclude:: examples/multichar_scroll.py + + Who's home indicator ==================== diff --git a/docs/recipes_remote_gpio.rst b/docs/recipes_remote_gpio.rst index 244efdc20..84f7e1cf8 100644 --- a/docs/recipes_remote_gpio.rst +++ b/docs/recipes_remote_gpio.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2019 Dave Jones -.. Copyright (c) 2017 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2019-2023 Dave Jones +.. Copyright (c) 2017 Ben Nuttall .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause =================== Remote GPIO Recipes @@ -63,7 +40,7 @@ Multi-room motion alert ======================= Install a Raspberry Pi with a :class:`MotionSensor` in each room of your house, -and have an class:`LED` indicator showing when there's motion in each room: +and have an :class:`LED` indicator showing when there's motion in each room: .. literalinclude:: examples/multi_room_motion_alert.py @@ -105,4 +82,4 @@ Note that in this case, the Sense HAT code must be run locally, and the GPIO remotely. -.. _Sense HAT: https://www.raspberrypi.org/products/sense-hat/ +.. _Sense HAT: https://www.raspberrypi.com/products/sense-hat/ diff --git a/docs/remote_gpio.rst b/docs/remote_gpio.rst index 2cfb7eab1..0c3e78a2d 100644 --- a/docs/remote_gpio.rst +++ b/docs/remote_gpio.rst @@ -1,33 +1,10 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2017-2019 Ben Nuttall -.. Copyright (c) 2017 rgm -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2017-2021 Ben Nuttall +.. Copyright (c) 2017 rgm .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ======================= Configuring Remote GPIO @@ -53,9 +30,9 @@ used. Preparing the Raspberry Pi ========================== -If you're using Raspbian (desktop - not Raspbian Lite) then you have everything -you need to use the remote GPIO feature. If you're using Raspbian Lite, or -another distribution, you'll need to install pigpio: +If you're using Raspberry Pi OS (desktop - not Lite) then you have everything +you need to use the remote GPIO feature. If you're using Raspberry Pi OS Lite, +or another distribution, you'll need to install pigpio: .. code-block:: console @@ -70,8 +47,8 @@ Raspberry Pi. Enable remote connections ------------------------- -On the Raspbian desktop image, you can enable *Remote GPIO* in the Raspberry -Pi configuration tool: +On the Raspberry Pi OS desktop image, you can enable *Remote GPIO* in the +Raspberry Pi configuration tool: .. image:: images/raspi-config.png :align: center @@ -134,11 +111,11 @@ the ``-n`` flag. For example: Preparing the control computer ============================== -If the control computer (the computer you're running your Python code from) is -a Raspberry Pi running Raspbian (or a PC running `Raspberry Pi Desktop x86`_), -then you have everything you need. If you're using another Linux distribution, -Mac OS or Windows then you'll need to install the `pigpio`_ Python library on -the PC. +If the control computer (the computer you're running your Python code from) is a +Raspberry Pi running Raspberry Pi OS (or a PC running `Raspberry Pi Desktop +x86`_), then you have everything you need. If you're using another Linux +distribution, Mac OS or Windows then you'll need to install the `pigpio`_ Python +library on the PC. Raspberry Pi @@ -370,7 +347,7 @@ Continue to: .. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO .. _pigpio: http://abyz.me.uk/rpi/pigpio/python.html .. _abyz.me.uk: http://abyz.me.uk/rpi/pigpio/download.html -.. _Raspberry Pi Desktop x86: https://www.raspberrypi.org/downloads/raspberry-pi-desktop/ +.. _Raspberry Pi Desktop x86: https://www.raspberrypi.com/software/raspberry-pi-desktop/ .. _get-pip: https://pip.pypa.io/en/stable/installing/ .. _follow this guide: https://projects.raspberrypi.org/en/projects/using-pip-on-windows -.. _Sense HAT: https://www.raspberrypi.org/products/sense-hat/ +.. _Sense HAT: https://www.raspberrypi.com/products/sense-hat/ diff --git a/docs/source_values.rst b/docs/source_values.rst index d303f67ce..af15c35d1 100644 --- a/docs/source_values.rst +++ b/docs/source_values.rst @@ -1,32 +1,9 @@ .. GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -.. Copyright (c) 2017-2019 Dave Jones -.. Copyright (c) 2016-2019 Ben Nuttall -.. -.. Redistribution and use in source and binary forms, with or without -.. modification, are permitted provided that the following conditions are met: -.. -.. * Redistributions of source code must retain the above copyright notice, -.. this list of conditions and the following disclaimer. .. -.. * Redistributions in binary form must reproduce the above copyright notice, -.. this list of conditions and the following disclaimer in the documentation -.. and/or other materials provided with the distribution. -.. -.. * Neither the name of the copyright holder nor the names of its contributors -.. may be used to endorse or promote products derived from this software -.. without specific prior written permission. +.. Copyright (c) 2017-2023 Dave Jones +.. Copyright (c) 2016-2019 Ben Nuttall .. -.. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -.. AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -.. IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -.. ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -.. LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -.. CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -.. SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -.. INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -.. CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -.. ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -.. POSSIBILITY OF SUCH DAMAGE. +.. SPDX-License-Identifier: BSD-3-Clause ============= Source/Values diff --git a/gpiozero/__init__.py b/gpiozero/__init__.py index 9ff0b8c9b..b318d66ca 100644 --- a/gpiozero/__init__.py +++ b/gpiozero/__init__.py @@ -1,8 +1,12 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2015-2019 Ben Nuttall +# +# Copyright (c) 2015-2023 Dave Jones +# Copyright (c) 2015-2021 Ben Nuttall # Copyright (c) 2019 tuftii <3215045+tuftii@users.noreply.github.com> # Copyright (c) 2019 Jeevan M R <14.jeevan@gmail.com> +# Copyright (c) 2019 ForToffee # Copyright (c) 2018 Claire Pollard # Copyright (c) 2016 pcopa # Copyright (c) 2016 Ian Harcombe @@ -10,48 +14,18 @@ # Copyright (c) 2016 Andrew Scheller # Copyright (c) 2015 Philip Howard # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, -) +# SPDX-License-Identifier: BSD-3-Clause from .pins import ( Factory, Pin, SPI, -) -from .pins.data import ( - PiBoardInfo, + BoardInfo, HeaderInfo, PinInfo, +) +from .pins.pi import ( + PiBoardInfo, pi_info, ) # Yes, import * is naughty, but exc imports nothing else so there's no cross @@ -67,6 +41,7 @@ SourceMixin, ValuesMixin, EventsMixin, + event, HoldMixin, ) from .input_devices import ( @@ -78,6 +53,7 @@ MotionSensor, LightSensor, DistanceSensor, + RotaryEncoder, ) from .spi_devices import ( SPIDevice, @@ -114,6 +90,9 @@ LEDCollection, LEDBoard, LEDBarGraph, + LEDCharDisplay, + LEDMultiCharDisplay, + LEDCharFont, LedBorg, PiHutXmasTree, PiLiter, @@ -127,17 +106,20 @@ TrafficLightsBuzzer, FishDish, TrafficHat, + TrafficpHat, Robot, RyanteckRobot, CamJamKitRobot, - PhaseEnableRobot, PololuDRV8835Robot, + PhaseEnableRobot, Energenie, PumpkinPi, JamHat, + Pibrella, ) from .internal_devices import ( InternalDevice, + PolledInternalDevice, PingServer, CPUTemperature, LoadAverage, diff --git a/gpiozero/boards.py b/gpiozero/boards.py index 4310df499..7abc9f4f1 100644 --- a/gpiozero/boards.py +++ b/gpiozero/boards.py @@ -1,63 +1,45 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Andrew Scheller -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2015-2019 Ben Nuttall +# +# Copyright (c) 2015-2024 Dave Jones +# Copyright (c) 2016-2022 Andrew Scheller +# Copyright (c) 2015-2021 Ben Nuttall +# Copyright (c) 2020 Ryan Walmsley +# Copyright (c) 2020 Jack Wearden # Copyright (c) 2019 tuftii <3215045+tuftii@users.noreply.github.com> +# Copyright (c) 2019 ForToffee # Copyright (c) 2018 SteveAmor # Copyright (c) 2018 Rick Ansell # Copyright (c) 2018 Claire Pollard # Copyright (c) 2016 Ian Harcombe # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -try: - from itertools import izip as zip -except ImportError: - pass +# SPDX-License-Identifier: BSD-3-Clause +import warnings from time import sleep -from itertools import repeat, cycle, chain +from itertools import repeat, cycle, chain, tee from threading import Lock -from collections import OrderedDict, Counter +from collections import OrderedDict, Counter, namedtuple +from collections.abc import MutableMapping +from pprint import pformat + +# Remove the try clause when 3.7 support is no longer trivial +try: + import importlib_resources as resources +except ImportError: + from importlib import resources from .exc import ( DeviceClosed, + PinInvalidPin, GPIOPinMissing, EnergenieSocketMissing, EnergenieBadSocket, - EnergenieBadInitialValue, OutputDeviceBadValue, + CompositeDeviceBadDevice, + BadWaitTime, ) from .input_devices import Button from .output_devices import ( @@ -72,7 +54,14 @@ ) from .threads import GPIOThread from .devices import Device, CompositeDevice -from .mixins import SharedMixin, SourceMixin, HoldMixin +from .mixins import SharedMixin, SourceMixin, HoldMixin, event +from .fonts import load_font_7seg, load_font_14seg + + +def pairwise(it): + a, b = tee(it) + next(b, None) + return zip(a, b) class CompositeOutputDevice(SourceMixin, CompositeDevice): @@ -138,7 +127,7 @@ def value(self): A tuple containing a value for each subordinate device. This property can also be set to update the state of all subordinate output devices. """ - return super(CompositeOutputDevice, self).value + return super().value @value.setter def value(self, value): @@ -160,18 +149,21 @@ class ButtonBoard(HoldMixin, CompositeDevice): leds = LEDBoard(2, 3, 4, 5) btns = ButtonBoard(6, 7, 8, 9) - leds.source = btns.values + leds.source = btns + pause() Alternatively you could represent the number of pressed buttons with an :class:`LEDBarGraph`:: from gpiozero import LEDBarGraph, ButtonBoard + from statistics import mean from signal import pause graph = LEDBarGraph(2, 3, 4, 5) - btns = ButtonBoard(6, 7, 8, 9) - graph.source = (sum(value) for value in btn.values) + bb = ButtonBoard(6, 7, 8, 9) + graph.source = (mean(values) for values in bb.values) + pause() :type pins: int or str @@ -220,28 +212,23 @@ class ButtonBoard(HoldMixin, CompositeDevice): many pins as necessary and use any names, provided they're not already in use by something else. """ - def __init__(self, *args, **kwargs): - pull_up = kwargs.pop('pull_up', True) - active_state = kwargs.pop('active_state', None) - bounce_time = kwargs.pop('bounce_time', None) - hold_time = kwargs.pop('hold_time', 1) - hold_repeat = kwargs.pop('hold_repeat', False) - pin_factory = kwargs.pop('pin_factory', None) - order = kwargs.pop('_order', None) - super(ButtonBoard, self).__init__( + def __init__(self, *pins, pull_up=True, active_state=None, + bounce_time=None, hold_time=1, hold_repeat=False, + _order=None, pin_factory=None, **named_pins): + super().__init__( *( Button(pin, pull_up=pull_up, active_state=active_state, bounce_time=bounce_time, hold_time=hold_time, - hold_repeat=hold_repeat) - for pin in args + hold_repeat=hold_repeat, pin_factory=pin_factory) + for pin in pins ), - _order=order, + _order=_order, pin_factory=pin_factory, **{ name: Button(pin, pull_up=pull_up, active_state=active_state, bounce_time=bounce_time, hold_time=hold_time, - hold_repeat=hold_repeat) - for name, pin in kwargs.items() + hold_repeat=hold_repeat, pin_factory=pin_factory) + for name, pin in named_pins.items() } ) if len(self) == 0: @@ -249,7 +236,7 @@ def __init__(self, *args, **kwargs): def get_new_handler(device): def fire_both_events(ticks, state): device._fire_events(ticks, device._state_to_value(state)) - self._fire_events(ticks, self.value) + self._fire_events(ticks, self.is_active) return fire_both_events # _handlers only exists to ensure that we keep a reference to the # generated fire_both_events handler for each Button (remember that @@ -272,20 +259,14 @@ def pull_up(self): """ return self[0].pull_up - @property - def when_changed(self): - return self._when_changed - - @when_changed.setter - def when_changed(self, value): - self._when_changed = self._wrap_callback(value) + when_changed = event() def _fire_changed(self): if self.when_changed: self.when_changed() def _fire_events(self, ticks, new_value): - super(ButtonBoard, self)._fire_events(ticks, new_value) + super()._fire_events(ticks, new_value) old_value, self._last_value = self._last_value, new_value if old_value is None: # Initial "indeterminate" value; don't do anything @@ -306,33 +287,29 @@ class LEDCollection(CompositeOutputDevice): Extends :class:`CompositeOutputDevice`. Abstract base class for :class:`LEDBoard` and :class:`LEDBarGraph`. """ - def __init__(self, *args, **kwargs): - pwm = kwargs.pop('pwm', False) - active_high = kwargs.pop('active_high', True) - initial_value = kwargs.pop('initial_value', False) - pin_factory = kwargs.pop('pin_factory', None) - order = kwargs.pop('_order', None) + def __init__(self, *pins, pwm=False, active_high=True, initial_value=False, + _order=None, pin_factory=None, **named_pins): LEDClass = PWMLED if pwm else LED - super(LEDCollection, self).__init__( + super().__init__( *( pin_or_collection if isinstance(pin_or_collection, LEDCollection) else LEDClass( - pin_or_collection, active_high, initial_value, - pin_factory=pin_factory + pin_or_collection, active_high=active_high, + initial_value=initial_value, pin_factory=pin_factory ) - for pin_or_collection in args + for pin_or_collection in pins ), - _order=order, + _order=_order, pin_factory=pin_factory, **{ name: pin_or_collection if isinstance(pin_or_collection, LEDCollection) else LEDClass( - pin_or_collection, active_high, initial_value, - pin_factory=pin_factory + pin_or_collection, active_high=active_high, + initial_value=initial_value, pin_factory=pin_factory ) - for name, pin_or_collection in kwargs.items() + for name, pin_or_collection in named_pins.items() } ) if len(self) == 0: @@ -359,6 +336,9 @@ def active_high(self): return self[0].active_high +LEDCollection.is_lit = LEDCollection.is_active + + class LEDBoard(LEDCollection): """ Extends :class:`LEDCollection` and represents a generic LED board or @@ -395,6 +375,14 @@ class LEDBoard(LEDCollection): found in when configured for output (warning: this can be on). If :data:`True`, the device will be switched on initially. + :type _order: list or None + :param _order: + If specified, this is the order of named items specified by keyword + arguments (to ensure that the :attr:`value` tuple is constructed with a + specific order). All keyword arguments *must* be included in the + collection. If omitted, an alphabetically sorted order will be selected + for keyword arguments. + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature @@ -408,18 +396,21 @@ class LEDBoard(LEDCollection): something else. You can also specify :class:`LEDBoard` instances to create trees of LEDs. """ - def __init__(self, *args, **kwargs): + def __init__(self, *pins, pwm=False, active_high=True, initial_value=False, + _order=None, pin_factory=None, **named_pins): self._blink_thread = None self._blink_leds = [] self._blink_lock = Lock() - super(LEDBoard, self).__init__(*args, **kwargs) + super().__init__(*pins, pwm=pwm, active_high=active_high, + initial_value=initial_value, _order=_order, + pin_factory=pin_factory, **named_pins) def close(self): try: self._stop_blink() except AttributeError: pass - super(LEDBoard, self).close() + super().close() def on(self, *args): """ @@ -447,7 +438,7 @@ def on(self, *args): for index in args: self[index].on() else: - super(LEDBoard, self).on() + super().on() def off(self, *args): """ @@ -475,7 +466,7 @@ def off(self, *args): for index in args: self[index].off() else: - super(LEDBoard, self).off() + super().off() def toggle(self, *args): """ @@ -502,7 +493,7 @@ def toggle(self, *args): for index in args: self[index].toggle() else: - super(LEDBoard, self).toggle() + super().toggle() def blink( self, on_time=1, off_time=1, fade_in_time=0, fade_out_time=0, @@ -544,8 +535,8 @@ def blink( raise ValueError('fade_out_time must be 0 with non-PWM LEDs') self._stop_blink() self._blink_thread = GPIOThread( - target=self._blink_device, - args=(on_time, off_time, fade_in_time, fade_out_time, n)) + self._blink_device, + (on_time, off_time, fade_in_time, fade_out_time, n)) self._blink_thread.start() if not background: self._blink_thread.join() @@ -679,20 +670,14 @@ class LEDBarGraph(LEDCollection): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, *pins, **kwargs): + def __init__(self, *pins, pwm=False, active_high=True, initial_value=0.0, + pin_factory=None): # Don't allow graphs to contain collections for pin in pins: if isinstance(pin, Device): raise CompositeDeviceBadDevice( 'Only pins may be specified for LEDBarGraph') - pwm = kwargs.pop('pwm', False) - active_high = kwargs.pop('active_high', True) - initial_value = kwargs.pop('initial_value', 0.0) - pin_factory = kwargs.pop('pin_factory', None) - if kwargs: - raise TypeError( - 'unexpected keyword argument: %s' % kwargs.popitem()[0]) - super(LEDBarGraph, self).__init__( + super().__init__( *pins, pwm=pwm, active_high=active_high, pin_factory=pin_factory) try: self.value = initial_value @@ -763,6 +748,497 @@ def lit_count(self, value): self.value = value / len(self) +class LEDCharFont(MutableMapping): + """ + Contains a mapping of values to tuples of LED states. + + This effectively acts as a "font" for :class:`LEDCharDisplay`, and two + default fonts (for 7-segment and 14-segment displays) are shipped with GPIO + Zero by default. You can construct your own font instance from a + :class:`dict` which maps values (usually single-character strings) to + a tuple of LED states:: + + from gpiozero import LEDCharDisplay, LEDCharFont + + my_font = LEDCharFont({ + ' ': (0, 0, 0, 0, 0, 0, 0), + 'D': (1, 1, 1, 1, 1, 1, 0), + 'A': (1, 1, 1, 0, 1, 1, 1), + 'd': (0, 1, 1, 1, 1, 0, 1), + 'a': (1, 1, 1, 1, 1, 0, 1), + }) + display = LEDCharDisplay(26, 13, 12, 22, 17, 19, 6, dp=5, font=my_font) + display.value = 'D' + + Font instances are mutable and can be changed while actively in use by + an instance of :class:`LEDCharDisplay`. However, changing the font will + *not* change the state of the LEDs in the display (though it may change + the :attr:`~LEDCharDisplay.value` of the display when next queried). + + .. note:: + + Your custom mapping should always include a value (typically space) + which represents all the LEDs off. This will usually be the default + value for an instance of :class:`LEDCharDisplay`. + + You may also wish to load fonts from a friendly text-based format. A simple + parser for such formats (supporting an arbitrary number of segments) is + provided by :func:`gpiozero.fonts.load_segment_font`. + """ + def __init__(self, font): + super().__init__() + self._map = { + char: tuple(int(bool(pin)) for pin in pins) + for char, pins in font.items() + } + self._refresh_rmap() + + def __repr__(self): + return f'{self.__class__.__name__}({pformat(self._map, indent=4)})' + + def _refresh_rmap(self): + # The reverse mapping is pre-calculated for speed of lookup. Given that + # the font mapping can be 1:n, we cannot guarantee the reverse is + # unique. In case the provided font is an ordered dictionary, we + # explicitly take only the first definition of each non-unique pin + # definition so that value lookups are predictable + rmap = {} + for char, pins in self._map.items(): + rmap.setdefault(pins, char) + self._rmap = rmap + + def __len__(self): + return len(self._map) + + def __iter__(self): + return iter(self._map) + + def __getitem__(self, char): + return self._map[char] + + def __setitem__(self, char, pins): + try: + # This is necessary to ensure that _rmap is correct in the case + # that we're overwriting an existing char->pins mapping + del self[char] + except KeyError: + pass + pins = tuple(int(bool(pin)) for pin in pins) + self._map[char] = pins + self._rmap.setdefault(pins, char) + + def __delitem__(self, char): + pins = self._map[char] + del self._map[char] + # If the reverse mapping of the char's pins maps to the char we need + # to find if it now maps to another char (given the n:1 mapping) + if self._rmap[pins] == char: + del self._rmap[pins] + for char, char_pins in self._map.items(): + if pins == char_pins: + self._rmap[pins] = char + break + + +class LEDCharDisplay(LEDCollection): + """ + Extends :class:`LEDCollection` for a multi-segment LED display. + + `Multi-segment LED displays`_ typically have 7 pins (labelled "a" through + "g") representing 7 LEDs layed out in a figure-of-8 fashion. Frequently, an + eigth pin labelled "dp" is included for a trailing decimal-point: + + .. code-block:: text + + a + ━━━━━ + f ┃ ┃ b + ┃ g ┃ + ━━━━━ + e ┃ ┃ c + ┃ ┃ + ━━━━━ • dp + d + + Other common layouts are 9, 14, and 16 segment displays which include + additional segments permitting more accurate renditions of alphanumerics. + For example: + + .. code-block:: text + + a + ━━━━━ + f ┃╲i┃j╱┃ b + ┃ ╲┃╱k┃ + g━━ ━━h + e ┃ ╱┃╲n┃ c + ┃╱l┃m╲┃ + ━━━━━ • dp + d + + Such displays have either a common anode, or common cathode pin. This class + defaults to the latter; when using a common anode display *active_high* + should be set to :data:`False`. + + Instances of this class can be used to display characters or control + individual LEDs on the display. For example:: + + from gpiozero import LEDCharDisplay + + char = LEDCharDisplay(4, 5, 6, 7, 8, 9, 10, active_high=False) + char.value = 'C' + + If the class is constructed with 7 or 14 segments, a default :attr:`font` + will be loaded, mapping some ASCII characters to typical layouts. In other + cases, the default mapping will simply assign " " (space) to all LEDs off. + You can assign your own mapping at construction time or after + instantiation. + + While the example above shows the display with a :class:`str` value, + theoretically the *font* can map any value that can be the key in a + :class:`dict`, so the value of the display can be likewise be any valid + key value (e.g. you could map integer digits to LED patterns). That said, + there is one exception to this: when *dp* is specified to enable the + decimal-point, the :attr:`value` must be a :class:`str` as the presence + or absence of a "." suffix indicates whether the *dp* LED is lit. + + :type pins: int or str + :param \\*pins: + Specify the GPIO pins that the multi-segment display is attached to. + Pins should be in the LED segment order A, B, C, D, E, F, G, and will + be named automatically by the class. If a decimal-point pin is + present, specify it separately as the *dp* parameter. + + :type dp: int or str + :param dp: + If a decimal-point segment is present, specify it as this named + parameter. + + :type font: dict or None + :param font: + A mapping of values (typically characters, but may also be numbers) to + tuples of LED states. A default mapping for ASCII characters is + provided for 7 and 14 segment displays. + + :param bool pwm: + If :data:`True`, construct :class:`PWMLED` instances for each pin. If + :data:`False` (the default), construct regular :class:`LED` instances. + + :param bool active_high: + If :data:`True` (the default), the :meth:`on` method will set all the + associated pins to HIGH. If :data:`False`, the :meth:`on` method will + set all pins to LOW (the :meth:`off` method always does the opposite). + + :param initial_value: + The initial value to display. Defaults to space (" ") which typically + maps to all LEDs being inactive. If :data:`None`, each device will be + left in whatever state the pin is found in when configured for output + (warning: this can be on). + + :type pin_factory: Factory or None + :param pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + + .. _Multi-segment LED displays: https://en.wikipedia.org/wiki/Seven-segment_display + """ + def __init__(self, *pins, dp=None, font=None, pwm=False, active_high=True, + initial_value=" ", pin_factory=None): + if not 1 < len(pins) <= 26: + raise PinInvalidPin( + 'Must have between 2 and 26 LEDs in LEDCharDisplay') + for pin in pins: + if isinstance(pin, LEDCollection): + raise PinInvalidPin( + 'Cannot use LEDCollection in LEDCharDisplay') + + if font is None: + if len(pins) == 7: + with resources.files('gpiozero.fonts').joinpath('7seg.txt').open() as f: + font = load_font_7seg(f) + elif len(pins) == 14: + with resources.files('gpiozero.fonts').joinpath('14seg.txt').open() as f: + font = load_font_14seg(f) + else: + # Construct a default dict containing a definition for " " + font = {" ": (0,) * len(pins)} + self._font = LEDCharFont(font) + + pins = {chr(ord('a') + i): pin for i, pin in enumerate(pins)} + order = sorted(pins.keys()) + if dp is not None: + pins['dp'] = dp + order.append('dp') + super().__init__( + pwm=pwm, active_high=active_high, initial_value=None, + _order=order, pin_factory=pin_factory, **pins) + if initial_value is not None: + self.value = initial_value + + @property + def font(self): + """ + An :class:`LEDCharFont` mapping characters to tuples of LED states. + The font is mutable after construction. You can assign a tuple of LED + states to a character to modify the font, delete an existing character + in the font, or assign a mapping of characters to tuples to replace the + entire font. + + Note that modifying the :attr:`font` never alters the underlying LED + states. Only assignment to :attr:`value`, or calling the inherited + :class:`LEDCollection` methods (:meth:`on`, :meth:`off`, etc.) modifies + LED states. However, modifying the font may alter the character + returned by querying :attr:`value`. + """ + return self._font + + @font.setter + def font(self, value): + self._font = LEDCharFont(value) + + @property + def value(self): + """ + The character the display should show. This is mapped by the current + :attr:`font` to a tuple of LED states which is applied to the + underlying LED objects when this attribute is set. + + When queried, the current LED states are looked up in the font to + determine the character shown. If the current LED states do not + correspond to any character in the :attr:`font`, the value is + :data:`None`. + + It is possible for multiple characters in the font to map to the same + LED states (e.g. S and 5). In this case, if the font was constructed + from an ordered mapping (which is the default), then the first matching + mapping will always be returned. This also implies that the value + queried need not match the value set. + """ + state = super().value + if hasattr(self, 'dp'): + state, dp = state[:-1], state[-1] + else: + dp = False + try: + result = self._font._rmap[state] + except KeyError: + # Raising exceptions on lookup is problematic; in case the LED + # state is not representable we simply return None (although + # technically that is a valid item we can map :) + return None + else: + if dp: + return result + '.' + else: + return result + + @value.setter + def value(self, value): + for led, v in zip(self, self._parse_state(value)): + led.value = v + + def _parse_state(self, value): + if hasattr(self, 'dp'): + if len(value) > 1 and value.endswith('.'): + value = value[:-1] + dp = 1 + else: + dp = 0 + return self._font[value] + (dp,) + else: + return self._font[value] + + +class LEDMultiCharDisplay(CompositeOutputDevice): + """ + Wraps :class:`LEDCharDisplay` for multi-character `multiplexed`_ LED + character displays. + + The class is constructed with a *char* which is an instance of the + :class:`LEDCharDisplay` class, capable of controlling the LEDs in one + character of the display, and an additional set of *pins* that represent + the common cathode (or anode) of each character. + + .. warning:: + + You should not attempt to connect the common cathode (or anode) off + each character directly to a GPIO. Rather, use a set of transistors (or + some other suitable component capable of handling the current of all + the segment LEDs simultaneously) to connect the common cathode to + ground (or the common anode to the supply) and control those + transistors from the GPIOs specified under *pins*. + + The *active_high* parameter defaults to :data:`True`. Note that it only + applies to the specified *pins*, which are assumed to be controlling a set + of transistors (hence the default). The specified *char* will use its own + *active_high* parameter. Finally, *initial_value* defaults to a tuple of + :attr:`~LEDCharDisplay.value` attribute of the specified display multiplied + by the number of *pins* provided. + + When the :attr:`value` is set such that one or more characters in the + display differ in value, a background thread is implicitly started to + rotate the active character, relying on `persistence of vision`_ to display + the complete value. + + .. _multiplexed: https://en.wikipedia.org/wiki/Multiplexed_display + .. _persistence of vision: https://en.wikipedia.org/wiki/Persistence_of_vision + """ + def __init__(self, char, *pins, active_high=True, initial_value=None, + pin_factory=None): + if not isinstance(char, LEDCharDisplay): + raise ValueError('char must be an LEDCharDisplay') + if initial_value is None: + initial_value = (char.value,) * len(pins) + if pin_factory is None: + pin_factory = char.pin_factory + self._plex_thread = None + self._plex_delay = 0.005 + plex = CompositeOutputDevice(*( + OutputDevice( + pin, active_high=active_high, initial_value=None, + pin_factory=pin_factory) + for pin in pins + )) + super().__init__( + plex=plex, char=char, pin_factory=pin_factory) + self.value = initial_value + + def close(self): + try: + self._stop_plex() + except AttributeError: + pass + super().close() + + def _stop_plex(self): + if self._plex_thread: + self._plex_thread.stop() + self._plex_thread = None + + @property + def plex_delay(self): + """ + The delay (measured in seconds) in the loop used to switch each + character in the multiplexed display on. Defaults to 0.005 seconds + which is generally sufficient to provide a "stable" (non-flickery) + display. + """ + return self._plex_delay + + @plex_delay.setter + def plex_delay(self, value): + if value < 0: + raise BadWaitTime('plex_delay must be 0 or greater') + self._plex_delay = float(value) + + @property + def value(self): + """ + The sequence of values to display. + + This can be any sequence containing keys from the + :attr:`~LEDCharDisplay.font` of the associated character display. For + example, if the value consists only of single-character strings, it's + valid to assign a string to this property (as a string is simply a + sequence of individual character keys):: + + from gpiozero import LEDCharDisplay, LEDMultiCharDisplay + + c = LEDCharDisplay(4, 5, 6, 7, 8, 9, 10) + d = LEDMultiCharDisplay(c, 19, 20, 21, 22) + d.value = 'LEDS' + + However, things get more complicated if a decimal point is in use as + then this class needs to know explicitly where to break the value for + use on each character of the display. This can be handled by simply + assigning a sequence of strings thus:: + + from gpiozero import LEDCharDisplay, LEDMultiCharDisplay + + c = LEDCharDisplay(4, 5, 6, 7, 8, 9, 10) + d = LEDMultiCharDisplay(c, 19, 20, 21, 22) + d.value = ('L.', 'E', 'D', 'S') + + This is how the value will always be represented when queried (as a + tuple of individual values) as it neatly handles dealing with + heterogeneous types and the aforementioned decimal point issue. + + .. note:: + + The value also controls whether a background thread is in use to + multiplex the display. When all positions in the value are equal + the background thread is disabled and all characters are + simultaneously enabled. + """ + return self._value + + @value.setter + def value(self, value): + if len(value) > len(self.plex): + raise ValueError( + 'length of value must not exceed the number of characters in ' + 'the display') + elif len(value) < len(self.plex): + # Right-align the short value on the display + value = (' ',) * (len(self.plex) - len(value)) + tuple(value) + else: + value = tuple(value) + + # Get the list of tuples of states that the character LEDs will pass + # through. Prune any entirely blank state (which we can skip by never + # activating the plex for them) but remember which plex index each + # (non-blank) state is associated with + states = {} + for index, char in enumerate(value): + state = self.char._parse_state(char) + if any(state): + states.setdefault(state, set()).add(index) + # Calculate the transitions between states for an ordering of chars + # based on activated LEDs. This a vague attempt at minimizing the + # number of LEDs that need flipping between chars; to do this + # "properly" is O(n!) which gets silly quickly so ... fudge it + order = sorted(states) + if len(order) > 1: + transitions = [ + [(self.plex[index], 0) for index in states[old]] + + [ + (led, new_value) + for led, old_value, new_value in zip(self.char, old, new) + if old_value ^ new_value + ] + + [(self.plex[index], 1) for index in states[new]] + for old, new in pairwise(order + [order[0]]) + ] + else: + transitions = [] + + # Stop any current display thread and disable the display + self._stop_plex() + self.plex.off() + + # If there's any characters to display, set the character LEDs to the + # state of the first character in the display order. If there's + # transitions to display, activate the plex thread; otherwise, just + # switch on each plex with a char to display + if order: + for led, state in zip(self.char, order[0]): + led.value = state + if transitions: + self._plex_thread = GPIOThread(self._show_chars, (transitions,)) + self._plex_thread.start() + else: + for index in states[order[0]]: + self.plex[index].on() + self._value = value + + def _show_chars(self, transitions): + for transition in cycle(transitions): + for device, value in transition: + device.value = value + if self._plex_thread.stopping.wait(self._plex_delay): + break + + class PiHutXmasTree(LEDBoard): """ Extends :class:`LEDBoard` for `The Pi Hut's Xmas board`_: a 3D Christmas @@ -834,13 +1310,13 @@ class PiHutXmasTree(LEDBoard): so on but for the sake of brevity we represent all 24 under this section. """ - def __init__(self, pwm=False, initial_value=False, pin_factory=None): + def __init__(self, *, pwm=False, initial_value=False, pin_factory=None): pins_dict = OrderedDict(star=2) pins = (4, 15, 13, 21, 25, 8, 5, 10, 16, 17, 27, 26, 24, 9, 12, 6, 20, 19, 14, 18, 11, 7, 23, 22) for i, pin in enumerate(pins): - pins_dict['led%d' % (i+1)] = pin - super(PiHutXmasTree, self).__init__( + pins_dict[f'led{i + 1:d}'] = pin + super().__init__( pwm=pwm, initial_value=initial_value, _order=pins_dict.keys(), pin_factory=pin_factory, @@ -879,10 +1355,11 @@ class LedBorg(RGBLED): .. _PiBorg LedBorg: https://www.piborg.org/ledborg """ - def __init__(self, initial_value=(0, 0, 0), pwm=True, pin_factory=None): - super(LedBorg, self).__init__(red=17, green=27, blue=22, - pwm=pwm, initial_value=initial_value, - pin_factory=pin_factory) + def __init__(self, *, initial_value=(0, 0, 0), pwm=True, pin_factory=None): + super().__init__( + red='BOARD11', green='BOARD13', blue='BOARD15', + pwm=pwm, initial_value=initial_value, pin_factory=pin_factory + ) class PiLiter(LEDBoard): @@ -918,10 +1395,12 @@ class PiLiter(LEDBoard): .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ - def __init__(self, pwm=False, initial_value=False, pin_factory=None): - super(PiLiter, self).__init__(4, 17, 27, 18, 22, 23, 24, 25, - pwm=pwm, initial_value=initial_value, - pin_factory=pin_factory) + def __init__(self, *, pwm=False, initial_value=False, pin_factory=None): + pins = ('BOARD7', 'BOARD11', 'BOARD13', 'BOARD12', + 'BOARD15', 'BOARD16', 'BOARD18', 'BOARD22') + super().__init__( + *pins, pwm=pwm, initial_value=initial_value, pin_factory=pin_factory + ) class PiLiterBarGraph(LEDBarGraph): @@ -954,9 +1433,10 @@ class PiLiterBarGraph(LEDBarGraph): .. _Ciseco Pi-LITEr: http://shop.ciseco.co.uk/pi-liter-8-led-strip-for-the-raspberry-pi/ """ - def __init__(self, pwm=False, initial_value=0.0, pin_factory=None): - pins = (4, 17, 27, 18, 22, 23, 24, 25) - super(PiLiterBarGraph, self).__init__( + def __init__(self, *, pwm=False, initial_value=0.0, pin_factory=None): + pins = ('BOARD7', 'BOARD11', 'BOARD13', 'BOARD12', + 'BOARD15', 'BOARD16', 'BOARD18', 'BOARD22') + super().__init__( *pins, pwm=pwm, initial_value=initial_value, pin_factory=pin_factory ) @@ -1032,7 +1512,7 @@ class TrafficLights(LEDBoard): The green :class:`LED` or :class:`PWMLED`. """ - def __init__(self, red=None, amber=None, green=None, + def __init__(self, red=None, amber=None, green=None, *, pwm=False, initial_value=False, yellow=None, pin_factory=None): if amber is not None and yellow is not None: @@ -1046,9 +1526,9 @@ def __init__(self, red=None, amber=None, green=None, devices['amber'] = amber devices['green'] = green if not all(p is not None for p in devices.values()): - raise GPIOPinMissing('%s pins must be provided' % - ', '.join(devices.keys())) - super(TrafficLights, self).__init__( + raise GPIOPinMissing( + f'{", ".join(devices.keys())} pins must be provided') + super().__init__( pwm=pwm, initial_value=initial_value, _order=devices.keys(), pin_factory=pin_factory, **devices) @@ -1058,7 +1538,7 @@ def __getattr__(self, name): name = 'yellow' elif name == 'yellow' and not self._display_yellow: name = 'amber' - return super(TrafficLights, self).__getattr__(name) + return super().__getattr__(name) class PiTraffic(TrafficLights): @@ -1097,10 +1577,11 @@ class PiTraffic(TrafficLights): .. _Low Voltage Labs PI-TRAFFIC: http://lowvoltagelabs.com/products/pi-traffic/ """ - def __init__(self, pwm=False, initial_value=False, pin_factory=None): - super(PiTraffic, self).__init__(9, 10, 11, - pwm=pwm, initial_value=initial_value, - pin_factory=pin_factory) + def __init__(self, *, pwm=False, initial_value=False, pin_factory=None): + super().__init__( + 'BOARD21', 'BOARD19', 'BOARD23', + pwm=pwm, initial_value=initial_value, pin_factory=pin_factory + ) class PiStop(TrafficLights): @@ -1141,22 +1622,22 @@ class PiStop(TrafficLights): .. _location: https://github.com/PiHw/Pi-Stop/blob/master/markdown_source/markdown/Discover-PiStop.md """ LOCATIONS = { - 'A': (7, 8, 25), - 'A+': (21, 20, 16), - 'B': (10, 9, 11), - 'B+': (13, 19, 26), - 'C': (18, 15, 14), - 'D': (2, 3, 4), + 'A': ('BOARD26', 'BOARD24', 'BOARD22'), + 'A+': ('BOARD40', 'BOARD38', 'BOARD36'), + 'B': ('BOARD19', 'BOARD21', 'BOARD23'), + 'B+': ('BOARD33', 'BOARD35', 'BOARD37'), + 'C': ('BOARD12', 'BOARD10', 'BOARD8'), + 'D': ('BOARD3', 'BOARD5', 'BOARD7'), } def __init__( - self, location=None, pwm=False, initial_value=False, + self, location=None, *, pwm=False, initial_value=False, pin_factory=None): gpios = self.LOCATIONS.get(location, None) if gpios is None: - raise ValueError('location must be one of: %s' % - ', '.join(sorted(self.LOCATIONS.keys()))) - super(PiStop, self).__init__( + locations = ', '.join(sorted(self.LOCATIONS.keys())) + raise ValueError(f'location must be one of: {locations}') + super().__init__( *gpios, pwm=pwm, initial_value=initial_value, pin_factory=pin_factory) @@ -1186,6 +1667,18 @@ class StatusZero(LEDBoard): not all strips are given labels, any remaining strips will not be initialised. + :param bool pwm: + If :data:`True`, construct :class:`PWMLED` instances to represent each + LED. If :data:`False` (the default), construct regular :class:`LED` + instances. + + :type initial_value: bool or None + :param bool initial_value: + If :data:`False` (the default), all LEDs will be off initially. If + :data:`None`, each device will be left in whatever state the pin is + found in when configured for output (warning: this can be on). If + :data:`True`, the device will be switched on initially. + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature @@ -1210,26 +1703,27 @@ class StatusZero(LEDBoard): """ default_labels = ('one', 'two', 'three') - def __init__(self, *labels, **kwargs): + def __init__(self, *labels, pwm=False, initial_value=False, + pin_factory=None): pins = ( - (17, 4), - (22, 27), - (9, 10), + ('BOARD11', 'BOARD7'), + ('BOARD15', 'BOARD13'), + ('BOARD21', 'BOARD19'), ) - pin_factory = kwargs.pop('pin_factory', None) if len(labels) == 0: labels = self.default_labels elif len(labels) > len(pins): - raise ValueError("StatusZero doesn't support more than three labels") + raise ValueError( + f"StatusZero doesn't support more than {len(pins)} labels") dup, count = Counter(labels).most_common(1)[0] if count > 1: - raise ValueError("Duplicate label %s" % dup) - super(StatusZero, self).__init__( + raise ValueError(f"Duplicate label {dup}") + super().__init__( _order=labels, pin_factory=pin_factory, **{ label: LEDBoard( red=red, green=green, _order=('red', 'green'), - pin_factory=pin_factory, **kwargs - ) + pwm=pwm, initial_value=initial_value, + pin_factory=pin_factory) for (green, red), label in zip(pins, labels) } ) @@ -1261,6 +1755,18 @@ class StatusBoard(CompositeOutputDevice): will be initialised with names 'one' to 'five'. If some, but not all strips are given labels, any remaining strips will not be initialised. + :param bool pwm: + If :data:`True`, construct :class:`PWMLED` instances to represent each + LED. If :data:`False` (the default), construct regular :class:`LED` + instances. + + :type initial_value: bool or None + :param bool initial_value: + If :data:`False` (the default), all LEDs will be off initially. If + :data:`None`, each device will be left in whatever state the pin is + found in when configured for output (warning: this can be on). If + :data:`True`, the device will be switched on initially. + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature @@ -1295,30 +1801,31 @@ class StatusBoard(CompositeOutputDevice): """ default_labels = ('one', 'two', 'three', 'four', 'five') - def __init__(self, *labels, **kwargs): + def __init__(self, *labels, pwm=False, initial_value=False, + pin_factory=None): pins = ( - (17, 4, 14), - (22, 27, 19), - (9, 10, 15), - (5, 11, 26), - (13, 6, 18), + ('BOARD11', 'BOARD7', 'BOARD8'), + ('BOARD15', 'BOARD13', 'BOARD35'), + ('BOARD21', 'BOARD19', 'BOARD10'), + ('BOARD29', 'BOARD23', 'BOARD37'), + ('BOARD33', 'BOARD31', 'BOARD12'), ) - pin_factory = kwargs.pop('pin_factory', None) if len(labels) == 0: labels = self.default_labels elif len(labels) > len(pins): raise ValueError("StatusBoard doesn't support more than five labels") dup, count = Counter(labels).most_common(1)[0] if count > 1: - raise ValueError("Duplicate label %s" % dup) - super(StatusBoard, self).__init__( + raise ValueError(f"Duplicate label {dup}") + super().__init__( _order=labels, pin_factory=pin_factory, **{ label: CompositeOutputDevice( button=Button(button, pin_factory=pin_factory), lights=LEDBoard( red=red, green=green, _order=('red', 'green'), - pin_factory=pin_factory, **kwargs - ), _order=('button', 'lights'), pin_factory=pin_factory + pwm=pwm, initial_value=initial_value, + pin_factory=pin_factory), + _order=('button', 'lights'), pin_factory=pin_factory ) for (green, red, button), label in zip(pins, labels) } @@ -1385,16 +1892,16 @@ class SnowPi(LEDBoard): The :class:`LED` or :class:`PWMLED` for the snow-man's nose. """ - def __init__(self, pwm=False, initial_value=False, pin_factory=None): - super(SnowPi, self).__init__( + def __init__(self, *, pwm=False, initial_value=False, pin_factory=None): + super().__init__( arms=LEDBoard( left=LEDBoard( - top=17, middle=18, bottom=22, + top='BOARD11', middle='BOARD12', bottom='BOARD15', pwm=pwm, initial_value=initial_value, _order=('top', 'middle', 'bottom'), pin_factory=pin_factory), right=LEDBoard( - top=7, middle=8, bottom=9, + top='BOARD26', middle='BOARD24', bottom='BOARD21', pwm=pwm, initial_value=initial_value, _order=('top', 'middle', 'bottom'), pin_factory=pin_factory), @@ -1402,12 +1909,12 @@ def __init__(self, pwm=False, initial_value=False, pin_factory=None): pin_factory=pin_factory ), eyes=LEDBoard( - left=23, right=24, + left='BOARD16', right='BOARD18', pwm=pwm, initial_value=initial_value, _order=('left', 'right'), pin_factory=pin_factory ), - nose=25, + nose='BOARD22', pwm=pwm, initial_value=initial_value, _order=('eyes', 'nose', 'arms'), pin_factory=pin_factory @@ -1446,17 +1953,17 @@ class TrafficLightsBuzzer(CompositeOutputDevice): The :class:`Button` instance passed as the *button* parameter. """ - def __init__(self, lights, buzzer, button, pin_factory=None): - super(TrafficLightsBuzzer, self).__init__( + def __init__(self, lights, buzzer, button, *, pin_factory=None): + super().__init__( lights=lights, buzzer=buzzer, button=button, _order=('lights', 'buzzer', 'button'), pin_factory=pin_factory ) -class FishDish(TrafficLightsBuzzer): +class FishDish(CompositeOutputDevice): """ - Extends :class:`TrafficLightsBuzzer` for the `Pi Supply FishDish`_: traffic + Extends :class:`CompositeOutputDevice` for the `Pi Supply FishDish`_: traffic light LEDs, a button and a buzzer. The FishDish pins are fixed and therefore there's no need to specify them @@ -1481,19 +1988,22 @@ class FishDish(TrafficLightsBuzzer): .. _Pi Supply FishDish: https://www.pi-supply.com/product/fish-dish-raspberry-pi-led-buzzer-board/ """ - def __init__(self, pwm=False, pin_factory=None): - super(FishDish, self).__init__( - TrafficLights(9, 22, 4, pwm=pwm, pin_factory=pin_factory), - Buzzer(8, pin_factory=pin_factory), - Button(7, pull_up=False, pin_factory=pin_factory), + def __init__(self, *, pwm=False, pin_factory=None): + super().__init__( + lights=TrafficLights( + 'BOARD21', 'BOARD15', 'BOARD7', pwm=pwm, pin_factory=pin_factory + ), + buzzer=Buzzer('BOARD24', pin_factory=pin_factory), + button=Button('BOARD26', pull_up=False, pin_factory=pin_factory), + _order=('lights', 'buzzer', 'button'), pin_factory=pin_factory ) -class TrafficHat(TrafficLightsBuzzer): +class TrafficHat(CompositeOutputDevice): """ - Extends :class:`TrafficLightsBuzzer` for the `Ryanteck Traffic HAT`_: traffic - light LEDs, a button and a buzzer. + Extends :class:`CompositeOutputDevice` for the `Pi Supply Traffic HAT`_: a + board with traffic light LEDs, a button and a buzzer. The Traffic HAT pins are fixed and therefore there's no need to specify them when constructing this class. The following example waits for the @@ -1515,48 +2025,102 @@ class TrafficHat(TrafficLightsBuzzer): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). - .. _Ryanteck Traffic HAT: https://ryanteck.uk/hats/1-traffichat-0635648607122.html + .. _Pi Supply Traffic HAT: https://uk.pi-supply.com/products/traffic-hat-for-raspberry-pi """ - def __init__(self, pwm=False, pin_factory=None): - super(TrafficHat, self).__init__( - TrafficLights(24, 23, 22, pwm=pwm, pin_factory=pin_factory), - Buzzer(5, pin_factory=pin_factory), - Button(25, pin_factory=pin_factory), + def __init__(self, *, pwm=False, pin_factory=None): + super().__init__( + lights=TrafficLights( + 'BOARD18', 'BOARD16', 'BOARD15', + pwm=pwm, pin_factory=pin_factory + ), + buzzer=Buzzer('BOARD29', pin_factory=pin_factory), + button=Button('BOARD22', pin_factory=pin_factory), + _order=('lights', 'buzzer', 'button'), pin_factory=pin_factory ) +class TrafficpHat(TrafficLights): + """ + Extends :class:`TrafficLights` for the `Pi Supply Traffic pHAT`_: a small + board with traffic light LEDs. + + The Traffic pHAT pins are fixed and therefore there's no need to specify + them when constructing this class. The following example then turns on all + the LEDs:: + + from gpiozero import TrafficpHat + phat = TrafficpHat() + phat.red.on() + phat.blink() + + :param bool pwm: + If :data:`True`, construct :class:`PWMLED` instances to represent each + LED. If :data:`False` (the default), construct regular :class:`LED` + instances. + + :type initial_value: bool or None + :param initial_value: + If :data:`False` (the default), all LEDs will be off initially. If + :data:`None`, each device will be left in whatever state the pin is + found in when configured for output (warning: this can be on). If + :data:`True`, the device will be switched on initially. + + :type pin_factory: Factory or None + :param pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + + .. _Pi Supply Traffic pHAT: http://pisupp.ly/trafficphat + """ + def __init__(self, *, pwm=False, initial_value=False, pin_factory=None): + super().__init__( + red='BOARD22', amber='BOARD18', green='BOARD16', + pwm=pwm, initial_value=initial_value, pin_factory=pin_factory + ) + + +def PhaseEnableRobot(left=None, right=None, pwm=True, pin_factory=None, *args): + """ + Deprecated alias of :class:`Robot`. The :class:`Robot` class can now be + constructed directly with :class:`Motor` or :class:`PhaseEnableMotor` + classes. + """ + warnings.warn( + DeprecationWarning( + "PhaseEnableRobot is deprecated; please construct Robot directly " + "with PhaseEnableMotor instances")) + return Robot( + PhaseEnableMotor(*left), + PhaseEnableMotor(*right), + pwm=pwm, pin_factory=pin_factory, + *args) + + class Robot(SourceMixin, CompositeDevice): """ Extends :class:`CompositeDevice` to represent a generic dual-motor robot. - This class is constructed with two tuples representing the forward and - backward pins of the left and right controllers respectively. For example, - if the left motor's controller is connected to GPIOs 4 and 14, while the - right motor's controller is connected to GPIOs 17 and 18 then the following - example will drive the robot forward:: + This class is constructed with two motor instances representing the left + and right wheels of the robot respectively. For example, if the left + motor's controller is connected to GPIOs 4 and 14, while the right motor's + controller is connected to GPIOs 17 and 18 then the following example will + drive the robot forward:: from gpiozero import Robot - robot = Robot(left=(4, 14), right=(17, 18)) + robot = Robot(left=Motor(4, 14), right=Motor(17, 18)) robot.forward() - :param tuple left: - A tuple of two (or three) GPIO pins representing the forward and - backward inputs of the left motor's controller. Use three pins if your - motor controller requires an enable pin. - - :param tuple right: - A tuple of two (or three) GPIO pins representing the forward and - backward inputs of the right motor's controller. Use three pins if your - motor controller requires an enable pin. + :type left: Motor or PhaseEnableMotor + :param left: + A :class:`~gpiozero.Motor` or a :class:`~gpiozero.PhaseEnableMotor` + for the left wheel of the robot. - :param bool pwm: - If :data:`True` (the default), construct :class:`PWMOutputDevice` - instances for the motor controller pins, allowing both direction and - variable speed control. If :data:`False`, construct - :class:`DigitalOutputDevice` instances, allowing only direction - control. + :type right: Motor or PhaseEnableMotor + :param right: + A :class:`~gpiozero.Motor` or a :class:`~gpiozero.PhaseEnableMotor` + for the right wheel of the robot. :type pin_factory: Factory or None :param pin_factory: @@ -1571,18 +2135,30 @@ class Robot(SourceMixin, CompositeDevice): The :class:`Motor` on the right of the robot. """ - def __init__(self, left=None, right=None, pwm=True, pin_factory=None, *args): - # *args is a hack to ensure a useful message is shown when pins are - # supplied as sequential positional arguments e.g. 2, 3, 4, 5 - if not isinstance(left, tuple) or not isinstance(right, tuple): - raise GPIOPinMissing('left and right motor pins must be given as ' - 'tuples') - super(Robot, self).__init__( - left_motor=Motor(*left, pwm=pwm, pin_factory=pin_factory), - right_motor=Motor(*right, pwm=pwm, pin_factory=pin_factory), - _order=('left_motor', 'right_motor'), - pin_factory=pin_factory - ) + def __init__(self, left, right, *, pin_factory=None): + if not isinstance(left, (Motor, PhaseEnableMotor)): + if isinstance(left, tuple): + warnings.warn( + DeprecationWarning( + "Passing a tuple as the left parameter of the Robot " + "constructor is deprecated; please pass a Motor or " + "PhaseEnableMotor instance instead")) + left = Motor(*left, pin_factory=pin_factory) + else: + raise GPIOPinMissing('left must be a Motor or PhaseEnableMotor') + if not isinstance(right, (Motor, PhaseEnableMotor)): + if isinstance(right, tuple): + warnings.warn( + DeprecationWarning( + "Passing a tuple as the right parameter of the Robot " + "constructor is deprecated; please pass a Motor or " + "PhaseEnableMotor instance instead")) + right = Motor(*right, pin_factory=pin_factory) + else: + raise GPIOPinMissing('right must be a Motor or PhaseEnableMotor') + super().__init__(left_motor=left, right_motor=right, + _order=('left_motor', 'right_motor'), + pin_factory=pin_factory) @property def value(self): @@ -1592,13 +2168,13 @@ def value(self): ``(1, 1)`` representing full speed forwards, and ``(0, 0)`` representing stopped. """ - return super(Robot, self).value + return super().value @value.setter def value(self, value): self.left_motor.value, self.right_motor.value = value - def forward(self, speed=1, **kwargs): + def forward(self, speed=1, *, curve_left=0, curve_right=0): """ Drive the robot forward by running both motors forward. @@ -1618,10 +2194,6 @@ def forward(self, speed=1, **kwargs): default is 0 (no curve). This parameter can only be specified as a keyword parameter, and is mutually exclusive with *curve_left*. """ - curve_left = kwargs.pop('curve_left', 0) - curve_right = kwargs.pop('curve_right', 0) - if kwargs: - raise TypeError('unexpected argument %s' % kwargs.popitem()[0]) if not 0 <= curve_left <= 1: raise ValueError('curve_left must be between 0 and 1') if not 0 <= curve_right <= 1: @@ -1632,7 +2204,7 @@ def forward(self, speed=1, **kwargs): self.left_motor.forward(speed * (1 - curve_left)) self.right_motor.forward(speed * (1 - curve_right)) - def backward(self, speed=1, **kwargs): + def backward(self, speed=1, *, curve_left=0, curve_right=0): """ Drive the robot backward by running both motors backward. @@ -1652,10 +2224,6 @@ def backward(self, speed=1, **kwargs): default is 0 (no curve). This parameter can only be specified as a keyword parameter, and is mutually exclusive with *curve_left*. """ - curve_left = kwargs.pop('curve_left', 0) - curve_right = kwargs.pop('curve_right', 0) - if kwargs: - raise TypeError('unexpected argument %s' % kwargs.popitem()[0]) if not 0 <= curve_left <= 1: raise ValueError('curve_left must be between 0 and 1') if not 0 <= curve_right <= 1: @@ -1733,13 +2301,14 @@ class RyanteckRobot(Robot): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). - .. _Ryanteck motor controller board: https://ryanteck.uk/add-ons/6-ryanteck-rpi-motor-controller-board-0635648607160.html + .. _Ryanteck motor controller board: https://uk.pi-supply.com/products/ryanteck-rtk-000-001-motor-controller-board-kit-raspberry-pi """ - def __init__(self, pwm=True, pin_factory=None): - super(RyanteckRobot, self).__init__( - left=(17, 18), right=(22, 23), pwm=pwm, pin_factory=pin_factory - ) + def __init__(self, *, pwm=True, pin_factory=None): + super().__init__( + left=Motor('BOARD11', 'BOARD12', pwm=pwm, pin_factory=pin_factory), + right=Motor('BOARD15', 'BOARD16', pwm=pwm, pin_factory=pin_factory), + pin_factory=pin_factory) class CamJamKitRobot(Robot): @@ -1769,150 +2338,16 @@ class CamJamKitRobot(Robot): .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ - def __init__(self, pwm=True, pin_factory=None): - super(CamJamKitRobot, self).__init__( - left=(9, 10), right=(7, 8), pwm=pwm, pin_factory=pin_factory - ) - - -class PhaseEnableRobot(SourceMixin, CompositeDevice): - """ - Extends :class:`CompositeDevice` to represent a dual-motor robot based - around a Phase/Enable motor board. - - This class is constructed with two tuples representing the phase - (direction) and enable (speed) pins of the left and right controllers - respectively. For example, if the left motor's controller is connected to - GPIOs 12 and 5, while the right motor's controller is connected to GPIOs 13 - and 6 so the following example will drive the robot forward:: - - from gpiozero import PhaseEnableRobot - - robot = PhaseEnableRobot(left=(5, 12), right=(6, 13)) - robot.forward() - - :param tuple left: - A tuple of two GPIO pins representing the phase and enable inputs - of the left motor's controller. - - :param tuple right: - A tuple of two GPIO pins representing the phase and enable inputs - of the right motor's controller. - - :param bool pwm: - If :data:`True` (the default), construct :class:`PWMOutputDevice` - instances for the motor controller's enable pins, allowing both - direction and variable speed control. If :data:`False`, construct - :class:`DigitalOutputDevice` instances, allowing only direction - control. - - :type pin_factory: Factory or None - :param pin_factory: - See :doc:`api_pins` for more information (this is an advanced feature - which most users can ignore). - - .. attribute:: left_motor - - The :class:`PhaseEnableMotor` on the left of the robot. - - .. attribute:: right_motor - - The :class:`PhaseEnableMotor` on the right of the robot. - """ - def __init__(self, left=None, right=None, pwm=True, pin_factory=None, *args): - # *args is a hack to ensure a useful message is shown when pins are - # supplied as sequential positional arguments e.g. 2, 3, 4, 5 - if not isinstance(left, tuple) or not isinstance(right, tuple): - raise GPIOPinMissing('left and right motor pins must be given as ' - 'tuples') - super(PhaseEnableRobot, self).__init__( - left_motor=PhaseEnableMotor(*left, pwm=pwm, pin_factory=pin_factory), - right_motor=PhaseEnableMotor(*right, pwm=pwm, pin_factory=pin_factory), - _order=('left_motor', 'right_motor'), - pin_factory=pin_factory - ) - - @property - def value(self): - """ - Returns a tuple of two floating point values (-1 to 1) representing the - speeds of the robot's two motors (left and right). This property can - also be set to alter the speed of both motors. - """ - return super(PhaseEnableRobot, self).value - - @value.setter - def value(self, value): - self.left_motor.value, self.right_motor.value = value - - def forward(self, speed=1): - """ - Drive the robot forward by running both motors forward. - - :param float speed: - Speed at which to drive the motors, as a value between 0 (stopped) - and 1 (full speed). The default is 1. - """ - self.left_motor.forward(speed) - self.right_motor.forward(speed) - - def backward(self, speed=1): - """ - Drive the robot backward by running both motors backward. - - :param float speed: - Speed at which to drive the motors, as a value between 0 (stopped) - and 1 (full speed). The default is 1. - """ - self.left_motor.backward(speed) - self.right_motor.backward(speed) - - def left(self, speed=1): - """ - Make the robot turn left by running the right motor forward and left - motor backward. - - :param float speed: - Speed at which to drive the motors, as a value between 0 (stopped) - and 1 (full speed). The default is 1. - """ - self.right_motor.forward(speed) - self.left_motor.backward(speed) - - def right(self, speed=1): - """ - Make the robot turn right by running the left motor forward and right - motor backward. - - :param float speed: - Speed at which to drive the motors, as a value between 0 (stopped) - and 1 (full speed). The default is 1. - """ - self.left_motor.forward(speed) - self.right_motor.backward(speed) - - def reverse(self): - """ - Reverse the robot's current motor directions. If the robot is currently - running full speed forward, it will run full speed backward. If the - robot is turning left at half-speed, it will turn right at half-speed. - If the robot is currently stopped it will remain stopped. - """ - self.left_motor.value = -self.left_motor.value - self.right_motor.value = -self.right_motor.value - - def stop(self): - """ - Stop the robot. - """ - self.left_motor.stop() - self.right_motor.stop() + def __init__(self, *, pwm=True, pin_factory=None): + super().__init__( + left=Motor('BOARD21', 'BOARD19', pwm=pwm, pin_factory=pin_factory), + right=Motor('BOARD26', 'BOARD24', pwm=pwm, pin_factory=pin_factory), + pin_factory=pin_factory) -class PololuDRV8835Robot(PhaseEnableRobot): +class PololuDRV8835Robot(Robot): """ - Extends :class:`PhaseEnableRobot` for the `Pololu DRV8835 Dual Motor Driver - Kit`_. + Extends :class:`Robot` for the `Pololu DRV8835 Dual Motor Driver Kit`_. The Pololu DRV8835 pins are fixed and therefore there's no need to specify them when constructing this class. The following example drives the robot @@ -1937,29 +2372,30 @@ class PololuDRV8835Robot(PhaseEnableRobot): .. _Pololu DRV8835 Dual Motor Driver Kit: https://www.pololu.com/product/2753 """ - def __init__(self, pwm=True, pin_factory=None): - super(PololuDRV8835Robot, self).__init__( - left=(5, 12), right=(6, 13), pwm=pwm, pin_factory=pin_factory - ) + def __init__(self, *, pwm=True, pin_factory=None): + super().__init__( + left=PhaseEnableMotor('BOARD29', 'BOARD32', pwm=pwm, pin_factory=pin_factory), + right=PhaseEnableMotor('BOARD31', 'BOARD33', pwm=pwm, pin_factory=pin_factory), + pin_factory=pin_factory) class _EnergenieMaster(SharedMixin, CompositeOutputDevice): - def __init__(self, pin_factory=None): + def __init__(self, *, pin_factory=None): self._lock = Lock() - super(_EnergenieMaster, self).__init__( + super().__init__( *( OutputDevice(pin, pin_factory=pin_factory) - for pin in (17, 22, 23, 27) + for pin in ('BOARD11', 'BOARD15', 'BOARD16', 'BOARD13') ), - mode=OutputDevice(24, pin_factory=pin_factory), - enable=OutputDevice(25, pin_factory=pin_factory), + mode=OutputDevice('BOARD18', pin_factory=pin_factory), + enable=OutputDevice('BOARD22', pin_factory=pin_factory), _order=('mode', 'enable'), pin_factory=pin_factory ) def close(self): if getattr(self, '_lock', None): with self._lock: - super(_EnergenieMaster, self).close() + super().close() self._lock = None @classmethod @@ -1998,11 +2434,14 @@ class Energenie(SourceMixin, Device): Which socket this instance should control. This is an integer number between 1 and 4. - :param bool initial_value: + :type initial_value: bool or None + :param initial_value: The initial state of the socket. As Energenie sockets provide no - means of reading their state, you must provide an initial state for + means of reading their state, you may provide an initial state for the socket, which will be set upon construction. This defaults to :data:`False` which will switch the socket off. + Specifying :data:`None` will not set any initial state nor transmit any + control signal to the device. :type pin_factory: Factory or None :param pin_factory: @@ -2011,20 +2450,18 @@ class Energenie(SourceMixin, Device): .. _Energenie socket: https://energenie4u.co.uk/index.php/catalogue/product/ENER002-2PI """ - def __init__(self, socket=None, initial_value=False, pin_factory=None): + def __init__(self, socket=None, *, initial_value=False, pin_factory=None): if socket is None: raise EnergenieSocketMissing('socket number must be provided') if not (1 <= socket <= 4): raise EnergenieBadSocket('socket number must be between 1 and 4') - if initial_value is None: - raise EnergenieBadInitialValue("initial value can't be None") self._value = None - super(Energenie, self).__init__(pin_factory=pin_factory) + super().__init__(pin_factory=pin_factory) self._socket = socket self._master = _EnergenieMaster(pin_factory=pin_factory) if initial_value: self.on() - else: + elif initial_value is not None: self.off() def close(self): @@ -2039,7 +2476,7 @@ def closed(self): def __repr__(self): try: self._check_open() - return "" % self._socket + return f"" except DeviceClosed: return "" @@ -2055,11 +2492,16 @@ def value(self): """ Returns :data:`True` if the socket is on and :data:`False` if the socket is off. Setting this property changes the state of the socket. + Returns :data:`None` only when constructed with :data:`initial_value` + set to :data:`None` and neither :data:`on()` nor :data:`off()` have + been called since construction. """ return self._value @value.setter def value(self, value): + if value is None: + raise TypeError('value cannot be None') value = bool(value) self._master.transmit(self._socket, value) self._value = value @@ -2132,16 +2574,18 @@ class PumpkinPi(LEDBoard): The :class:`LED` or :class:`PWMLED` for each of the pumpkin's eyes. """ - def __init__(self, pwm=False, initial_value=False, pin_factory=None): - super(PumpkinPi, self).__init__( + def __init__(self, *, pwm=False, initial_value=False, pin_factory=None): + super().__init__( sides=LEDBoard( left=LEDBoard( - bottom=18, midbottom=17, middle=16, midtop=13, top=24, + bottom='BOARD12', midbottom='BOARD11', middle='BOARD36', + midtop='BOARD33', top='BOARD18', pwm=pwm, initial_value=initial_value, _order=('bottom', 'midbottom', 'middle', 'midtop', 'top'), pin_factory=pin_factory), right=LEDBoard( - bottom=19, midbottom=20, middle=21, midtop=22, top=23, + bottom='BOARD35', midbottom='BOARD38', middle='BOARD40', + midtop='BOARD15', top='BOARD16', pwm=pwm, initial_value=initial_value, _order=('bottom', 'midbottom', 'middle', 'midtop', 'top'), pin_factory=pin_factory), @@ -2150,7 +2594,7 @@ def __init__(self, pwm=False, initial_value=False, pin_factory=None): pin_factory=pin_factory ), eyes=LEDBoard( - left=12, right=6, + left='BOARD32', right='BOARD31', pwm=pwm, initial_value=initial_value, _order=('left', 'right'), pin_factory=pin_factory @@ -2179,7 +2623,7 @@ class JamHat(CompositeOutputDevice): hat.off() :param bool pwm: - If :data:`True`, construct :class: PWMLED instances to represent each + If :data:`True`, construct :class:`PWMLED` instances to represent each LED on the board. If :data:`False` (the default), construct regular :class:`LED` instances. @@ -2188,7 +2632,7 @@ class JamHat(CompositeOutputDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). - .. _ModMyPi JamHat: https://www.modmypi.com/jam-hat + .. _ModMyPi JamHat: https://thepihut.com/products/jam-hat .. attribute:: lights_1, lights_2 @@ -2207,19 +2651,22 @@ class JamHat(CompositeOutputDevice): .. attribute:: buzzer - The :class:`Buzzer` at the bottom right of the JamHat. + The :class:`TonalBuzzer` at the bottom right of the JamHat. """ - def __init__(self, pwm=False, pin_factory=None): - super(JamHat, self).__init__( - lights_1=LEDBoard(red=5, yellow=12, green=16, - pwm=pwm, _order=('red', 'yellow', 'green'), - pin_factory=pin_factory), - lights_2=LEDBoard(red=6, yellow=13, green=17, - pwm=pwm, _order=('red', 'yellow', 'green'), - pin_factory=pin_factory), - button_1=Button(19, pull_up=False, pin_factory=pin_factory), - button_2=Button(18, pull_up=False, pin_factory=pin_factory), - buzzer=TonalBuzzer(20, pin_factory=pin_factory), + def __init__(self, *, pwm=False, pin_factory=None): + super().__init__( + lights_1=LEDBoard( + red='BOARD29', yellow='BOARD32', green='BOARD36', + pwm=pwm, _order=('red', 'yellow', 'green'), + pin_factory=pin_factory + ), + lights_2=LEDBoard( + red='BOARD31', yellow='BOARD33', green='BOARD11', + pwm=pwm, _order=('red', 'yellow', 'green'), + pin_factory=pin_factory), + button_1=Button('BOARD35', pull_up=False, pin_factory=pin_factory), + button_2=Button('BOARD12', pull_up=False, pin_factory=pin_factory), + buzzer=TonalBuzzer('BOARD38', pin_factory=pin_factory), _order=('lights_1', 'lights_2', 'button_1', 'button_2', 'buzzer'), pin_factory=pin_factory ) @@ -2229,11 +2676,119 @@ def on(self): Turns all the LEDs on and makes the buzzer play its mid tone. """ self.buzzer.value = 0 - super(JamHat, self).on() + super().on() + + def off(self): + """ + Turns all the LEDs off and stops the buzzer. + """ + self.buzzer.value = None + super().off() + + +class Pibrella(CompositeOutputDevice): + """ + Extends :class:`CompositeOutputDevice` for the Cyntech/Pimoroni `Pibrella`_ + board. + + The Pibrella board comprises 3 LEDs, a button, a tonal buzzer, four general + purpose input channels, and four general purpose output channels (with LEDs). + + This class exposes the LEDs, button and buzzer. + + Usage:: + + from gpiozero import Pibrella + + pb = Pibrella() + + pb.button.wait_for_press() + pb.lights.on() + pb.buzzer.play('A4') + pb.off() + + The four input and output channels are exposed so you can create GPIO Zero + devices using these pins without looking up their respective pin numbers:: + + from gpiozero import Pibrella, LED, Button + + pb = Pibrella() + btn = Button(pb.inputs.a, pull_up=False) + led = LED(pb.outputs.e) + + btn.when_pressed = led.on + + :param bool pwm: + If :data:`True`, construct :class:`PWMLED` instances to represent each + LED on the board, otherwise if :data:`False` (the default), construct + regular :class:`LED` instances. + + :type pin_factory: Factory or None + :param pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + + .. _Pibrella: http://www.pibrella.com/ + + .. attribute:: lights + + :class:`TrafficLights` instance representing the three LEDs + + .. attribute:: red, amber, green + + :class:`LED` or :class:`PWMLED` instances representing the red, + amber, and green LEDs + + .. attribute:: button + + The red :class:`Button` object on the Pibrella + + .. attribute:: buzzer + + A :class:`TonalBuzzer` object representing the buzzer + + .. attribute:: inputs + + A :func:`~collections.namedtuple` of the input pin numbers + + .. attribute:: a, b, c, d + + .. attribute:: outputs + + A :func:`~collections.namedtuple` of the output pin numbers + + .. attribute:: e, f, g, h + """ + def __init__(self, *, pwm=False, pin_factory=None): + super().__init__( + lights=TrafficLights( + red='BOARD13', amber='BOARD11', green='BOARD7', + pwm=pwm, pin_factory=pin_factory + ), + button=Button('BOARD23', pull_up=False, pin_factory=pin_factory), + buzzer=TonalBuzzer('BOARD12', pin_factory=pin_factory), + _order=('lights', 'button', 'buzzer'), + pin_factory=pin_factory + ) + InputPins = namedtuple('InputPins', ['a', 'b', 'c', 'd']) + OutputPins = namedtuple('OutputPins', ['e', 'f', 'g', 'h']) + self.inputs = InputPins( + a='BOARD21', b='BOARD26', c='BOARD24', d='BOARD19' + ) + self.outputs = OutputPins( + e='BOARD15', f='BOARD16', g='BOARD18', h='BOARD22' + ) + + def on(self): + """ + Turns all the LEDs on and makes the buzzer play its mid tone. + """ + self.buzzer.value = 0 + super().on() def off(self): """ Turns all the LEDs off and stops the buzzer. """ self.buzzer.value = None - super(JamHat, self).off() + super().off() diff --git a/gpiozero/compat.py b/gpiozero/compat.py index 04e506f60..11159c92e 100644 --- a/gpiozero/compat.py +++ b/gpiozero/compat.py @@ -1,181 +1,44 @@ # vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2019 Ben Nuttall +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li # Copyright (c) 2018 Rick Ansell # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause -import math -import cmath -import weakref import operator -import functools - -# Handles pre 3.3 versions of Python without collections.abc -try: - from collections.abc import Mapping -except ImportError: - from collections import Mapping - -# Back-ported from python 3.5; see -# github.com/PythonCHB/close_pep/blob/master/is_close.py for original -# implementation -def isclose(a, b, rel_tol=1e-9, abs_tol=0.0): - if rel_tol < 0.0 or abs_tol < 0.0: - raise ValueError('error tolerances must be non-negative') - if a == b: # fast-path for exact equality - return True - if cmath.isinf(a) or cmath.isinf(b): - return False - diff = abs(b - a) - return ( - (diff <= abs(rel_tol * b)) or - (diff <= abs(rel_tol * a)) or - (diff <= abs_tol) - ) - - -# Backported from py3.4 -def mean(data): - if iter(data) is data: - data = list(data) - n = len(data) - if not n: - raise ValueError('cannot calculate mean of empty data') - return sum(data) / n - - -# Backported from py3.4 -def median(data): - data = sorted(data) - n = len(data) - if not n: - raise ValueError('cannot calculate median of empty data') - elif n % 2: - return data[n // 2] - else: - i = n // 2 - return (data[i - 1] + data[i]) / 2 +from functools import reduce +from collections.abc import Mapping -# Backported from py3.3 -def log2(x): - return math.log(x, 2) - - -# Copied from the MIT-licensed https://github.com/slezica/python-frozendict +# Derived from the MIT-licensed https://github.com/slezica/python-frozendict class frozendict(Mapping): def __init__(self, *args, **kwargs): - self.__dict = dict(*args, **kwargs) - self.__hash = None + self._dict = dict(*args, **kwargs) + self._hash = None def __getitem__(self, key): - return self.__dict[key] + return self._dict[key] + + def __contains__(self, key): + return key in self._dict def copy(self, **add_or_replace): return frozendict(self, **add_or_replace) def __iter__(self): - return iter(self.__dict) + return iter(self._dict) def __len__(self): - return len(self.__dict) + return len(self._dict) def __repr__(self): - return '' % repr(self.__dict) + return f'<{self.__class__.__name__} {self._dict!r}>' def __hash__(self): - if self.__hash is None: - hashes = map(hash, self.items()) - self.__hash = functools.reduce(operator.xor, hashes, 0) - return self.__hash - - -# Backported from py3.4 -class WeakMethod(weakref.ref): - """ - A custom `weakref.ref` subclass which simulates a weak reference to - a bound method, working around the lifetime problem of bound methods. - """ - - __slots__ = "_func_ref", "_meth_type", "_alive", "__weakref__" - - def __new__(cls, meth, callback=None): - try: - obj = meth.__self__ - func = meth.__func__ - except AttributeError: - raise TypeError("argument should be a bound method, not {0}" - .format(type(meth))) - def _cb(arg): - # The self-weakref trick is needed to avoid creating a reference - # cycle. - self = self_wr() - if self._alive: - self._alive = False - if callback is not None: - callback(self) - self = weakref.ref.__new__(cls, obj, _cb) - self._func_ref = weakref.ref(func, _cb) - self._meth_type = type(meth) - self._alive = True - self_wr = weakref.ref(self) - return self - - def __call__(self): - obj = super(WeakMethod, self).__call__() - func = self._func_ref() - if obj is None or func is None: - return None - return self._meth_type(func, obj) - - def __eq__(self, other): - if isinstance(other, WeakMethod): - if not self._alive or not other._alive: - return self is other - return weakref.ref.__eq__(self, other) and self._func_ref == other._func_ref - return False - - def __ne__(self, other): - if isinstance(other, WeakMethod): - if not self._alive or not other._alive: - return self is not other - return weakref.ref.__ne__(self, other) or self._func_ref != other._func_ref - return True - - __hash__ = weakref.ref.__hash__ + if self._hash is None: + self._hash = reduce(operator.xor, map(hash, self.items()), 0) + return self._hash diff --git a/gpiozero/devices.py b/gpiozero/devices.py index 5a5236db8..a27936b87 100644 --- a/gpiozero/devices.py +++ b/gpiozero/devices.py @@ -1,58 +1,33 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones +# +# Copyright (c) 2015-2024 Dave Jones +# Copyright (c) 2020 Fangchen Li # Copyright (c) 2015-2019 Ben Nuttall # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -nstr = str -str = type('') +# SPDX-License-Identifier: BSD-3-Clause import os import atexit import weakref import warnings -from collections import namedtuple, OrderedDict +from collections import namedtuple from itertools import chain from types import FunctionType -from threading import Lock -from .pins import Pin +# NOTE: Remove try when compatibility moves beyond Python 3.10 +try: + from importlib_metadata import entry_points +except ImportError: + from importlib.metadata import entry_points + from .threads import _threads_shutdown from .mixins import ( ValuesMixin, SharedMixin, - ) +) from .exc import ( BadPinFactory, DeviceClosed, @@ -60,19 +35,26 @@ CompositeDeviceBadOrder, CompositeDeviceBadDevice, GPIOPinMissing, - GPIOPinInUse, GPIODeviceClosed, + NativePinFactoryFallback, PinFactoryFallback, - ) +) + from .compat import frozendict +native_fallback_message = ( + 'Falling back to the experimental pin factory NativeFactory because no other ' + 'pin factory could be loaded. For best results, install RPi.GPIO or pigpio. ' + 'See https://gpiozero.readthedocs.io/en/stable/api_pins.html for more information.' +) + class GPIOMeta(type): # NOTE Yes, this is a metaclass. Don't be scared - it's a simple one. def __new__(mcls, name, bases, cls_dict): # Construct the class as normal - cls = super(GPIOMeta, mcls).__new__(mcls, name, bases, cls_dict) + cls = super().__new__(mcls, name, bases, cls_dict) # If there's a method in the class which has no docstring, search # the base classes recursively for a docstring to copy for attr_name, attr in cls_dict.items(): @@ -95,15 +77,16 @@ def __call__(cls, *args, **kwargs): # already exists. Only construct the instance if the key's new. key = cls._shared_key(*args, **kwargs) try: - self = cls._instances[key] + self = cls._instances[key]() self._refs += 1 - except (KeyError, ReferenceError) as e: - self = super(GPIOMeta, cls).__call__(*args, **kwargs) + except (KeyError, AttributeError) as e: + self = super().__call__(*args, **kwargs) self._refs = 1 # Replace the close method with one that merely decrements # the refs counter and calls the original close method when # it reaches zero old_close = self.close + def close(): self._refs = max(0, self._refs - 1) if not self._refs: @@ -117,11 +100,12 @@ def close(): # just ignore the resulting KeyError here - # it's already gone pass + self.close = close - cls._instances[key] = weakref.proxy(self) + cls._instances[key] = weakref.ref(self) else: # Construct the instance as normal - self = super(GPIOMeta, cls).__call__(*args, **kwargs) + self = super().__call__(*args, **kwargs) # At this point __new__ and __init__ have all been run. We now fix the # set of attributes on the class by dir'ing the instance and creating a # frozenset of the result called __attrs__ (which is queried by @@ -132,8 +116,7 @@ def close(): return self -# Cross-version compatible method of using a metaclass -class GPIOBase(GPIOMeta(nstr('GPIOBase'), (), {})): +class GPIOBase(metaclass=GPIOMeta): def __setattr__(self, name, value): # This overridden __setattr__ simply ensures that additional attributes # cannot be set on the class after construction (it manages this in @@ -143,21 +126,41 @@ def __setattr__(self, name, value): # repeating the "source" and "values" property code in myriad places if hasattr(self, '__attrs__') and name not in self.__attrs__: raise AttributeError( - "'%s' object has no attribute '%s'" % ( - self.__class__.__name__, name)) - return super(GPIOBase, self).__setattr__(name, value) + f"'{self.__class__.__name__}' object has no attribute '{name}'") + return super().__setattr__(name, value) def __del__(self): + # NOTE: Yes, we implicitly call close() on __del__(), and yes for you + # dear hacker-on-this-library, this means pain! + # + # It's entirely for the convenience of command line experimenters and + # newbies who want to re-gain those pins when stuff falls out of scope + # without managing their object lifetimes "properly" with "with" (but, + # hey, this is an educational library at heart so that's the way we + # roll). + # + # What does this mean for you? It means that in close() you cannot + # assume *anything*. If someone calls a constructor with a fundamental + # mistake like the wrong number of params, then your close() method is + # going to be called before __init__ ever ran so all those attributes + # you *think* exist, erm, don't. Basically if you refer to anything in + # "self" within your close method, be preprared to catch AttributeError + # on its access to avoid spurious warnings for the end user. + # + # "But we're exiting anyway; surely exceptions in __del__ get + # squashed?" Yes, but they still cause verbose warnings and remember + # that this is an educational library; keep it friendly! self.close() def close(self): """ - Shut down the device and release all associated resources. This method - can be called on an already closed device without raising an exception. + Shut down the device and release all associated resources (such as GPIO + pins). - This method is primarily intended for interactive use at the command - line. It disables the device and releases its pin(s) for use by another - device. + This method is idempotent (can be called on an already closed device + without any side-effects). It is primarily intended for interactive use + at the command line. It disables the device and releases its pin(s) for + use by another device. You can attempt to do this simply by deleting an object, but unless you've cleaned up all references to the object this may not work (even @@ -190,8 +193,10 @@ def close(self): """ # This is a placeholder which is simply here to ensure close() can be # safely called from subclasses without worrying whether super-classes - # have it (which in turn is useful in conjunction with the SourceMixin - # class). + # have it (which in turn is useful in conjunction with the mixin + # classes). + # + # P.S. See note in __del__ above. pass @property @@ -206,7 +211,7 @@ def closed(self): def _check_open(self): if self.closed: raise DeviceClosed( - '%s is closed or uninitialized' % self.__class__.__name__) + f"{self.__class__.__name__} is closed or uninitialized") def __enter__(self): return self @@ -233,73 +238,91 @@ class Device(ValuesMixin, GPIOBase): allocating pins, providing low level interfaces (e.g. SPI), and clock facilities (querying and calculating elapsed times). """ - pin_factory = None # instance of a Factory sub-class + pin_factory = None # instance of a Factory sub-class - def __init__(self, **kwargs): - # Force pin_factory to be keyword-only, even in Python 2 - pin_factory = kwargs.pop('pin_factory', None) + def __init__(self, *, pin_factory=None): if pin_factory is None: - if Device.pin_factory is None: - Device.pin_factory = Device._default_pin_factory() + Device.ensure_pin_factory() self.pin_factory = Device.pin_factory else: self.pin_factory = pin_factory - if kwargs: - raise TypeError("Device.__init__() got unexpected keyword " - "argument '%s'" % kwargs.popitem()[0]) - super(Device, self).__init__() + super().__init__() + + @staticmethod + def ensure_pin_factory(): + """ + Ensures that :attr:`Device.pin_factory` is set appropriately. + + This is called implicitly upon construction of any device, but there + are some circumstances where you may need to call it manually. + Specifically, when you wish to retrieve board information without + constructing any devices, e.g.:: + + Device.ensure_pin_factory() + info = Device.pin_factory.board_info + + If :attr:`Device.pin_factory` is not :data:`None`, this function does + nothing. Otherwise it will attempt to locate and initialize a default + pin factory. This may raise a number of different exceptions including + :exc:`ImportError` if no valid pin driver can be imported. + """ + if Device.pin_factory is None: + Device.pin_factory = Device._default_pin_factory() @staticmethod def _default_pin_factory(): - # We prefer RPi.GPIO here as it supports PWM, and all Pi revisions. If - # no third-party libraries are available, however, we fall back to a - # pure Python implementation which supports platforms like PyPy + # We prefer lgpio here as it supports PWM, and all Pi revisions without + # banging on registers directly. If no third-party libraries are + # available, however, we fall back to a pure Python implementation + # which supports platforms like PyPy # # NOTE: If the built-in pin factories are expanded, the dict must be # updated along with the entry-points in setup.py. - default_factories = OrderedDict(( - ('rpigpio', 'gpiozero.pins.rpigpio:RPiGPIOFactory'), - ('rpio', 'gpiozero.pins.rpio:RPIOFactory'), - ('pigpio', 'gpiozero.pins.pigpio:PiGPIOFactory'), - ('native', 'gpiozero.pins.native:NativeFactory'), - )) + default_factories = { + 'lgpio': 'gpiozero.pins.lgpio:LGPIOFactory', + 'rpigpio': 'gpiozero.pins.rpigpio:RPiGPIOFactory', + 'pigpio': 'gpiozero.pins.pigpio:PiGPIOFactory', + 'native': 'gpiozero.pins.native:NativeFactory', + } name = os.environ.get('GPIOZERO_PIN_FACTORY') if name is None: # If no factory is explicitly specified, try various names in - # "preferred" order. For speed, we select from the dictionary above - # rather than importing pkg_resources and using load_entry_point + # "preferred" order for name, entry_point in default_factories.items(): try: mod_name, cls_name = entry_point.split(':', 1) module = __import__(mod_name, fromlist=(cls_name,)) - return getattr(module, cls_name)() + pin_factory = getattr(module, cls_name)() + if name == 'native': + warnings.warn(NativePinFactoryFallback(native_fallback_message)) + return pin_factory except Exception as e: warnings.warn( - PinFactoryFallback( - 'Falling back from %s: %s' % (name, str(e)))) + PinFactoryFallback(f'Falling back from {name}: {e!s}')) raise BadPinFactory('Unable to load any default pin factory!') - elif name in default_factories: - # As above, this is a fast-path optimization to avoid loading - # pkg_resources (which it turns out was 80% of gpiozero's import - # time!) - mod_name, cls_name = default_factories[name].split(':', 1) - module = __import__(mod_name, fromlist=(cls_name,)) - return getattr(module, cls_name)() else: - # Slow path: load pkg_resources and try and find the specified - # entry-point. Try with the name verbatim first. If that fails, - # attempt with the lower-cased name (this ensures compatibility - # names work but we're still case insensitive for all factories) - import pkg_resources - group = 'gpiozero_pin_factories' - for factory in pkg_resources.iter_entry_points(group, name): - return factory.load()() - for factory in pkg_resources.iter_entry_points(group, name.lower()): - return factory.load()() - raise BadPinFactory('Unable to find pin factory "%s"' % name) + # Use importlib's entry_points to try and find the specified + # entry-point. Try with name verbatim first. If that fails, attempt + # with the lower-cased name (this ensures compatibility names work + # but we're still case insensitive for all factories) + with warnings.catch_warnings(): + # The dict interface of entry_points is deprecated ... already + # and this deprecation is for us to worry about, not our users + group = entry_points(group='gpiozero_pin_factories') + for ep in group: + if ep.name == name: + return ep.load()() + for ep in group: + if ep.name == name.lower(): + return ep.load()() + raise BadPinFactory(f'Unable to find pin factory {name!r}') def __repr__(self): - return "" % (self.__class__.__name__) + try: + self._check_open() + return f"" + except DeviceClosed: + return f"" def _conflicts_with(self, other): """ @@ -383,38 +406,37 @@ class CompositeDevice(Device): their :attr:`value` attributes will be accessible as named elements of the composite device's tuple :attr:`value`. """ - def __init__(self, *args, **kwargs): + + def __init__(self, *args, _order=None, pin_factory=None, **kwargs): self._all = () self._named = frozendict({}) self._namedtuple = None - self._order = kwargs.pop('_order', None) - pin_factory = kwargs.pop('pin_factory', None) + self._order = _order try: if self._order is None: self._order = sorted(kwargs.keys()) else: for missing_name in set(kwargs.keys()) - set(self._order): raise CompositeDeviceBadOrder( - '%s missing from _order' % missing_name) + f'{missing_name} missing from _order') self._order = tuple(self._order) for name in set(self._order) & set(dir(self)): - raise CompositeDeviceBadName( - '%s is a reserved name' % name) + raise CompositeDeviceBadName(f'{name} is a reserved name') for dev in chain(args, kwargs.values()): if not isinstance(dev, Device): raise CompositeDeviceBadDevice( - "%s doesn't inherit from Device" % dev) + f"{dev} doesn't inherit from Device") self._named = frozendict(kwargs) self._namedtuple = namedtuple( - '%sValue' % self.__class__.__name__, chain( - ('device_%d' % i for i in range(len(args))), self._order)) + f'{self.__class__.__name__}Value', + chain((f'device_{i}' for i in range(len(args))), self._order)) except: for dev in chain(args, kwargs.values()): if isinstance(dev, Device): dev.close() raise self._all = args + tuple(kwargs[v] for v in self._order) - super(CompositeDevice, self).__init__(pin_factory=pin_factory) + super().__init__(pin_factory=pin_factory) def __getattr__(self, name): # if _named doesn't exist yet, pretend it's an empty dict @@ -423,24 +445,34 @@ def __getattr__(self, name): try: return self._named[name] except KeyError: - raise AttributeError("no such attribute %s" % name) + raise AttributeError(f"no such attribute {name}") def __setattr__(self, name, value): # make named components read-only properties if name in self._named: - raise AttributeError("can't set attribute %s" % name) - return super(CompositeDevice, self).__setattr__(name, value) + raise AttributeError(f"can't set attribute {name}") + return super().__setattr__(name, value) def __repr__(self): try: self._check_open() - return "" % ( - self.__class__.__name__, - len(self), ','.join(self._order), - len(self) - len(self._named) - ) + named = len(self._named) + names = ', '.join(self._order) + unnamed = len(self) - len(self._named) + if named > 0 and unnamed > 0: + return ( + f"") + elif named > 0: + return ( + f"") + else: + return ( + f"") except DeviceClosed: - return "" % (self.__class__.__name__) + return super().__repr__() def __len__(self): return len(self._all) @@ -459,8 +491,7 @@ def all(self): def close(self): if getattr(self, '_all', None): for device in self._all: - if isinstance(device, Device): - device.close() + device.close() self._all = () @property @@ -508,8 +539,9 @@ class GPIODevice(Device): will be raised. If the pin is already in use by another device, :exc:`GPIOPinInUse` will be raised. """ - def __init__(self, pin=None, **kwargs): - super(GPIODevice, self).__init__(**kwargs) + + def __init__(self, pin=None, *, pin_factory=None): + super().__init__(pin_factory=pin_factory) # self._pin must be set before any possible exceptions can be raised # because it's accessed in __del__. However, it mustn't be given the # value of pin until we've verified that it isn't already allocated @@ -534,19 +566,22 @@ def _read(self): raise def close(self): - super(GPIODevice, self).close() + super().close() if getattr(self, '_pin', None) is not None: - self.pin_factory.release_pins(self, self._pin.number) + self.pin_factory.release_pins(self, self._pin.info.name) self._pin.close() self._pin = None @property def closed(self): - return self._pin is None + try: + return self._pin is None + except AttributeError: + return True def _check_open(self): try: - super(GPIODevice, self)._check_open() + super()._check_open() except DeviceClosed as e: # For backwards compatibility; GPIODeviceClosed is deprecated raise GPIODeviceClosed(str(e)) @@ -568,10 +603,11 @@ def value(self): def __repr__(self): try: - return "" % ( - self.__class__.__name__, self.pin, self.is_active) + return ( + f"") except DeviceClosed: - return "" % self.__class__.__name__ + return f"" def _devices_shutdown(): @@ -594,4 +630,5 @@ def _shutdown(): _threads_shutdown() _devices_shutdown() + atexit.register(_shutdown) diff --git a/gpiozero/exc.py b/gpiozero/exc.py index ff0e9c65c..c167a36c1 100644 --- a/gpiozero/exc.py +++ b/gpiozero/exc.py @@ -1,41 +1,13 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016-2019 Andrew Scheller -# Copyright (c) 2019 Ben Nuttall -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2019 Kosovan Sofiia +# Copyright (c) 2019 Ben Nuttall +# Copyright (c) 2016 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, -) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause class GPIOZeroError(Exception): @@ -77,9 +49,6 @@ class EnergenieSocketMissing(CompositeDeviceError, ValueError): class EnergenieBadSocket(CompositeDeviceError, ValueError): "Error raised when an invalid socket number is passed to :class:`Energenie`" -class EnergenieBadInitialValue(CompositeDeviceError, ValueError): - "Error raised when an invalid initial value is passed to :class:`Energenie`" - class SPIError(GPIOZeroError): "Base class for errors related to the SPI implementation" @@ -104,6 +73,9 @@ class SPIFixedSelect(SPIError, AttributeError): class SPIFixedWordSize(SPIError, AttributeError): "Error raised when the number of bits per word cannot be changed" +class SPIFixedRate(SPIError, AttributeError): + "Error raised when the baud-rate of the interface cannot be changed" + class SPIInvalidWordSize(SPIError, ValueError): "Error raised when an invalid (out of range) number of bits per word is specified" @@ -192,7 +164,13 @@ class SPIWarning(GPIOZeroWarning): "Base class for warnings related to the SPI implementation" class SPISoftwareFallback(SPIWarning): - "Warning raised when falling back to the software implementation" + "Warning raised when falling back to the SPI software implementation" + +class PWMWarning(GPIOZeroWarning): + "Base class for PWM warnings" + +class PWMSoftwareFallback(PWMWarning): + "Warning raised when falling back to the PWM software implementation" class PinWarning(GPIOZeroWarning): "Base class for warnings related to pin implementations" @@ -200,6 +178,9 @@ class PinWarning(GPIOZeroWarning): class PinFactoryFallback(PinWarning): "Warning raised when a default pin factory fails to load and a fallback is tried" +class NativePinFactoryFallback(PinWarning): + "Warning raised when all other default pin factories fail to load and NativeFactory is used" + class PinNonPhysical(PinWarning): "Warning raised when a non-physical pin is specified in a constructor" diff --git a/gpiozero/fonts/14seg.txt b/gpiozero/fonts/14seg.txt new file mode 100644 index 000000000..4098b0ebd --- /dev/null +++ b/gpiozero/fonts/14seg.txt @@ -0,0 +1,54 @@ +# This file defines the default alphabet for 14-segment displays. The format is +# fairly simple: the file is loaded as a whole and all blank and #-prefixed +# lines are stripped out. Then all blank columns are stripped out. Finally the +# remaining rows and columns are divided into 5x5 cells with the following +# format: +# +# X.a.. +# fijkb +# .g.h. +# elmnc +# ..d.. +# +# Where X is the character being defined, and a..n are the segments that are +# active. a, d, g, and h are considered active if they are "-". b, c, e, f, j, +# and m are considered active if they are "|". i and n are active when they +# are "\". Finally, k and l are active when they are "/". All other characters +# marked "." are ignored but may be set to anything for the purposes of making +# the character's shape more obvious. +# +# Note that blank columns are stripped, so when defining the space (" ") +# character you will need to use place-holder characters for unused positions. +# Furthermore, the parser checks that definitions are multiples of 5 wide and +# 5 high. If a character's definition has entirely empty rows or columns you +# may need more place-holder characters to satisfy this limitation. + + .... 0--- 1.. 2--- 3--- 4 5--- 6--- 7---. 8--- 9--- +..... | /| /| | | | | | | / | | | | +..... | / | | --- -- ---| --- |--- | --- ---| +..... |/ | | | | | | | | | | | | +..... --- --- --- --- --- --- + +A--- B--- C---. D--- E---. F---. G--- H I---. J K . L M +| | | | | | | | | | | | | | | / | |\ /| +|---| -| | | | |--- |--- | -- |---| | | |-- | | ' | +| | | | | | | | | | | | | | | | | \ | | | +' ' --- --- --- --- ' --- ' ' --- --- ' ' ---. ' ' + +N O--- P--- Q--- R--- S--- T---. U V . W X . Y . Z---. +|\ | | | | | | | | | | | | | | / | | \ / \ / / +| \ | | | |--- | | |-- --- | | | | / | . | X | / +| \| | | | | \| | \ | | | | |/ |/ \| / \ | / +' ' --- --- ' ' --- --- ' ' ' ' ' --- + +&---. $--- (---. )--- [---. ]--- %.... *.... +.... -.... /.... =.... \.... + \ / | | | | | | | / .\|/ . | . . / . .\ + -- --- | | | | . .--- .--- .--- . / .--- . \ +| \ | | | | | | ./ | ./|\ . | . ./ . . \ + --- --- --- --- --- --- . . . . . .--- . + +_.... '.... +. . | +. . +. . +.--- . diff --git a/gpiozero/fonts/7seg.txt b/gpiozero/fonts/7seg.txt new file mode 100644 index 000000000..9b7b18807 --- /dev/null +++ b/gpiozero/fonts/7seg.txt @@ -0,0 +1,23 @@ +# This file defines the default alphabet for 7-segment displays. The format is +# fairly simple: the file is loaded as a whole and all blank and #-prefixed +# lines are stripped out. Then all blank columns are stripped out. Finally the +# remaining rows and columns are divided into 3x3 cells with the following +# format: +# +# Xa. +# fgb +# edc +# +# Where X is the character being defined, and a..g are the segments that are +# active. a, d, and g are considered active if they are "_" and inactive if +# they are anything else. b, c, e, and f are considered active if they are "|" +# and inactive if they are anything else. The top-right character (marked "." +# in the diagram above) is ignored. The result is fairly visually obvious :) + + . 0_ 1. 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_ +... |.| ..| ._| ._| |_| |_. |_. ..| |_| |_| +... |_| ..| |_. ._| ..| ._| |_| ..| |_| ._| + +A_ B. C_ D. E_ F_ G_ H. I. J. L. N_ O. P_ Q_ R. S_ T. U. Y. +|_| |_. |.. ._| |_. |_. |.. |_| |.. ..| |.. |.| ._. |_| |_| ._. |_. |_. |.| |_| +|.| |_| |_. |_| |_. |.. |_| |.| |.. ._| |__ |.| |_| |.. ..| |.. ._| |_. |_| ._| diff --git a/gpiozero/fonts/__init__.py b/gpiozero/fonts/__init__.py new file mode 100644 index 000000000..4354fcfe2 --- /dev/null +++ b/gpiozero/fonts/__init__.py @@ -0,0 +1,224 @@ +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2021-2023 Dave Jones +# +# SPDX-License-Identifier: BSD-3-Clause + +import io +from collections import Counter +from itertools import zip_longest +from pathlib import Path + + +def load_segment_font(filename_or_obj, width, height, pins): + """ + A generic function for parsing segment font definition files. + + If you're working with "standard" `7-segment`_ or `14-segment`_ displays + you *don't* want this function; see :func:`load_font_7seg` or + :func:`load_font_14seg` instead. However, if you are working with another + style of segmented display and wish to construct a parser for a custom + format, this is the function you want. + + The *filename_or_obj* parameter is simply the file-like object or filename + to load. This is typically passed in from the calling function. + + The *width* and *height* parameters give the width and height in characters + of each character definition. For example, these are 3 and 3 for 7-segment + displays. Finally, *pins* is a list of tuples that defines the position of + each pin definition in the character array, and the character that marks + that position "active". + + For example, for 7-segment displays this function is called as follows:: + + load_segment_font(filename_or_obj, width=3, height=3, pins=[ + (1, '_'), (5, '|'), (8, '|'), (7, '_'), + (6, '|'), (3, '|'), (4, '_')]) + + This dictates that each character will be defined by a 3x3 character grid + which will be converted into a nine-character string like so: + + .. code-block:: text + + 012 + 345 ==> '012345678' + 678 + + Position 0 is always assumed to be the character being defined. The *pins* + list then specifies: the first pin is the character at position 1 which + will be "on" when that character is "_". The second pin is the character + at position 5 which will be "on" when that character is "|", and so on. + + .. _7-segment: https://en.wikipedia.org/wiki/Seven-segment_display + .. _14-segment: https://en.wikipedia.org/wiki/Fourteen-segment_display + """ + assert 0 < len(pins) <= (width * height) - 1 + if isinstance(filename_or_obj, bytes): + filename_or_obj = filename_or_obj.decode('utf-8') + opened = isinstance(filename_or_obj, (str, Path)) + if opened: + filename_or_obj = io.open(filename_or_obj, 'r') + try: + lines = filename_or_obj.read() + if isinstance(lines, bytes): + lines = lines.decode('utf-8') + lines = lines.splitlines() + finally: + if opened: + filename_or_obj.close() + + # Strip out comments and blank lines, but remember the original line + # numbers of each row for error reporting purposes + rows = [ + (index, line) for index, line in enumerate(lines, start=1) + # Strip comments and blank (or whitespace) lines + if line.strip() and not line.startswith('#') + ] + line_numbers = { + row_index: line_index + for row_index, (line_index, row) in enumerate(rows) + } + rows = [row for index, row in rows] + if len(rows) % height: + raise ValueError( + f'number of definition lines is not divisible by {height}') + + # Strip out blank columns then transpose back to rows, and make sure + # everything is the right "shape" + for n in range(0, len(rows), height): + cols = [ + col for col in zip_longest(*rows[n:n + height], fillvalue=' ') + # Strip blank (or whitespace) columns + if ''.join(col).strip() + ] + rows[n:n + height] = list(zip(*cols)) + for row_index, row in enumerate(rows): + if len(row) % width: + raise ValueError( + f'length of definitions starting on line ' + f'{line_numbers[row_index]} is not divisible by {width}') + + # Split rows up into character definitions. After this, chars will be a + # list of strings each with width x height characters. The first character + # in each string will be the character being defined + chars = [ + ''.join( + char + for row in rows[y::height] + for char in row + )[x::width] + for y in range(height) + for x in range(width) + ] + chars = [''.join(char) for char in zip(*chars)] + + # Strip out blank entries (a consequence of zip_longest above) and check + # there're no repeat definitions + chars = [char for char in chars if char.strip()] + counts = Counter(char[0] for char in chars) + for char, count in counts.most_common(): + if count > 1: + raise ValueError(f'multiple definitions for {char!r}') + + return { + char[0]: tuple(int(char[pos] == on) for pos, on in pins) + for char in chars + } + + +def load_font_7seg(filename_or_obj): + """ + Given a filename or a file-like object, parse it as an font definition for + a `7-segment display`_, returning a :class:`dict` suitable for use with + :class:`~gpiozero.LEDCharDisplay`. + + The file-format is a simple text-based format in which blank and #-prefixed + lines are ignored. All other lines are assumed to be groups of character + definitions which are cells of 3x3 characters laid out as follows: + + .. code-block:: text + + Ca + fgb + edc + + Where C is the character being defined, and a-g define the states of the + LEDs for that position. a, d, and g are on if they are "_". b, c, e, and + f are on if they are "|". Any other character in these positions is + considered off. For example, you might define the following characters: + + .. code-block:: text + + . 0_ 1. 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_ + ... |.| ..| ._| ._| |_| |_. |_. ..| |_| |_| + ... |_| ..| |_. ._| ..| ._| |_| ..| |_| ._| + + In the example above, empty locations are marked with "." but could mostly + be left as spaces. However, the first item defines the space (" ") + character and needs *some* non-space characters in its definition as the + parser also strips empty columns (as typically occur between character + definitions). This is also why the definition for "1" must include + something to fill the middle column. + + .. _7-segment display: https://en.wikipedia.org/wiki/Seven-segment_display + """ + return load_segment_font(filename_or_obj, width=3, height=3, pins=[ + (1, '_'), (5, '|'), (8, '|'), (7, '_'), + (6, '|'), (3, '|'), (4, '_')]) + + +def load_font_14seg(filename_or_obj): + """ + Given a filename or a file-like object, parse it as a font definition for a + `14-segment display`_, returning a :class:`dict` suitable for use with + :class:`~gpiozero.LEDCharDisplay`. + + The file-format is a simple text-based format in which blank and #-prefixed + lines are ignored. All other lines are assumed to be groups of character + definitions which are cells of 5x5 characters laid out as follows: + + .. code-block:: text + + X.a.. + fijkb + .g.h. + elmnc + ..d.. + + Where X is the character being defined, and a-n define the states of the + LEDs for that position. a, d, g, and h are on if they are "-". b, c, e, f, + j, and m are on if they are "|". i and n are on if they are "\\". Finally, + k and l are on if they are "/". Any other character in these positions is + considered off. For example, you might define the following characters: + + .. code-block:: text + + .... 0--- 1.. 2--- 3--- 4 5--- 6--- 7---. 8--- 9--- + ..... | /| /| | | | | | | / | | | | + ..... | / | | --- -- ---| --- |--- | --- ---| + ..... |/ | | | | | | | | | | | | + ..... --- --- --- --- --- --- + + In the example above, several locations have extraneous characters. For + example, the "/" in the center of the "0" definition, or the "-" in the + middle of the "8". These locations are ignored, but filled in nonetheless + to make the shape more obvious. + + These extraneous locations could equally well be left as spaces. However, + the first item defines the space (" ") character and needs *some* non-space + characters in its definition as the parser also strips empty columns (as + typically occur between character definitions) and verifies that + definitions are 5 columns wide and 5 rows high. + + This also explains why place-holder characters (".") have been inserted at + the top of the definition of the "1" character. Otherwise the parser will + strip these empty columns and decide the definition is invalid (as the + result is only 3 columns wide). + + .. _14-segment display: https://en.wikipedia.org/wiki/Fourteen-segment_display + """ + return load_segment_font(filename_or_obj, width=5, height=5, pins=[ + (2, '-'), (9, '|'), (19, '|'), (22, '-'), + (15, '|'), (5, '|'), (11, '-'), (13, '-'), + (6, '\\'), (7, '|'), (8, '/'), (16, '/'), + (17, '|'), (18, '\\')]) diff --git a/gpiozero/input_devices.py b/gpiozero/input_devices.py index 5804794c7..b632a02e9 100644 --- a/gpiozero/input_devices.py +++ b/gpiozero/input_devices.py @@ -1,57 +1,38 @@ # vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Andrew Scheller -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2015-2019 Ben Nuttall +# +# Copyright (c) 2015-2023 Dave Jones +# Copyright (c) 2015-2021 Ben Nuttall +# Copyright (c) 2020 Robert Erdin +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2020 Dan Jackson +# Copyright (c) 2016-2020 Andrew Scheller +# Copyright (c) 2019 Kosovan Sofiia # Copyright (c) 2018 Philippe Muller # Copyright (c) 2016 Steveis # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, -) +# SPDX-License-Identifier: BSD-3-Clause import warnings -from time import sleep, time +from time import sleep from threading import Event, Lock +from itertools import tee +from statistics import median, mean + +from .exc import ( + InputDeviceError, + DeviceClosed, + DistanceSensorNoEcho, + PinInvalidState, + PWMSoftwareFallback, +) +from .devices import GPIODevice, CompositeDevice +from .mixins import GPIOQueue, EventsMixin, HoldMixin, event try: - from statistics import median + from .pins.pigpio import PiGPIOFactory except ImportError: - from .compat import median - -from .exc import InputDeviceError, DeviceClosed, DistanceSensorNoEcho, \ - PinInvalidState -from .devices import GPIODevice -from .mixins import GPIOQueue, EventsMixin, HoldMixin + PiGPIOFactory = None class InputDevice(GPIODevice): @@ -93,9 +74,9 @@ class InputDevice(GPIODevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, pin=None, pull_up=False, active_state=None, + def __init__(self, pin=None, *, pull_up=False, active_state=None, pin_factory=None): - super(InputDevice, self).__init__(pin, pin_factory=pin_factory) + super().__init__(pin, pin_factory=pin_factory) try: self.pin.function = 'input' pull = {None: 'floating', True: 'up', False: 'down'}[pull_up] @@ -108,14 +89,14 @@ def __init__(self, pin=None, pull_up=False, active_state=None, if pull_up is None: if active_state is None: raise PinInvalidState( - 'Pin %d is defined as floating, but "active_state" is not ' - 'defined' % self.pin.number) + f'Pin {self.pin.info.name} is defined as floating, but ' + f'"active_state" is not defined') self._active_state = bool(active_state) else: if active_state is not None: raise PinInvalidState( - 'Pin %d is not floating, but "active_state" is not None' % - self.pin.number) + f'Pin {self.pin.info.name} is not floating, but ' + f'"active_state" is not None') self._active_state = False if pull_up else True self._inactive_state = not self._active_state @@ -133,10 +114,12 @@ def pull_up(self): def __repr__(self): try: - return "" % ( - self.__class__.__name__, self.pin, self.pull_up, self.is_active) + return ( + f"") except: - return super(InputDevice, self).__repr__() + return super().__repr__() class DigitalInputDevice(EventsMixin, InputDevice): @@ -156,7 +139,7 @@ class DigitalInputDevice(EventsMixin, InputDevice): :type pull_up: bool or None :param pull_up: - See descrpition under :class:`InputDevice` for more information. + See description under :class:`InputDevice` for more information. :type active_state: bool or None :param active_state: @@ -174,10 +157,9 @@ class DigitalInputDevice(EventsMixin, InputDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, pull_up=False, active_state=None, bounce_time=None, - pin_factory=None): - super(DigitalInputDevice, self).__init__( + def __init__(self, pin=None, *, pull_up=False, active_state=None, + bounce_time=None, pin_factory=None): + super().__init__( pin, pull_up=pull_up, active_state=active_state, pin_factory=pin_factory) try: @@ -227,7 +209,7 @@ class SmoothedInputDevice(EventsMixin, InputDevice): :type pull_up: bool or None :param pull_up: - See descrpition under :class:`InputDevice` for more information. + See description under :class:`InputDevice` for more information. :type active_state: bool or None :param active_state: @@ -268,11 +250,11 @@ class SmoothedInputDevice(EventsMixin, InputDevice): which most users can ignore). """ def __init__( - self, pin=None, pull_up=False, active_state=None, threshold=0.5, + self, pin=None, *, pull_up=False, active_state=None, threshold=0.5, queue_len=5, sample_wait=0.0, partial=False, average=median, ignore=None, pin_factory=None): self._queue = None - super(SmoothedInputDevice, self).__init__( + super().__init__( pin, pull_up=pull_up, active_state=active_state, pin_factory=pin_factory) try: @@ -289,26 +271,26 @@ def close(self): except AttributeError: # If the queue isn't initialized (it's None), or _queue hasn't been # set ignore the error because we're trying to close anyway - if self._queue is not None: - raise + pass except RuntimeError: # Cannot join thread before it starts; we don't care about this # because we're trying to close the thread anyway pass self._queue = None - super(SmoothedInputDevice, self).close() + super().close() def __repr__(self): try: self._check_open() except DeviceClosed: - return super(SmoothedInputDevice, self).__repr__() + return super().__repr__() else: if self.partial or self._queue.full.is_set(): - return super(SmoothedInputDevice, self).__repr__() + return super().__repr__() else: - return "" % ( - self.__class__.__name__, self.pin, self.pull_up) + return ( + f"") @property def queue_len(self): @@ -424,10 +406,10 @@ class Button(HoldMixin, DigitalInputDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, pull_up=True, active_state=None, bounce_time=None, - hold_time=1, hold_repeat=False, pin_factory=None): - super(Button, self).__init__( + def __init__(self, pin=None, *, pull_up=True, active_state=None, + bounce_time=None, hold_time=1, hold_repeat=False, + pin_factory=None): + super().__init__( pin, pull_up=pull_up, active_state=active_state, bounce_time=bounce_time, pin_factory=pin_factory) self.hold_time = hold_time @@ -438,7 +420,7 @@ def value(self): """ Returns 1 if the button is currently pressed, and 0 if it is not. """ - return super(Button, self).value + return super().value Button.is_pressed = Button.is_active Button.pressed_time = Button.active_time @@ -478,7 +460,7 @@ class LineSensor(SmoothedInputDevice): :type pull_up: bool or None :param pull_up: - See descrpition under :class:`InputDevice` for more information. + See description under :class:`InputDevice` for more information. :type active_state: bool or None :param active_state: @@ -511,19 +493,15 @@ class LineSensor(SmoothedInputDevice): .. _CamJam #3 EduKit: http://camjam.me/?page_id=1035 """ - def __init__( - self, pin=None, pull_up=False, active_state=None, queue_len=5, - sample_rate=100, threshold=0.5, partial=False, pin_factory=None): - super(LineSensor, self).__init__( + def __init__(self, pin=None, *, pull_up=False, active_state=None, + queue_len=5, sample_rate=100, threshold=0.5, partial=False, + pin_factory=None): + super().__init__( pin, pull_up=pull_up, active_state=active_state, threshold=threshold, queue_len=queue_len, sample_wait=1 / sample_rate, partial=partial, pin_factory=pin_factory) - try: - self._queue.start() - except: - self.close() - raise + self._queue.start() @property def value(self): @@ -532,7 +510,7 @@ def value(self): is nearer 0 for black under the sensor, and nearer 1 for white under the sensor. """ - return super(LineSensor, self).value + return super().value @property def line_detected(self): @@ -572,7 +550,7 @@ class MotionSensor(SmoothedInputDevice): :type pull_up: bool or None :param pull_up: - See descrpition under :class:`InputDevice` for more information. + See description under :class:`InputDevice` for more information. :type active_state: bool or None :param active_state: @@ -585,7 +563,7 @@ class MotionSensor(SmoothedInputDevice): :param float sample_rate: The number of values to read from the device (and append to the - internal queue) per second. Defaults to 100. + internal queue) per second. Defaults to 10. :param float threshold: Defaults to 0.5. When the average of all values in the internal queue @@ -604,18 +582,14 @@ class MotionSensor(SmoothedInputDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, pull_up=False, active_state=None, queue_len=1, - sample_rate=10, threshold=0.5, partial=False, pin_factory=None): - super(MotionSensor, self).__init__( + def __init__(self, pin=None, *, pull_up=False, active_state=None, + queue_len=1, sample_rate=10, threshold=0.5, partial=False, + pin_factory=None): + super().__init__( pin, pull_up=pull_up, active_state=active_state, threshold=threshold, queue_len=queue_len, sample_wait=1 / - sample_rate, partial=partial, pin_factory=pin_factory) - try: - self._queue.start() - except: - self.close() - raise + sample_rate, partial=partial, pin_factory=pin_factory, average=mean) + self._queue.start() @property def value(self): @@ -625,7 +599,7 @@ def value(self): a *queue_len* greater than 1, this will be an averaged value where values closer to 1 imply motion detection. """ - return super(MotionSensor, self).value + return super().value MotionSensor.motion_detected = MotionSensor.is_active MotionSensor.when_motion = MotionSensor.when_activated @@ -688,10 +662,9 @@ class LightSensor(SmoothedInputDevice): .. _CamJam #2 EduKit: http://camjam.me/?page_id=623 """ - def __init__( - self, pin=None, queue_len=5, charge_time_limit=0.01, - threshold=0.1, partial=False, pin_factory=None): - super(LightSensor, self).__init__( + def __init__(self, pin=None, *, queue_len=5, charge_time_limit=0.01, + threshold=0.1, partial=False, pin_factory=None): + super().__init__( pin, pull_up=False, threshold=threshold, queue_len=queue_len, sample_wait=0.0, partial=partial, pin_factory=pin_factory) try: @@ -719,25 +692,25 @@ def _read(self): self.pin.function = 'output' self.pin.state = False sleep(0.1) - # Time the charging of the capacitor - start = self.pin_factory.ticks() self._charge_time = None self._charged.clear() + # Time the charging of the capacitor + start = self.pin_factory.ticks() self.pin.function = 'input' self._charged.wait(self.charge_time_limit) if self._charge_time is None: return 0.0 else: - return 1.0 - ( - self.pin_factory.ticks_diff(self._charge_time, start) / - self.charge_time_limit) + return 1.0 - min(1.0, + (self.pin_factory.ticks_diff(self._charge_time, start) / + self.charge_time_limit)) @property def value(self): """ Returns a value between 0 (dark) and 1 (light). """ - return super(LightSensor, self).value + return super().value LightSensor.light_detected = LightSensor.is_active LightSensor.when_light = LightSensor.when_activated @@ -772,7 +745,7 @@ class DistanceSensor(SmoothedInputDevice): Alternatively, the 3V3 tolerant HC-SR04P sensor (which does not require a voltage divider) will work with this class. - + .. note:: @@ -821,7 +794,7 @@ class DistanceSensor(SmoothedInputDevice): :param int queue_len: The length of the queue used to store values read from the sensor. - This defaults to 30. + This defaults to 9. :param float max_distance: The :attr:`value` attribute reports a normalized value between 0 (too @@ -847,11 +820,11 @@ class DistanceSensor(SmoothedInputDevice): """ ECHO_LOCK = Lock() - def __init__( - self, echo=None, trigger=None, queue_len=9, max_distance=1, - threshold_distance=0.3, partial=False, pin_factory=None): + def __init__(self, echo=None, trigger=None, *, queue_len=9, + max_distance=1, threshold_distance=0.3, partial=False, + pin_factory=None): self._trigger = None - super(DistanceSensor, self).__init__( + super().__init__( echo, pull_up=False, queue_len=queue_len, sample_wait=0.06, partial=partial, ignore=frozenset({None}), pin_factory=pin_factory ) @@ -861,7 +834,7 @@ def __init__( self._max_distance = max_distance self.threshold = threshold_distance / max_distance self.speed_of_sound = 343.26 # m/s - self._trigger = GPIODevice(trigger) + self._trigger = GPIODevice(trigger, pin_factory=pin_factory) self._echo = Event() self._echo_rise = None self._echo_fall = None @@ -875,14 +848,19 @@ def __init__( self.close() raise + if PiGPIOFactory is None or not isinstance(self.pin_factory, PiGPIOFactory): + warnings.warn(PWMSoftwareFallback( + 'For more accurate readings, use the pigpio pin factory.' + 'See https://gpiozero.readthedocs.io/en/stable/api_input.html#distancesensor-hc-sr04 for more info' + )) + def close(self): try: self._trigger.close() except AttributeError: - if self._trigger is not None: - raise + pass self._trigger = None - super(DistanceSensor, self).close() + super().close() @property def max_distance(self): @@ -934,7 +912,7 @@ def value(self): difference, and 1, indicating the reflector is at or beyond the specified *max_distance*. """ - return super(DistanceSensor, self).value + return super().value @property def trigger(self): @@ -1005,3 +983,389 @@ def in_range(self): DistanceSensor.when_in_range = DistanceSensor.when_deactivated DistanceSensor.wait_for_out_of_range = DistanceSensor.wait_for_active DistanceSensor.wait_for_in_range = DistanceSensor.wait_for_inactive + + +class RotaryEncoder(EventsMixin, CompositeDevice): + """ + Represents a simple two-pin incremental `rotary encoder`_ device. + + These devices typically have three pins labelled "A", "B", and "C". Connect + A and B directly to two GPIO pins, and C ("common") to one of the ground + pins on your Pi. Then simply specify the A and B pins as the arguments when + constructing this classs. + + For example, if your encoder's A pin is connected to GPIO 21, and the B + pin to GPIO 20 (and presumably the C pin to a suitable GND pin), while an + LED (with a suitable 300Ω resistor) is connected to GPIO 5, the following + session will result in the brightness of the LED being controlled by + dialling the rotary encoder back and forth:: + + >>> from gpiozero import RotaryEncoder + >>> from gpiozero.tools import scaled_half + >>> rotor = RotaryEncoder(21, 20) + >>> led = PWMLED(5) + >>> led.source = scaled_half(rotor.values) + + :type a: int or str + :param a: + The GPIO pin connected to the "A" output of the rotary encoder. + + :type b: int or str + :param b: + The GPIO pin connected to the "B" output of the rotary encoder. + + :type bounce_time: float or None + :param bounce_time: + If :data:`None` (the default), no software bounce compensation will be + performed. Otherwise, this is the length of time (in seconds) that the + component will ignore changes in state after an initial change. + + :type max_steps: int + :param max_steps: + The number of steps clockwise the encoder takes to change the + :attr:`value` from 0 to 1, or counter-clockwise from 0 to -1. + If this is 0, then the encoder's :attr:`value` never changes, but you + can still read :attr:`steps` to determine the integer number of steps + the encoder has moved clockwise or counter clockwise. + + :type threshold_steps: tuple of int + :param threshold_steps: + A (min, max) tuple of steps between which the device will be considered + "active", inclusive. In other words, when :attr:`steps` is greater than + or equal to the *min* value, and less than or equal the *max* value, + the :attr:`active` property will be :data:`True` and the appropriate + events (:attr:`when_activated`, :attr:`when_deactivated`) will be + fired. Defaults to (0, 0). + + :type wrap: bool + :param wrap: + If :data:`True` and *max_steps* is non-zero, when the :attr:`steps` + reaches positive or negative *max_steps* it wraps around by negation. + Defaults to :data:`False`. + + :type pin_factory: Factory or None + :param pin_factory: + See :doc:`api_pins` for more information (this is an advanced feature + which most users can ignore). + + .. _rotary encoder: https://en.wikipedia.org/wiki/Rotary_encoder + """ + # The rotary encoder's two pins move through the following sequence when + # the encoder is rotated one step clockwise: + # + # ────┐ ┌─────┐ ┌──────── + # _ │ │ │ │ counter ┌───┐ + # A │ │ │ │ clockwise ┌─── │ 0 │ ───┐ clockwise + # └─────┘ └─────┘ (CCW) │ └───┘ │ (CW) + # : : : : │ ┌───┐ ┌───┐ │ + # ───────┐ : ┌─────┐ : ┌───── ▾ │ 1 │ │ 2 │ ▾ + # _ : │ : │ : │ : │ └───┘ └───┘ + # B : │ : │ : │ : │ │ ┌───┐ │ + # : └─────┘ : └─────┘ └─── │ 3 │ ───┘ + # : : : : : : : : └───┘ + # 0 2 3 1 0 2 3 1 0 + # + # Treating the A pin as a "high" bit, and the B pin as a "low" bit, this + # means that the pins return the sequence 0, 2, 3, 1 for each step that the + # encoder takes clockwise. Conversely, the pins return the sequence 0, 1, + # 3, 2 for each step counter-clockwise. + # + # We can treat these values as edges to take in a simple state machine, + # which is represented in the dictionary below: + + TRANSITIONS = { + 'idle': ['idle', 'ccw1', 'cw1', 'idle'], + 'ccw1': ['idle', 'ccw1', 'ccw3', 'ccw2'], + 'ccw2': ['idle', 'ccw1', 'ccw3', 'ccw2'], + 'ccw3': ['-1', 'idle', 'ccw3', 'ccw2'], + 'cw1': ['idle', 'cw3', 'cw1', 'cw2'], + 'cw2': ['idle', 'cw3', 'cw1', 'cw2'], + 'cw3': ['+1', 'cw3', 'idle', 'cw2'], + } + + # The state machine here includes more than just the strictly necessary + # edges; it also permits "wiggle" between intermediary states so that the + # overall graph looks like this: + # + # ┌──────┐ + # │ │ + # ┌─────┤ idle ├────┐ + # │1 │ │ 2│ + # │ └──────┘ │ + # ▾ ▴ ▴ ▾ + # ┌────────┐ │ │ ┌───────┐ + # │ │ 0│ │0 │ │ + # ┌───┤ ccw1 ├──┤ ├──┤ cw1 ├───┐ + # │2 │ │ │ │ │ │ 1│ + # │ └─┬──────┘ │ │ └─────┬─┘ │ + # │ 3│ ▴ │ │ ▴ │3 │ + # │ ▾ │1 │ │ 2│ ▾ │ + # │ ┌──────┴─┐ │ │ ┌─┴─────┐ │ + # │ │ │ 0│ │0 │ │ │ + # │ │ ccw2 ├──┤ ├──┤ cw2 │ │ + # │ │ │ │ │ │ │ │ + # │ └─┬──────┘ │ │ └─────┬─┘ │ + # │ 2│ ▴ │ │ ▴ │1 │ + # │ ▾ │3 │ │ 3│ ▾ │ + # │ ┌──────┴─┐ │ │ ┌─┴─────┐ │ + # │ │ │ │ │ │ │ │ + # └──▸│ ccw3 │ │ │ │ cw3 │◂──┘ + # │ │ │ │ │ │ + # └───┬────┘ │ │ └───┬───┘ + # 0│ │ │ │0 + # ▾ │ │ ▾ + # ┌────────┐ │ │ ┌───────┐ + # │ │ │ │ │ │ + # │ -1 ├──┘ └──┤ +1 │ + # │ │ │ │ + # └────────┘ └───────┘ + # + # Note that, once we start down the clockwise (cw) or counter-clockwise + # (ccw) path, we don't allow the state to pick the alternate direction + # without passing through the idle state again. This seems to work well in + # practice with several encoders, even quite jiggly ones with no debounce + # hardware or software + + def __init__(self, a, b, *, bounce_time=None, max_steps=16, + threshold_steps=(0, 0), wrap=False, pin_factory=None): + min_thresh, max_thresh = threshold_steps + if max_thresh < min_thresh: + raise ValueError('maximum threshold cannot be less than minimum') + self._steps = 0 + self._max_steps = int(max_steps) + self._threshold = (int(min_thresh), int(max_thresh)) + self._wrap = bool(wrap) + self._state = 'idle' + self._edge = 0 + self._when_rotated = None + self._when_rotated_cw = None + self._when_rotated_ccw = None + self._rotate_event = Event() + self._rotate_cw_event = Event() + self._rotate_ccw_event = Event() + super().__init__( + a=InputDevice(a, pull_up=True, pin_factory=pin_factory), + b=InputDevice(b, pull_up=True, pin_factory=pin_factory), + _order=('a', 'b'), pin_factory=pin_factory) + self.a.pin.bounce_time = bounce_time + self.b.pin.bounce_time = bounce_time + self.a.pin.edges = 'both' + self.b.pin.edges = 'both' + self.a.pin.when_changed = self._a_changed + self.b.pin.when_changed = self._b_changed + # Call _fire_events once to set initial state of events + self._fire_events(self.pin_factory.ticks(), self.is_active) + + def __repr__(self): + try: + self._check_open() + return ( + f"") + except DeviceClosed: + return super().__repr__() + + def _a_changed(self, ticks, state): + edge = (self.a._state_to_value(state) << 1) | (self._edge & 0x1) + self._change_state(ticks, edge) + + def _b_changed(self, ticks, state): + edge = (self._edge & 0x2) | self.b._state_to_value(state) + self._change_state(ticks, edge) + + def _change_state(self, ticks, edge): + self._edge = edge + new_state = RotaryEncoder.TRANSITIONS[self._state][edge] + if new_state == '+1': + self._steps = ( + self._steps + 1 + if not self._max_steps or self._steps < self._max_steps else + -self._max_steps if self._wrap else self._max_steps + ) + self._rotate_cw_event.set() + self._fire_rotated_cw() + self._rotate_cw_event.clear() + elif new_state == '-1': + self._steps = ( + self._steps - 1 + if not self._max_steps or self._steps > -self._max_steps else + self._max_steps if self._wrap else -self._max_steps + ) + self._rotate_ccw_event.set() + self._fire_rotated_ccw() + self._rotate_ccw_event.clear() + else: + self._state = new_state + return + self._rotate_event.set() + self._fire_rotated() + self._rotate_event.clear() + self._fire_events(ticks, self.is_active) + self._state = 'idle' + + def wait_for_rotate(self, timeout=None): + """ + Pause the script until the encoder is rotated at least one step in + either direction, or the timeout is reached. + + :type timeout: float or None + :param timeout: + Number of seconds to wait before proceeding. If this is + :data:`None` (the default), then wait indefinitely until the + encoder is rotated. + """ + return self._rotate_event.wait(timeout) + + def wait_for_rotate_clockwise(self, timeout=None): + """ + Pause the script until the encoder is rotated at least one step + clockwise, or the timeout is reached. + + :type timeout: float or None + :param timeout: + Number of seconds to wait before proceeding. If this is + :data:`None` (the default), then wait indefinitely until the + encoder is rotated clockwise. + """ + return self._rotate_cw_event.wait(timeout) + + def wait_for_rotate_counter_clockwise(self, timeout=None): + """ + Pause the script until the encoder is rotated at least one step + counter-clockwise, or the timeout is reached. + + :type timeout: float or None + :param timeout: + Number of seconds to wait before proceeding. If this is + :data:`None` (the default), then wait indefinitely until the + encoder is rotated counter-clockwise. + """ + return self._rotate_ccw_event.wait(timeout) + + when_rotated = event( + """ + The function to be run when the encoder is rotated in either direction. + + This can be set to a function which accepts no (mandatory) parameters, + or a Python function which accepts a single mandatory parameter (with + as many optional parameters as you like). If the function accepts a + single mandatory parameter, the device that activated will be passed + as that parameter. + + Set this property to :data:`None` (the default) to disable the event. + """) + + when_rotated_clockwise = event( + """ + The function to be run when the encoder is rotated clockwise. + + This can be set to a function which accepts no (mandatory) parameters, + or a Python function which accepts a single mandatory parameter (with + as many optional parameters as you like). If the function accepts a + single mandatory parameter, the device that activated will be passed + as that parameter. + + Set this property to :data:`None` (the default) to disable the event. + """) + + when_rotated_counter_clockwise = event( + """ + The function to be run when the encoder is rotated counter-clockwise. + + This can be set to a function which accepts no (mandatory) parameters, + or a Python function which accepts a single mandatory parameter (with + as many optional parameters as you like). If the function accepts a + single mandatory parameter, the device that activated will be passed + as that parameter. + + Set this property to :data:`None` (the default) to disable the event. + """) + + @property + def steps(self): + """ + The "steps" value of the encoder starts at 0. It increments by one for + every step the encoder is rotated clockwise, and decrements by one for + every step it is rotated counter-clockwise. The steps value is + limited by :attr:`max_steps`. It will not advance beyond positive or + negative :attr:`max_steps`, unless :attr:`wrap` is :data:`True` in + which case it will roll around by negation. If :attr:`max_steps` is + zero then steps are not limited at all, and will increase infinitely + in either direction, but :attr:`value` will return a constant zero. + + Note that, in contrast to most other input devices, because the rotary + encoder has no absolute position the :attr:`steps` attribute (and + :attr:`value` by corollary) is writable. + """ + return self._steps + + def _fire_rotated(self): + if self.when_rotated: + self.when_rotated() + + def _fire_rotated_cw(self): + if self.when_rotated_clockwise: + self.when_rotated_clockwise() + + def _fire_rotated_ccw(self): + if self.when_rotated_counter_clockwise: + self.when_rotated_counter_clockwise() + + @steps.setter + def steps(self, value): + value = int(value) + if self._max_steps: + value = max(-self._max_steps, min(self._max_steps, value)) + self._steps = value + + @property + def value(self): + """ + Represents the value of the rotary encoder as a value between -1 and 1. + The value is calculated by dividing the value of :attr:`steps` into the + range from negative :attr:`max_steps` to positive :attr:`max_steps`. + + Note that, in contrast to most other input devices, because the rotary + encoder has no absolute position the :attr:`value` attribute is + writable. + """ + try: + return self._steps / self._max_steps + except ZeroDivisionError: + return 0 + + @value.setter + def value(self, value): + self._steps = int(max(-1, min(1, float(value))) * self._max_steps) + + @property + def is_active(self): + return self._threshold[0] <= self._steps <= self._threshold[1] + + @property + def max_steps(self): + """ + The number of discrete steps the rotary encoder takes to move + :attr:`value` from 0 to 1 clockwise, or 0 to -1 counter-clockwise. In + another sense, this is also the total number of discrete states this + input can represent. + """ + return self._max_steps + + @property + def threshold_steps(self): + """ + The mininum and maximum number of steps between which :attr:`is_active` + will return :data:`True`. Defaults to (0, 0). + """ + return self._threshold + + @property + def wrap(self): + """ + If :data:`True`, when :attr:`value` reaches its limit (-1 or 1), it + "wraps around" to the opposite limit. When :data:`False`, the value + (and the corresponding :attr:`steps` attribute) simply don't advance + beyond their limits. + """ + return self._wrap diff --git a/gpiozero/internal_devices.py b/gpiozero/internal_devices.py index 3aa095a36..370bc3939 100644 --- a/gpiozero/internal_devices.py +++ b/gpiozero/internal_devices.py @@ -1,55 +1,24 @@ # vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2017-2019 Ben Nuttall -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2017-2021 Ben Nuttall # Copyright (c) 2019 Jeevan M R <14.jeevan@gmail.com> # Copyright (c) 2019 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, -) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import os import io +import warnings import subprocess from datetime import datetime, time -import warnings from .devices import Device -from .mixins import EventsMixin -from .exc import ThresholdOutOfRange +from .mixins import EventsMixin, event +from .threads import GPIOThread +from .exc import ThresholdOutOfRange, DeviceClosed class InternalDevice(EventsMixin, Device): @@ -59,17 +28,98 @@ class InternalDevice(EventsMixin, Device): usually represent operating system services like the internal clock, file systems or network facilities. """ - # XXX Add some mechanism to monitor state and fire events on change. + def __init__(self, *, pin_factory=None): + self._closed = False + super().__init__(pin_factory=pin_factory) + + def close(self): + self._closed = True + super().close() + @property + def closed(self): + return self._closed -class PingServer(InternalDevice): + def __repr__(self): + try: + self._check_open() + return f"" + except DeviceClosed: + return f"" + + +class PolledInternalDevice(InternalDevice): + """ + Extends :class:`InternalDevice` to provide a background thread to poll + internal devices that lack any other mechanism to inform the instance of + changes. """ - Extends :class:`InternalDevice` to provide a device which is active when a - *host* on the network can be pinged. + def __init__(self, *, event_delay=1.0, pin_factory=None): + self._event_thread = None + self._event_delay = event_delay + super().__init__(pin_factory=pin_factory) - The following example lights an LED while a server is reachable (note the - use of :attr:`~SourceMixin.source_delay` to ensure the server is not - flooded with pings):: + def close(self): + try: + self._start_stop_events(False) + except AttributeError: + pass # pragma: no cover + super().close() + + @property + def event_delay(self): + """ + The delay between sampling the device's value for the purposes of + firing events. + + Note that this only applies to events assigned to attributes like + :attr:`~EventsMixin.when_activated` and + :attr:`~EventsMixin.when_deactivated`. When using the + :attr:`~SourceMixin.source` and :attr:`~ValuesMixin.values` properties, + the sampling rate is controlled by the + :attr:`~SourceMixin.source_delay` property. + """ + return self._event_delay + + @event_delay.setter + def event_delay(self, value): + self._event_delay = float(value) + + def wait_for_active(self, timeout=None): + self._start_stop_events(True) + try: + return super().wait_for_active(timeout) + finally: + self._start_stop_events( + self.when_activated or self.when_deactivated) + + def wait_for_inactive(self, timeout=None): + self._start_stop_events(True) + try: + return super().wait_for_inactive(timeout) + finally: + self._start_stop_events( + self.when_activated or self.when_deactivated) + + def _watch_value(self): + while not self._event_thread.stopping.wait(self._event_delay): + self._fire_events(self.pin_factory.ticks(), self.is_active) + + def _start_stop_events(self, enabled): + if self._event_thread and not enabled: + self._event_thread.stop() + self._event_thread = None + elif not self._event_thread and enabled: + self._event_thread = GPIOThread(self._watch_value) + self._event_thread.start() + + +class PingServer(PolledInternalDevice): + """ + Extends :class:`PolledInternalDevice` to provide a device which is active + when a *host* (domain name or IP address) can be pinged. + + The following example lights an LED while ``google.com`` is reachable:: from gpiozero import PingServer, LED from signal import pause @@ -77,29 +127,34 @@ class PingServer(InternalDevice): google = PingServer('google.com') led = LED(4) - led.source_delay = 60 # check once per minute - led.source = google + google.when_activated = led.on + google.when_deactivated = led.off pause() :param str host: The hostname or IP address to attempt to ping. + :type event_delay: float + :param event_delay: + The number of seconds between pings (defaults to 10 seconds). + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, host, pin_factory=None): + def __init__(self, host, *, event_delay=10.0, pin_factory=None): self._host = host - super(PingServer, self).__init__(pin_factory=pin_factory) - self._fire_events(self.pin_factory.ticks(), None) + super().__init__(event_delay=event_delay, pin_factory=pin_factory) + self._fire_events(self.pin_factory.ticks(), self.is_active) def __repr__(self): try: - return '' % self.host - except: - return super(PingServer, self).__repr__() + self._check_open() + return f'' + except DeviceClosed: + return super().__repr__() @property def host(self): @@ -111,8 +166,8 @@ def host(self): @property def value(self): """ - Returns :data:`True` if the host returned a single ping, and - :data:`False` otherwise. + Returns :data:`1` if the host returned a single ping, and :data:`0` + otherwise. """ # XXX This is doing a DNS lookup every time it's queried; should we # call gethostbyname in the constructor and ping that instead (good @@ -124,15 +179,43 @@ def value(self): ['ping', '-c1', self.host], stdout=devnull, stderr=devnull) except subprocess.CalledProcessError: - return False + return 0 else: - return True + return 1 + when_activated = event( + """ + The function to run when the device changes state from inactive + (host unresponsive) to active (host responsive). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) + + when_deactivated = event( + """ + The function to run when the device changes state from inactive + (host responsive) to active (host unresponsive). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. -class CPUTemperature(InternalDevice): + Set this property to ``None`` (the default) to disable the event. + """) + + +class CPUTemperature(PolledInternalDevice): """ - Extends :class:`InternalDevice` to provide a device which is active when - the CPU temperature exceeds the *threshold* value. + Extends :class:`PolledInternalDevice` to provide a device which is active + when the CPU temperature exceeds the *threshold* value. The following example plots the CPU's temperature on an LED bar graph:: @@ -143,7 +226,7 @@ class CPUTemperature(InternalDevice): # bar graph is a bit more "lively" cpu = CPUTemperature(min_temp=50, max_temp=90) - print('Initial temperature: {}C'.format(cpu.temperature)) + print(f'Initial temperature: {cpu.temperature}C') graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True) graph.source = cpu @@ -168,30 +251,42 @@ class CPUTemperature(InternalDevice): The temperature above which the device will be considered "active". (see :attr:`is_active`). This defaults to 80.0. + :type event_delay: float + :param event_delay: + The number of seconds between file reads (defaults to 5 seconds). + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, sensor_file='/sys/class/thermal/thermal_zone0/temp', - min_temp=0.0, max_temp=100.0, threshold=80.0, pin_factory=None): + def __init__(self, sensor_file='/sys/class/thermal/thermal_zone0/temp', *, + min_temp=0.0, max_temp=100.0, threshold=80.0, event_delay=5.0, + pin_factory=None): self.sensor_file = sensor_file - super(CPUTemperature, self).__init__(pin_factory=pin_factory) - if min_temp >= max_temp: - raise ValueError('max_temp must be greater than min_temp') - self.min_temp = min_temp - self.max_temp = max_temp - if not min_temp <= threshold <= max_temp: - warnings.warn(ThresholdOutOfRange( - 'threshold is outside of the range (min_temp, max_temp)')) - self.threshold = threshold - self._fire_events(self.pin_factory.ticks(), None) + super().__init__(event_delay=event_delay, pin_factory=pin_factory) + try: + if min_temp >= max_temp: + raise ValueError('max_temp must be greater than min_temp') + self.min_temp = min_temp + self.max_temp = max_temp + if not min_temp <= threshold <= max_temp: + warnings.warn(ThresholdOutOfRange( + 'threshold is outside of the range (min_temp, max_temp)')) + self.threshold = threshold + self._fire_events(self.pin_factory.ticks(), self.is_active) + except: + self.close() + raise def __repr__(self): try: - return '' % self.temperature - except: - return super(CPUTemperature, self).__repr__() + self._check_open() + return ( + f'') + except DeviceClosed: + return super().__repr__() @property def temperature(self): @@ -199,7 +294,7 @@ def temperature(self): Returns the current CPU temperature in degrees celsius. """ with io.open(self.sensor_file, 'r') as f: - return float(f.readline().strip()) / 1000 + return float(f.read().strip()) / 1000 @property def value(self): @@ -220,11 +315,39 @@ def is_active(self): """ return self.temperature > self.threshold + when_activated = event( + """ + The function to run when the device changes state from inactive to + active (temperature reaches *threshold*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) + + when_deactivated = event( + """ + The function to run when the device changes state from active to + inactive (temperature drops below *threshold*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) + -class LoadAverage(InternalDevice): +class LoadAverage(PolledInternalDevice): """ - Extends :class:`InternalDevice` to provide a device which is active when - the CPU load average exceeds the *threshold* value. + Extends :class:`PolledInternalDevice` to provide a device which is active + when the CPU load average exceeds the *threshold* value. The following example plots the load average on an LED bar graph:: @@ -261,13 +384,18 @@ class LoadAverage(InternalDevice): The number of minutes over which to average the load. Must be 1, 5 or 15. This defaults to 5. + :type event_delay: float + :param event_delay: + The number of seconds between file reads (defaults to 10 seconds). + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, load_average_file='/proc/loadavg', min_load_average=0.0, - max_load_average=1.0, threshold=0.8, minutes=5, pin_factory=None): + def __init__(self, load_average_file='/proc/loadavg', *, + min_load_average=0.0, max_load_average=1.0, threshold=0.8, + minutes=5, event_delay=10.0, pin_factory=None): if min_load_average >= max_load_average: raise ValueError( 'max_load_average must be greater than min_load_average') @@ -286,14 +414,17 @@ def __init__(self, load_average_file='/proc/loadavg', min_load_average=0.0, 5: 1, 15: 2, }[minutes] - super(LoadAverage, self).__init__(pin_factory=pin_factory) + super().__init__(event_delay=event_delay, pin_factory=pin_factory) self._fire_events(self.pin_factory.ticks(), None) def __repr__(self): try: - return '' % self.load_average - except: - return super(LoadAverage, self).__repr__() + self._check_open() + return ( + f'') + except DeviceClosed: + return super().__repr__() @property def load_average(self): @@ -301,7 +432,7 @@ def load_average(self): Returns the current load average. """ with io.open(self.load_average_file, 'r') as f: - file_columns = f.readline().strip().split() + file_columns = f.read().strip().split() return float(file_columns[self._load_average_file_column]) @property @@ -322,16 +453,44 @@ def is_active(self): """ return self.load_average > self.threshold + when_activated = event( + """ + The function to run when the device changes state from inactive to + active (load average reaches *threshold*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) -class TimeOfDay(InternalDevice): + when_deactivated = event( + """ + The function to run when the device changes state from active to + inactive (load average drops below *threshold*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) + + +class TimeOfDay(PolledInternalDevice): """ - Extends :class:`InternalDevice` to provide a device which is active when - the computer's clock indicates that the current time is between + Extends :class:`PolledInternalDevice` to provide a device which is active + when the computer's clock indicates that the current time is between *start_time* and *end_time* (inclusive) which are :class:`~datetime.time` instances. The following example turns on a lamp attached to an :class:`Energenie` - plug between 7 and 8 AM:: + plug between 07:00AM and 08:00AM:: from gpiozero import TimeOfDay, Energenie from datetime import time @@ -340,7 +499,8 @@ class TimeOfDay(InternalDevice): lamp = Energenie(1) morning = TimeOfDay(time(7), time(8)) - lamp.source = morning + morning.when_activated = lamp.on + morning.when_deactivated = lamp.off pause() @@ -357,29 +517,41 @@ class TimeOfDay(InternalDevice): If :data:`True` (the default), a naive UTC time will be used for the comparison rather than a local time-zone reading. + :type event_delay: float + :param event_delay: + The number of seconds between file reads (defaults to 10 seconds). + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, start_time, end_time, utc=True, pin_factory=None): + def __init__(self, start_time, end_time, *, utc=True, event_delay=5.0, + pin_factory=None): self._start_time = None self._end_time = None self._utc = True - super(TimeOfDay, self).__init__(pin_factory=pin_factory) - self._start_time = self._validate_time(start_time) - self._end_time = self._validate_time(end_time) - if self.start_time == self.end_time: - raise ValueError('end_time cannot equal start_time') - self._utc = utc - self._fire_events(self.pin_factory.ticks(), None) + super().__init__(event_delay=event_delay, pin_factory=pin_factory) + try: + self._start_time = self._validate_time(start_time) + self._end_time = self._validate_time(end_time) + if self.start_time == self.end_time: + raise ValueError('end_time cannot equal start_time') + self._utc = utc + self._fire_events(self.pin_factory.ticks(), self.is_active) + except: + self.close() + raise def __repr__(self): try: - return '' % ( - self.start_time, self.end_time, ('local', 'UTC')[self.utc]) - except: - return super(TimeOfDay, self).__repr__() + self._check_open() + return ( + f'') + except DeviceClosed: + return super().__repr__() def _validate_time(self, value): if isinstance(value, datetime): @@ -414,24 +586,51 @@ def utc(self): @property def value(self): """ - Returns :data:`True` when the system clock reads between - :attr:`start_time` and :attr:`end_time`, and :data:`False` otherwise. - If :attr:`start_time` is greater than :attr:`end_time` (indicating a - period that crosses midnight), then this returns :data:`True` when the - current time is greater than :attr:`start_time` or less than - :attr:`end_time`. + Returns :data:`1` when the system clock reads between :attr:`start_time` + and :attr:`end_time`, and :data:`0` otherwise. If :attr:`start_time` is + greater than :attr:`end_time` (indicating a period that crosses + midnight), then this returns :data:`1` when the current time is + greater than :attr:`start_time` or less than :attr:`end_time`. """ now = datetime.utcnow().time() if self.utc else datetime.now().time() if self.start_time < self.end_time: - return self.start_time <= now <= self.end_time + return int(self.start_time <= now <= self.end_time) else: - return not self.end_time < now < self.start_time + return int(not self.end_time < now < self.start_time) + + when_activated = event( + """ + The function to run when the device changes state from inactive to + active (time reaches *start_time*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) + + when_deactivated = event( + """ + The function to run when the device changes state from active to + inactive (time reaches *end_time*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + Set this property to ``None`` (the default) to disable the event. + """) -class DiskUsage(InternalDevice): + +class DiskUsage(PolledInternalDevice): """ - Extends :class:`InternalDevice` to provide a device which is active when - the disk space used exceeds the *threshold* value. + Extends :class:`PolledInternalDevice` to provide a device which is active + when the disk space used exceeds the *threshold* value. The following example plots the disk usage on an LED bar graph:: @@ -440,7 +639,7 @@ class DiskUsage(InternalDevice): disk = DiskUsage() - print('Current disk usage: {}%'.format(disk.usage)) + print(f'Current disk usage: {disk.usage}%') graph = LEDBarGraph(5, 6, 13, 19, 25, pwm=True) graph.source = disk @@ -455,13 +654,19 @@ class DiskUsage(InternalDevice): The disk usage percentage above which the device will be considered "active" (see :attr:`is_active`). This defaults to 90.0. + :type event_delay: float + :param event_delay: + The number of seconds between file reads (defaults to 30 seconds). + :type pin_factory: Factory or None :param pin_factory: See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, filesystem='/', threshold=90.0, pin_factory=None): - super(DiskUsage, self).__init__(pin_factory=pin_factory) + def __init__(self, filesystem='/', *, threshold=90.0, event_delay=30.0, + pin_factory=None): + super().__init__( + event_delay=event_delay, pin_factory=pin_factory) os.statvfs(filesystem) if not 0 <= threshold <= 100: warnings.warn(ThresholdOutOfRange( @@ -472,9 +677,12 @@ def __init__(self, filesystem='/', threshold=90.0, pin_factory=None): def __repr__(self): try: - return '' % self.usage - except: - return super(DiskUsage, self).__repr__() + self._check_open() + return ( + f'') + except DeviceClosed: + return super().__repr__() @property def usage(self): @@ -506,3 +714,31 @@ def is_active(self): *threshold*. """ return self.usage > self.threshold + + when_activated = event( + """ + The function to run when the device changes state from inactive to + active (disk usage reaches *threshold*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) + + when_deactivated = event( + """ + The function to run when the device changes state from active to + inactive (disk usage drops below *threshold*). + + This can be set to a function which accepts no (mandatory) + parameters, or a Python function which accepts a single mandatory + parameter (with as many optional parameters as you like). If the + function accepts a single mandatory parameter, the device that + activated it will be passed as that parameter. + + Set this property to ``None`` (the default) to disable the event. + """) diff --git a/gpiozero/mixins.py b/gpiozero/mixins.py index 55acb5c0e..1b3b34ac2 100644 --- a/gpiozero/mixins.py +++ b/gpiozero/mixins.py @@ -1,53 +1,21 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2018-2019 Ben Nuttall -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2018-2021 Ben Nuttall +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2016 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -nstr = str -str = type('') +# SPDX-License-Identifier: BSD-3-Clause import inspect import weakref +import warnings from functools import wraps, partial from threading import Event from collections import deque -try: - from statistics import median -except ImportError: - from .compat import median -import warnings +from statistics import median from .threads import GPIOThread from .exc import ( @@ -63,7 +31,8 @@ 'e.g. btn.when_pressed = pressed() instead of btn.when_pressed = pressed' ) -class ValuesMixin(object): + +class ValuesMixin: """ Adds a :attr:`values` property to the class which returns an infinite generator of readings from the :attr:`~Device.value` property. There is @@ -87,7 +56,7 @@ def values(self): break -class SourceMixin(object): +class SourceMixin: """ Adds a :attr:`source` property to the class which, given an iterable or a :class:`ValuesMixin` descendent, sets :attr:`~Device.value` to each member @@ -103,11 +72,11 @@ def __init__(self, *args, **kwargs): self._source = None self._source_thread = None self._source_delay = 0.01 - super(SourceMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def close(self): self.source = None - super(SourceMixin, self).close() + super().close() def _copy_values(self, source): for v in source: @@ -146,11 +115,11 @@ def source(self, value): value = value.values self._source = value if value is not None: - self._source_thread = GPIOThread(target=self._copy_values, args=(value,)) + self._source_thread = GPIOThread(self._copy_values, (value,)) self._source_thread.start() -class SharedMixin(object): +class SharedMixin: """ This mixin marks a class as "shared". In this case, the meta-class (GPIOMeta) will use :meth:`_shared_key` to convert the constructor @@ -167,19 +136,96 @@ class SharedMixin(object): def __del__(self): self._refs = 0 - super(SharedMixin, self).__del__() + super().__del__() @classmethod def _shared_key(cls, *args, **kwargs): """ - Given the constructor arguments, returns an immutable key representing - the instance. The default simply assumes all positional arguments are - immutable. + This is called with the constructor arguments to generate a unique + key (which must be storable in a :class:`dict` and, thus, immutable + and hashable) representing the instance that can be shared. This must + be overridden by descendents. """ - return args + raise NotImplementedError -class EventsMixin(object): +class event: + """ + A descriptor representing a callable event on a class descending from + :class:`EventsMixin`. + + Instances of this class are very similar to a :class:`property` but also + deal with notifying the owning class when events are assigned (or + unassigned) and wrapping callbacks implicitly as appropriate. + """ + def __init__(self, doc=None): + self.handlers = {} + self.__doc__ = doc + + def _wrap_callback(self, instance, fn): + if not callable(fn): + raise BadEventHandler('value must be None or a callable') + # If fn is wrapped with partial (i.e. partial, partialmethod, or wraps + # has been used to produce it) we need to dig out the "real" function + # that's been wrapped along with all the mandatory positional args + # used in the wrapper so we can test the binding + args = () + wrapped_fn = fn + while isinstance(wrapped_fn, partial): + args = wrapped_fn.args + args + wrapped_fn = wrapped_fn.func + if inspect.isbuiltin(wrapped_fn): + # We can't introspect the prototype of builtins. In this case we + # assume that the builtin has no (mandatory) parameters; this is + # the most reasonable assumption on the basis that pre-existing + # builtins have no knowledge of gpiozero, and the sole parameter + # we would pass is a gpiozero object + return fn + else: + # Try binding ourselves to the argspec of the provided callable. + # If this works, assume the function is capable of accepting no + # parameters + try: + inspect.getcallargs(wrapped_fn, *args) + return fn + except TypeError: + try: + # If the above fails, try binding with a single parameter + # (ourselves). If this works, wrap the specified callback + inspect.getcallargs(wrapped_fn, *(args + (instance,))) + @wraps(fn) + def wrapper(): + return fn(instance) + return wrapper + except TypeError: + raise BadEventHandler( + 'value must be a callable which accepts up to one ' + 'mandatory parameter') + + def __get__(self, instance, owner=None): + if instance is None: + return self + else: + return self.handlers.get(id(instance)) + + def __set__(self, instance, value): + if value is None: + try: + del self.handlers[id(instance)] + except KeyError: + warnings.warn(CallbackSetToNone(callback_warning)) + else: + self.handlers[id(instance)] = self._wrap_callback(instance, value) + enabled = any( + obj.handlers.get(id(instance)) + for name in dir(type(instance)) + for obj in (getattr(type(instance), name),) + if isinstance(obj, event) + ) + instance._start_stop_events(enabled) + + +class EventsMixin: """ Adds edge-detected :meth:`when_activated` and :meth:`when_deactivated` events to a device based on changes to the :attr:`~Device.is_active` @@ -194,14 +240,30 @@ class EventsMixin(object): initialization to set initial states. """ def __init__(self, *args, **kwargs): - super(EventsMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._active_event = Event() self._inactive_event = Event() - self._when_activated = None - self._when_deactivated = None self._last_active = None self._last_changed = self.pin_factory.ticks() + def _all_events(self): + """ + Generator function which yields all :class:`event` instances defined + against this class. + """ + for name in dir(type(self)): + obj = getattr(type(self), name) + if isinstance(obj, event): + yield obj + + def close(self): + for ev in self._all_events(): + try: + del ev.handlers[id(self)] + except KeyError: + pass + super().close() + def wait_for_active(self, timeout=None): """ Pause the script until the device is activated, or the timeout is @@ -228,8 +290,7 @@ def wait_for_inactive(self, timeout=None): """ return self._inactive_event.wait(timeout) - @property - def when_activated(self): + when_activated = event( """ The function to run when the device changes state from inactive to active. @@ -237,21 +298,13 @@ def when_activated(self): This can be set to a function which accepts no (mandatory) parameters, or a Python function which accepts a single mandatory parameter (with as many optional parameters as you like). If the function accepts a - single mandatory parameter, the device that activated will be passed + single mandatory parameter, the device that activated it will be passed as that parameter. Set this property to :data:`None` (the default) to disable the event. - """ - return self._when_activated + """) - @when_activated.setter - def when_activated(self, value): - if self.when_activated is None and value is None: - warnings.warn(CallbackSetToNone(callback_warning)) - self._when_activated = self._wrap_callback(value) - - @property - def when_deactivated(self): + when_deactivated = event( """ The function to run when the device changes state from active to inactive. @@ -259,18 +312,11 @@ def when_deactivated(self): This can be set to a function which accepts no (mandatory) parameters, or a Python function which accepts a single mandatory parameter (with as many optional parameters as you like). If the function accepts a - single mandatory parameter, the device that deactivated will be + single mandatory parameter, the device that deactivated it will be passed as that parameter. Set this property to :data:`None` (the default) to disable the event. - """ - return self._when_deactivated - - @when_deactivated.setter - def when_deactivated(self, value): - if self.when_deactivated is None and value is None: - warnings.warn(CallbackSetToNone(callback_warning)) - self._when_deactivated = self._wrap_callback(value) + """) @property def active_time(self): @@ -296,48 +342,6 @@ def inactive_time(self): else: return None - def _wrap_callback(self, fn): - if fn is None: - return None - elif not callable(fn): - raise BadEventHandler('value must be None or a callable') - # If fn is wrapped with partial (i.e. partial, partialmethod, or wraps - # has been used to produce it) we need to dig out the "real" function - # that's been wrapped along with all the mandatory positional args - # used in the wrapper so we can test the binding - args = () - wrapped_fn = fn - while isinstance(wrapped_fn, partial): - args = wrapped_fn.args + args - wrapped_fn = wrapped_fn.func - if inspect.isbuiltin(wrapped_fn): - # We can't introspect the prototype of builtins. In this case we - # assume that the builtin has no (mandatory) parameters; this is - # the most reasonable assumption on the basis that pre-existing - # builtins have no knowledge of gpiozero, and the sole parameter - # we would pass is a gpiozero object - return fn - else: - # Try binding ourselves to the argspec of the provided callable. - # If this works, assume the function is capable of accepting no - # parameters - try: - inspect.getcallargs(wrapped_fn, *args) - return fn - except TypeError: - try: - # If the above fails, try binding with a single parameter - # (ourselves). If this works, wrap the specified callback - inspect.getcallargs(wrapped_fn, *(args + (self,))) - @wraps(fn) - def wrapper(): - return fn(self) - return wrapper - except TypeError: - raise BadEventHandler( - 'value must be a callable which accepts up to one ' - 'mandatory parameter') - def _fire_activated(self): # These methods are largely here to be overridden by descendents if self.when_activated: @@ -349,9 +353,22 @@ def _fire_deactivated(self): self.when_deactivated() def _fire_events(self, ticks, new_active): - # NOTE: in contrast to the pin when_changed event, this method takes - # ticks and *is_active* (i.e. the device's .is_active) as opposed to a - # pin's *state*. + """ + This method should be called by descendents whenever the + :attr:`~Device.is_active` property is likely to have changed (for + example, in response to a pin's :attr:`~gpiozero.Pin.state` changing). + + The *ticks* parameter must be set to the time when the change occurred; + this can usually be obtained from the pin factory's + :meth:`gpiozero.Factory.ticks` method but some pin implementations will + implicitly provide the ticks when an event occurs as part of their + reporting mechanism. + + The *new_active* parameter must be set to the device's + :attr:`~Device.is_active` value at the time indicated by *ticks* (which + is not necessarily the value of :attr:`~Device.is_active` right now, if + the pin factory provides means of reporting a pin's historical state). + """ old_active, self._last_active = self._last_active, new_active if old_active is None: # Initial "indeterminate" state; set events but don't fire @@ -371,6 +388,23 @@ def _fire_events(self, ticks, new_active): self._inactive_event.set() self._fire_deactivated() + def _start_stop_events(self, enabled): + """ + This is a stub method that only exists to be overridden by descendents. + It is called when :class:`event` properties are assigned (including + when set to :data:`None) to permit the owning instance to activate or + deactivate monitoring facilities. + + For example, if a descendent requires a background thread to monitor a + device, it would be preferable to only run the thread if event handlers + are present to respond to it. + + The *enabled* parameter is :data:`False` when all :class:`event` + properties on the owning class are :data:`None`, and :data:`True` + otherwise. + """ + pass + class HoldMixin(EventsMixin): """ @@ -380,7 +414,7 @@ class HoldMixin(EventsMixin): """ def __init__(self, *args, **kwargs): self._hold_thread = None - super(HoldMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._when_held = None self._held_from = None self._hold_time = 1 @@ -391,22 +425,21 @@ def close(self): if self._hold_thread is not None: self._hold_thread.stop() self._hold_thread = None - super(HoldMixin, self).close() + super().close() def _fire_activated(self): - super(HoldMixin, self)._fire_activated() + super()._fire_activated() self._hold_thread.holding.set() def _fire_deactivated(self): self._held_from = None - super(HoldMixin, self)._fire_deactivated() + super()._fire_deactivated() def _fire_held(self): if self.when_held: self.when_held() - @property - def when_held(self): + when_held = event( """ The function to run when the device has remained active for :attr:`hold_time` seconds. @@ -418,12 +451,7 @@ def when_held(self): as that parameter. Set this property to :data:`None` (the default) to disable the event. - """ - return self._when_held - - @when_held.setter - def when_held(self, value): - self._when_held = self._wrap_callback(value) + """) @property def hold_time(self): @@ -484,7 +512,7 @@ class HoldThread(GPIOThread): device is active. """ def __init__(self, parent): - super(HoldThread, self).__init__( + super().__init__( target=self.held, args=(weakref.proxy(parent),)) self.holding = Event() self.start() @@ -520,13 +548,13 @@ def __init__( self, parent, queue_len=5, sample_wait=0.0, partial=False, average=median, ignore=None): assert callable(average) - super(GPIOQueue, self).__init__(target=self.fill) if queue_len < 1: raise BadQueueLen('queue_len must be at least one') if sample_wait < 0: raise BadWaitTime('sample_wait must be 0 or greater') if ignore is None: ignore = set() + super().__init__(target=self.fill) self.queue = deque(maxlen=queue_len) self.partial = bool(partial) self.sample_wait = float(sample_wait) diff --git a/gpiozero/output_devices.py b/gpiozero/output_devices.py index a57947d8d..9680f15ef 100644 --- a/gpiozero/output_devices.py +++ b/gpiozero/output_devices.py @@ -1,59 +1,41 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Andrew Scheller -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2015-2019 Ben Nuttall +# +# Copyright (c) 2015-2023 Dave Jones +# Copyright (c) 2022 gnicki2000 <89583687+gnicki2000@users.noreply.github.com> +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2015-2020 Ben Nuttall # Copyright (c) 2019 tuftii <3215045+tuftii@users.noreply.github.com> # Copyright (c) 2019 tuftii +# Copyright (c) 2019 Yisrael Dov Lebow 🐻 +# Copyright (c) 2019 Kosovan Sofiia +# Copyright (c) 2016-2019 Andrew Scheller # Copyright (c) 2016 Ian Harcombe # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, -) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause +import warnings from threading import Lock from itertools import repeat, cycle, chain from colorzero import Color from collections import OrderedDict -try: - from math import log2 -except ImportError: - from .compat import log2 +from math import log2 -from .exc import OutputDeviceBadValue, GPIOPinMissing +from .exc import ( + OutputDeviceBadValue, + GPIOPinMissing, + PWMSoftwareFallback, + DeviceClosed, +) from .devices import GPIODevice, Device, CompositeDevice from .mixins import SourceMixin from .threads import GPIOThread from .tones import Tone +try: + from .pins.pigpio import PiGPIOFactory +except ImportError: + PiGPIOFactory = None class OutputDevice(SourceMixin, GPIODevice): @@ -87,10 +69,9 @@ class OutputDevice(SourceMixin, GPIODevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, active_high=True, initial_value=False, - pin_factory=None): - super(OutputDevice, self).__init__(pin, pin_factory=pin_factory) + def __init__(self, pin=None, *, active_high=True, initial_value=False, + pin_factory=None): + super().__init__(pin, pin_factory=pin_factory) self._lock = Lock() self.active_high = active_high if initial_value is None: @@ -137,7 +118,7 @@ def value(self): Returns 1 if the device is currently active and 0 otherwise. Setting this property changes the state of the device. """ - return super(OutputDevice, self).value + return super().value @value.setter def value(self, value): @@ -165,10 +146,12 @@ def active_high(self, value): def __repr__(self): try: - return '' % ( - self.__class__.__name__, self.pin, self.active_high, self.is_active) + return ( + f'') except: - return super(OutputDevice, self).__repr__() + return super().__repr__() class DigitalOutputDevice(OutputDevice): @@ -202,18 +185,16 @@ class DigitalOutputDevice(OutputDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, active_high=True, initial_value=False, - pin_factory=None): + def __init__(self, pin=None, *, active_high=True, initial_value=False, + pin_factory=None): self._blink_thread = None self._controller = None - super(DigitalOutputDevice, self).__init__( - pin, active_high, initial_value, pin_factory=pin_factory - ) + super().__init__(pin, active_high=active_high, + initial_value=initial_value, pin_factory=pin_factory) @property def value(self): - return super(DigitalOutputDevice, self).value + return super().value @value.setter def value(self, value): @@ -222,7 +203,7 @@ def value(self, value): def close(self): self._stop_blink() - super(DigitalOutputDevice, self).close() + super().close() def on(self): self._stop_blink() @@ -254,8 +235,7 @@ def blink(self, on_time=1, off_time=1, n=None, background=True): """ self._stop_blink() self._blink_thread = GPIOThread( - target=self._blink_device, args=(on_time, off_time, n) - ) + self._blink_device, (on_time, off_time, n)) self._blink_thread.start() if not background: self._blink_thread.join() @@ -403,16 +383,14 @@ class PWMOutputDevice(OutputDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, active_high=True, initial_value=0, frequency=100, - pin_factory=None): + def __init__(self, pin=None, *, active_high=True, initial_value=0, + frequency=100, pin_factory=None): self._blink_thread = None self._controller = None if not 0 <= initial_value <= 1: raise OutputDeviceBadValue("initial_value must be between 0 and 1") - super(PWMOutputDevice, self).__init__( - pin, active_high, initial_value=None, pin_factory=pin_factory - ) + super().__init__(pin, active_high=active_high, initial_value=None, + pin_factory=pin_factory) try: # XXX need a way of setting these together self.pin.frequency = frequency @@ -431,7 +409,7 @@ def close(self): except AttributeError: # If the pin's already None, ignore the exception pass - super(PWMOutputDevice, self).close() + super().close() def _state_to_value(self, state): return float(state if self.active_high else 1 - state) @@ -442,7 +420,7 @@ def _value_to_state(self, value): def _write(self, value): if not 0 <= value <= 1: raise OutputDeviceBadValue("PWM value must be between 0 and 1") - super(PWMOutputDevice, self)._write(value) + super()._write(value) @property def value(self): @@ -450,7 +428,7 @@ def value(self): The duty cycle of the PWM device. 0.0 is off, 1.0 is fully on. Values in between may be specified for varying levels of power in the device. """ - return super(PWMOutputDevice, self).value + return super().value @value.setter def value(self, value): @@ -525,8 +503,8 @@ def blink( """ self._stop_blink() self._blink_thread = GPIOThread( - target=self._blink_device, - args=(on_time, off_time, fade_in_time, fade_out_time, n) + self._blink_device, + (on_time, off_time, fade_in_time, fade_out_time, n) ) self._blink_thread.start() if not background: @@ -626,13 +604,12 @@ class TonalBuzzer(SourceMixin, CompositeDevice): :class:`~gpiozero.pins.pigpio.PiGPIOFactory`. """ - def __init__(self, pin=None, initial_value=None, mid_tone=Tone("A4"), + def __init__(self, pin=None, *, initial_value=None, mid_tone=Tone("A4"), octaves=1, pin_factory=None): self._mid_tone = None - super(TonalBuzzer, self).__init__( - pwm_device=PWMOutputDevice( - pin=pin, pin_factory=pin_factory - ), pin_factory=pin_factory) + super().__init__( + pwm_device=PWMOutputDevice(pin=pin, pin_factory=pin_factory), + pin_factory=pin_factory) try: self._mid_tone = Tone(mid_tone) if not (0 < octaves <= 9): @@ -642,14 +619,14 @@ def __init__(self, pin=None, initial_value=None, mid_tone=Tone("A4"), self.min_tone.note except ValueError: raise ValueError( - '%r is too low for %d octaves' % - (self._mid_tone, self._octaves)) + f'{self._mid_tone!r} is too low for {self._octaves} ' + f'octaves') try: self.max_tone.note except ValueError: raise ValueError( - '%r is too high for %d octaves' % - (self._mid_tone, self._octaves)) + f'{self._mid_tone!r} is too high for {self._octaves} ' + f'octaves') self.value = initial_value except: self.close() @@ -657,14 +634,17 @@ def __init__(self, pin=None, initial_value=None, mid_tone=Tone("A4"), def __repr__(self): try: + self._check_open() if self.value is None: - return '' % ( - self.pwm_device.pin,) + return ( + f'') else: - return '' % ( - self.pwm_device.pin, self.tone.note) - except: - return super(TonalBuzzer, self).__repr__() + return ( + f'') + except DeviceClosed: + return super().__repr__() def play(self, tone): """ @@ -730,12 +710,11 @@ def value(self): if self.pwm_device.pin.frequency is None: return None else: - try: - return log2( - self.pwm_device.pin.frequency / self.mid_tone.frequency - ) / self.octaves - except ZeroDivisionError: - return 0.0 + # Can't have zero-division here; zero-frequency Tone cannot be + # generated and self.octaves cannot be zero either + return log2( + self.pwm_device.pin.frequency / self.mid_tone.frequency + ) / self.octaves @value.setter def value(self, value): @@ -887,19 +866,17 @@ class RGBLED(SourceMixin, Device): .. _colorzero: https://colorzero.readthedocs.io/ """ - def __init__( - self, red=None, green=None, blue=None, active_high=True, - initial_value=(0, 0, 0), pwm=True, pin_factory=None): + def __init__(self, red=None, green=None, blue=None, *, active_high=True, + initial_value=(0, 0, 0), pwm=True, pin_factory=None): self._leds = () self._blink_thread = None if not all(p is not None for p in [red, green, blue]): raise GPIOPinMissing('red, green, and blue pins must be provided') LEDClass = PWMLED if pwm else LED - super(RGBLED, self).__init__(pin_factory=pin_factory) + super().__init__(pin_factory=pin_factory) self._leds = tuple( - LEDClass(pin, active_high, pin_factory=pin_factory) - for pin in (red, green, blue) - ) + LEDClass(pin, active_high=active_high, pin_factory=pin_factory) + for pin in (red, green, blue)) self.value = initial_value def close(self): @@ -908,7 +885,7 @@ def close(self): for led in self._leds: led.close() self._leds = () - super(RGBLED, self).close() + super().close() @property def closed(self): @@ -1075,8 +1052,8 @@ def blink( raise ValueError('fade_out_time must be 0 with non-PWM RGBLEDs') self._stop_blink() self._blink_thread = GPIOThread( - target=self._blink_device, - args=( + self._blink_device, + ( on_time, off_time, fade_in_time, fade_out_time, on_color, off_color, n ) @@ -1213,21 +1190,20 @@ class Motor(SourceMixin, CompositeDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, forward=None, backward=None, enable=None, pwm=True, + def __init__(self, forward, backward, *, enable=None, pwm=True, pin_factory=None): - if not all(p is not None for p in [forward, backward]): - raise GPIOPinMissing( - 'forward and backward pins must be provided' - ) PinClass = PWMOutputDevice if pwm else DigitalOutputDevice devices = OrderedDict(( - ('forward_device', PinClass(forward)), - ('backward_device', PinClass(backward)), + ('forward_device', PinClass(forward, pin_factory=pin_factory)), + ('backward_device', PinClass(backward, pin_factory=pin_factory)), )) if enable is not None: - devices['enable_device'] = DigitalOutputDevice(enable, - initial_value=True) - super(Motor, self).__init__(_order=devices.keys(), **devices) + devices['enable_device'] = DigitalOutputDevice( + enable, + initial_value=True, + pin_factory=pin_factory + ) + super().__init__(_order=devices.keys(), pin_factory=pin_factory, **devices) @property def value(self): @@ -1354,16 +1330,12 @@ class PhaseEnableMotor(SourceMixin, CompositeDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__(self, phase=None, enable=None, pwm=True, pin_factory=None): - if not all([phase, enable]): - raise GPIOPinMissing('phase and enable pins must be provided') + def __init__(self, phase, enable, *, pwm=True, pin_factory=None): PinClass = PWMOutputDevice if pwm else DigitalOutputDevice - super(PhaseEnableMotor, self).__init__( + super().__init__( phase_device=DigitalOutputDevice(phase, pin_factory=pin_factory), enable_device=PinClass(enable, pin_factory=pin_factory), - _order=('phase_device', 'enable_device'), - pin_factory=pin_factory - ) + _order=('phase_device', 'enable_device'), pin_factory=pin_factory) @property def value(self): @@ -1480,6 +1452,12 @@ class Servo(SourceMixin, CompositeDevice): servo.value = 0.5 + .. note:: + + To reduce servo jitter, use the pigpio pin driver rather than the default + RPi.GPIO driver (pigpio uses DMA sampling for much more precise edge + timing). See :ref:`changing-pin-factory` for further information. + :type pin: int or str :param pin: The GPIO pin that the servo is connected to. See :ref:`pin-numbering` @@ -1509,10 +1487,9 @@ class Servo(SourceMixin, CompositeDevice): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, initial_value=0.0, - min_pulse_width=1/1000, max_pulse_width=2/1000, - frame_width=20/1000, pin_factory=None): + def __init__(self, pin=None, *, initial_value=0.0, min_pulse_width=1/1000, + max_pulse_width=2/1000, frame_width=20/1000, + pin_factory=None): if min_pulse_width >= max_pulse_width: raise ValueError('min_pulse_width must be less than max_pulse_width') if max_pulse_width >= frame_width: @@ -1522,12 +1499,18 @@ def __init__( self._dc_range = (max_pulse_width - min_pulse_width) / frame_width self._min_value = -1 self._value_range = 2 - super(Servo, self).__init__( + super().__init__( pwm_device=PWMOutputDevice( - pin, frequency=int(1 / frame_width), pin_factory=pin_factory - ), + pin, frequency=int(1 / frame_width), pin_factory=pin_factory), pin_factory=pin_factory ) + + if PiGPIOFactory is None or not isinstance(self.pin_factory, PiGPIOFactory): + warnings.warn(PWMSoftwareFallback( + 'To reduce servo jitter, use the pigpio pin factory.' + 'See https://gpiozero.readthedocs.io/en/stable/api_output.html#servo for more info' + )) + try: self.value = initial_value except: @@ -1567,6 +1550,10 @@ def pulse_width(self): else: return self.pwm_device.pin.state * self.frame_width + @pulse_width.setter + def pulse_width(self, value): + self.pwm_device.pin.state = value / self.frame_width + def min(self): """ Set the servo to its minimum position. @@ -1721,11 +1708,9 @@ class AngularServo(Servo): See :doc:`api_pins` for more information (this is an advanced feature which most users can ignore). """ - def __init__( - self, pin=None, initial_angle=0.0, - min_angle=-90, max_angle=90, - min_pulse_width=1/1000, max_pulse_width=2/1000, - frame_width=20/1000, pin_factory=None): + def __init__(self, pin=None, *, initial_angle=0.0, min_angle=-90, + max_angle=90, min_pulse_width=1/1000, max_pulse_width=2/1000, + frame_width=20/1000, pin_factory=None): self._min_angle = min_angle self._angular_range = max_angle - min_angle if initial_angle is None: @@ -1735,12 +1720,12 @@ def __init__( initial_value = 2 * ((initial_angle - min_angle) / self._angular_range) - 1 else: raise OutputDeviceBadValue( - "AngularServo angle must be between %s and %s, or None" % - (min_angle, max_angle)) - super(AngularServo, self).__init__( - pin, initial_value, min_pulse_width, max_pulse_width, frame_width, - pin_factory=pin_factory - ) + f"AngularServo angle must be between {min_angle} and " + f"{max_angle}, or None") + super().__init__(pin, initial_value=initial_value, + min_pulse_width=min_pulse_width, + max_pulse_width=max_pulse_width, + frame_width=frame_width, pin_factory=pin_factory) @property def min_angle(self): @@ -1794,5 +1779,5 @@ def angle(self, angle): self._min_value) else: raise OutputDeviceBadValue( - "AngularServo angle must be between %s and %s, or None" % - (self.min_angle, self.max_angle)) + f"AngularServo angle must be between {self.min_angle} and " + f"{self.max_angle}, or None") diff --git a/gpiozero/pins/__init__.py b/gpiozero/pins/__init__.py index 15a5463de..0fbc407b4 100644 --- a/gpiozero/pins/__init__.py +++ b/gpiozero/pins/__init__.py @@ -1,49 +1,26 @@ # vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones +# +# Copyright (c) 2015-2023 Dave Jones # Copyright (c) 2018 Rick Ansell # Copyright (c) 2018 Mike Kazantsev +# Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause +import warnings from weakref import ref -from collections import defaultdict from threading import Lock +from textwrap import dedent +from itertools import cycle +from operator import attrgetter +from collections import defaultdict, namedtuple +from .style import Style +from ..devices import Device from ..exc import ( + PinInvalidPin, PinInvalidFunction, PinSetInput, PinFixedPull, @@ -51,15 +28,18 @@ PinSPIUnsupported, PinPWMUnsupported, PinEdgeDetectUnsupported, + PinMultiplePins, + PinNoPins, SPIFixedClockMode, SPIFixedBitOrder, SPIFixedSelect, SPIFixedWordSize, + SPIFixedRate, GPIOPinInUse, - ) +) -class Factory(object): +class Factory: """ Generates pins and SPI interfaces for devices. This is an abstract base class for pin factories. Descendents *must* override the following @@ -67,6 +47,7 @@ class Factory(object): * :meth:`ticks` * :meth:`ticks_diff` + * :meth:`_get_board_info` Descendents *may* override the following methods, if applicable: @@ -76,36 +57,51 @@ class Factory(object): * :meth:`release_all` * :meth:`pin` * :meth:`spi` - * :meth:`_get_pi_info` """ def __init__(self): self._reservations = defaultdict(list) self._res_lock = Lock() - def reserve_pins(self, requester, *pins): + def __enter__(self): + return self + + def __exit__(self, *exc_info): + self.close() + + def reserve_pins(self, requester, *names): """ Called to indicate that the device reserves the right to use the - specified *pins*. This should be done during device construction. If - pins are reserved, you must ensure that the reservation is released by - eventually called :meth:`release_pins`. + specified pin *names*. This should be done during device construction. + If pins are reserved, you must ensure that the reservation is released + by eventually called :meth:`release_pins`. """ with self._res_lock: + pins = ( + info + for name in names + for header, info in self.board_info.find_pin(name) + ) for pin in pins: for reserver_ref in self._reservations[pin]: reserver = reserver_ref() if reserver is not None and requester._conflicts_with(reserver): - raise GPIOPinInUse('pin %s is already in use by %r' % - (pin, reserver)) + raise GPIOPinInUse( + f'pin {pin.name} is already in use by {reserver!r}') self._reservations[pin].append(ref(requester)) - def release_pins(self, reserver, *pins): + def release_pins(self, reserver, *names): """ - Releases the reservation of *reserver* against *pins*. This is + Releases the reservation of *reserver* against pin *names*. This is typically called during :meth:`~gpiozero.Device.close` to clean up reservations taken during construction. Releasing a reservation that is not currently held will be silently ignored (to permit clean-up after failed / partial construction). """ + pins = ( + info + for name in names + for header, info in self.board_info.find_pin(name) + ) with self._res_lock: for pin in pins: self._reservations[pin] = [ @@ -124,7 +120,8 @@ def release_all(self, reserver): # then causes a subtle bug in LocalPiFactory which does something # horribly naughty (with good reason) and makes its _reservations # dictionary equivalent to a class-level one. - self.release_pins(reserver, *self._reservations) + self.release_pins(reserver, *( + pin.name for pin in self._reservations)) def close(self): """ @@ -134,7 +131,7 @@ def close(self): """ pass - def pin(self, spec): + def pin(self, name): """ Creates an instance of a :class:`Pin` descendent representing the specified pin. @@ -146,7 +143,7 @@ def pin(self, spec): :meth:`pin` for the same pin specification must return the same object. """ - raise PinUnsupported( + raise PinUnsupported( # pragma: no cover "Individual pins are not supported by this pin factory") def spi(self, **spi_args): @@ -157,7 +154,8 @@ def spi(self, **spi_args): be used; attempting to mix *port* and *device* with pin numbers will raise :exc:`SPIBadArgs`. """ - raise PinSPIUnsupported('SPI not supported by this pin factory') + raise PinSPIUnsupported( # pragma: no cover + 'SPI not supported by this pin factory') def ticks(self): """ @@ -181,22 +179,26 @@ def ticks_diff(self, later, earlier): """ raise NotImplementedError - def _get_pi_info(self): - return None + def _get_board_info(self): + raise NotImplementedError - pi_info = property( - lambda self: self._get_pi_info(), + board_info = property( + lambda self: self._get_board_info(), doc="""\ - Returns a :class:`PiBoardInfo` instance representing the Pi that - instances generated by this factory will be attached to. - - If the pins represented by this class are not *directly* attached to a - Pi (e.g. the pin is attached to a board attached to the Pi, or the pins - are not on a Pi at all), this may return :data:`None`. + Returns a :class:`BoardInfo` instance (or derivative) representing the + board that instances generated by this factory will be attached to. """) + def _get_pi_info(self): + warnings.warn( + DeprecationWarning( + "Please use Factory.board_info instead of Factory.pi_info")) + return self._get_board_info() -class Pin(object): + pi_info = property(lambda self: self._get_pi_info()) + + +class Pin: """ Abstract base class representing a pin attached to some form of controller, be it GPIO, SPI, ADC, etc. @@ -205,6 +207,7 @@ class Pin(object): represent the capabilities of pins. Descendents *must* override the following methods: + * :meth:`_get_info` * :meth:`_get_function` * :meth:`_set_function` * :meth:`_get_state` @@ -228,8 +231,14 @@ class Pin(object): * :meth:`_set_when_changed` """ + def __enter__(self): + return self + + def __exit__(self, *exc_info): + self.close() + def __repr__(self): - return "" + return "" # pragma: no cover def close(self): """ @@ -263,7 +272,7 @@ def input_with_pull(self, pull): pin.function = 'input' pin.pull = pull - However, descendents may override this order to provide the smallest + However, descendents may override this in order to provide the smallest possible delay between configuring the pin for input and pulling the pin up/down (which can be important for avoiding "blips" in some configurations). @@ -271,13 +280,23 @@ def input_with_pull(self, pull): self.function = 'input' self.pull = pull + def _get_info(self): + raise NotImplementedError + + info = property( + lambda self: self._get_info(), + doc="""\ + Returns the :class:`PinInfo` associated with the pin. This can be used + to determine physical properties of the pin, including its location on + the header, fixed pulls, and the various specs that can be used to + identify it. + """) + def _get_function(self): - return "input" + raise NotImplementedError def _set_function(self, value): - if value != "input": - raise PinInvalidFunction( - "Cannot set the function of pin %r to %s" % (self, value)) + raise NotImplementedError function = property( lambda self: self._get_function(), @@ -295,10 +314,10 @@ def _set_function(self, value): """) def _get_state(self): - return 0 + raise NotImplementedError def _set_state(self, value): - raise PinSetInput("Cannot set the state of input pin %r" % self) + raise PinSetInput(f"Cannot set the state of pin {self!r}") # pragma: no cover state = property( lambda self: self._get_state(), @@ -327,10 +346,11 @@ def _set_state(self, value): """) def _get_pull(self): - return 'floating' + return 'floating' # pragma: no cover def _set_pull(self, value): - raise PinFixedPull("Cannot change pull-up on pin %r" % self) + raise PinFixedPull( # pragma: no cover + f"Cannot change pull-up on pin {self!r}") pull = property( lambda self: self._get_pull(), @@ -347,11 +367,12 @@ def _set_pull(self, value): """) def _get_frequency(self): - return None + return None # pragma: no cover def _set_frequency(self, value): if value is not None: - raise PinPWMUnsupported("PWM is not supported on pin %r" % self) + raise PinPWMUnsupported( # pragma: no cover + f"PWM is not supported on pin {self!r}") frequency = property( lambda self: self._get_frequency(), @@ -367,12 +388,12 @@ def _set_frequency(self, value): """) def _get_bounce(self): - return None + return None # pragma: no cover def _set_bounce(self, value): - if value is not None: + if value is not None: # pragma: no cover raise PinEdgeDetectUnsupported( - "Edge detection is not supported on pin %r" % self) + f"Edge detection is not supported on pin {self!r}") bounce = property( lambda self: self._get_bounce(), @@ -409,11 +430,11 @@ def _set_bounce(self, value): """) def _get_edges(self): - return 'none' + return 'none' # pragma: no cover def _set_edges(self, value): - raise PinEdgeDetectUnsupported( - "Edge detection is not supported on pin %r" % self) + raise PinEdgeDetectUnsupported( # pragma: no cover + f"Edge detection is not supported on pin {self!r}") edges = property( lambda self: self._get_edges(), @@ -439,11 +460,11 @@ def _set_edges(self, value): """) def _get_when_changed(self): - return None + return None # pragma: no cover def _set_when_changed(self, value): - raise PinEdgeDetectUnsupported( - "Edge detection is not supported on pin %r" % self) + raise PinEdgeDetectUnsupported( # pragma: no cover + f"Edge detection is not supported on pin {self!r}") when_changed = property( lambda self: self._get_when_changed(), @@ -471,7 +492,7 @@ def _set_when_changed(self, value): """) -class SPI(object): +class SPI(Device): """ Abstract interface for `Serial Peripheral Interface`_ (SPI) implementations. Descendents *must* override the following methods: @@ -626,10 +647,11 @@ def clock_phase(self, value): self.clock_mode = self.clock_mode & (~1) | bool(value) def _get_clock_mode(self): - raise NotImplementedError + raise NotImplementedError # pragma: no cover def _set_clock_mode(self, value): - raise SPIFixedClockMode("clock_mode cannot be changed on %r" % self) + raise SPIFixedClockMode( # pragma: no cover + f"clock_mode cannot be changed on {self!r}") clock_mode = property( lambda self: self._get_clock_mode(), @@ -656,10 +678,11 @@ def _set_clock_mode(self, value): """) def _get_lsb_first(self): - return False + return False # pragma: no cover def _set_lsb_first(self, value): - raise SPIFixedBitOrder("lsb_first cannot be changed on %r" % self) + raise SPIFixedBitOrder( # pragma: no cover + f"lsb_first cannot be changed on {self!r}") lsb_first = property( lambda self: self._get_lsb_first(), @@ -708,10 +731,11 @@ def _set_lsb_first(self, value): """) def _get_select_high(self): - return False + return False # pragma: no cover def _set_select_high(self, value): - raise SPIFixedSelect("select_high cannot be changed on %r" % self) + raise SPIFixedSelect( # pragma: no cover + f"select_high cannot be changed on {self!r}") select_high = property( lambda self: self._get_select_high(), @@ -753,10 +777,11 @@ def _set_select_high(self, value): """) def _get_bits_per_word(self): - return 8 + return 8 # pragma: no cover def _set_bits_per_word(self, value): - raise SPIFixedWordSize("bits_per_word cannot be changed on %r" % self) + raise SPIFixedWordSize( # pragma: no cover + f"bits_per_word cannot be changed on {self!r}") bits_per_word = property( lambda self: self._get_bits_per_word(), @@ -768,3 +793,626 @@ def _set_bits_per_word(self, value): Several implementations do not support non-byte-sized words. """) + + def _get_rate(self): + return 100000 # pragma: no cover + + def _set_rate(self, value): + raise SPIFixedRate( # pragma: no cover + f"rate cannot be changed on {self!r}") + + rate = property( + lambda self: self._get_rate(), + lambda self, value: self._set_rate(value), + doc="""\ + Controls the speed of the SPI interface in Hz (or baud). + + Note that most software SPI implementations ignore this property, and + will raise :exc:`SPIFixedRate` if an attempt is made to set it, as they + have no rate control (they simply bit-bang as fast as possible because + typically this isn't very fast anyway, and introducing measures to + limit the rate would simply slow them down to the point of being + useless). + """) + + +class PinInfo(namedtuple('PinInfo', ( + 'number', + 'name', + 'names', + 'pull', + 'row', + 'col', + 'interfaces', + ))): + """ + This class is a :func:`~collections.namedtuple` derivative used to + represent information about a pin present on a GPIO header. The following + attributes are defined: + + .. attribute:: number + + An integer containing the physical pin number on the header (starting + from 1 in accordance with convention). + + .. attribute:: name + + A string describing the function of the pin. Some common examples + include "GND" (for pins connecting to ground), "3V3" (for pins which + output 3.3 volts), "GPIO9" (for GPIO9 in the board's numbering scheme), + etc. + + .. attribute:: names + + A set of all the names that can be used to identify this pin with + :meth:`BoardInfo.find_pin`. The :attr:`name` attribute is the "typical" + name for this pin, and will be one of the values in this set. + + When "gpio" is in :attr:`interfaces`, these names can be used with + :meth:`Factory.pin` to construct a :class:`Pin` instance representing + this pin. + + .. attribute:: pull + + A string indicating the fixed pull of the pin, if any. This is a blank + string if the pin has no fixed pull, but may be "up" in the case of + pins typically used for I2C such as GPIO2 and GPIO3 on a Raspberry Pi. + + .. attribute:: row + + An integer indicating on which row the pin is physically located in + the header (1-based) + + .. attribute:: col + + An integer indicating in which column the pin is physically located + in the header (1-based) + + .. attribute:: interfaces + + A :class:`dict` mapping interfaces that this pin can be a part of to + the description of that pin in that interface (e.g. "i2c" might map to + "I2C0 SDA"). Typical keys are "gpio", "spi", "i2c", "uart", "pwm", + "smi", and "dpi". + + .. autoattribute:: pull_up + + .. autoattribute:: function + """ + __slots__ = () # workaround python issue #24931 + + @property + def function(self): + """ + Deprecated alias of :attr:`name`. + """ + warnings.warn( + DeprecationWarning( + "PinInfo.function is deprecated; please use PinInfo.name")) + return self.name + + @property + def pull_up(self): + """ + Deprecated variant of :attr:`pull`. + """ + warnings.warn( + DeprecationWarning( + "PinInfo.pull_up is deprecated; please use PinInfo.pull")) + return self.pull == 'up' + + +class HeaderInfo(namedtuple('HeaderInfo', ( + 'name', + 'rows', + 'columns', + 'pins', + ))): + """ + This class is a :func:`~collections.namedtuple` derivative used to + represent information about a pin header on a board. The object can be used + in a format string with various custom specifications:: + + from gpiozero.pins.native import NativeFactory + + factory = NativeFactory() + j8 = factory.board_info.headers['J8'] + print(f'{j8}') + print(f'{j8:full}') + p1 = factory.board_info.headers['P1'] + print(f'{p1:col2}') + print(f'{p1:row1}') + + "color" and "mono" can be prefixed to format specifications to force the + use of `ANSI color codes`_. If neither is specified, ANSI codes will only + be used if stdout is detected to be a tty. "rev" can be added to output the + row or column in reverse order:: + + # force use of ANSI codes + j8 = factory.board_info.headers['J8'] + print(f'{j8:color row2}') + # force plain ASCII + print(f'{j8:mono row2}') + # output in reverse order + print(f'{j8:color rev row1}') + + The following attributes are defined: + + .. automethod:: pprint + + .. attribute:: name + + The name of the header, typically as it appears silk-screened on the + board (e.g. "P1" or "J8"). + + .. attribute:: rows + + The number of rows on the header. + + .. attribute:: columns + + The number of columns on the header. + + .. attribute:: pins + + A dictionary mapping physical pin numbers to :class:`PinInfo` tuples. + + .. _ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code + """ + __slots__ = () # workaround python issue #24931 + + def _pin_style(self, pin, style): + if 'gpio' in pin.interfaces: + return style('bold green') + elif pin.name == '5V': + return style('bold red') + elif pin.name in {'3V3', '1V8'}: + return style('bold cyan') + elif pin.name in {'GND', 'NC'}: + return style('bold black') + else: + return style('yellow') + + def _format_full(self, style): + Cell = namedtuple('Cell', ('content', 'align', 'style')) + + lines = [] + for row in range(self.rows): + line = [] + for col in range(self.columns): + pin = (row * self.columns) + col + 1 + try: + pin = self.pins[pin] + cells = [ + Cell(pin.name, '><'[col % 2], self._pin_style(pin, style)), + Cell(f'({pin.number})', '><'[col % 2], ''), + ] + if col % 2: + cells = reversed(cells) + line.extend(cells) + except KeyError: + line.append(Cell('', '<', '')) + lines.append(line) + cols = list(zip(*lines)) + col_lens = [max(len(cell.content) for cell in col) for col in cols] + lines = [ + ' '.join( + f'{cell.style}{cell.content:{cell.align}{width}s}{style:reset}' + for cell, width, align in zip(line, col_lens, cycle('><'))) + for line in lines + ] + return '\n'.join(lines) + + def _format_pin(self, pin, style): + return ''.join(( + style('on black'), + ( + ' ' if pin is None else + self._pin_style(pin, style) + + ('1' if pin.number == 1 else + '2' if pin.number == 2 and self.rows == self.columns else + 'o') + ), + style('reset') + )) + + def _format_row(self, row, style, rev=False): + step = -1 if rev else 1 + if row > self.rows: + raise ValueError(f'invalid row {row} for header {self.name}') + start_pin = (row - 1) * self.columns + 1 + return ''.join( + self._format_pin(pin, style) + for n in range(start_pin, start_pin + self.columns)[::step] + for pin in (self.pins.get(n),) + ) + + def _format_col(self, col, style, rev=False): + step = -1 if rev else 1 + if col > self.columns: + raise ValueError(f'invalid col {col} for header {self.name}') + return ''.join( + self._format_pin(pin, style) + for n in range(col, self.rows * self.columns + 1, self.columns)[::step] + for pin in (self.pins.get(n),) + ) + + def __format__(self, format_spec): + style, content = Style.from_style_content(format_spec) + content = set(content.split()) + try: + content.remove('rev') + rev = True + except KeyError: + rev = False + if len(content) != 1: + raise ValueError('Invalid format specifier') + content = content.pop() + if content == 'full': + return self._format_full(style) + elif content.startswith('row') and content[3:].isdigit(): + return self._format_row(int(content[3:]), style, rev=rev) + elif content.startswith('col') and content[3:].isdigit(): + return self._format_col(int(content[3:]), style, rev=rev) + else: + raise ValueError('Invalid format specifier') + + def pprint(self, color=None): + """ + Pretty-print a diagram of the header pins. + + If *color* is :data:`None` (the default, the diagram will include ANSI + color codes if stdout is a color-capable terminal). Otherwise *color* + can be set to :data:`True` or :data:`False` to force color or + monochrome output. + """ + style = Style(color) + print(f'{self:{style} full}') + + +class BoardInfo(namedtuple('BoardInfo', ( + 'revision', + 'model', + 'pcb_revision', + 'released', + 'soc', + 'manufacturer', + 'memory', + 'storage', + 'usb', + 'usb3', + 'ethernet', + 'eth_speed', + 'wifi', + 'bluetooth', + 'csi', + 'dsi', + 'headers', + 'board', + ))): + """ + This class is a :func:`~collections.namedtuple` derivative used to + represent information about a particular board. While it is a tuple, it is + strongly recommended that you use the following named attributes to access + the data contained within. The object can be used in format strings with + various custom format specifications:: + + from gpiozero.pins.native import NativeFactory + + factory = NativeFactory() + print(f'{factory.board_info}' + print(f'{factory.board_info:full}' + print(f'{factory.board_info:board}' + print(f'{factory.board_info:specs}' + print(f'{factory.board_info:headers}' + + "color" and "mono" can be prefixed to format specifications to force the + use of `ANSI color codes`_. If neither is specified, ANSI codes will only + be used if stdout is detected to be a tty:: + + # force use of ANSI codes + print(f'{factory.board_info:color board}') + # force plain ASCII + print(f'{factory.board_info:mono board}') + + .. _ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code + + .. automethod:: physical_pin + + .. automethod:: physical_pins + + .. automethod:: pprint + + .. automethod:: pulled_up + + .. automethod:: to_gpio + + .. attribute:: revision + + A string indicating the revision of the board. This is unique to each + revision and can be considered the "key" from which all other + attributes are derived. However, in itself the string is fairly + meaningless. + + .. attribute:: model + + A string containing the model of the board (for example, "B", "B+", + "A+", "2B", "CM", "Zero", etc.) + + .. attribute:: pcb_revision + + A string containing the PCB revision number which is silk-screened onto + the board. + + .. attribute:: released + + A string containing an approximate release date for this board + (formatted as yyyyQq, e.g. 2012Q1 means the first quarter of 2012). + + .. attribute:: soc + + A string indicating the SoC (`system on a chip`_) that powers this + board. + + .. attribute:: manufacturer + + A string indicating the name of the manufacturer (e.g. "Sony"). + + .. attribute:: memory + + An integer indicating the amount of memory (in Mb) connected to the + SoC. + + .. note:: + + This can differ substantially from the amount of RAM available to + the operating system as the GPU's memory is typically shared with + the CPU. + + .. attribute:: storage + + A string indicating the typical bootable storage used with this board, + e.g. "SD", "MicroSD", or "eMMC". + + .. attribute:: usb + + An integer indicating how many USB ports are physically present on + this board, of any type. + + .. note:: + + This does *not* include any (typically micro-USB) port used to + power the board. + + .. attribute:: usb3 + + An integer indicating how many of the USB ports are USB3 ports on this + board. + + .. attribute:: ethernet + + An integer indicating how many Ethernet ports are physically present + on this board. + + .. attribute:: eth_speed + + An integer indicating the maximum speed (in Mbps) of the Ethernet ports + (if any). If no Ethernet ports are present, this is 0. + + .. attribute:: wifi + + A bool indicating whether this board has wifi built-in. + + .. attribute:: bluetooth + + A bool indicating whether this board has bluetooth built-in. + + .. attribute:: csi + + An integer indicating the number of CSI (camera) ports available on + this board. + + .. attribute:: dsi + + An integer indicating the number of DSI (display) ports available on + this board. + + .. attribute:: headers + + A dictionary which maps header labels to :class:`HeaderInfo` tuples. + For example, to obtain information about header P1 you would query + ``headers['P1']``. To obtain information about pin 12 on header J8 you + would query ``headers['J8'].pins[12]``. + + A rendered version of this data can be obtained by using the + :class:`BoardInfo` object in a format string:: + + from gpiozero.pins.native import NativeFactory + + factory = NativeFactory() + print(f'{factory.board_info:headers}') + + .. attribute:: board + + An ASCII art rendition of the board, primarily intended for console + pretty-print usage. A more usefully rendered version of this data can + be obtained by using the :class:`BoardInfo` object in a format string. + For example:: + + from gpiozero.pins.native import NativeFactory + + factory = NativeFactory() + print(f'{factory.board_info:board}') + + .. autoattribute:: description + + .. _system on a chip: https://en.wikipedia.org/wiki/System_on_a_chip + """ + __slots__ = () # workaround python issue #24931 + + def find_pin(self, name): + """ + A generator function which, given a pin *name*, yields tuples of + :class:`HeaderInfo` and :class:`PinInfo` instances for which *name* + equals :attr:`PinInfo.name`. + """ + for header in self.headers.values(): + for pin in header.pins.values(): + if name in pin.names: + yield header, pin + + def physical_pins(self, function): + """ + Return the physical pins supporting the specified *function* as tuples + of ``(header, pin_number)`` where *header* is a string specifying the + header containing the *pin_number*. Note that the return value is a + :class:`set` which is not indexable. Use :func:`physical_pin` if you + are expecting a single return value. + + :param str function: + The pin function you wish to search for. Usually this is something + like "GPIO9" for GPIO pin 9, or "GND" for all the pins connecting + to electrical ground. + """ + warnings.warn( + DeprecationWarning( + "PiBoardInfo.physical_pins is deprecated; please use " + "BoardInfo.find_pin instead")) + return { + (header.name, pin.number) + for header, pin in self.find_pin(function) + } + + def physical_pin(self, function): + """ + Return the physical pin supporting the specified *function* as a tuple + of ``(header, pin_number)`` where *header* is a string specifying the + header containing the *pin_number*. If no pins support the desired + *function*, this function raises :exc:`PinNoPins`. If multiple pins + support the desired *function*, :exc:`PinMultiplePins` will be raised + (use :func:`physical_pins` if you expect multiple pins in the result, + such as for electrical ground). + + :param str function: + The pin function you wish to search for. Usually this is something + like "GPIO9" for GPIO pin 9. + """ + warnings.warn( + DeprecationWarning( + "PiBoardInfo.physical_pin is deprecated; please use " + "BoardInfo.find_pin instead")) + result = self.physical_pins(function) + if len(result) > 1: + raise PinMultiplePins(f'multiple pins can be used for {function}') + elif result: + return result.pop() + else: + raise PinNoPins(f'no pins can be used for {function}') + + def pulled_up(self, function): + """ + Returns a bool indicating whether a physical pull-up is attached to + the pin supporting the specified *function*. Either :exc:`PinNoPins` + or :exc:`PinMultiplePins` may be raised if the function is not + associated with a single pin. + + :param str function: + The pin function you wish to determine pull-up for. Usually this is + something like "GPIO9" for GPIO pin 9. + """ + warnings.warn( + DeprecationWarning( + "PiBoardInfo.pulled_up is deprecated; please use " + "BoardInfo.find_pin and PinInfo.pull instead")) + for header, pin in self.find_pin(function): + return pin.pull == 'up' + return False + + def to_gpio(self, name): + """ + Parses a pin *name*, returning the primary name of the pin (which can + be used to construct it), or raising a :exc:`ValueError` exception if + the name does not represent a GPIO pin. + + The *name* may be given in any of the following forms: + + * An integer, which will be accepted as a GPIO number + * 'GPIOn' where n is the GPIO number + * 'h:n' where h is the header name and n is the physical pin number + """ + for header, pin in self.find_pin(name): + if 'gpio' in pin.interfaces: + return pin.name + else: + raise PinInvalidPin(f'{name} is not a GPIO pin') + raise PinInvalidPin(f'{name} is not a valid pin name') + + def __repr__(self): + fields=', '.join( + f'{name}=...' if name in ('headers', 'board') else + f'{name}={value!r}' + for name, value in zip(self._fields, self) + ) + return f'{self.__class__.__name__}({fields})' + + def __format__(self, format_spec): + style, content = Style.from_style_content(format_spec) + if content == 'full': + return '\n\n'.join(( + f'{self:{style} specs}', + f'{self:{style} board}', + f'{self:{style} headers}', + )) + elif content == 'board': + kw = self._asdict() + kw.update({ + name: header + for name, header in self.headers.items() + }) + return self.board.format(style=style, **kw) + elif content == 'specs': + if self.memory < 1024: + memory = f'{self.memory}MB' + else: + memory = f'{int(self.memory / 1024)}GB' + return dedent(f"""\ + {style:bold}Description {style:reset}: {self.description} + {style:bold}Revision {style:reset}: {self.revision} + {style:bold}SoC {style:reset}: {self.soc} + {style:bold}RAM {style:reset}: {memory} + {style:bold}Storage {style:reset}: {self.storage} + {style:bold}USB ports {style:reset}: {self.usb} (of which {self.usb3} USB3) + {style:bold}Ethernet ports {style:reset}: {self.ethernet} ({self.eth_speed}Mbps max. speed) + {style:bold}Wi-fi {style:reset}: {self.wifi} + {style:bold}Bluetooth {style:reset}: {self.bluetooth} + {style:bold}Camera ports (CSI) {style:reset}: {self.csi} + {style:bold}Display ports (DSI){style:reset}: {self.dsi}""" + ) + elif content == 'headers': + return '\n\n'.join( + f'{style:bold}{header.name}{style:reset}:\n' + f'{header:{style} full}' + for header in self.headers.values() + ) + else: + raise ValueError('Invalid format specifier') + + def pprint(self, color=None): + """ + Pretty-print a representation of the board along with header diagrams. + + If *color* is :data:`None` (the default), the diagram will include ANSI + color codes if stdout is a color-capable terminal. Otherwise *color* + can be set to :data:`True` or :data:`False` to force color or monochrome + output. + """ + style = Style(color) + print(f'{self:{style} full}') + + @property + def description(self): + """ + A string containing a textual description of the board typically + containing the :attr:`model`, for example "Raspberry Pi 3B" + """ + return f'{self.model} rev {self.pcb_revision}' diff --git a/gpiozero/pins/data.py b/gpiozero/pins/data.py index 306251237..549f4551c 100644 --- a/gpiozero/pins/data.py +++ b/gpiozero/pins/data.py @@ -1,231 +1,177 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2018-2019 Ben Nuttall -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016-2018 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2019 Ben Nuttall +# Copyright (c) 2017-2018 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - -import os -import sys -from textwrap import dedent -from itertools import cycle -from operator import attrgetter -from collections import namedtuple - -from ..exc import PinUnknownPi, PinMultiplePins, PinNoPins, PinInvalidPin -from ..devices import Device - - -# Some useful constants for describing pins - -V1_8 = '1V8' -V3_3 = '3V3' -V5 = '5V' -GND = 'GND' -NC = 'NC' # not connected -GPIO0 = 'GPIO0' -GPIO1 = 'GPIO1' -GPIO2 = 'GPIO2' -GPIO3 = 'GPIO3' -GPIO4 = 'GPIO4' -GPIO5 = 'GPIO5' -GPIO6 = 'GPIO6' -GPIO7 = 'GPIO7' -GPIO8 = 'GPIO8' -GPIO9 = 'GPIO9' -GPIO10 = 'GPIO10' -GPIO11 = 'GPIO11' -GPIO12 = 'GPIO12' -GPIO13 = 'GPIO13' -GPIO14 = 'GPIO14' -GPIO15 = 'GPIO15' -GPIO16 = 'GPIO16' -GPIO17 = 'GPIO17' -GPIO18 = 'GPIO18' -GPIO19 = 'GPIO19' -GPIO20 = 'GPIO20' -GPIO21 = 'GPIO21' -GPIO22 = 'GPIO22' -GPIO23 = 'GPIO23' -GPIO24 = 'GPIO24' -GPIO25 = 'GPIO25' -GPIO26 = 'GPIO26' -GPIO27 = 'GPIO27' -GPIO28 = 'GPIO28' -GPIO29 = 'GPIO29' -GPIO30 = 'GPIO30' -GPIO31 = 'GPIO31' -GPIO32 = 'GPIO32' -GPIO33 = 'GPIO33' -GPIO34 = 'GPIO34' -GPIO35 = 'GPIO35' -GPIO36 = 'GPIO36' -GPIO37 = 'GPIO37' -GPIO38 = 'GPIO38' -GPIO39 = 'GPIO39' -GPIO40 = 'GPIO40' -GPIO41 = 'GPIO41' -GPIO42 = 'GPIO42' -GPIO43 = 'GPIO43' -GPIO44 = 'GPIO44' -GPIO45 = 'GPIO45' +# SPDX-License-Identifier: BSD-3-Clause # Board layout ASCII art REV1_BOARD = """\ {style:white on green}+------------------{style:black on white}| |{style:white on green}--{style:on cyan}| |{style:on green}------+{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on yellow}|C|{style:white on green} {style:on cyan}|A|{style:on green} |{style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on yellow}+-+{style:white on green} {style:on cyan}+-+{style:on green} |{style:reset} -{style:white on green}| |{style:reset} -{style:white on green}| {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|SoC|{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:bold}Pi Model{style:normal} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:bold}{model:3s}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:on black}|C|{style:black on white}+======{style:reset} -{style:white on green}| {style:on black}|S|{style:black on white}| Net{style:reset} -{style:white on green}| {style:on black}|I|{style:black on white}+======{style:reset} -{style:black on white}=pwr{style:on green} {style:on white}|HDMI|{style:white on green} |{style:reset} -{style:white on green}+----------------{style:black on white}| |{style:white on green}----------+{style:reset}""" +{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on yellow}|C|{style:white on green}{P2:{style} row8}{style:white on green} {style:on cyan}|A|{style:on green} |{style:reset} +{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on yellow}+-+{style:white on green}{P2:{style} row7}{P3:{style} row7}{style:white on cyan}+-+{style:on green} |{style:reset} +{style:white on green}| {P2:{style} row6}{P3:{style} row6}{style:white on green} |{style:reset} +{style:white on green}| {style:on black}+---+{style:on green} {P2:{style} row5}{P3:{style} row5}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:on black}|SoC|{style:on green} {P2:{style} row4}{P3:{style} row4}{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|S{style:on green} {style:bold}Pi Model{style:normal} {style:on black}+---+{style:on green} {P2:{style} row3}{P3:{style} row3}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|I{style:on green} {style:bold}{model:3s}V{pcb_revision:3s}{style:normal} {P2:{style} row2}{P3:{style} row2}{style:white on green} P3 |{style:reset} +{style:white on green}| {style:on black}|0{style:on green} P2 {P2:{style} row1}{P3:{style} row1}{style:white on green} {style:black on white}+======{style:reset} +{style:white on green}| {style:on black}C|{style:on green} {style:black on white}| Net{style:reset} +{style:white on green}| {style:on black}S|{style:on green} {style:black on white}+======{style:reset} +{style:black on white}=pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}I|{style:on green} |{style:reset} +{style:white on green}+----------------{style:black on white}| |{style:white on green}--{style:on black}0|{style:on green}------+{style:reset}""" REV2_BOARD = """\ {style:white on green}+------------------{style:black on white}| |{style:white on green}--{style:on cyan}| |{style:on green}------+{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on yellow}|C|{style:white on green} {style:on cyan}|A|{style:on green} |{style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on yellow}+-+{style:white on green} {style:on cyan}+-+{style:on green} |{style:reset} -{style:white on green}| {P5:{style} col1}{style:white on green} |{style:reset} -{style:white on green}| P5 {P5:{style} col2}{style:white on green} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|SoC|{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:bold}Pi Model{style:normal} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:bold}{model:3s}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:on black}|C|{style:black on white}+======{style:reset} -{style:white on green}| {style:on black}|S|{style:black on white}| Net{style:reset} -{style:white on green}| {style:on black}|I|{style:black on white}+======{style:reset} -{style:black on white}=pwr{style:on green} {style:on white}|HDMI|{style:white on green} |{style:reset} -{style:white on green}+----------------{style:black on white}| |{style:white on green}----------+{style:reset}""" +{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on yellow}|C|{style:white on green}{P2:{style} row8}{style:white on green} {style:on cyan}|A|{style:on green} |{style:reset} +{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on yellow}+-+{style:white on green}{P2:{style} row7}{P3:{style} row7}{style:white on cyan}+-+{style:on green} |{style:reset} +{style:white on green}| {P5:{style} col1}{style:white on green} {P2:{style} row6}{P3:{style} row6}{style:white on green} |{style:reset} +{style:white on green}| P5 {P5:{style} col2}{style:white on green} {style:on black}+---+{style:on green} {P2:{style} row5}{P3:{style} row5}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:on black}|SoC|{style:on green} {P2:{style} row4}{P3:{style} row4}{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|S{style:on green} {style:bold}Pi Model{style:normal} {style:on black}+---+{style:on green} {P2:{style} row3}{P3:{style} row3}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|I{style:on green} {style:bold}{model:3s}V{pcb_revision:3s}{style:normal} {P2:{style} row2}{P3:{style} row2}{style:white on green} P3 |{style:reset} +{style:white on green}| {style:on black}|0{style:on green} P2 {P2:{style} row1}{P3:{style} row1}{style:white on green} {style:black on white}+======{style:reset} +{style:white on green}| {style:on black}C|{style:on green} {style:black on white}| Net{style:reset} +{style:white on green}| {P6:{style} row2}{style:white on green} {style:on black}S|{style:on green} {style:black on white}+======{style:reset} +{style:black on white}=pwr{style:white on green} P6 {P6:{style} row1}{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}I|{style:on green} |{style:reset} +{style:white on green}+----------------{style:black on white}| |{style:white on green}--{style:on black}0|{style:on green}------+{style:reset}""" A_BOARD = """\ {style:white on green}+------------------{style:black on white}| |{style:white on green}--{style:on cyan}| |{style:on green}------+{style:reset} -{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on yellow}|C|{style:white on green} {style:on cyan}|A|{style:on green} |{style:reset} -{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on yellow}+-+{style:white on green} {style:on cyan}+-+{style:on green} |{style:reset} -{style:white on green}| {P5:{style} col1}{style:white on green} |{style:reset} -{style:white on green}| P5 {P5:{style} col2}{style:white on green} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|SoC|{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:bold}Pi Model{style:normal} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:bold}{model:3s}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:on black}|C|{style:on green} |{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} |{style:reset} -{style:white on green}| {style:on black}|I|{style:on green} |{style:reset} -{style:black on white}=pwr{style:on green} {style:on white}|HDMI|{style:white on green} |{style:reset} -{style:white on green}+----------------{style:black on white}| |{style:white on green}----------+{style:reset}""" +{style:white on green}| {P1:{style} col2}{style:white on green} P1 {style:black on yellow}|C|{style:white on green}{P2:{style} row8}{style:white on green} {style:on cyan}|A|{style:on green} |{style:reset} +{style:white on green}| {P1:{style} col1}{style:white on green} {style:black on yellow}+-+{style:white on green}{P2:{style} row7}{P3:{style} row7}{style:white on cyan}+-+{style:on green} |{style:reset} +{style:white on green}| {P5:{style} col1}{style:white on green} {P2:{style} row6}{P3:{style} row6}{style:white on green} |{style:reset} +{style:white on green}| P5 {P5:{style} col2}{style:white on green} {style:on black}+---+{style:on green} {P2:{style} row5}{P3:{style} row5}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:on black}|SoC|{style:on green} {P2:{style} row4}{P3:{style} row4}{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|S{style:on green} {style:bold}Pi Model{style:normal} {style:on black}+---+{style:on green} {P2:{style} row3}{P3:{style} row3}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|I{style:on green} {style:bold}{model:3s}V{pcb_revision:3s}{style:normal} {P2:{style} row2}{P3:{style} row2}{style:white on green} P3 |{style:reset} +{style:white on green}| {style:on black}|0{style:on green} P2 {P2:{style} row1}{P3:{style} row1}{style:white on green} |{style:reset} +{style:white on green}| {style:on black}C|{style:on green} |{style:reset} +{style:white on green}| {P6:{style} row2}{style:white on green} {style:on black}S|{style:on green} |{style:reset} +{style:black on white}=pwr{style:white on green} P6 {P6:{style} row1}{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}I|{style:on green} |{style:reset} +{style:white on green}+----------------{style:black on white}| |{style:white on green}--{style:on black}0|{style:on green}------+{style:reset}""" BPLUS_BOARD = """\ {style:white on green},--------------------------------.{style:reset} {style:white on green}| {J8:{style} col2}{style:white on green} J8 {style:black on white}+===={style:reset} {style:white on green}| {J8:{style} col1}{style:white on green} {style:black on white}| USB{style:reset} {style:white on green}| {style:black on white}+===={style:reset} -{style:white on green}| {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:white on green}| {style:on black}+----+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:on black}|SoC |{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:on black}| |{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:on black}+----+{style:on green} |{style:reset} -{style:white on green}| {style:on black}|C|{style:on green} {style:black on white}+======{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:black on white}| Net{style:reset} -{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}|I||A|{style:on green} {style:black on white}+======{style:reset} -{style:white on green}`-{style:black on white}| |{style:white on green}--------{style:black on white}| |{style:white on green}----{style:on black}|V|{style:on green}-------'{style:reset}""" +{style:white on green}| {RUN:{style} rev col1}{style:white on green} RUN{style:bold} Pi Model {model:4s}V{pcb_revision:3s}{style:normal} |{style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|{style:black on white}S{style:white on green} {style:on black}|SoC|{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|{style:black on white}I{style:white on green} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|0{style:on green} {style:on black}C|{style:on green} |{style:reset} +{style:white on green}| {style:black on white}S{style:white on black}|{style:on green} {style:black on white}+======{style:reset} +{style:white on green}| {style:black on white}I{style:white on black}|{style:on green} {style:on black}|A|{style:on green} {style:black on white}| Net{style:reset} +{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}0|{style:on green} {style:on black}|u|{style:on green} {style:black on white}+======{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------{style:black on white}| |{style:white on green}-----{style:on black}|x|{style:on green}--------'{style:reset}""" B3PLUS_BOARD = """\ {style:white on green},--------------------------------.{style:reset} -{style:white on green}| {J8:{style} col2}{style:white on green} J8 {style:black on white}+===={style:reset} -{style:white on green}| {J8:{style} col1}{style:white on green} P {style:black on white}| USB{style:reset} -{style:white on green}| {style:black on white} Wi {style:white on green} {style:white on black}oo{style:on green}o {style:black on white}+===={style:reset} -{style:white on green}| {style:black on white} Fi {style:white on green} {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} {style:white on black}oo{style:on green}E |{style:reset} -{style:white on green}| {style:black on white},----.{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:black on white}|SoC |{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:black on white}| |{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:black on white}`----'{style:white on green} |{style:reset} -{style:white on green}| {style:on black}|C|{style:on green} {style:black on white}+======{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:black on white}| Net{style:reset} -{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}|I||A|{style:on green} {style:black on white}+======{style:reset} -{style:white on green}`-{style:black on white}| |{style:white on green}--------{style:black on white}| |{style:white on green}----{style:on black}|V|{style:on green}-------'{style:reset}""" +{style:white on green}| {J8:{style} col2}{style:white on green} J8 PoE {style:black on white}+===={style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} {POE:{style} row1}{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:black on white} Wi {style:white on green} {POE:{style} row2}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:black on white} Fi {style:white on green} {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} |{style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:black on white},---.{style:on green} {RUN:{style} col1}{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|{style:black on white}S{style:white on green} {style:black on white}|SoC|{style:white on green} RUN {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|{style:black on white}I{style:white on green} {style:black on white}`---'{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|0{style:on green} {style:on black}C|{style:on green} |{style:reset} +{style:white on green}| {style:black on white}S{style:white on black}|{style:on green} {style:black on white}+======{style:reset} +{style:white on green}| {style:black on white}I{style:white on black}|{style:on green} {style:on black}|A|{style:on green} {style:black on white}| Net{style:reset} +{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}0|{style:on green} {style:on black}|u|{style:on green} {style:black on white}+======{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------{style:black on white}| |{style:white on green}-----{style:on black}|x|{style:on green}--------'{style:reset}""" + +B4_BOARD = """\ +{style:white on green},--------------------------------.{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} J8 {style:black on white}+======{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} J14 {style:black on white}| Net{style:reset} +{style:white on green}| {style:black on white} Wi {style:white on green} {J14:{style} row1}{style:on green} {style:black on white}+======{style:reset} +{style:white on green}| {style:black on white} Fi {style:white on green} {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} {J14:{style} row2}{style:normal white on green} |{style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:black on white},---.{style:on green} {style:white on black}+---+{style:on green} {style:blue on white}+===={style:reset} +{style:white on green}| {style:on black}|{style:black on white}S{style:white on green} {style:black on white}|SoC|{style:on green} {style:white on black}|RAM|{style:on green} {style:blue on white}|USB3{style:reset} +{style:white on green}| {style:on black}|{style:black on white}I{style:white on green} {style:black on white}`---'{style:on green} {style:white on black}+---+{style:on green} {style:blue on white}+===={style:reset} +{style:white on green}| {style:on black}|0{style:on green} {style:on black}C|{style:white on green} |{style:reset} +{style:white on green}| {J2:{style} rev col1}{style:white on green} J2 {style:black on white}S{style:white on black}|{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:black on white}I{style:white on black}|{style:on green} {style:white on black}|A|{style:white on green} {style:black on white}|USB2{style:reset} +{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|hd|{style:white on green} {style:black on white}|hd|{style:white on green} {style:on black}0|{style:on green} {style:on black}|u|{style:on green} {style:black on white}+===={style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}---{style:black on white}|m0|{style:white on green}---{style:black on white}|m1|{style:white on green}----{style:on black}|x|{style:on green}-------'{style:reset}""" + +B5_BOARD = """\ +{style:white on green},--------------------------------.{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} J8 {style:black on white}:{style:on green} {style:on white}+===={style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} {style:black on white}:{style:on green} {style:on white}|USB2{style:reset} +{style:white on green}| {style:black on white} Wi {style:white on green} {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} fan {style:black on white}+===={style:reset} +{style:white on green}| {style:black on white} Fi {style:white on green} {style:on black}+---+{style:on green} {style:on black}+---+{style:on green} |{style:reset} +{style:white on green}| {style:on black}|RAM|{style:on green} {style:on black}|RP1|{style:on green} {style:blue on white}+===={style:reset} +{style:white on green}|{style:black on yellow}|p{style:white on green} {style:on black}+---+{style:on green} {style:on black}+---+{style:on green} {style:blue on white}|USB3{style:reset} +{style:white on green}|{style:black on yellow}|{style:on white}c{style:white on green} {style:black on white}-------{style:white on green} {style:blue on white}+===={style:reset} +{style:white on green}|{style:black on yellow}|i{style:white on green} {style:black on white} SoC {style:white on green} {style:black on yellow}|c|c{style:white on green} J14 |{style:reset} +{style:bold white on green}({style:normal} {style:black on white}-------{style:white on green} J7{style:black on yellow}|{style:on white}s{style:on yellow}|{style:on white}s{style:white on green} {J14:{style} row1}{style:on green} {style:black on white}+======{style:reset} +{style:white on green}| J2 bat uart {J7:{style} row1}{style:black on yellow}|{style:on white}i{style:on yellow}|{style:on white}i{style:white on green} {J14:{style} row2}{style:on green} {style:black on white}| Net{style:reset} +{style:white on green}| {style:black on white}pwr{style:white on green}\\{style:black on white}..|hd|...|hd|{style:white on green}{J7:{style} row2}{style:black on yellow}|1|0{style:white on green} {style:black on white}+======{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}-{J2:{style} col1}{style:black on white}|m0|{style:white on green}---{style:black on white}|m1|{style:white on green}--------------'{style:reset}""" APLUS_BOARD = """\ {style:white on green},--------------------------.{style:reset} {style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} {style:white on green}| {J8:{style} col1}{style:white on green} |{style:reset} {style:white on green}| |{style:reset} -{style:white on green}| {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:white on green}| {style:on black}+----+{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:on black}|SoC |{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:on black}| |{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:on black}+----+{style:on green} |{style:reset} -{style:white on green}| {style:on black}|C|{style:on green} |{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} |{style:reset} -{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}|I||A|{style:on green} |{style:reset} -{style:white on green}`-{style:black on white}| |{style:white on green}--------{style:black on white}| |{style:white on green}----{style:on black}|V|{style:on green}-'{style:reset}""" +{style:white on green}| {RUN:{style} rev col1}{style:white on green} RUN {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} |{style:reset} +{style:white on green}| {style:on black}|D{style:on green} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|S{style:on green} {style:on black}|SoC|{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|I{style:on green} {style:on black}+---+{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|0{style:on green} {style:on black}C|{style:on green} |{style:reset} +{style:white on green}| {style:on black}S|{style:on green} |{style:reset} +{style:white on green}| {style:on black}I|{style:on green} {style:on black}|A|{style:on green} |{style:reset} +{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}0|{style:on green} {style:on black}|u|{style:on green} |{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------{style:black on white}| |{style:white on green}-----{style:on black}|x|{style:on green}--'{style:reset}""" A3PLUS_BOARD = """\ {style:white on green},--------------------------.{style:reset} {style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} -{style:white on green}| {J8:{style} col1}{style:white on green} |{style:reset} -{style:white on green}| {style:black on white} Wi {style:white on green} |{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} RUN|{style:reset} +{style:white on green}| {style:black on white} Wi {style:white on green} {RUN:{style} col1}{style:white on green}|{style:reset} {style:white on green}| {style:black on white} Fi {style:white on green} {style:bold}Pi Model {model:4s}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:white on green}| {style:black on white},----.{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|D|{style:on green} {style:black on white}|SoC |{style:on green} {style:black on white}| USB{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} {style:black on white}| |{style:on green} {style:black on white}+===={style:reset} -{style:white on green}| {style:on black}|I|{style:on green} {style:black on white}`----'{style:white on green} |{style:reset} -{style:white on green}| {style:on black}|C|{style:on green} |{style:reset} -{style:white on green}| {style:on black}|S|{style:on green} |{style:reset} -{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}|I||A|{style:on green} |{style:reset} -{style:white on green}`-{style:black on white}| |{style:white on green}--------{style:black on white}| |{style:white on green}----{style:on black}|V|{style:on green}-'{style:reset}""" +{style:white on green}| {style:on black}|D{style:on green} {style:black on white},---.{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|{style:black on white}S{style:white on green} {style:black on white}|SoC|{style:on green} {style:black on white}| USB{style:reset} +{style:white on green}| {style:on black}|{style:black on white}I{style:white on green} {style:black on white}`---'{style:on green} {style:black on white}+===={style:reset} +{style:white on green}| {style:on black}|0{style:on green} {style:on black}C|{style:on green} |{style:reset} +{style:white on green}| {style:black on white}S{style:white on black}|{style:on green} |{style:reset} +{style:white on green}| {style:black on white}I{style:white on black}|{style:on green} {style:on black}|A|{style:on green} |{style:reset} +{style:white on green}| {style:black on white}pwr{style:white on green} {style:black on white}|HDMI|{style:white on green} {style:on black}0|{style:on green} {style:on black}|u|{style:on green} |{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------{style:black on white}| |{style:white on green}-----{style:on black}|x|{style:on green}--'{style:reset}""" ZERO12_BOARD = """\ -{style:white on green},-------------------------.{style:reset} -{style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} -{style:white on green}| {J8:{style} col1}{style:white on green} |{style:reset} -{style:black on white}---+{style:white on green} {style:on black}+---+{style:on green} {style:bold}PiZero{style:normal} |{style:reset} -{style:black on white} sd|{style:white on green} {style:on black}|SoC|{style:on green} {style:bold}V{pcb_revision:3s}{style:normal} |{style:reset} -{style:black on white}---+|hdmi|{style:white on green} {style:on black}+---+{style:on green} {style:black on white}usb{style:on green} {style:black on white}pwr{style:white on green} |{style:reset} -{style:white on green}`---{style:black on white}| |{style:white on green}--------{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}-'{style:reset}""" +{style:white on green},--{J8:{style} col2}{style:white on green}---.{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} J8|{style:reset} +{style:black on white}---+{style:white on green} {style:bold}Pi{model:6s}{style:normal} RUN {RUN:{style} rev col1}{style:white on green} |{style:reset} +{style:black on white} sd|{style:white on green} {style:bold}V{pcb_revision:3s}{style:normal} {style:white on black}+---+{style:white on green} TV {TV:{style} col1}{style:white on green} |{style:reset} +{style:black on white}---+{style:white on green} {style:white on black}|SoC|{style:white on green} |{style:reset} +{style:white on green}| {style:black on white}hdmi{style:white on green} {style:white on black}+---+{style:white on green} {style:black on white}usb{style:on green} {style:black on white}pwr{style:white on green} |{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------------{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}-'{style:reset}""" ZERO13_BOARD = """\ -{style:white on green}.-------------------------.{style:reset} -{style:white on green}| {J8:{style} col2}{style:white on green} J8 |{style:reset} -{style:white on green}| {J8:{style} col1}{style:white on green} {style:black on white}|c{style:reset} -{style:black on white}---+{style:white on green} {style:on black}+---+{style:on green} {style:bold}Pi{model:6s}{style:normal}{style:black on white}|s{style:reset} -{style:black on white} sd|{style:white on green} {style:on black}|SoC|{style:on green} {style:bold}V{pcb_revision:3s}{style:normal} {style:black on white}|i{style:reset} -{style:black on white}---+|hdmi|{style:white on green} {style:on black}+---+{style:on green} {style:black on white}usb{style:on green} {style:on white}pwr{style:white on green} |{style:reset} -{style:white on green}`---{style:black on white}| |{style:white on green}--------{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}-'{style:reset}""" +{style:white on green},--{J8:{style} col2}{style:white on green}---.{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} J8|{style:reset} +{style:black on white}---+{style:white on green} {style:bold}Pi{model:6s}{style:normal} RUN {RUN:{style} rev col1}{style:white on green} {style:black on white}c{style:white on black}|{style:reset} +{style:black on white} sd|{style:white on green} {style:bold}V{pcb_revision:3s}{style:normal} {style:white on black}+---+{style:white on green} TV {TV:{style} col1}{style:white on green} {style:black on white}s{style:white on black}|{style:reset} +{style:black on white}---+{style:white on green} {style:white on black}|SoC|{style:white on green} {style:black on white}i{style:white on black}|{style:reset} +{style:white on green}| {style:black on white}hdmi{style:white on green} {style:white on black}+---+{style:white on green} {style:black on white}usb{style:on green} {style:black on white}pwr{style:white on green} |{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------------{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}-'{style:reset}""" + +ZERO2_BOARD = """\ +{style:white on green},--{J8:{style} col2}{style:white on green}---.{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} J8|{style:reset} +{style:black on white}---+{style:white on green} {style:normal on black}+---+{style:on green} {style:bold}Pi{model:6s} {style:normal black on white}c{style:white on black}|{style:reset} +{style:black on white} sd|{style:white on green} {style:white on black}|SoC|{style:white on green} {style:black on white} Wi {style:bold white on green}V{pcb_revision:3s} {style:normal black on white}s{style:white on black}|{style:reset} +{style:black on white}---+{style:white on green} {style:white on black}+---+{style:white on green} {style:black on white} Fi {style:on green} {style:on white}i{style:white on black}|{style:reset} +{style:white on green}| {style:black on white}hdmi{style:white on green} {style:black on white}usb{style:on green} {style:black on white}pwr{style:white on green} |{style:reset} +{style:white on green}`-{style:black on white}| |{style:white on green}------------{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}-'{style:reset} +""" CM_BOARD = """\ {style:white on green}+---------------------------------------+{style:reset} @@ -234,1109 +180,543 @@ {style:white on green}| {style:on black}|SoC|{style:on green} |{style:reset} {style:white on green}) {style:on black}+---+{style:on green} ({style:reset} {style:white on green}| {style:on black}O{style:on green} _ {style:on black}O{style:on green} |{style:reset} -{style:white on green}||||||{style:reset} {style:white on green}||||||||||||||||||||||||||||||||||{style:reset} -""" - -# Pin maps for various board revisions and headers - -REV1_P1 = { -# pin func pullup pin func pullup - 1: (V3_3, False), 2: (V5, False), - 3: (GPIO0, True), 4: (V5, False), - 5: (GPIO1, True), 6: (GND, False), - 7: (GPIO4, False), 8: (GPIO14, False), - 9: (GND, False), 10: (GPIO15, False), - 11: (GPIO17, False), 12: (GPIO18, False), - 13: (GPIO21, False), 14: (GND, False), - 15: (GPIO22, False), 16: (GPIO23, False), - 17: (V3_3, False), 18: (GPIO24, False), - 19: (GPIO10, False), 20: (GND, False), - 21: (GPIO9, False), 22: (GPIO25, False), - 23: (GPIO11, False), 24: (GPIO8, False), - 25: (GND, False), 26: (GPIO7, False), - } +{style:white on green}||||||{style:reset} {style:white on green}||||||||||||||||||||||||||||||||||{style:reset}""" -REV2_P1 = { - 1: (V3_3, False), 2: (V5, False), - 3: (GPIO2, True), 4: (V5, False), - 5: (GPIO3, True), 6: (GND, False), - 7: (GPIO4, False), 8: (GPIO14, False), - 9: (GND, False), 10: (GPIO15, False), - 11: (GPIO17, False), 12: (GPIO18, False), - 13: (GPIO27, False), 14: (GND, False), - 15: (GPIO22, False), 16: (GPIO23, False), - 17: (V3_3, False), 18: (GPIO24, False), - 19: (GPIO10, False), 20: (GND, False), - 21: (GPIO9, False), 22: (GPIO25, False), - 23: (GPIO11, False), 24: (GPIO8, False), - 25: (GND, False), 26: (GPIO7, False), - } - -REV2_P5 = { - 1: (V5, False), 2: (V3_3, False), - 3: (GPIO28, False), 4: (GPIO29, False), - 5: (GPIO30, False), 6: (GPIO31, False), - 7: (GND, False), 8: (GND, False), - } - -PLUS_J8 = { - 1: (V3_3, False), 2: (V5, False), - 3: (GPIO2, True), 4: (V5, False), - 5: (GPIO3, True), 6: (GND, False), - 7: (GPIO4, False), 8: (GPIO14, False), - 9: (GND, False), 10: (GPIO15, False), - 11: (GPIO17, False), 12: (GPIO18, False), - 13: (GPIO27, False), 14: (GND, False), - 15: (GPIO22, False), 16: (GPIO23, False), - 17: (V3_3, False), 18: (GPIO24, False), - 19: (GPIO10, False), 20: (GND, False), - 21: (GPIO9, False), 22: (GPIO25, False), - 23: (GPIO11, False), 24: (GPIO8, False), - 25: (GND, False), 26: (GPIO7, False), - 27: (GPIO0, False), 28: (GPIO1, False), - 29: (GPIO5, False), 30: (GND, False), - 31: (GPIO6, False), 32: (GPIO12, False), - 33: (GPIO13, False), 34: (GND, False), - 35: (GPIO19, False), 36: (GPIO16, False), - 37: (GPIO26, False), 38: (GPIO20, False), - 39: (GND, False), 40: (GPIO21, False), +CM3PLUS_BOARD = """\ +{style:white on green}+---------------------------------------+{style:reset} +{style:white on green}| {style:yellow on black}O{style:bold white on green} Raspberry Pi {model:4s} {style:normal yellow on black}O{style:white on green} |{style:reset} + {style:white on green}) Version {pcb_revision:3s} {style:black on white},---.{style:white on green} ({style:reset} +{style:white on green}| {style:black on white}|SoC|{style:white on green} |{style:reset} + {style:white on green}) {style:black on white}`---'{style:white on green} ({style:reset} +{style:white on green}| {style:on black}O{style:on green} _ {style:on black}O{style:on green} |{style:reset} +{style:white on green}||||||{style:reset} {style:white on green}||||||||||||||||||||||||||||||||||{style:reset}""" + +CM4_BOARD = """\ +{style:white on green},--{style:black on white}csi1{style:white on green}---{style:black on white}dsi0{style:white on green}---{style:black on white}dsi1{style:white on green}-----------{style:bold},-------------.{style:normal}-----------.{style:reset} +{style:white on green}| {style:black on white}----{style:white on green} {style:black on white}----{style:white on green} {style:black on white}----{style:white on green} J2 {J2:{style} col2}{style:bold white on green}| |{style:normal}{J3:{style} rev col1}{style:white on green} |{style:reset} +{style:white on green}{style:black on white}c|{style:white on green} {style:bold}Pi {model:7s} Rev {pcb_revision:3s}{style:normal} {J2:{style} col1}{style:bold white on green}| {style:normal black on white} Wi {style:white on green} {style:bold}|{style:normal}J3 |{style:reset} +{style:white on green}{style:black on white}s|{style:white on green} {style:bold}IO Board{style:normal} {style:bold}| {style:normal black on white} Fi {style:white on green} {style:bold}|{style:normal} |{style:reset} +{style:white on green}{style:black on white}i|{style:white on green} J6 {J6:{style} col2}{style:bold white on green} | {style:normal white on black}+--+{style:on green}{style:bold}| {style:normal white on black}|P|{style:on green} |{style:reset} +{style:white on green}| J8 {J6:{style} col1}{style:bold white on green} | {style:normal black on white},----.{style:on green} {style:white on black}|eM|{style:bold on green}| {style:normal white on black}}}-{{{style:on green} |{style:reset} +{style:white on green}| {J8:{style} col2}{style:white on green} {style:bold}| {style:normal black on white}|SoC |{style:on green} {style:white on black}|MC|{style:bold on green}| {style:normal white on black}|C|{style:on green} |{style:reset} +{style:white on green}| {J8:{style} col1}{style:white on green} J9 {style:bold}| {style:normal black on white}| |{style:on green} {style:white on black}+--+{style:bold on green}| {style:normal white on black}|I|{style:on green} |{style:reset} +{style:white on green}| {style:black on white},---.{style:white on green} {J9:{style} row1}{style:bold white on green} | {style:normal black on white}`----'{style:white on green} {style:bold}| {style:normal white on black}|e|{style:on green} |{style:reset} +{style:white on green}|{style:black on white}( ={style:on green}O{style:on white} |{style:white on green} {J9:{style} row2}{style:bold white on green} | {style:normal white on black}+----+{style:on green} {style:bold}|{style:normal} |{style:reset} +{style:white on green}| {style:black on white}) + |{style:white on green} {style:bold}| {style:normal white on black}|RAM |{style:bold white on green} |{style:normal} |{style:reset} +{style:white on green}|{style:black on white}( ={style:on green}O{style:on white} |{style:white on green} {style:bold}`--{style:normal white on black}+----+{style:bold on green}-----'{style:normal} |{style:reset} +{style:white on green}| {style:black on white}`---'{style:white on green} |{style:reset} +{style:white on green}| {J1:{style} rev col1}{style:white on green} J1 |{style:reset} +{style:white on green}| |{style:reset} +{style:white on green}| {style:black on white}|Net |{style:on green} {style:black on white}|USB|{style:on green} {style:black on white}|uSD|{style:white on green} {style:on black}|p|{style:on green}|{style:reset} +{style:white on green}| {style:black on white}|HDMI|{style:on green} {style:black on white}|HDMI|{style:white on green} {style:black on white}| |{style:on green} {style:black on white}| 2 |{style:on green} {style:black on white}usb{style:white on green} {style:black on white}| |{style:white on green} {style:on black}|w|{style:on green}|{style:reset} +{style:white on green}`----{style:black on white}| 0 |{style:white on green}---{style:black on white}| 1 |{style:white on green}-------{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}-{style:black on white}| |{style:white on green}------------{style:white on black}|r|{style:on green}'{style:reset}""" + +# TODO: Add a CM5_BOARD ascii-art diagram + +P400_BOARD = """\ + {style:white on red},------+----+----+----+----+---+--+--+--+--------------------+---.{style:reset} + {style:white on red},' |{style:white on black}Net {style:white on red}|{style:white on black}USB {style:white on red}|{style:cyan on black}USB {style:white on red}|{style:cyan on black}USB {style:white on red}|{style:white on black}pwr{style:white on red}|{style:white on black}hd{style:white on red}|{style:white on black}hd{style:white on red}|{style:white on black}sd{style:white on red}|{J8:{style} col2}{style:white on red}| `.{style:reset} + {style:white on red}/ {style:black}=={style:white} |{style:white on black} {style:white on red}|{style:white on black} 2 {style:white on red}|{style:cyan on black} 3 {style:white on red}|{style:cyan on black} 3 {style:white on red}|{style:white on black} {style:white on red}|{style:white on black}m1{style:white on red}|{style:white on black}m0{style:white on red}|{style:white on black} {style:white on red}|{J8:{style} col1}{style:white on red}| \\{style:reset} +{style:black on white},------------------------------------------------------------------------.{style:reset} +{style:black on white}| ___ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ {style:bold on white}o o {style:green}o{style:normal black}____ |{style:reset} +{style:black on white}| |Esc|F1{style:red}11{style:black}|F2{style:red}12{style:black}|F3 |F4 |F5 |F6 |F7 |F8 |F9 |F10{style:red}o{style:black}|NumL|Pt{style:red}Sq{style:black}|Dl{style:red}In{style:black}| |{style:reset} +{style:black on white}| ___ ___ ____ ____ ____ ____ ____ ___ ____ ____ ____ ___ ____ _______ |{style:reset} +{style:black on white}| |¬ |! |" |£ |$ |% |^ |& {style:red}7{style:black}|* {style:red}8{style:black}|( {style:red}9{style:black}|) {style:red}*{style:black}|_ |+ |BkSpc | |{style:reset} +{style:black on white}| |` ||1 |2 |3 |4 |5 |6 |7 |8 |9 |0 |- |= |<-- | |{style:reset} +{style:black on white}| _____ ___ ____ ____ ____ ____ ____ ___ ____ ____ ____ ____ __ ______ |{style:reset} +{style:black on white}| |Tab |Q |W |E |R |T |Y |U {style:red}4{style:black}|I {style:red}5{style:black}|O {style:red}6{style:black}|P {style:red}-{style:black}|{{ |}} |Enter | |{style:reset} +{style:black on white}| |->| | | | | | | | | | | |[ |] |<-' | |{style:reset} +{style:black on white}| ______ ____ ____ ____ ____ ____ ____ ___ ____ ____ ____ ____ __ | |{style:reset} +{style:black on white}| |Caps |A |S |D |F |G |H |J {style:red}1{style:black}|K {style:red}2{style:black}|L {style:red}3{style:black}|: {style:red}+{style:black}|@ |~ | | |{style:reset} +{style:black on white}| |Lock | | | | | | | | | |; |' |# | | |{style:reset} +{style:black on white}| _____ ___ ___ ____ ____ ____ ____ ____ ___ ____ ____ ____ __________ |{style:reset} +{style:black on white}| |Shift|| |Z |X |C |V |B |N |M {style:red}0{style:black}|< |> {style:red}.{style:black}|? {style:red}/{style:black}|Shift | |{style:reset} +{style:black on white}| |^ |\\ | | | | | | | |, |. |/ |^ | |{style:reset} +{style:black on white}| ____ ___ ____ ____ _______________________ ____ ____ _____ |{style:reset} +{style:black on white}| |Ctrl|{style:red}Fn{style:black} | {style:red}**{style:black} |Alt | |Alt |Ctrl|____|^{style:red}PgUp{style:black}|____ |{style:reset} +{style:black on white}| | | | {style:red}{{}}{style:black} | | | | |<{style:red}Hom{style:black}|v{style:red}PgDn{style:black}|>{style:red}End{style:black}| |{style:reset} +{style:black on white}`------------------------------------------------------------------------'{style:reset} + Raspberry Pi {style:bold red}{model}{style:reset} Rev {pcb_revision}""" + +# TODO: Add a P500_BOARD ascii-art diagram + +# Pin maps for various board revisions and headers. Much of the information +# below is derived from the BCM2835 ARM Peripherals datasheet, but Gadgetoid's +# superb https://pinout.xyz site was also a great deal of help in filling in +# the gaps! + +import re +def gpiof(*names): + return { + 'gpio' if re.match(r'GPIO\d+$', name) else + 'i2c' if re.match(r'I2C\d (SDA|SCL)$', name) else + 'spi' if re.match(r'SPI\d (SCLK|MOSI|MISO|CE\d)$', name) else + 'uart' if re.match(r'UART\d (RXD|TXD|RTS|CTS)$', name) else + 'smi' if re.match(r'SMI (name[AD]\d+|SOE / SE|SWE / SRW)$', name) else + 'dpi' if re.match(r'DPI (D\d+|PCLK|DE|[HV]SYNC)$', name) else + 'pwm' if re.match(r'PWM\d+ \d+$', name) else + 'pcm' if re.match(r'PCM (CLK|FS|DIN|DOUT)$', name) else + 'sdio' if re.match(r'SD\d+ (CLK|CMD|DAT\d+)$', name) else + 'jtag' if re.match(r'JTAG (TDI|TDO|TCK|TMS|RTCK|TRST)$', name) else + 'mii' if re.match(r'(RG)?MII ', name) else + '': name + for name in names + if name } -CM_SODIMM = { - 1: (GND, False), 2: ('EMMC DISABLE N', False), - 3: (GPIO0, False), 4: (NC, False), - 5: (GPIO1, False), 6: (NC, False), - 7: (GND, False), 8: (NC, False), - 9: (GPIO2, False), 10: (NC, False), - 11: (GPIO3, False), 12: (NC, False), - 13: (GND, False), 14: (NC, False), - 15: (GPIO4, False), 16: (NC, False), - 17: (GPIO5, False), 18: (NC, False), - 19: (GND, False), 20: (NC, False), - 21: (GPIO6, False), 22: (NC, False), - 23: (GPIO7, False), 24: (NC, False), - 25: (GND, False), 26: (GND, False), - 27: (GPIO8, False), 28: (GPIO28, False), - 29: (GPIO9, False), 30: (GPIO29, False), - 31: (GND, False), 32: (GND, False), - 33: (GPIO10, False), 34: (GPIO30, False), - 35: (GPIO11, False), 36: (GPIO31, False), - 37: (GND, False), 38: (GND, False), - 39: ('GPIO0-27 VREF', False), 40: ('GPIO0-27 VREF', False), +V1_8 = {'': '1V8'} +V3_3 = {'': '3V3'} +V5 = {'': '5V'} +GND = {'': 'GND'} +NC = {'': 'NC'} # not connected + +# gpio alt0 alt1 alt2 alt3 alt4 alt5 +PI1_GPIO0 = gpiof('GPIO0', 'I2C0 SDA', 'SMI SA5', 'DPI PCLK') +PI1_GPIO1 = gpiof('GPIO1', 'I2C0 SCL', 'SMI SA4', 'DPI DE') +PI1_GPIO2 = gpiof('GPIO2', 'I2C1 SDA', 'SMI SA3', 'DPI VSYNC') +PI1_GPIO3 = gpiof('GPIO3', 'I2C1 SCL', 'SMI SA2', 'DPI HSYNC') +PI1_GPIO4 = gpiof('GPIO4', 'GPCLK0', 'SMI SA1', 'DPI D0', '', '', 'JTAG TDI') +PI1_GPIO5 = gpiof('GPIO5', 'GPCLK1', 'SMI SA0', 'DPI D1', '', '', 'JTAG TDO') +PI1_GPIO6 = gpiof('GPIO6', 'GPCLK2', 'SMI SOE / SE', 'DPI D2', '', '', 'JTAG RTCK') +PI1_GPIO7 = gpiof('GPIO7', 'SPI0 CE1', 'SMI SWE / SRW', 'DPI D3') +PI1_GPIO8 = gpiof('GPIO8', 'SPI0 CE0', 'SMI SD0', 'DPI D4') +PI1_GPIO9 = gpiof('GPIO9', 'SPI0 MISO', 'SMI SD1', 'DPI D5') +PI1_GPIO10 = gpiof('GPIO10', 'SPI0 MOSI', 'SMI SD2', 'DPI D6') +PI1_GPIO11 = gpiof('GPIO11', 'SPI0 SCLK', 'SMI SD3', 'DPI D7') +PI1_GPIO12 = gpiof('GPIO12', 'PWM0 0', 'SMI SD4', 'DPI D8', '', '', 'JTAG TMS' ) +PI1_GPIO13 = gpiof('GPIO13', 'PWM0 1', 'SMI SD5', 'DPI D9', '', '', 'JTAG TCK' ) +PI1_GPIO14 = gpiof('GPIO14', 'UART0 TXD', 'SMI SD6', 'DPI D10' '', '', 'UART1 TXD') +PI1_GPIO15 = gpiof('GPIO15', 'UART0 RXD', 'SMI SD7', 'DPI D11' '', '', 'UART1 RXD') +PI1_GPIO16 = gpiof('GPIO16', '', 'SMI SD8', 'DPI D11', 'UART0 CTS', 'SPI1 CE2', 'UART1 CTS') +PI1_GPIO17 = gpiof('GPIO17', '', 'SMI SD9', 'DPI D13', 'UART0 RTS', 'SPI1 CE1', 'UART1 RTS') +PI1_GPIO18 = gpiof('GPIO18', 'PCM CLK', 'SMI SD10', 'DPI D14', 'BSC SDA / MOSI', 'SPI1 CE0', 'PWM0 0') +PI1_GPIO19 = gpiof('GPIO19', 'PCM FS', 'SMI SD11', 'DPI D15', 'BSC SCL / SCLK', 'SPI1 MISO', 'PWM0 1') +PI1_GPIO20 = gpiof('GPIO20', 'PCM DIN', 'SMI SD12', 'DPI D16', 'BSC MISO', 'SPI1 MOSI', 'GPCLK0') +PI1_GPIO21 = gpiof('GPIO21', 'PCM DOUT', 'SMI SD13', 'DPI D17', 'BSC CE', 'SPI1 SCLK', 'GPCLK1') +PI1_GPIO22 = gpiof('GPIO22', 'SD0 CLK', 'SMI SD14', 'DPI D18', 'SD1 CLK', 'JTAG TRST') +PI1_GPIO23 = gpiof('GPIO23', 'SD0 CMD', 'SMI SD15', 'DPI D19', 'SD1 CMD', 'JTAG RTCK') +PI1_GPIO24 = gpiof('GPIO24', 'SD0 DAT0', 'SMI SD16', 'DPI D20', 'SD1 DAT0', 'JTAG TDO') +PI1_GPIO25 = gpiof('GPIO25', 'SD0 DAT1', 'SMI SD17', 'DPI D21', 'SD1 DAT1', 'JTAG TCK') +PI1_GPIO26 = gpiof('GPIO26', 'SD0 DAT2', '', 'DPI D22', 'SD1 DAT2', 'JTAG TDI') +PI1_GPIO27 = gpiof('GPIO27', 'SD0 DAT3', '', 'DPI D23', 'SD1 DAT3', 'JTAG TMS') +PI1_GPIO28 = gpiof('GPIO28', 'I2C0 SDA', 'SMI SA5', 'PCM CLK') +PI1_GPIO29 = gpiof('GPIO29', 'I2C0 SCL', 'SMI SA4', 'PCM FS') +PI1_GPIO30 = gpiof('GPIO30', '', 'SMI SA3', 'PCM DIN', 'UART0 CTS', '', 'UART1 CTS') +PI1_GPIO31 = gpiof('GPIO31', '', 'SMI SA2', 'PCM DOUT', 'UART0 RTS', '', 'UART1 RTS') +PI1_GPIO32 = gpiof('GPIO32', 'GPCLK0', 'SMI SA1', '', 'UART0 TXD', '', 'UART1 TXD') +PI1_GPIO33 = gpiof('GPIO33', '', 'SMI SA0', '', 'UART0 RXD', '', 'UART1 RXD') +PI1_GPIO34 = gpiof('GPIO34', 'GPCLK0', 'SMI SOE / SE') +PI1_GPIO35 = gpiof('GPIO35', 'SPI0 CE1', 'SMI SWE / SRW') +PI1_GPIO36 = gpiof('GPIO36', 'SPI0 CE0', 'SMI SD0', 'UART0 TXD') +PI1_GPIO37 = gpiof('GPIO37', 'SPI0 MISO', 'SMI SD1', 'UART0 RXD') +PI1_GPIO38 = gpiof('GPIO38', 'SPI0 MOSI', 'SMI SD2', 'UART0 RTS') +PI1_GPIO39 = gpiof('GPIO39', 'SPI0 SCLK', 'SMI SD3', 'UART0 CTS') +PI1_GPIO40 = gpiof('GPIO40', 'PWM0 0', 'SMI SD4', '', '', 'SPI2 MISO', 'UART1 TXD') +PI1_GPIO41 = gpiof('GPIO41', 'PWM0 1', 'SMI SD5', '', '', 'SPI2 MOSI', 'UART1 RXD') +PI1_GPIO42 = gpiof('GPIO42', 'GPCLK1', 'SMI SD6', '', '', 'SPI2 SCLK', 'UART1 RTS') +PI1_GPIO43 = gpiof('GPIO43', 'GPCLK2', 'SMI SD7', '', '', 'SPI2 CE0', 'UART1 CTS') +PI1_GPIO44 = gpiof('GPIO44', 'GPCLK1', 'I2C0 SDA', 'I2C1 SDA', '', 'SPI2 CE1') +PI1_GPIO45 = gpiof('GPIO45', 'PWM0 1', 'I2C0 SCL', 'I2C1 SCL', '', 'SPI2 CE2') +PI1_GPIO46 = gpiof('GPIO46') +PI1_GPIO47 = gpiof('GPIO47') +PI1_GPIO48 = gpiof('GPIO48') +PI1_GPIO49 = gpiof('GPIO49') +PI1_GPIO50 = gpiof('GPIO50') +PI1_GPIO51 = gpiof('GPIO51') +PI1_GPIO52 = gpiof('GPIO52') +PI1_GPIO53 = gpiof('GPIO53') + +# gpio alt0 alt1 alt2 alt3 alt4 alt5 +PI4_GPIO0 = gpiof('GPIO0', 'I2C0 SDA', 'SMI SA5', 'DPI PCLK', 'SPI3 CE0', 'UART2 TXD', 'I2C6 SDA') +PI4_GPIO1 = gpiof('GPIO1', 'I2C0 SCL', 'SMI SA4', 'DPI DE', 'SPI3 MISO', 'UART2 RXD', 'I2C6 SCL') +PI4_GPIO2 = gpiof('GPIO2', 'I2C1 SDA', 'SMI SA3', 'DPI VSYNC', 'SPI3 MOSI', 'UART2 CTS', 'I2C3 SDA') +PI4_GPIO3 = gpiof('GPIO3', 'I2C1 SCL', 'SMI SA2', 'DPI HSYNC', 'SPI3 SCLK', 'UART2 RTS', 'I2C3 SCL') +PI4_GPIO4 = gpiof('GPIO4', 'GPCLK0', 'SMI SA1', 'DPI D0', 'SPI4 CE0', 'UART3 TXD', 'I2C3 SDA') +PI4_GPIO5 = gpiof('GPIO5', 'GPCLK1', 'SMI SA0', 'DPI D1', 'SPI4 MISO', 'UART3 RXD', 'I2C3 SCL') +PI4_GPIO6 = gpiof('GPIO6', 'GPCLK2', 'SMI SOE / SE', 'DPI D2', 'SPI4 MOSI', 'UART3 CTS', 'I2C4 SDA') +PI4_GPIO7 = gpiof('GPIO7', 'SPI0 CE1', 'SMI SWE / SRW', 'DPI D3', 'SPI4 SCLK', 'UART3 RTS', 'I2C4 SCL') +PI4_GPIO8 = gpiof('GPIO8', 'SPI0 CE0', 'SMI SD0', 'DPI D4', 'BSC CE', 'UART4 TXD', 'I2C4 SDA') +PI4_GPIO9 = gpiof('GPIO9', 'SPI0 MISO', 'SMI SD1', 'DPI D5', 'BSC MISO', 'UART4 RXD', 'I2C4 SCL') +PI4_GPIO10 = gpiof('GPIO10', 'SPI0 MOSI', 'SMI SD2', 'DPI D6', 'BSC SDA / MOSI', 'UART4 CTS', 'I2C5 SDA') +PI4_GPIO11 = gpiof('GPIO11', 'SPI0 SCLK', 'SMI SD3', 'DPI D7', 'BSC SCL / SCLK', 'UART4 RTS', 'I2C5 SCL') +PI4_GPIO12 = gpiof('GPIO12', 'PWM0 0', 'SMI SD4', 'DPI D8', 'SPI5 CE0', 'UART5 TXD', 'I2C5 SDA') +PI4_GPIO13 = gpiof('GPIO13', 'PWM0 1', 'SMI SD5', 'DPI D9', 'SPI5 MISO', 'UART5 RXD', 'I2C5 SCL') +PI4_GPIO14 = gpiof('GPIO14', 'UART0 TXD', 'SMI SD6', 'DPI D10', 'SPI5 MOSI', 'UART5 CTS', 'UART1 TXD') +PI4_GPIO15 = gpiof('GPIO15', 'UART0 RXD', 'SMI SD7', 'DPI D11', 'SPI5 SCLK', 'UART5 RTS', 'UART1 RXD') +PI4_GPIO16 = gpiof('GPIO16', '', 'SMI SD8', 'DPI D12', 'UART0 CTS', 'SPI1 CE2', 'UART1 CTS') +PI4_GPIO17 = gpiof('GPIO17', '', 'SMI SD9', 'DPI D13', 'UART0 RTS', 'SPI1 CE1', 'UART1 RTS') +PI4_GPIO18 = gpiof('GPIO18', 'PCM CLK', 'SMI SD10', 'DPI D14', 'SPI6 CE0', 'SPI1 CE0', 'PWM0 0') +PI4_GPIO19 = gpiof('GPIO19', 'PCM FS', 'SMI SD11', 'DPI D15', 'SPI6 MISO', 'SPI1 MISO', 'PWM0 1') +PI4_GPIO20 = gpiof('GPIO20', 'PCM DIN', 'SMI SD12', 'DPI D16', 'SPI6 MOSI', 'SPI1 MOSI', 'GPCLK0') +PI4_GPIO21 = gpiof('GPIO21', 'PCM DOUT', 'SMI SD13', 'DPI D17', 'SPI6 SCLK', 'SPI1 SCLK', 'GPCLK1') +PI4_GPIO22 = gpiof('GPIO22', 'SD0 CLK', 'SMI SD14', 'DPI D18', 'SD1 CLK', 'JTAG TRST', 'I2C6 SDA') +PI4_GPIO23 = gpiof('GPIO23', 'SD0 CMD', 'SMI SD15', 'DPI D19', 'SD1 CMD', 'JTAG RTCK', 'I2C6 SCL') +PI4_GPIO24 = gpiof('GPIO24', 'SD0 DAT0', 'SMI SD16', 'DPI D20', 'SD1 DAT0', 'JTAG TDO', 'SPI3 CE1') +PI4_GPIO25 = gpiof('GPIO25', 'SD0 DAT1', 'SMI SD17', 'DPI D21', 'SD1 DAT1', 'JTAG TCK', 'SPI4 CE1') +PI4_GPIO26 = gpiof('GPIO26', 'SDA DAT2', '', 'DPI D22', 'SD1 DAT2', 'JTAG TDI', 'SPI5 CE1') +PI4_GPIO27 = gpiof('GPIO27', 'SDA DAT3', '', 'DPI D23', 'SD1 DAT3', 'JTAG TMS', 'SPI6 CE1') +PI4_GPIO28 = gpiof('GPIO28', 'I2C0 SDA', 'SMI SA5', 'PCM CLK', '', 'MII RX ERR', 'RGMII MDIO') +PI4_GPIO29 = gpiof('GPIO29', 'I2C0 SCL', 'SMI SA4', 'PCM FS', '', 'MII TX ERR', 'RGMII MDC') +PI4_GPIO30 = gpiof('GPIO30', '', 'SMI SA3', 'PCM DIN', 'UART0 CTS', 'MII CRS', 'UART1 CTS') +PI4_GPIO31 = gpiof('GPIO31', '', 'SMI SA2', 'PCM DOUT', 'UART0 RTS', 'MII COL', 'UART1 RTS') +PI4_GPIO32 = gpiof('GPIO32', 'GPCLK0', 'SMI SA1', '', 'UART0 TXD', 'SD CARD PRES', 'UART1 TXD') +PI4_GPIO33 = gpiof('GPIO33', '', 'SMI SA0', '', 'UART0 RXD', 'SD CARD WRPROT', 'UART1 RXD') +PI4_GPIO34 = gpiof('GPIO34', 'GPCLK0', 'SMI SOE / SE', '', 'SD1 CLK', 'SD CARD LED', 'RGMII IRQ') +PI4_GPIO35 = gpiof('GPIO35', 'SPI0 CE1', 'SMI SWE / SRW', '', 'SD1 CMD', 'RGMII START STOP') +PI4_GPIO36 = gpiof('GPIO36', 'SPI0 CE0', 'SMI SD0', 'UART0 TXD', 'SD1 DAT0', 'RGMII RX OK', 'MII RX ERR') +PI4_GPIO37 = gpiof('GPIO37', 'SPI0 MISO', 'SMI SD1', 'UART0 RXD', 'SD1 DAT1', 'RGMII MDIO', 'MII TX ERR') +PI4_GPIO38 = gpiof('GPIO38', 'SPI0 MOSI', 'SMI SD2', 'UART0 RTS', 'SD1 DAT2', 'RGMII MDC', 'MII CRS') +PI4_GPIO39 = gpiof('GPIO39', 'SPI0 SCLK', 'SMI SD3', 'UART0 CTS', 'SD1 DAT3', 'RGMII IRQ', 'MII COL') +PI4_GPIO40 = gpiof('GPIO40', 'PWM1 0', 'SMI SD4', '', 'SD1 DAT4', 'SPI0 MISO', 'UART1 TXD') +PI4_GPIO41 = gpiof('GPIO41', 'PWM1 1', 'SMI SD5', '', 'SD1 DAT5', 'SPI0 MOSI', 'UART1 RXD') +PI4_GPIO42 = gpiof('GPIO42', 'GPCLK1', 'SMI SD6', '', 'SD1 DAT6', 'SPI0 SCLK', 'UART1 RTS') +PI4_GPIO43 = gpiof('GPIO43', 'GPCLK2', 'SMI SD7', '', 'SD1 DAT7', 'SPI0 CE0', 'UART1 CTS') +PI4_GPIO44 = gpiof('GPIO44', 'GPCLK1', 'I2C0 SDA', 'I2C1 SDA', '', 'SPI0 CE1', 'SD CARD VOLT') +PI4_GPIO45 = gpiof('GPIO45', 'PWM0 1', 'I2C0 SCL', 'I2C1 SCL', '', 'SPI0 CE2', 'SD CARD PWR0') +PI4_GPIO46 = gpiof('GPIO46') +PI4_GPIO47 = gpiof('GPIO47') +PI4_GPIO48 = gpiof('GPIO48') +PI4_GPIO49 = gpiof('GPIO49') +PI4_GPIO50 = gpiof('GPIO50') +PI4_GPIO51 = gpiof('GPIO51') +PI4_GPIO52 = gpiof('GPIO52') +PI4_GPIO53 = gpiof('GPIO53') +PI4_GPIO54 = gpiof('GPIO54') +PI4_GPIO55 = gpiof('GPIO55') +PI4_GPIO56 = gpiof('GPIO56') +PI4_GPIO57 = gpiof('GPIO57') + +# TODO: The Alt functions on the Pi 5 pins are different to the Pi 4 pins (see the RP1 datasheet), so theoretically we need a load of PI5_GPIO entries (and then update the PI5_J8 header below) + +del gpiof +del re + +REV1_P1 = (13, 2, { + 1: V3_3, 2: V5, + 3: PI1_GPIO0, 4: V5, + 5: PI1_GPIO1, 6: GND, + 7: PI1_GPIO4, 8: PI1_GPIO14, + 9: GND, 10: PI1_GPIO15, + 11: PI1_GPIO17, 12: PI1_GPIO18, + 13: PI1_GPIO21, 14: GND, + 15: PI1_GPIO22, 16: PI1_GPIO23, + 17: V3_3, 18: PI1_GPIO24, + 19: PI1_GPIO10, 20: GND, + 21: PI1_GPIO9, 22: PI1_GPIO25, + 23: PI1_GPIO11, 24: PI1_GPIO8, + 25: GND, 26: PI1_GPIO7, +}) + +REV2_P1 = (13, 2, { + 1: V3_3, 2: V5, + 3: PI1_GPIO2, 4: V5, + 5: PI1_GPIO3, 6: GND, + 7: PI1_GPIO4, 8: PI1_GPIO14, + 9: GND, 10: PI1_GPIO15, + 11: PI1_GPIO17, 12: PI1_GPIO18, + 13: PI1_GPIO27, 14: GND, + 15: PI1_GPIO22, 16: PI1_GPIO23, + 17: V3_3, 18: PI1_GPIO24, + 19: PI1_GPIO10, 20: GND, + 21: PI1_GPIO9, 22: PI1_GPIO25, + 23: PI1_GPIO11, 24: PI1_GPIO8, + 25: GND, 26: PI1_GPIO7, +}) + +REV2_P5 = (4, 2, { + 1: V5, 2: V3_3, + 3: PI1_GPIO28, 4: PI1_GPIO29, + 5: PI1_GPIO30, 6: PI1_GPIO31, + 7: GND, 8: GND, +}) + +PI1_P2 = (8, 1, { + 1: {'': 'GPU JTAG'}, + 2: {'': 'GPU JTAG'}, + 3: {'': 'GPU JTAG'}, + 4: {'': 'GPU JTAG'}, + 5: {'': 'GPU JTAG'}, + 6: {'': 'GPU JTAG'}, + 7: {'': 'GPU JTAG'}, + 8: {'': 'GPU JTAG'}, +}) + +PI1_P3 = (7, 1, { + 1: {'': 'LAN JTAG'}, + 2: {'': 'LAN JTAG'}, + 3: {'': 'LAN JTAG'}, + 4: {'': 'LAN JTAG'}, + 5: {'': 'LAN JTAG'}, + 6: {'': 'LAN JTAG'}, + 7: {'': 'LAN JTAG'}, +}) + +REV2_P6 = (2, 1, { + 1: {'': 'RUN'}, + 2: GND, +}) + +PLUS_J8 = (20, 2, { + 1: V3_3, 2: V5, + 3: PI1_GPIO2, 4: V5, + 5: PI1_GPIO3, 6: GND, + 7: PI1_GPIO4, 8: PI1_GPIO14, + 9: GND, 10: PI1_GPIO15, + 11: PI1_GPIO17, 12: PI1_GPIO18, + 13: PI1_GPIO27, 14: GND, + 15: PI1_GPIO22, 16: PI1_GPIO23, + 17: V3_3, 18: PI1_GPIO24, + 19: PI1_GPIO10, 20: GND, + 21: PI1_GPIO9, 22: PI1_GPIO25, + 23: PI1_GPIO11, 24: PI1_GPIO8, + 25: GND, 26: PI1_GPIO7, + 27: PI1_GPIO0, 28: PI1_GPIO1, + 29: PI1_GPIO5, 30: GND, + 31: PI1_GPIO6, 32: PI1_GPIO12, + 33: PI1_GPIO13, 34: GND, + 35: PI1_GPIO19, 36: PI1_GPIO16, + 37: PI1_GPIO26, 38: PI1_GPIO20, + 39: GND, 40: PI1_GPIO21, +}) + +PLUS_POE = (2, 2, { + 1: {'': 'TR01 TAP'}, 2: {'': 'TR00 TAP'}, + 3: {'': 'TR03 TAP'}, 4: {'': 'TR02 TAP'}, +}) + +PLUS_RUN = (2, 1, { + 1: {'': 'POWER ENABLE'}, + 2: {'': 'RUN'}, +}) + +ZERO_RUN = REV2_P6 + +ZERO_TV = (2, 1, { + 1: {'': 'COMPOSITE'}, + 2: GND, +}) + +PI4_J8 = (20, 2, { + 1: V3_3, 2: V5, + 3: PI4_GPIO2, 4: V5, + 5: PI4_GPIO3, 6: GND, + 7: PI4_GPIO4, 8: PI4_GPIO14, + 9: GND, 10: PI4_GPIO15, + 11: PI4_GPIO17, 12: PI4_GPIO18, + 13: PI4_GPIO27, 14: GND, + 15: PI4_GPIO22, 16: PI4_GPIO23, + 17: V3_3, 18: PI4_GPIO24, + 19: PI4_GPIO10, 20: GND, + 21: PI4_GPIO9, 22: PI4_GPIO25, + 23: PI4_GPIO11, 24: PI4_GPIO8, + 25: GND, 26: PI4_GPIO7, + 27: PI4_GPIO0, 28: PI4_GPIO1, + 29: PI4_GPIO5, 30: GND, + 31: PI4_GPIO6, 32: PI4_GPIO12, + 33: PI4_GPIO13, 34: GND, + 35: PI4_GPIO19, 36: PI4_GPIO16, + 37: PI4_GPIO26, 38: PI4_GPIO20, + 39: GND, 40: PI4_GPIO21, +}) + +PI4_J2 = (3, 1, { + 1: {'': 'GLOBAL ENABLE'}, + 2: GND, + 3: {'': 'RUN'}, +}) + +PI4_J14 = PLUS_POE + +PI5_J8 = PI4_J8 # This is slightly wrong, but see the PI5_GPIO comment above + +PI5_J2 = (2, 1, { + 1: {'': 'RUN'}, + 2: GND, +}) + +PI5_J7 = (2, 1, { + 1: {'': 'COMPOSITE'}, + 2: GND, +}) + +CM_SODIMM = (100, 2, { + 1: GND, 2: {'': 'EMMC DISABLE N'}, + 3: PI1_GPIO0, 4: NC, + 5: PI1_GPIO1, 6: NC, + 7: GND, 8: NC, + 9: PI1_GPIO2, 10: NC, + 11: PI1_GPIO3, 12: NC, + 13: GND, 14: NC, + 15: PI1_GPIO4, 16: NC, + 17: PI1_GPIO5, 18: NC, + 19: GND, 20: NC, + 21: PI1_GPIO6, 22: NC, + 23: PI1_GPIO7, 24: NC, + 25: GND, 26: GND, + 27: PI1_GPIO8, 28: PI1_GPIO28, + 29: PI1_GPIO9, 30: PI1_GPIO29, + 31: GND, 32: GND, + 33: PI1_GPIO10, 34: PI1_GPIO30, + 35: PI1_GPIO11, 36: PI1_GPIO31, + 37: GND, 38: GND, + 39: {'': 'GPIO0-27 VREF'}, 40: {'': 'GPIO0-27 VREF'}, # Gap in SODIMM pins - 41: ('GPIO28-45 VREF', False), 42: ('GPIO28-45 VREF', False), - 43: (GND, False), 44: (GND, False), - 45: (GPIO12, False), 46: (GPIO32, False), - 47: (GPIO13, False), 48: (GPIO33, False), - 49: (GND, False), 50: (GND, False), - 51: (GPIO14, False), 52: (GPIO34, False), - 53: (GPIO15, False), 54: (GPIO35, False), - 55: (GND, False), 56: (GND, False), - 57: (GPIO16, False), 58: (GPIO36, False), - 59: (GPIO17, False), 60: (GPIO37, False), - 61: (GND, False), 62: (GND, False), - 63: (GPIO18, False), 64: (GPIO38, False), - 65: (GPIO19, False), 66: (GPIO39, False), - 67: (GND, False), 68: (GND, False), - 69: (GPIO20, False), 70: (GPIO40, False), - 71: (GPIO21, False), 72: (GPIO41, False), - 73: (GND, False), 74: (GND, False), - 75: (GPIO22, False), 76: (GPIO42, False), - 77: (GPIO23, False), 78: (GPIO43, False), - 79: (GND, False), 80: (GND, False), - 81: (GPIO24, False), 82: (GPIO44, False), - 83: (GPIO25, False), 84: (GPIO45, False), - 85: (GND, False), 86: (GND, False), - 87: (GPIO26, False), 88: ('GPIO46 1V8', False), - 89: (GPIO27, False), 90: ('GPIO47 1V8', False), - 91: (GND, False), 92: (GND, False), - 93: ('DSI0 DN1', False), 94: ('DSI1 DP0', False), - 95: ('DSI0 DP1', False), 96: ('DSI1 DN0', False), - 97: (GND, False), 98: (GND, False), - 99: ('DSI0 DN0', False), 100: ('DSI1 CP', False), - 101: ('DSI0 DP0', False), 102: ('DSI1 CN', False), - 103: (GND, False), 104: (GND, False), - 105: ('DSI0 CN', False), 106: ('DSI1 DP3', False), - 107: ('DSI0 CP', False), 108: ('DSI1 DN3', False), - 109: (GND, False), 110: (GND, False), - 111: ('HDMI CK N', False), 112: ('DSI1 DP2', False), - 113: ('HDMI CK P', False), 114: ('DSI1 DN2', False), - 115: (GND, False), 116: (GND, False), - 117: ('HDMI D0 N', False), 118: ('DSI1 DP1', False), - 119: ('HDMI D0 P', False), 120: ('DSI1 DN1', False), - 121: (GND, False), 122: (GND, False), - 123: ('HDMI D1 N', False), 124: (NC, False), - 125: ('HDMI D1 P', False), 126: (NC, False), - 127: (GND, False), 128: (NC, False), - 129: ('HDMI D2 N', False), 130: (NC, False), - 131: ('HDMI D2 P', False), 132: (NC, False), - 133: (GND, False), 134: (GND, False), - 135: ('CAM1 DP3', False), 136: ('CAM0 DP0', False), - 137: ('CAM1 DN3', False), 138: ('CAM0 DN0', False), - 139: (GND, False), 140: (GND, False), - 141: ('CAM1 DP2', False), 142: ('CAM0 CP', False), - 143: ('CAM1 DN2', False), 144: ('CAM0 CN', False), - 145: (GND, False), 146: (GND, False), - 147: ('CAM1 CP', False), 148: ('CAM0 DP1', False), - 149: ('CAM1 CN', False), 150: ('CAM0 DN1', False), - 151: (GND, False), 152: (GND, False), - 153: ('CAM1 DP1', False), 154: (NC, False), - 155: ('CAM1 DN1', False), 156: (NC, False), - 157: (GND, False), 158: (NC, False), - 159: ('CAM1 DP0', False), 160: (NC, False), - 161: ('CAM1 DN0', False), 162: (NC, False), - 163: (GND, False), 164: (GND, False), - 165: ('USB DP', False), 166: ('TVDAC', False), - 167: ('USB DM', False), 168: ('USB OTGID', False), - 169: (GND, False), 170: (GND, False), - 171: ('HDMI CEC', False), 172: ('VC TRST N', False), - 173: ('HDMI SDA', False), 174: ('VC TDI', False), - 175: ('HDMI SCL', False), 176: ('VC TMS', False), - 177: ('RUN', False), 178: ('VC TDO', False), - 179: ('VDD CORE', False), 180: ('VC TCK', False), - 181: (GND, False), 182: (GND, False), - 183: (V1_8, False), 184: (V1_8, False), - 185: (V1_8, False), 186: (V1_8, False), - 187: (GND, False), 188: (GND, False), - 189: ('VDAC', False), 190: ('VDAC', False), - 191: (V3_3, False), 192: (V3_3, False), - 193: (V3_3, False), 194: (V3_3, False), - 195: (GND, False), 196: (GND, False), - 197: ('VBAT', False), 198: ('VBAT', False), - 199: ('VBAT', False), 200: ('VBAT', False), - } - -CM3_SODIMM = CM_SODIMM.copy() -CM3_SODIMM.update({ - 4: ('NC / SDX VREF', False), - 6: ('NC / SDX VREF', False), - 8: (GND, False), - 10: ('NC / SDX CLK', False), - 12: ('NC / SDX CMD', False), - 14: (GND, False), - 16: ('NC / SDX D0', False), - 18: ('NC / SDX D1', False), - 20: (GND, False), - 22: ('NC / SDX D2', False), - 24: ('NC / SDX D3', False), - 88: ('HDMI HPD N 1V8', False), - 90: ('EMMC EN N 1V8', False), - }) + 41: {'': 'GPIO28-45 VREF'}, 42: {'': 'GPIO28-45 VREF'}, + 43: GND, 44: GND, + 45: PI1_GPIO12, 46: PI1_GPIO32, + 47: PI1_GPIO13, 48: PI1_GPIO33, + 49: GND, 50: GND, + 51: PI1_GPIO14, 52: PI1_GPIO34, + 53: PI1_GPIO15, 54: PI1_GPIO35, + 55: GND, 56: GND, + 57: PI1_GPIO16, 58: PI1_GPIO36, + 59: PI1_GPIO17, 60: PI1_GPIO37, + 61: GND, 62: GND, + 63: PI1_GPIO18, 64: PI1_GPIO38, + 65: PI1_GPIO19, 66: PI1_GPIO39, + 67: GND, 68: GND, + 69: PI1_GPIO20, 70: PI1_GPIO40, + 71: PI1_GPIO21, 72: PI1_GPIO41, + 73: GND, 74: GND, + 75: PI1_GPIO22, 76: PI1_GPIO42, + 77: PI1_GPIO23, 78: PI1_GPIO43, + 79: GND, 80: GND, + 81: PI1_GPIO24, 82: PI1_GPIO44, + 83: PI1_GPIO25, 84: PI1_GPIO45, + 85: GND, 86: GND, + 87: PI1_GPIO26, 88: {'': 'GPIO46 1V8'}, + 89: PI1_GPIO27, 90: {'': 'GPIO47 1V8'}, + 91: GND, 92: GND, + 93: {'': 'DSI0 DN1'}, 94: {'': 'DSI1 DP0'}, + 95: {'': 'DSI0 DP1'}, 96: {'': 'DSI1 DN0'}, + 97: GND, 98: GND, + 99: {'': 'DSI0 DN0'}, 100: {'': 'DSI1 CP'}, + 101: {'': 'DSI0 DP0'}, 102: {'': 'DSI1 CN'}, + 103: GND, 104: GND, + 105: {'': 'DSI0 CN'}, 106: {'': 'DSI1 DP3'}, + 107: {'': 'DSI0 CP'}, 108: {'': 'DSI1 DN3'}, + 109: GND, 110: GND, + 111: {'': 'HDMI CK N'}, 112: {'': 'DSI1 DP2'}, + 113: {'': 'HDMI CK P'}, 114: {'': 'DSI1 DN2'}, + 115: GND, 116: GND, + 117: {'': 'HDMI D0 N'}, 118: {'': 'DSI1 DP1'}, + 119: {'': 'HDMI D0 P'}, 120: {'': 'DSI1 DN1'}, + 121: GND, 122: GND, + 123: {'': 'HDMI D1 N'}, 124: NC, + 125: {'': 'HDMI D1 P'}, 126: NC, + 127: GND, 128: NC, + 129: {'': 'HDMI D2 N'}, 130: NC, + 131: {'': 'HDMI D2 P'}, 132: NC, + 133: GND, 134: GND, + 135: {'': 'CAM1 DP3'}, 136: {'': 'CAM0 DP0'}, + 137: {'': 'CAM1 DN3'}, 138: {'': 'CAM0 DN0'}, + 139: GND, 140: GND, + 141: {'': 'CAM1 DP2'}, 142: {'': 'CAM0 CP'}, + 143: {'': 'CAM1 DN2'}, 144: {'': 'CAM0 CN'}, + 145: GND, 146: GND, + 147: {'': 'CAM1 CP'}, 148: {'': 'CAM0 DP1'}, + 149: {'': 'CAM1 CN'}, 150: {'': 'CAM0 DN1'}, + 151: GND, 152: GND, + 153: {'': 'CAM1 DP1'}, 154: NC, + 155: {'': 'CAM1 DN1'}, 156: NC, + 157: GND, 158: NC, + 159: {'': 'CAM1 DP0'}, 160: NC, + 161: {'': 'CAM1 DN0'}, 162: NC, + 163: GND, 164: GND, + 165: {'': 'USB DP'}, 166: {'': 'TVDAC'}, + 167: {'': 'USB DM'}, 168: {'': 'USB OTGID'}, + 169: GND, 170: GND, + 171: {'': 'HDMI CEC'}, 172: {'': 'VC TRST N'}, + 173: {'': 'HDMI SDA'}, 174: {'': 'VC TDI'}, + 175: {'': 'HDMI SCL'}, 176: {'': 'VC TMS'}, + 177: {'': 'RUN'}, 178: {'': 'VC TDO'}, + 179: {'': 'VDD CORE'}, 180: {'': 'VC TCK'}, + 181: GND, 182: GND, + 183: V1_8, 184: V1_8, + 185: V1_8, 186: V1_8, + 187: GND, 188: GND, + 189: {'': 'VDAC'}, 190: {'': 'VDAC'}, + 191: V3_3, 192: V3_3, + 193: V3_3, 194: V3_3, + 195: GND, 196: GND, + 197: {'': 'VBAT'}, 198: {'': 'VBAT'}, + 199: {'': 'VBAT'}, 200: {'': 'VBAT'}, +}) + +CM3_SODIMM = (CM_SODIMM[0], CM_SODIMM[1], CM_SODIMM[2].copy()) +CM3_SODIMM[-1].update({ + 4: {'': 'NC / SDX VREF'}, + 6: {'': 'NC / SDX VREF'}, + 8: GND, + 10: {'': 'NC / SDX CLK'}, + 12: {'': 'NC / SDX CMD'}, + 14: GND, + 16: {'': 'NC / SDX D0'}, + 18: {'': 'NC / SDX D1'}, + 20: GND, + 22: {'': 'NC / SDX D2'}, + 24: {'': 'NC / SDX D3'}, + 88: {'': 'HDMI HPD N 1V8'}, + 90: {'': 'EMMC EN N 1V8'}, +}) + +CM4_J6 = (2, 2, { + 1: {'': '1-2 CAM0+DISP0'}, 2: {'': '1-2 CAM0+DISP0'}, + 3: {'': '3-4 CAM0+DISP0'}, 4: {'': '3-4 CAM0+DISP0'}, +}) + +CM4_J2 = (7, 2, { + 1: {'': '1-2 DISABLE eMMC BOOT'}, 2: {'': '1-2 DISABLE eMMC BOOT'}, + 3: {'': '3-4 WRITE-PROT EEPROM'}, 4: {'': '3-4 WRITE-PROT EEPROM'}, + 5: {'': 'AIN0 MXL7704'}, 6: {'': 'AIN1 MXL7704'}, + 7: GND, 8: {'': 'SYNC_IN'}, + 9: {'': 'SYNC OUT'}, 10: GND, + 11: {'': 'TV OUT'}, 12: GND, + 13: {'': '13-14 WAKE'}, 14: {'': '13-14 WAKE'}, +}) + +CM4_J1 = PI4_J2 +CM4_J9 = PLUS_POE + +CM4_J3 = (3, 1, { + 1: {'': 'WL DISABLE'}, + 2: GND, + 3: {'': 'BT DISABLE'}, +}) # The following data is sourced from a combination of the following locations: # # http://elinux.org/RPi_HardwareHistory # http://elinux.org/RPi_Low-level_peripherals # https://git.drogon.net/?p=wiringPi;a=blob;f=wiringPi/wiringPi.c#l807 -# https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md +# https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#raspberry-pi-revision-codes PI_REVISIONS = { - # rev model pcb_rev released soc manufacturer ram storage usb eth wifi bt csi dsi headers board - 0x2: ('B', '1.0', '2012Q1', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, REV1_BOARD, ), - 0x3: ('B', '1.0', '2012Q3', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1}, REV1_BOARD, ), - 0x4: ('B', '2.0', '2012Q3', 'BCM2835', 'Sony', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0x5: ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0x6: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0x7: ('A', '2.0', '2013Q1', 'BCM2835', 'Egoman', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, A_BOARD, ), - 0x8: ('A', '2.0', '2013Q1', 'BCM2835', 'Sony', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, A_BOARD, ), - 0x9: ('A', '2.0', '2013Q1', 'BCM2835', 'Qisda', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, A_BOARD, ), - 0xd: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0xe: ('B', '2.0', '2012Q4', 'BCM2835', 'Sony', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0xf: ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P5': REV2_P5}, REV2_BOARD, ), - 0x10: ('B+', '1.2', '2014Q3', 'BCM2835', 'Sony', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'J8': PLUS_J8}, BPLUS_BOARD, ), - 0x11: ('CM', '1.1', '2014Q2', 'BCM2835', 'Sony', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, CM_BOARD, ), - 0x12: ('A+', '1.1', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'J8': PLUS_J8}, APLUS_BOARD, ), - 0x13: ('B+', '1.2', '2015Q1', 'BCM2835', 'Egoman', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'J8': PLUS_J8}, BPLUS_BOARD, ), - 0x14: ('CM', '1.1', '2014Q2', 'BCM2835', 'Embest', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, CM_BOARD, ), - 0x15: ('A+', '1.1', '2014Q4', 'BCM2835', 'Embest', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'J8': PLUS_J8}, APLUS_BOARD, ), + # rev model pcb_rev released soc manufacturer ram storage usb eth wifi bt csi dsi headers board + 0x2: ('B', '1.0', '2012Q1', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1, 'P2': PI1_P2, 'P3': PI1_P3}, REV1_BOARD, ), + 0x3: ('B', '1.0', '2012Q3', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV1_P1, 'P2': PI1_P2, 'P3': PI1_P3}, REV1_BOARD, ), + 0x4: ('B', '2.0', '2012Q3', 'BCM2835', 'Sony', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, REV2_BOARD, ), + 0x5: ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, REV2_BOARD, ), + 0x6: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 256, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, REV2_BOARD, ), + 0x7: ('A', '2.0', '2013Q1', 'BCM2835', 'Egoman', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, A_BOARD, ), + 0x8: ('A', '2.0', '2013Q1', 'BCM2835', 'Sony', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, A_BOARD, ), + 0x9: ('A', '2.0', '2013Q1', 'BCM2835', 'Qisda', 256, 'SD', 1, 0, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, A_BOARD, ), + 0xd: ('B', '2.0', '2012Q4', 'BCM2835', 'Egoman', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, REV2_BOARD, ), + 0xe: ('B', '2.0', '2012Q4', 'BCM2835', 'Sony', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, REV2_BOARD, ), + 0xf: ('B', '2.0', '2012Q4', 'BCM2835', 'Qisda', 512, 'SD', 2, 1, False, False, 1, 1, {'P1': REV2_P1, 'P2': PI1_P2, 'P3': PI1_P3, 'P5': REV2_P5, 'P6': REV2_P6}, REV2_BOARD, ), + 0x10: ('B+', '1.2', '2014Q3', 'BCM2835', 'Sony', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'J8': PLUS_J8, 'RUN': ZERO_RUN}, BPLUS_BOARD, ), + 0x11: ('CM', '1.1', '2014Q2', 'BCM2835', 'Sony', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, CM_BOARD, ), + 0x12: ('A+', '1.1', '2014Q4', 'BCM2835', 'Sony', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'J8': PLUS_J8, 'RUN': ZERO_RUN}, APLUS_BOARD, ), + 0x13: ('B+', '1.2', '2015Q1', 'BCM2835', 'Egoman', 512, 'MicroSD', 4, 1, False, False, 1, 1, {'J8': PLUS_J8, 'RUN': ZERO_RUN}, BPLUS_BOARD, ), + 0x14: ('CM', '1.1', '2014Q2', 'BCM2835', 'Embest', 512, 'eMMC', 1, 0, False, False, 2, 2, {'SODIMM': CM_SODIMM}, CM_BOARD, ), + 0x15: ('A+', '1.1', '2014Q4', 'BCM2835', 'Embest', 256, 'MicroSD', 1, 0, False, False, 1, 1, {'J8': PLUS_J8, 'RUN': ZERO_RUN}, APLUS_BOARD, ), } - -# ANSI color codes, for the pretty printers (nothing comprehensive, just enough -# for our purposes) - -class Style(object): - def __init__(self, color=None): - self.color = self._term_supports_color() if color is None else bool(color) - self.effects = { - 'reset': 0, - 'bold': 1, - 'normal': 22, - } - self.colors = { - 'black': 0, - 'red': 1, - 'green': 2, - 'yellow': 3, - 'blue': 4, - 'magenta': 5, - 'cyan': 6, - 'white': 7, - 'default': 9, - } - - @staticmethod - def _term_supports_color(): - try: - stdout_fd = sys.stdout.fileno() - except IOError: - return False - else: - is_a_tty = os.isatty(stdout_fd) - is_windows = sys.platform.startswith('win') - return is_a_tty and not is_windows - - @classmethod - def from_style_content(cls, format_spec): - specs = set(format_spec.split()) - style = specs & {'mono', 'color'} - content = specs - style - if len(style) > 1: - raise ValueError('cannot specify both mono and color styles') - try: - style = style.pop() - except KeyError: - style = 'color' if cls._term_supports_color() else 'mono' - if len(content) > 1: - raise ValueError('cannot specify more than one content element') - try: - content = content.pop() - except KeyError: - content = 'full' - return cls(style == 'color'), content - - def __call__(self, format_spec): - specs = format_spec.split() - codes = [] - fore = True - for spec in specs: - if spec == 'on': - fore = False - else: - try: - codes.append(self.effects[spec]) - except KeyError: - try: - if fore: - codes.append(30 + self.colors[spec]) - else: - codes.append(40 + self.colors[spec]) - except KeyError: - raise ValueError( - 'invalid format specification "%s"' % spec) - if self.color: - return '\x1b[%sm' % (';'.join(str(code) for code in codes)) - else: - return '' - - def __format__(self, format_spec): - if format_spec == '': - return 'color' if self.color else 'mono' - else: - return self(format_spec) - - -class PinInfo(namedtuple('PinInfo', ( - 'number', - 'function', - 'pull_up', - 'row', - 'col', - ))): - """ - This class is a :func:`~collections.namedtuple` derivative used to - represent information about a pin present on a GPIO header. The following - attributes are defined: - - .. attribute:: number - - An integer containing the physical pin number on the header (starting - from 1 in accordance with convention). - - .. attribute:: function - - A string describing the function of the pin. Some common examples - include "GND" (for pins connecting to ground), "3V3" (for pins which - output 3.3 volts), "GPIO9" (for GPIO9 in the Broadcom numbering - scheme), etc. - - .. attribute:: pull_up - - A bool indicating whether the pin has a physical pull-up resistor - permanently attached (this is usually :data:`False` but GPIO2 and GPIO3 - are *usually* :data:`True`). This is used internally by gpiozero to - raise errors when pull-down is requested on a pin with a physical - pull-up resistor. - - .. attribute:: row - - An integer indicating on which row the pin is physically located in - the header (1-based) - - .. attribute:: col - - An integer indicating in which column the pin is physically located - in the header (1-based) - """ - __slots__ = () # workaround python issue #24931 - - -class HeaderInfo(namedtuple('HeaderInfo', ( - 'name', - 'rows', - 'columns', - 'pins', - ))): - """ - This class is a :func:`~collections.namedtuple` derivative used to - represent information about a pin header on a board. The object can be used - in a format string with various custom specifications:: - - from gpiozero import * - - print('{0}'.format(pi_info().headers['J8'])) - print('{0:full}'.format(pi_info().headers['J8'])) - print('{0:col2}'.format(pi_info().headers['P1'])) - print('{0:row1}'.format(pi_info().headers['P1'])) - - "color" and "mono" can be prefixed to format specifications to force the - use of `ANSI color codes`_. If neither is specified, ANSI codes will only - be used if stdout is detected to be a tty:: - - print('{0:color row2}'.format(pi_info().headers['J8'])) # force use of ANSI codes - print('{0:mono row2}'.format(pi_info().headers['P1'])) # force plain ASCII - - The following attributes are defined: - - .. automethod:: pprint - - .. attribute:: name - - The name of the header, typically as it appears silk-screened on the - board (e.g. "P1" or "J8"). - - .. attribute:: rows - - The number of rows on the header. - - .. attribute:: columns - - The number of columns on the header. - - .. attribute:: pins - - A dictionary mapping physical pin numbers to :class:`PinInfo` tuples. - - .. _ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code - """ - __slots__ = () # workaround python issue #24931 - - def _func_style(self, function, style): - if function == V5: - return style('bold red') - elif function in (V3_3, V1_8): - return style('bold cyan') - elif function in (GND, NC): - return style('bold black') - elif function.startswith('GPIO') and function[4:].isdigit(): - return style('bold green') - else: - return style('yellow') - - def _format_full(self, style): - Cell = namedtuple('Cell', ('content', 'align', 'style')) - - lines = [] - for row in range(self.rows): - line = [] - for col in range(self.columns): - pin = (row * self.columns) + col + 1 - try: - pin = self.pins[pin] - cells = [ - Cell(pin.function, '><'[col % 2], self._func_style(pin.function, style)), - Cell('(%d)' % pin.number, '><'[col % 2], ''), - ] - if col % 2: - cells = reversed(cells) - line.extend(cells) - except KeyError: - line.append(Cell('', '<', '')) - lines.append(line) - cols = list(zip(*lines)) - col_lens = [max(len(cell.content) for cell in col) for col in cols] - lines = [ - ' '.join( - '{cell.style}{cell.content:{cell.align}{width}s}{style:reset}'.format( - cell=cell, width=width, style=style) - for cell, width, align in zip(line, col_lens, cycle('><'))) - for line in lines - ] - return '\n'.join(lines) - - def _format_pin(self, pin, style): - return ''.join(( - style('on black'), - ( - ' ' if pin is None else - self._func_style(pin.function, style) + - ('1' if pin.number == 1 else 'o') - ), - style('reset') - )) - - def _format_row(self, row, style): - if row > self.rows: - raise ValueError('invalid row %d for header %s' % (row, self.name)) - start_pin = (row - 1) * self.columns + 1 - return ''.join( - self._format_pin(pin, style) - for n in range(start_pin, start_pin + self.columns) - for pin in (self.pins.get(n),) - ) - - def _format_col(self, col, style): - if col > self.columns: - raise ValueError('invalid col %d for header %s' % (col, self.name)) - return ''.join( - self._format_pin(pin, style) - for n in range(col, self.rows * self.columns + 1, self.columns) - for pin in (self.pins.get(n),) - ) - - def __format__(self, format_spec): - style, content = Style.from_style_content(format_spec) - if content == 'full': - return self._format_full(style) - elif content.startswith('row') and content[3:].isdigit(): - return self._format_row(int(content[3:]), style) - elif content.startswith('col') and content[3:].isdigit(): - return self._format_col(int(content[3:]), style) - - def pprint(self, color=None): - """ - Pretty-print a diagram of the header pins. - - If *color* is :data:`None` (the default, the diagram will include ANSI - color codes if stdout is a color-capable terminal). Otherwise *color* - can be set to :data:`True` or :data:`False` to force color or - monochrome output. - """ - print('{0:{style} full}'.format(self, style=Style(color))) - - -class PiBoardInfo(namedtuple('PiBoardInfo', ( - 'revision', - 'model', - 'pcb_revision', - 'released', - 'soc', - 'manufacturer', - 'memory', - 'storage', - 'usb', - 'ethernet', - 'wifi', - 'bluetooth', - 'csi', - 'dsi', - 'headers', - 'board', - ))): - """ - This class is a :func:`~collections.namedtuple` derivative used to - represent information about a particular model of Raspberry Pi. While it is - a tuple, it is strongly recommended that you use the following named - attributes to access the data contained within. The object can be used - in format strings with various custom format specifications:: - - from gpiozero import * - - print('{0}'.format(pi_info())) - print('{0:full}'.format(pi_info())) - print('{0:board}'.format(pi_info())) - print('{0:specs}'.format(pi_info())) - print('{0:headers}'.format(pi_info())) - - "color" and "mono" can be prefixed to format specifications to force the - use of `ANSI color codes`_. If neither is specified, ANSI codes will only - be used if stdout is detected to be a tty:: - - print('{0:color board}'.format(pi_info())) # force use of ANSI codes - print('{0:mono board}'.format(pi_info())) # force plain ASCII - - .. _ANSI color codes: https://en.wikipedia.org/wiki/ANSI_escape_code - - .. automethod:: physical_pin - - .. automethod:: physical_pins - - .. automethod:: pprint - - .. automethod:: pulled_up - - .. automethod:: to_gpio - - .. attribute:: revision - - A string indicating the revision of the Pi. This is unique to each - revision and can be considered the "key" from which all other - attributes are derived. However, in itself the string is fairly - meaningless. - - .. attribute:: model - - A string containing the model of the Pi (for example, "B", "B+", "A+", - "2B", "CM" (for the Compute Module), or "Zero"). - - .. attribute:: pcb_revision - - A string containing the PCB revision number which is silk-screened onto - the Pi (on some models). - - .. note:: - - This is primarily useful to distinguish between the model B - revision 1.0 and 2.0 (not to be confused with the model 2B) which - had slightly different pinouts on their 26-pin GPIO headers. - - .. attribute:: released - - A string containing an approximate release date for this revision of - the Pi (formatted as yyyyQq, e.g. 2012Q1 means the first quarter of - 2012). - - .. attribute:: soc - - A string indicating the SoC (`system on a chip`_) that this revision - of the Pi is based upon. - - .. attribute:: manufacturer - - A string indicating the name of the manufacturer (usually "Sony" but a - few others exist). - - .. attribute:: memory - - An integer indicating the amount of memory (in Mb) connected to the - SoC. - - .. note:: - - This can differ substantially from the amount of RAM available - to the operating system as the GPU's memory is shared with the - CPU. When the camera module is activated, at least 128Mb of RAM - is typically reserved for the GPU. - - .. attribute:: storage - - A string indicating the type of bootable storage used with this - revision of Pi, e.g. "SD", "MicroSD", or "eMMC" (for the Compute - Module). - - .. attribute:: usb - - An integer indicating how many USB ports are physically present on - this revision of the Pi. - - .. note:: - - This does *not* include the micro-USB port used to power the Pi. - - .. attribute:: ethernet - - An integer indicating how many Ethernet ports are physically present - on this revision of the Pi. - - .. attribute:: wifi - - A bool indicating whether this revision of the Pi has wifi built-in. - - .. attribute:: bluetooth - - A bool indicating whether this revision of the Pi has bluetooth - built-in. - - .. attribute:: csi - - An integer indicating the number of CSI (camera) ports available on - this revision of the Pi. - - .. attribute:: dsi - - An integer indicating the number of DSI (display) ports available on - this revision of the Pi. - - .. attribute:: headers - - A dictionary which maps header labels to :class:`HeaderInfo` tuples. - For example, to obtain information about header P1 you would query - ``headers['P1']``. To obtain information about pin 12 on header J8 you - would query ``headers['J8'].pins[12]``. - - A rendered version of this data can be obtained by using the - :class:`PiBoardInfo` object in a format string:: - - from gpiozero import * - print('{0:headers}'.format(pi_info())) - - .. attribute:: board - - An ASCII art rendition of the board, primarily intended for console - pretty-print usage. A more usefully rendered version of this data can - be obtained by using the :class:`PiBoardInfo` object in a format - string. For example:: - - from gpiozero import * - print('{0:board}'.format(pi_info())) - - .. _system on a chip: https://en.wikipedia.org/wiki/System_on_a_chip - """ - __slots__ = () # workaround python issue #24931 - - @classmethod - def from_revision(cls, revision): - if revision & 0x800000: - # New-style revision, parse information from bit-pattern: - # - # MSB -----------------------> LSB - # uuuuuuuuFMMMCCCCPPPPTTTTTTTTRRRR - # - # uuuuuuuu - Unused - # F - New flag (1=valid new-style revision, 0=old-style) - # MMM - Memory size (0=256, 1=512, 2=1024) - # CCCC - Manufacturer (0=Sony, 1=Egoman, 2=Embest, 3=Sony Japan, 4=Embest, 5=Stadium) - # PPPP - Processor (0=2835, 1=2836, 2=2837) - # TTTTTTTT - Type (0=A, 1=B, 2=A+, 3=B+, 4=2B, 5=Alpha (??), 6=CM, - # 8=3B, 9=Zero, 10=CM3, 12=Zero W, 13=3B+, 14=3A+) - # RRRR - Revision (0, 1, 2, etc.) - revcode_memory = (revision & 0x700000) >> 20 - revcode_manufacturer = (revision & 0xf0000) >> 16 - revcode_processor = (revision & 0xf000) >> 12 - revcode_type = (revision & 0xff0) >> 4 - revcode_revision = (revision & 0x0f) - try: - model = { - 0: 'A', - 1: 'B', - 2: 'A+', - 3: 'B+', - 4: '2B', - 6: 'CM', - 8: '3B', - 9: 'Zero', - 10: 'CM3', - 12: 'Zero W', - 13: '3B+', - 14: '3A+', - 16: 'CM3+', - }.get(revcode_type, '???') - if model in ('A', 'B'): - pcb_revision = { - 0: '1.0', # is this right? - 1: '1.0', - 2: '2.0', - }.get(revcode_revision, 'Unknown') - else: - pcb_revision = '1.%d' % revcode_revision - soc = { - 0: 'BCM2835', - 1: 'BCM2836', - 2: 'BCM2837', - }.get(revcode_processor, 'Unknown') - manufacturer = { - 0: 'Sony', - 1: 'Egoman', - 2: 'Embest', - 3: 'Sony Japan', - 4: 'Embest', - 5: 'Stadium', - }.get(revcode_manufacturer, 'Unknown') - memory = { - 0: 256, - 1: 512, - 2: 1024, - }.get(revcode_memory, None) - released = { - 'A': '2013Q1', - 'B': '2012Q1' if pcb_revision == '1.0' else '2012Q4', - 'A+': '2014Q4' if memory == 512 else '2016Q3', - 'B+': '2014Q3', - '2B': '2015Q1' if pcb_revision in ('1.0', '1.1') else '2016Q3', - 'CM': '2014Q2', - '3B': '2016Q1' if manufacturer in ('Sony', 'Embest') else '2016Q4', - 'Zero': '2015Q4' if pcb_revision == '1.2' else '2016Q2', - 'CM3': '2017Q1', - 'Zero W': '2017Q1', - '3B+': '2018Q1', - '3A+': '2018Q4', - 'CM3+': '2019Q1', - }.get(model, 'Unknown') - storage = { - 'A': 'SD', - 'B': 'SD', - 'CM': 'eMMC', - 'CM3': 'eMMC / off-board', - 'CM3+': 'eMMC / off-board', - }.get(model, 'MicroSD') - usb = { - 'A': 1, - 'A+': 1, - 'Zero': 1, - 'Zero W': 1, - 'B': 2, - 'CM': 1, - 'CM3': 1, - '3A+': 1, - 'CM3+': 1, - }.get(model, 4) - ethernet = { - 'A': 0, - 'A+': 0, - 'Zero': 0, - 'Zero W': 0, - 'CM': 0, - 'CM3': 0, - '3A+': 0, - 'CM3+': 0, - }.get(model, 1) - wifi = { - '3B': True, - 'Zero W': True, - '3B+': True, - '3A+': True, - }.get(model, False) - bluetooth = { - '3B': True, - 'Zero W': True, - '3B+': True, - '3A+': True, - }.get(model, False) - csi = { - 'Zero': 0 if pcb_revision == '1.0' else 1, - 'Zero W': 1, - 'CM': 2, - 'CM3': 2, - 'CM3+': 2, - }.get(model, 1) - dsi = { - 'Zero': 0, - 'Zero W': 0, - 'CM': 2, - 'CM3': 2, - 'CM3+': 2, - }.get(model, csi) - headers = { - 'A': {'P1': REV2_P1, 'P5': REV2_P5}, - 'B': {'P1': REV1_P1} if pcb_revision == '1.0' else {'P1': REV2_P1, 'P5': REV2_P5}, - 'CM': {'SODIMM': CM_SODIMM}, - 'CM3': {'SODIMM': CM3_SODIMM}, - 'CM3+': {'SODIMM': CM3_SODIMM}, - }.get(model, {'J8': PLUS_J8}) - board = { - 'A': A_BOARD, - 'B': REV1_BOARD if pcb_revision == '1.0' else REV2_BOARD, - 'A+': APLUS_BOARD, - 'CM': CM_BOARD, - 'CM3': CM_BOARD, - 'CM3+': CM_BOARD, - 'Zero': ZERO12_BOARD if pcb_revision == '1.2' else ZERO13_BOARD, - 'Zero W': ZERO13_BOARD, - '3A+': A3PLUS_BOARD, - '3B+': B3PLUS_BOARD, - }.get(model, BPLUS_BOARD) - except KeyError: - raise PinUnknownPi('unable to parse new-style revision "%x"' % revision) - else: - # Old-style revision, use the lookup table - try: - ( - model, - pcb_revision, - released, - soc, - manufacturer, - memory, - storage, - usb, - ethernet, - wifi, - bluetooth, - csi, - dsi, - headers, - board, - ) = PI_REVISIONS[revision] - except KeyError: - raise PinUnknownPi('unknown old-style revision "%x"' % revision) - headers = { - header: HeaderInfo(name=header, rows=max(header_data) // 2, columns=2, pins={ - number: PinInfo( - number=number, function=function, pull_up=pull_up, - row=row + 1, col=col + 1) - for number, (function, pull_up) in header_data.items() - for row, col in (divmod(number - 1, 2),) - }) - for header, header_data in headers.items() - } - return cls( - '%04x' % revision, - model, - pcb_revision, - released, - soc, - manufacturer, - memory, - storage, - usb, - ethernet, - wifi, - bluetooth, - csi, - dsi, - headers, - board, - ) - - def physical_pins(self, function): - """ - Return the physical pins supporting the specified *function* as tuples - of ``(header, pin_number)`` where *header* is a string specifying the - header containing the *pin_number*. Note that the return value is a - :class:`set` which is not indexable. Use :func:`physical_pin` if you - are expecting a single return value. - - :param str function: - The pin function you wish to search for. Usually this is something - like "GPIO9" for Broadcom GPIO pin 9, or "GND" for all the pins - connecting to electrical ground. - """ - return { - (header, pin.number) - for (header, info) in self.headers.items() - for pin in info.pins.values() - if pin.function == function - } - - def physical_pin(self, function): - """ - Return the physical pin supporting the specified *function*. If no pins - support the desired *function*, this function raises :exc:`PinNoPins`. - If multiple pins support the desired *function*, :exc:`PinMultiplePins` - will be raised (use :func:`physical_pins` if you expect multiple pins - in the result, such as for electrical ground). - - :param str function: - The pin function you wish to search for. Usually this is something - like "GPIO9" for Broadcom GPIO pin 9. - """ - result = self.physical_pins(function) - if len(result) > 1: - raise PinMultiplePins('multiple pins can be used for %s' % function) - elif result: - return result.pop() - else: - raise PinNoPins('no pins can be used for %s' % function) - - def pulled_up(self, function): - """ - Returns a bool indicating whether a physical pull-up is attached to - the pin supporting the specified *function*. Either :exc:`PinNoPins` - or :exc:`PinMultiplePins` may be raised if the function is not - associated with a single pin. - - :param str function: - The pin function you wish to determine pull-up for. Usually this is - something like "GPIO9" for Broadcom GPIO pin 9. - """ - try: - header, number = self.physical_pin(function) - except PinNoPins: - return False - else: - return self.headers[header].pins[number].pull_up - - def to_gpio(self, spec): - """ - Parses a pin *spec*, returning the equivalent Broadcom GPIO port number - or raising a :exc:`ValueError` exception if the spec does not represent - a GPIO port. - - The *spec* may be given in any of the following forms: - - * An integer, which will be accepted as a GPIO number - * 'GPIOn' where n is the GPIO number - * 'WPIn' where n is the `wiringPi`_ pin number - * 'BCMn' where n is the GPIO number (alias of GPIOn) - * 'BOARDn' where n is the physical pin number on the main header - * 'h:n' where h is the header name and n is the physical pin number - (for example J8:5 is physical pin 5 on header J8, which is the main - header on modern Raspberry Pis) - - .. _wiringPi: http://wiringpi.com/pins/ - """ - if isinstance(spec, int): - if not 0 <= spec < 54: - raise PinInvalidPin('invalid GPIO port %d specified ' - '(range 0..53) ' % spec) - return spec - else: - if isinstance(spec, bytes): - spec = spec.decode('ascii') - spec = spec.upper() - if spec.isdigit(): - return self.to_gpio(int(spec)) - if spec.startswith('GPIO') and spec[4:].isdigit(): - return self.to_gpio(int(spec[4:])) - elif spec.startswith('BCM') and spec[3:].isdigit(): - return self.to_gpio(int(spec[3:])) - elif spec.startswith('WPI') and spec[3:].isdigit(): - main_head = 'P1' if 'P1' in self.headers else 'J8' - try: - return self.to_gpio({ - 0: '%s:11' % main_head, - 1: '%s:12' % main_head, - 2: '%s:13' % main_head, - 3: '%s:15' % main_head, - 4: '%s:16' % main_head, - 5: '%s:18' % main_head, - 6: '%s:22' % main_head, - 7: '%s:7' % main_head, - 8: '%s:3' % main_head, - 9: '%s:5' % main_head, - 10: '%s:24' % main_head, - 11: '%s:26' % main_head, - 12: '%s:19' % main_head, - 13: '%s:21' % main_head, - 14: '%s:23' % main_head, - 15: '%s:8' % main_head, - 16: '%s:10' % main_head, - 17: 'P5:3', - 18: 'P5:4', - 19: 'P5:5', - 20: 'P5:6', - 21: '%s:29' % main_head, - 22: '%s:31' % main_head, - 23: '%s:33' % main_head, - 24: '%s:35' % main_head, - 25: '%s:37' % main_head, - 26: '%s:32' % main_head, - 27: '%s:36' % main_head, - 28: '%s:38' % main_head, - 29: '%s:40' % main_head, - 30: '%s:27' % main_head, - 31: '%s:28' % main_head, - }[int(spec[3:])]) - except KeyError: - raise PinInvalidPin('%s is not a valid wiringPi pin' % spec) - elif ':' in spec: - header, pin = spec.split(':', 1) - if pin.isdigit(): - try: - header = self.headers[header] - except KeyError: - raise PinInvalidPin( - 'there is no %s header on this Pi' % header) - try: - function = header.pins[int(pin)].function - except KeyError: - raise PinInvalidPin( - 'no such pin %s on header %s' % (pin, header.name)) - if function.startswith('GPIO') and function[4:].isdigit(): - return self.to_gpio(int(function[4:])) - else: - raise PinInvalidPin('%s is not a GPIO pin' % spec) - elif spec.startswith('BOARD') and spec[5:].isdigit(): - main_head = ({'P1', 'J8', 'SODIMM'} & set(self.headers)).pop() - return self.to_gpio('%s:%s' % (main_head, spec[5:])) - raise PinInvalidPin('%s is not a valid pin spec' % spec) - - def __repr__(self): - return '{cls}({fields})'.format( - cls=self.__class__.__name__, - fields=', '.join( - ( - '{name}=...' if name in ('headers', 'board') else - '{name}={value!r}').format(name=name, value=value) - for name, value in zip(self._fields, self) - ) - ) - - def __format__(self, format_spec): - style, content = Style.from_style_content(format_spec) - if content == 'full': - return dedent("""\ - {self:{style} board} - - {self:{style} specs} - - {self:{style} headers}""" - ).format(self=self, style=style) - elif content == 'board': - kw = self._asdict() - kw.update({ - name: header - for name, header in self.headers.items() - }) - return self.board.format(style=style, **kw) - elif content == 'specs': - return dedent("""\ - {style:bold}Revision {style:reset}: {revision} - {style:bold}SoC {style:reset}: {soc} - {style:bold}RAM {style:reset}: {memory}Mb - {style:bold}Storage {style:reset}: {storage} - {style:bold}USB ports {style:reset}: {usb} {style:yellow}(excluding power){style:reset} - {style:bold}Ethernet ports {style:reset}: {ethernet} - {style:bold}Wi-fi {style:reset}: {wifi} - {style:bold}Bluetooth {style:reset}: {bluetooth} - {style:bold}Camera ports (CSI) {style:reset}: {csi} - {style:bold}Display ports (DSI){style:reset}: {dsi}""" - ).format(style=style, **self._asdict()) - elif content == 'headers': - return '\n\n'.join( - dedent("""\ - {style:bold}{header.name}{style:reset}: - {header:{style} full}""" - ).format(header=header, style=style) - for header in sorted(self.headers.values(), key=attrgetter('name')) - ) - - def pprint(self, color=None): - """ - Pretty-print a representation of the board along with header diagrams. - - If *color* is :data:`None` (the default), the diagram will include ANSI - color codes if stdout is a color-capable terminal. Otherwise *color* - can be set to :data:`True` or :data:`False` to force color or monochrome - output. - """ - print('{0:{style} full}'.format(self, style=Style(color))) - - -def pi_info(revision=None): - """ - Returns a :class:`PiBoardInfo` instance containing information about a - *revision* of the Raspberry Pi. - - :param str revision: - The revision of the Pi to return information about. If this is omitted - or :data:`None` (the default), then the library will attempt to determine - the model of Pi it is running on and return information about that. - """ - if revision is None: - if Device.pin_factory is None: - Device.pin_factory = Device._default_pin_factory() - result = Device.pin_factory.pi_info - if result is None: - raise PinUnknownPi('The default pin_factory is not attached to a Pi') - else: - return result - else: - if isinstance(revision, bytes): - revision = revision.decode('ascii') - if isinstance(revision, str): - revision = int(revision, base=16) - else: - # be nice to people passing an int (or something numeric anyway) - revision = int(revision) - return PiBoardInfo.from_revision(revision) +SPI_HARDWARE_PINS = { + 0: { + 'clock': 'GPIO11', + 'mosi': 'GPIO10', + 'miso': 'GPIO9', + 'select': ('GPIO8', 'GPIO7'), + }, +} diff --git a/gpiozero/pins/lgpio.py b/gpiozero/pins/lgpio.py new file mode 100644 index 000000000..9fa873942 --- /dev/null +++ b/gpiozero/pins/lgpio.py @@ -0,0 +1,365 @@ +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2021-2023 Dave Jones +# +# SPDX-License-Identifier: BSD-3-Clause + +import os + +import lgpio + +from . import SPI +from .pi import spi_port_device +from .local import LocalPiFactory, LocalPiPin +from ..mixins import SharedMixin +from ..exc import ( + PinInvalidFunction, + PinSetInput, + PinFixedPull, + PinInvalidPull, + PinInvalidBounce, + PinInvalidState, + SPIBadArgs, + SPIInvalidClockMode, + PinPWMFixedValue, + DeviceClosed +) + +try: + # Patch several constants which changed incompatibly between lg 0.1.6.0 + # and 0.2.0.0 + lgpio.SET_PULL_NONE +except AttributeError: + lgpio.SET_PULL_NONE = lgpio.SET_BIAS_DISABLE + lgpio.SET_PULL_UP = lgpio.SET_BIAS_PULL_UP + lgpio.SET_PULL_DOWN = lgpio.SET_BIAS_PULL_DOWN + + +class LGPIOFactory(LocalPiFactory): + """ + Extends :class:`~gpiozero.pins.local.LocalPiFactory`. Uses the `lgpio`_ + library to interface to the local computer's GPIO pins. The lgpio library + simply talks to Linux gpiochip devices; it is not specific to the Raspberry + Pi although this class is currently constructed under the assumption that + it is running on a Raspberry Pi. + + You can construct lgpio pins manually like so:: + + from gpiozero.pins.lgpio import LGPIOFactory + from gpiozero import LED + + factory = LGPIOFactory(chip=0) + led = LED(12, pin_factory=factory) + + The *chip* parameter to the factory constructor specifies which gpiochip + device to attempt to open. It defaults to 0 and thus doesn't normally need + to be specified (the example above only includes it for completeness). + + The lgpio library relies on access to the :file:`/dev/gpiochip*` devices. + If you run into issues, please check that your user has read/write access + to the specific gpiochip device you are attempting to open (0 by default). + + .. _lgpio: http://abyz.me.uk/lg/py_lgpio.html + """ + def __init__(self, chip=None): + super().__init__() + chip = 4 if (self._get_revision() & 0xff0) >> 4 == 0x17 else 0 + self._handle = lgpio.gpiochip_open(chip) + self._chip = chip + self.pin_class = LGPIOPin + + def close(self): + super().close() + if self._handle is not None: + lgpio.gpiochip_close(self._handle) + self._handle = None + + @property + def chip(self): + return self._chip + + def _get_spi_class(self, shared, hardware): + # support via lgpio instead of spidev + if hardware: + return [LGPIOHardwareSPI, LGPIOHardwareSPIShared][shared] + return super()._get_spi_class(shared, hardware=False) + + +class LGPIOPin(LocalPiPin): + """ + Extends :class:`~gpiozero.pins.local.LocalPiPin`. Pin implementation for + the `lgpio`_ library. See :class:`LGPIOFactory` for more information. + + .. _lgpio: http://abyz.me.uk/lg/py_lgpio.html + """ + GPIO_IS_KERNEL = 1 << 0 + GPIO_IS_OUT = 1 << 1 + GPIO_IS_ACTIVE_LOW = 1 << 2 + GPIO_IS_OPEN_DRAIN = 1 << 3 + GPIO_IS_OPEN_SOURCE = 1 << 4 + GPIO_IS_BIAS_PULL_UP = 1 << 5 + GPIO_IS_BIAS_PULL_DOWN = 1 << 6 + GPIO_IS_BIAS_DISABLE = 1 << 7 + GPIO_IS_LG_INPUT = 1 << 8 + GPIO_IS_LG_OUTPUT = 1 << 9 + GPIO_IS_LG_ALERT = 1 << 10 + GPIO_IS_LG_GROUP = 1 << 11 + + GPIO_LINE_FLAGS_MASK = ( + GPIO_IS_ACTIVE_LOW | GPIO_IS_OPEN_DRAIN | GPIO_IS_OPEN_SOURCE | + GPIO_IS_BIAS_PULL_UP | GPIO_IS_BIAS_PULL_DOWN | GPIO_IS_BIAS_DISABLE) + + GPIO_EDGES = { + 'both': lgpio.BOTH_EDGES, + 'rising': lgpio.RISING_EDGE, + 'falling': lgpio.FALLING_EDGE, + } + + GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} + + def __init__(self, factory, info): + super().__init__(factory, info) + self._pwm = None + self._bounce = None + self._callback = None + self._edges = lgpio.BOTH_EDGES + lgpio.gpio_claim_input( + self.factory._handle, self._number, lgpio.SET_PULL_NONE) + + def close(self): + if self.factory._handle is not None: + # Closing is really just "resetting" the function of the pin; + # we let the factory close deal with actually freeing stuff + lgpio.gpio_claim_input( + self.factory._handle, self._number, lgpio.SET_PULL_NONE) + + def _get_function(self): + mode = lgpio.gpio_get_mode(self.factory._handle, self._number) + return ['input', 'output'][bool(mode & self.GPIO_IS_OUT)] + + def _set_function(self, value): + if self._callback is not None: + self._callback.cancel() + self._callback = None + try: + { + 'input': lgpio.gpio_claim_input, + 'output': lgpio.gpio_claim_output, + }[value](self.factory._handle, self._number) + except KeyError: + raise PinInvalidFunction( + f'invalid function "{value}" for pin {self!r}') + + def _get_state(self): + if self._pwm: + return self._pwm[1] / 100 + else: + return bool(lgpio.gpio_read(self.factory._handle, self._number)) + + def _set_state(self, value): + if self._pwm: + freq, duty = self._pwm + self._pwm = (freq, int(value * 100)) + try: + lgpio.tx_pwm(self.factory._handle, self._number, *self._pwm) + except lgpio.error: + raise PinInvalidState( + f'invalid state "{value}" for pin {self!r}') + elif self.function == 'input': + raise PinSetInput(f'cannot set state of pin {self!r}') + else: + lgpio.gpio_write(self.factory._handle, self._number, bool(value)) + + def _get_pull(self): + mode = lgpio.gpio_get_mode(self.factory._handle, self._number) + if mode & self.GPIO_IS_BIAS_PULL_UP: + return 'up' + elif mode & self.GPIO_IS_BIAS_PULL_DOWN: + return 'down' + else: + return 'floating' + + def _set_pull(self, value): + if self.function != 'input': + raise PinFixedPull(f'cannot set pull on non-input pin {self!r}') + if self.info.pull not in (value, ''): + raise PinFixedPull( + f'{self!r} has a physical pull-{self.info.pull} resistor') + try: + flags = { + 'up': lgpio.SET_PULL_UP, + 'down': lgpio.SET_PULL_DOWN, + 'floating': lgpio.SET_PULL_NONE, + }[value] + except KeyError: + raise PinInvalidPull(f'invalid pull "{value}" for pin {self!r}') + else: + # Simply calling gpio_claim_input is insufficient to change the + # line flags on a pin; it needs to be freed and re-claimed + lgpio.gpio_free(self.factory._handle, self._number) + lgpio.gpio_claim_input(self.factory._handle, self._number, flags) + + def _get_frequency(self): + if self._pwm: + freq, duty = self._pwm + return freq + else: + return None + + def _set_frequency(self, value): + if not self._pwm and value is not None and value > 0: + if self.function != 'output': + raise PinPWMFixedValue(f'cannot start PWM on pin {self!r}') + lgpio.tx_pwm(self.factory._handle, self._number, value, 0) + self._pwm = (value, 0) + elif self._pwm and value is not None and value > 0: + freq, duty = self._pwm + lgpio.tx_pwm(self.factory._handle, self._number, value, duty) + self._pwm = (value, duty) + elif self._pwm and (value is None or value == 0): + lgpio.tx_pwm(self.factory._handle, self._number, 0, 0) + self._pwm = None + + def _get_bounce(self): + return None if not self._bounce else self._bounce / 1000000 + + def _set_bounce(self, value): + if value is None: + value = 0 + elif value < 0: + raise PinInvalidBounce('bounce must be 0 or greater') + value = int(value * 1000000) + lgpio.gpio_set_debounce_micros( + self.factory._handle, self._number, value) + self._bounce = value + + def _get_edges(self): + return self.GPIO_EDGES_NAMES[self._edges] + + def _set_edges(self, value): + f = self.when_changed + self.when_changed = None + try: + self._edges = self.GPIO_EDGES[value] + finally: + self.when_changed = f + + def _call_when_changed(self, chip, gpio, level, ticks): + super()._call_when_changed(ticks / 1000000000, level) + + def _enable_event_detect(self): + lgpio.gpio_claim_alert( + self.factory._handle, self._number, self._edges, + lgpio.gpio_get_mode(self.factory._handle, self._number) & + self.GPIO_LINE_FLAGS_MASK) + self._callback = lgpio.callback( + self.factory._handle, self._number, self._edges, + self._call_when_changed) + + def _disable_event_detect(self): + if self._callback is not None: + self._callback.cancel() + self._callback = None + lgpio.gpio_claim_input( + self.factory._handle, self._number, + lgpio.gpio_get_mode(self.factory._handle, self._number) & + self.GPIO_LINE_FLAGS_MASK) + + +class LGPIOHardwareSPI(SPI): + """ + Hardware SPI implementation for the `lgpio`_ library. Uses the ``spi_*`` + functions from the lgpio API. + + .. _lgpio: http://abyz.me.uk/lg/py_lgpio.html + """ + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): + port, device = spi_port_device( + clock_pin, mosi_pin, miso_pin, select_pin) + self._port = port + self._device = device + self._baud = 500000 + self._spi_flags = 0 + self._handle = None + super().__init__(pin_factory=pin_factory) + to_reserve = {clock_pin, select_pin} + if mosi_pin is not None: + to_reserve.add(mosi_pin) + if miso_pin is not None: + to_reserve.add(miso_pin) + self.pin_factory.reserve_pins(self, *to_reserve) + self._handle = lgpio.spi_open(port, device, self._baud, self._spi_flags) + + def _conflicts_with(self, other): + return not ( + isinstance(other, LGPIOHardwareSPI) and + (self._port, self._device) != (other._port, other._device) + ) + + def close(self): + if not self.closed: + lgpio.spi_close(self._handle) + self._handle = None + self.pin_factory.release_all(self) + super().close() + + @property + def closed(self): + return self._handle is None + + def __repr__(self): + try: + self._check_open() + return f'SPI(port={self._port:d}, device={self._device:d})' + except DeviceClosed: + return 'SPI(closed)' + + def _get_clock_mode(self): + return self._spi_flags + + def _set_clock_mode(self, value): + self._check_open() + if not 0 <= value < 4: + raise SPIInvalidClockMode(f"{value} is not a valid SPI clock mode") + lgpio.spi_close(self._handle) + self._spi_flags = value + self._handle = lgpio.spi_open( + self._port, self._device, self._baud, self._spi_flags) + + def _get_rate(self): + return self._baud + + def _set_rate(self, value): + self._check_open() + value = int(value) + lgpio.spi_close(self._handle) + self._baud = value + self._handle = lgpio.spi_open( + self._port, self._device, self._baud, self._spi_flags) + + def read(self, n): + self._check_open() + count, data = lgpio.spi_read(self._handle, n) + if count < 0: + raise IOError(f'SPI transfer error {count}') + return [int(b) for b in data] + + def write(self, data): + self._check_open() + count = lgpio.spi_write(self._handle, data) + if count < 0: + raise IOError(f'SPI transfer error {count}') + return len(data) + + def transfer(self, data): + self._check_open() + count, data = lgpio.spi_xfer(self._handle, data) + if count < 0: + raise IOError(f'SPI transfer error {count}') + return [int(b) for b in data] + + +class LGPIOHardwareSPIShared(SharedMixin, LGPIOHardwareSPI): + @classmethod + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): + return (clock_pin, select_pin) diff --git a/gpiozero/pins/local.py b/gpiozero/pins/local.py index 79f3fad88..83f5e9537 100644 --- a/gpiozero/pins/local.py +++ b/gpiozero/pins/local.py @@ -1,52 +1,19 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2018 Martchus -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2016 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -nstr = str -str = type('') +# SPDX-License-Identifier: BSD-3-Clause import io import errno import struct -import warnings from collections import defaultdict from threading import Lock -try: - from time import monotonic -except ImportError: - from time import time as monotonic +from time import monotonic try: from spidev import SpiDev @@ -54,19 +21,41 @@ SpiDev = None from . import SPI -from .pi import PiFactory, PiPin, SPI_HARDWARE_PINS -from .spi import SPISoftwareBus -from ..devices import Device, SharedMixin +from .pi import PiFactory, PiPin, SPI_HARDWARE_PINS, spi_port_device +from .spi import SPISoftware +from ..devices import Device +from ..mixins import SharedMixin from ..output_devices import OutputDevice from ..exc import DeviceClosed, PinUnknownPi, SPIInvalidClockMode +def get_pi_revision(): + revision = None + try: + with io.open('/proc/device-tree/system/linux,revision', 'rb') as f: + revision = hex(struct.unpack('>L', f.read(4))[0])[2:] + except IOError as e: + if e.errno != errno.ENOENT: + raise e + with io.open('/proc/cpuinfo', 'r') as f: + for line in f: + if line.startswith('Revision'): + revision = line.split(':')[1].strip().lower() + if revision is not None: + overvolted = revision.startswith('100') + if overvolted: + revision = revision[-4:] + return int(revision, base=16) + raise PinUnknownPi( + 'unable to locate Pi revision in /proc/device-tree or /proc/cpuinfo') + + class LocalPiFactory(PiFactory): """ Extends :class:`~gpiozero.pins.pi.PiFactory`. Abstract base class representing pins attached locally to a Pi. This forms the base class for local-only pin interfaces (:class:`~gpiozero.pins.rpigpio.RPiGPIOPin`, - :class:`~gpiozero.pins.rpio.RPIOPin`, and + :class:`~gpiozero.pins.lgpio.LGPIOPin`, and :class:`~gpiozero.pins.native.NativePin`). """ pins = {} @@ -74,13 +63,7 @@ class LocalPiFactory(PiFactory): _res_lock = Lock() def __init__(self): - super(LocalPiFactory, self).__init__() - self.spi_classes = { - ('hardware', 'exclusive'): LocalPiHardwareSPI, - ('hardware', 'shared'): LocalPiHardwareSPIShared, - ('software', 'exclusive'): LocalPiSoftwareSPI, - ('software', 'shared'): LocalPiSoftwareSPIShared, - } + super().__init__() # Override the reservations and pins dict to be this class' attributes. # This is a bit of a dirty hack, but ensures that anyone evil enough to # mix pin implementations doesn't try and control the same pin with @@ -90,23 +73,15 @@ def __init__(self): self._res_lock = LocalPiFactory._res_lock def _get_revision(self): - revision = None - try: - with io.open('/proc/device-tree/system/linux,revision', 'rb') as f: - revision = hex(struct.unpack(nstr('>L'), f.read(4))[0])[2:] - except IOError as e: - if e.errno != errno.ENOENT: - raise e - with io.open('/proc/cpuinfo', 'r') as f: - for line in f: - if line.startswith('Revision'): - revision = line.split(':')[1].strip().lower() - if revision is not None: - overvolted = revision.startswith('100') - if overvolted: - revision = revision[-4:] - return int(revision, base=16) - raise PinUnknownPi('unable to locate Pi revision in /proc/device-tree or /proc/cpuinfo') + return get_pi_revision() + + def _get_spi_class(self, shared, hardware): + return { + (False, True): LocalPiHardwareSPI, + (True, True): LocalPiHardwareSPIShared, + (False, False): LocalPiSoftwareSPI, + (True, False): LocalPiSoftwareSPIShared, + }[shared, hardware] @staticmethod def ticks(): @@ -114,12 +89,7 @@ def ticks(): @staticmethod def ticks_diff(later, earlier): - # NOTE: technically the guarantee to always return a positive result - # cannot be maintained in versions where monotonic() is not available - # and we fall back to time(). However, in that situation we've no - # access to a true monotonic source, and no idea how far the clock has - # skipped back so this is the best we can do anyway. - return max(0, later - earlier) + return later - earlier class LocalPiPin(PiPin): @@ -138,41 +108,47 @@ def _call_when_changed(self, ticks=None, state=None): an opaque value that should only be compared with the associated :meth:`Factory.ticks_diff` method. """ - super(LocalPiPin, self)._call_when_changed( + super()._call_when_changed( self._factory.ticks() if ticks is None else ticks, self.state if state is None else state) -class LocalPiHardwareSPI(SPI, Device): - def __init__(self, factory, port, device): - self._port = port - self._device = device - self._interface = None +class LocalPiHardwareSPI(SPI): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): + self._port, self._device = spi_port_device( + clock_pin, mosi_pin, miso_pin, select_pin) + self._bus = None if SpiDev is None: raise ImportError('failed to import spidev') - super(LocalPiHardwareSPI, self).__init__() - pins = SPI_HARDWARE_PINS[port] - self.pin_factory.reserve_pins( - self, - pins['clock'], - pins['mosi'], - pins['miso'], - pins['select'][device] - ) - self._interface = SpiDev() - self._interface.open(port, device) - self._interface.max_speed_hz = 500000 + super().__init__(pin_factory=pin_factory) + to_reserve = {clock_pin, select_pin} + if mosi_pin is not None: + to_reserve.add(mosi_pin) + if miso_pin is not None: + to_reserve.add(miso_pin) + self.pin_factory.reserve_pins(self, *to_reserve) + self._bus = SpiDev() + self._bus.open(self._port, self._device) + self._bus.max_speed_hz = 500000 + + def _conflicts_with(self, other): + if isinstance(other, LocalPiHardwareSPI): + return ( + (self._port == other._port) and + (self._device == other._device)) + else: + return True def close(self): - if self._interface is not None: - self._interface.close() - self._interface = None + if self._bus is not None: + self._bus.close() + self._bus = None self.pin_factory.release_all(self) - super(LocalPiHardwareSPI, self).close() + super().close() @property def closed(self): - return self._interface is None + return self._bus is None def __repr__(self): try: @@ -187,124 +163,52 @@ def transfer(self, data): :attr:`bits_per_word` bits or less) to the SPI interface, and reads an equivalent number of words, returning them as a list of integers. """ - return self._interface.xfer2(data) + return self._bus.xfer2(data) def _get_clock_mode(self): - return self._interface.mode + return self._bus.mode def _set_clock_mode(self, value): - self._interface.mode = value + self._bus.mode = value def _get_lsb_first(self): - return self._interface.lsbfirst + return self._bus.lsbfirst def _set_lsb_first(self, value): - self._interface.lsbfirst = bool(value) + self._bus.lsbfirst = bool(value) def _get_select_high(self): - return self._interface.cshigh + return self._bus.cshigh def _set_select_high(self, value): - self._interface.cshigh = bool(value) + self._bus.cshigh = bool(value) def _get_bits_per_word(self): - return self._interface.bits_per_word + return self._bus.bits_per_word def _set_bits_per_word(self, value): - self._interface.bits_per_word = value - - -class LocalPiSoftwareSPI(SPI, OutputDevice): - def __init__(self, factory, clock_pin, mosi_pin, miso_pin, select_pin): - self._bus = None - super(LocalPiSoftwareSPI, self).__init__(select_pin, active_high=False) - try: - self._clock_phase = False - self._lsb_first = False - self._bits_per_word = 8 - self._bus = SPISoftwareBus(clock_pin, mosi_pin, miso_pin) - except: - self.close() - raise - - def _conflicts_with(self, other): - # XXX Need to refine this - return not ( - isinstance(other, LocalPiSoftwareSPI) and - (self.pin.number != other.pin.number) - ) - - def close(self): - if self._bus is not None: - self._bus.close() - self._bus = None - super(LocalPiSoftwareSPI, self).close() - - @property - def closed(self): - return self._bus is None - - def __repr__(self): - try: - self._check_open() - return 'SPI(clock_pin=%d, mosi_pin=%d, miso_pin=%d, select_pin=%d)' % ( - self._bus.clock.pin.number, - self._bus.mosi.pin.number, - self._bus.miso.pin.number, - self.pin.number) - except DeviceClosed: - return 'SPI(closed)' - - def transfer(self, data): - with self._bus.lock: - self.on() - try: - return self._bus.transfer( - data, self._clock_phase, self._lsb_first, self._bits_per_word) - finally: - self.off() + self._bus.bits_per_word = value - def _get_clock_mode(self): - with self._bus.lock: - return (not self._bus.clock.active_high) << 1 | self._clock_phase + def _get_rate(self): + return self._bus.max_speed_hz - def _set_clock_mode(self, value): - if not (0 <= value < 4): - raise SPIInvalidClockMode("%d is not a valid clock mode" % value) - with self._bus.lock: - self._bus.clock.active_high = not (value & 2) - self._clock_phase = bool(value & 1) + def _set_rate(self, value): + self._bus.max_speed_hz = int(value) - def _get_lsb_first(self): - return self._lsb_first - def _set_lsb_first(self, value): - self._lsb_first = bool(value) - - def _get_bits_per_word(self): - return self._bits_per_word - - def _set_bits_per_word(self, value): - if value < 1: - raise ValueError('bits_per_word must be positive') - self._bits_per_word = int(value) - - def _get_select_high(self): - return self.active_high - - def _set_select_high(self, value): - with self._bus.lock: - self.active_high = value - self.off() +class LocalPiSoftwareSPI(SPISoftware): + pass class LocalPiHardwareSPIShared(SharedMixin, LocalPiHardwareSPI): @classmethod - def _shared_key(cls, factory, port, device): - return (port, device) + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, + pin_factory): + return (clock_pin, select_pin) class LocalPiSoftwareSPIShared(SharedMixin, LocalPiSoftwareSPI): @classmethod - def _shared_key(cls, factory, clock_pin, mosi_pin, miso_pin, select_pin): - return (select_pin,) + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, + pin_factory): + return (clock_pin, select_pin) diff --git a/gpiozero/pins/mock.py b/gpiozero/pins/mock.py index 02a354f0b..52bd517bd 100644 --- a/gpiozero/pins/mock.py +++ b/gpiozero/pins/mock.py @@ -1,76 +1,52 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2024 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2016 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import os from collections import namedtuple -from time import time, sleep +from time import time, sleep, monotonic from threading import Thread, Event +from math import isclose + +# NOTE: Remove try when compatibility moves beyond Python 3.10 try: - from math import isclose + from importlib_metadata import entry_points except ImportError: - from ..compat import isclose - -import pkg_resources + from importlib.metadata import entry_points from ..exc import ( PinPWMUnsupported, PinSetInput, PinFixedPull, + PinInvalidPin, PinInvalidFunction, PinInvalidPull, PinInvalidBounce, ) from ..devices import Device -from .local import LocalPiPin, LocalPiFactory +from ..mixins import SharedMixin +from . import SPI +from .pi import PiPin, PiFactory +from .spi import SPISoftware PinState = namedtuple('PinState', ('timestamp', 'state')) -class MockPin(LocalPiPin): + +class MockPin(PiPin): """ A mock pin used primarily for testing. This class does *not* support PWM. """ - - def __init__(self, factory, number): - super(MockPin, self).__init__(factory, number) + def __init__(self, factory, info): + super().__init__(factory, info) self._function = 'input' - self._pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating' + self._pull = info.pull or 'floating' self._state = self._pull == 'up' self._bounce = None self._edges = 'both' @@ -97,14 +73,14 @@ def _get_state(self): def _set_state(self, value): if self._function == 'input': - raise PinSetInput('cannot set state of pin %r' % self) + raise PinSetInput(f'cannot set state of pin {self!r}') assert self._function == 'output' assert 0 <= value <= 1 self._change_state(bool(value)) def _change_state(self, value): if self._state != value: - t = time() + t = monotonic() self._state = value self.states.append(PinState(t - self._last_change, value)) self._last_change = t @@ -123,9 +99,9 @@ def _get_pull(self): def _set_pull(self, value): if self.function != 'input': - raise PinFixedPull('cannot set pull on non-input pin %r' % self) - if value != 'up' and self.factory.pi_info.pulled_up(repr(self)): - raise PinFixedPull('%r has a physical pull-up resistor' % self) + raise PinFixedPull(f'cannot set pull on non-input pin {self!r}') + if self.info.pull and value != self.info.pull: + raise PinFixedPull(f'{self!r} has a fixed pull resistor') if value not in ('floating', 'up', 'down'): raise PinInvalidPull('pull must be floating, up, or down') self._pull = value @@ -159,6 +135,9 @@ def _disable_event_detect(self): def _enable_event_detect(self): pass + def _call_when_changed(self): + super()._call_when_changed(self._last_change, self._state) + def drive_high(self): assert self._function == 'input' if self._change_state(True): @@ -172,7 +151,7 @@ def drive_low(self): self._call_when_changed() def clear_states(self): - self._last_change = time() + self._last_change = monotonic() self.states = [PinState(0.0, self._state)] def assert_states(self, expected_states): @@ -196,8 +175,8 @@ class MockConnectedPin(MockPin): mock pin. This is used in the "real pins" portion of the test suite to check that one pin can influence another. """ - def __init__(self, factory, number, input_pin=None): - super(MockConnectedPin, self).__init__(factory, number) + def __init__(self, factory, info, input_pin=None): + super().__init__(factory, info) self.input_pin = input_pin def _change_state(self, value): @@ -206,7 +185,7 @@ def _change_state(self, value): self.input_pin.drive_high() else: self.input_pin.drive_low() - return super(MockConnectedPin, self)._change_state(value) + return super()._change_state(value) class MockChargingPin(MockPin): @@ -216,14 +195,14 @@ class MockChargingPin(MockPin): (as if attached to, e.g. a typical circuit using an LDR and a capacitor to time the charging rate). """ - def __init__(self, factory, number, charge_time=0.01): - super(MockChargingPin, self).__init__(factory, number) + def __init__(self, factory, info, charge_time=0.01): + super().__init__(factory, info) self.charge_time = charge_time # dark charging time self._charge_stop = Event() self._charge_thread = None def _set_function(self, value): - super(MockChargingPin, self)._set_function(value) + super()._set_function(value) if value == 'input': if self._charge_thread: self._charge_stop.set() @@ -235,12 +214,14 @@ def _set_function(self, value): if self._charge_thread: self._charge_stop.set() self._charge_thread.join() + else: + assert False def _charge(self): if not self._charge_stop.wait(self.charge_time): try: self.drive_high() - except AssertionError: + except AssertionError: # pragma: no cover # Charging pins are typically flipped between input and output # repeatedly; if another thread has already flipped us to # output ignore the assertion-error resulting from attempting @@ -255,14 +236,14 @@ class MockTriggerPin(MockPin): corresponding pin instance. When this pin is driven high it will trigger the echo pin to drive high for the echo time. """ - def __init__(self, factory, number, echo_pin=None, echo_time=0.04): - super(MockTriggerPin, self).__init__(factory, number) + def __init__(self, factory, info, echo_pin=None, echo_time=0.04): + super().__init__(factory, info) self.echo_pin = echo_pin self.echo_time = echo_time # longest echo time self._echo_thread = None def _set_state(self, value): - super(MockTriggerPin, self)._set_state(value) + super()._set_state(value) if value: if self._echo_thread: self._echo_thread.join() @@ -280,17 +261,17 @@ class MockPWMPin(MockPin): """ This derivative of :class:`MockPin` adds PWM support. """ - def __init__(self, factory, number): - super(MockPWMPin, self).__init__(factory, number) + def __init__(self, factory, info): + super().__init__(factory, info) self._frequency = None def close(self): self.frequency = None - super(MockPWMPin, self).close() + super().close() def _set_state(self, value): if self._function == 'input': - raise PinSetInput('cannot set state of pin %r' % self) + raise PinSetInput(f'cannot set state of pin {self!r}') assert self._function == 'output' assert 0 <= value <= 1 self._change_state(float(value)) @@ -313,13 +294,12 @@ class MockSPIClockPin(MockPin): rather, construct a :class:`MockSPIDevice` with various pin numbers, and this class will be used for the clock pin. """ - def __init__(self, factory, number): - super(MockSPIClockPin, self).__init__(factory, number) - if not hasattr(self, 'spi_devices'): - self.spi_devices = [] + def __init__(self, factory, info): + super().__init__(factory, info) + self.spi_devices = getattr(self, 'spi_devices', []) def _set_state(self, value): - super(MockSPIClockPin, self)._set_state(value) + super()._set_state(value) for dev in self.spi_devices: dev.on_clock() @@ -331,26 +311,40 @@ class MockSPISelectPin(MockPin): tests; rather, construct a :class:`MockSPIDevice` with various pin numbers, and this class will be used for the select pin. """ - def __init__(self, factory, number): - super(MockSPISelectPin, self).__init__(factory, number) - if not hasattr(self, 'spi_device'): - self.spi_device = None + def __init__(self, factory, info): + super().__init__(factory, info) + self.spi_device = getattr(self, 'spi_device', None) def _set_state(self, value): - super(MockSPISelectPin, self)._set_state(value) + super()._set_state(value) if self.spi_device: self.spi_device.on_select() -class MockSPIDevice(object): - def __init__( - self, clock_pin, mosi_pin=None, miso_pin=None, select_pin=None, - clock_polarity=False, clock_phase=False, lsb_first=False, - bits_per_word=8, select_high=False): - self.clock_pin = Device.pin_factory.pin(clock_pin, pin_class=MockSPIClockPin) - self.mosi_pin = None if mosi_pin is None else Device.pin_factory.pin(mosi_pin) - self.miso_pin = None if miso_pin is None else Device.pin_factory.pin(miso_pin) - self.select_pin = None if select_pin is None else Device.pin_factory.pin(select_pin, pin_class=MockSPISelectPin) +class MockSPIDevice: + """ + This class is used to test :class:`SPIDevice` implementations. It can be + used to mock up the slave side of simple SPI devices, e.g. the MCP3xxx + series of ADCs. + + Descendants should override the :meth:`on_start` and/or :meth:`on_bit` + methods to respond to SPI interface events. The :meth:`rx_word` and + :meth:`tx_word` methods can be used facilitate communications within these + methods. Such descendents can then be passed as the *spi_class* parameter + of the :class:`MockFactory` constructor to have instances attached to any + SPI interface requested by an :class:`SPIDevice`. + """ + def __init__(self, clock_pin, mosi_pin=None, miso_pin=None, + select_pin=None, *, clock_polarity=False, clock_phase=False, + lsb_first=False, bits_per_word=8, select_high=False, + pin_factory=None): + if pin_factory is None: + pin_factory = Device.pin_factory + assert isinstance(pin_factory, MockFactory) + self.clock_pin = pin_factory.pin(clock_pin, pin_class=MockSPIClockPin) + self.mosi_pin = None if mosi_pin is None else pin_factory.pin(mosi_pin) + self.miso_pin = None if miso_pin is None else pin_factory.pin(miso_pin) + self.select_pin = None if select_pin is None else pin_factory.pin(select_pin, pin_class=MockSPISelectPin) self.clock_polarity = clock_polarity self.clock_phase = clock_phase self.lsb_first = lsb_first @@ -444,38 +438,40 @@ def tx_word(self, value, bits_per_word=None): self.tx_buf.extend(bits) -class MockFactory(LocalPiFactory): +class MockFactory(PiFactory): """ - Factory for generating mock pins. The *revision* parameter specifies what - revision of Pi the mock factory pretends to be (this affects the result of - the :attr:`~gpiozero.Factory.pi_info` attribute as well as where pull-ups - are assumed to be). The *pin_class* attribute specifies which mock pin - class will be generated by the :meth:`pin` method by default. This can be - changed after construction by modifying the :attr:`pin_class` attribute. + Factory for generating mock pins. + + The *revision* parameter specifies what revision of Pi the mock factory + pretends to be (this affects the result of the :attr:`Factory.board_info` + attribute as well as where pull-ups are assumed to be). + + The *pin_class* attribute specifies which mock pin class will be generated + by the :meth:`pin` method by default. This can be changed after + construction by modifying the :attr:`pin_class` attribute. .. attribute:: pin_class - This attribute stores the :class:`MockPin` class (or descendent) that + This attribute stores the :class:`MockPin` class (or descendant) that will be used when constructing pins with the :meth:`pin` method (if no *pin_class* parameter is used to override it). It defaults on construction to the value of the *pin_class* parameter in the constructor, or :class:`MockPin` if that is unspecified. """ def __init__(self, revision=None, pin_class=None): - super(MockFactory, self).__init__() + super().__init__() if revision is None: revision = os.environ.get('GPIOZERO_MOCK_REVISION', 'a02082') if pin_class is None: pin_class = os.environ.get('GPIOZERO_MOCK_PIN_CLASS', MockPin) - self._revision = revision + self._revision = int(revision, base=16) if isinstance(pin_class, bytes): pin_class = pin_class.decode('ascii') if isinstance(pin_class, str): - dist = pkg_resources.get_distribution('gpiozero') - group = 'gpiozero_mock_pin_classes' - pin_class = pkg_resources.load_entry_point(dist, group, pin_class.lower()) + group = entry_points(group='gpiozero_mock_pin_classes') + pin_class = group[pin_class.lower()].load() if not issubclass(pin_class, MockPin): - raise ValueError('invalid mock pin_class: %r' % pin_class) + raise ValueError(f'invalid mock pin_class: {pin_class!r}') self.pin_class = pin_class def _get_revision(self): @@ -490,24 +486,50 @@ def reset(self): self.pins.clear() self._reservations.clear() - def pin(self, spec, pin_class=None, **kwargs): + def pin(self, name, pin_class=None, **kwargs): """ - The pin method for :class:`MockFactory` additionally takes a *pin_class* - attribute which can be used to override the class' :attr:`pin_class` - attribute. Any additional keyword arguments will be passed along to the - pin constructor (useful with things like :class:`MockConnectedPin` which - expect to be constructed with another pin). + The pin method for :class:`MockFactory` additionally takes a + *pin_class* attribute which can be used to override the class' + :attr:`pin_class` attribute. Any additional keyword arguments will be + passed along to the pin constructor (useful with things like + :class:`MockConnectedPin` which expect to be constructed with another + pin). """ if pin_class is None: pin_class = self.pin_class - n = self.pi_info.to_gpio(spec) - try: - pin = self.pins[n] - except KeyError: - pin = pin_class(self, n, **kwargs) - self.pins[n] = pin - else: - # Ensure the pin class expected supports PWM (or not) - if issubclass(pin_class, MockPWMPin) != isinstance(pin, MockPWMPin): - raise ValueError('pin %d is already in use as a %s' % (n, pin.__class__.__name__)) - return pin + for header, info in self.board_info.find_pin(name): + try: + pin = self.pins[info] + except KeyError: + pin = pin_class(self, info, **kwargs) + self.pins[info] = pin + else: + # Ensure the pin class expected supports PWM (or not) + if issubclass(pin_class, MockPWMPin) != isinstance(pin, MockPWMPin): + raise ValueError( + f'pin {info.name} is already in use as a ' + f'{pin.__class__.__name__}') + return pin + raise PinInvalidPin(f'{name} is not a valid pin name') + + def _get_spi_class(self, shared, hardware): + return MockSPIInterfaceShared if shared else MockSPIInterface + + @staticmethod + def ticks(): + return monotonic() + + @staticmethod + def ticks_diff(later, earlier): + return later - earlier + + +class MockSPIInterface(SPISoftware): + pass + + +class MockSPIInterfaceShared(SharedMixin, MockSPIInterface): + @classmethod + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, + pin_factory): + return (clock_pin, select_pin) diff --git a/gpiozero/pins/native.py b/gpiozero/pins/native.py index f2fd47979..406b54b05 100644 --- a/gpiozero/pins/native.py +++ b/gpiozero/pins/native.py @@ -1,56 +1,24 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2015-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2016-2020 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -nstr = str -str = type('') +# SPDX-License-Identifier: BSD-3-Clause import io import os +import sys import mmap import errno import struct import select -import warnings from time import sleep from threading import Thread, Event, RLock -from collections import Counter -try: - from queue import Queue, Empty -except ImportError: - from Queue import Queue, Empty +from queue import Queue, Empty +from pathlib import Path from .local import LocalPiPin, LocalPiFactory from ..exc import ( @@ -59,16 +27,110 @@ PinInvalidFunction, PinFixedPull, PinSetInput, - ) +) -class GPIOMemory(object): +def dt_resolve_alias(alias, root=Path('/proc/device-tree')): + """ + Returns the full :class:`~pathlib.Path` of a device-tree alias. For + example: + >>> dt_resolve_alias('gpio') + '/proc/device-tree/soc/gpio@7e200000' + >>> dt_resolve_alias('ethernet0', root='/proc/device-tree') + '/proc/device-tree/scb/ethernet@7d580000' + """ + if not isinstance(root, Path): + root = Path(root) + filename = root / 'aliases' / alias + with filename.open('rb') as f: + node, tail = f.read().split(b'\0', 1) + fs_encoding = sys.getfilesystemencoding() + return root / node.decode(fs_encoding).lstrip('/') + +def dt_peripheral_reg(node, root=Path('/proc/device-tree')): + """ + Returns the :class:`range` covering the registers of the specified *node* + of the device-tree, mapped to the CPU's address space. For example: + + >>> reg = dt_peripheral_reg(dt_resolve_alias('gpio')) + >>> f'{reg.start:#x}..{reg.stop:#x}' + '0xfe200000..0xfe2000b4' + >>> hex(dt_peripheral_reg(dt_resolve_alias('ethernet0')).start) + '0xfd580000' + """ + # Returns a tuple of (address-cells, size-cells) for *node* + def _cells(node): + with (node / '#address-cells').open('rb') as f: + address_cells = struct.unpack('>L', f.read())[0] + with (node / '#size-cells').open('rb') as f: + size_cells = struct.unpack('>L', f.read())[0] + return (address_cells, size_cells) + + # Returns a generator function which, given a file-like object *source* + # iteratively decodes it, yielding a tuple of values from it. Each tuple + # contains one integer for each specified *length*, which is the number of + # 32-bit device-tree cells that make up that value. + def _reader(*lengths): + structs = [struct.Struct(f'>{cells}L') for cells in lengths] + offsets = [sum(s.size for s in structs[:i]) + for i in range(len(structs))] + buf_len = sum(s.size for s in structs) + + def fn(source): + while True: + buf = source.read(buf_len) + if not buf: + break + elif len(buf) < buf_len: + raise IOError(f'failed to read {buf_len} bytes') + row = () + for offset, s in zip(offsets, structs): + cells = s.unpack_from(buf, offset) + value = 0 + for cell in cells: + value = (value << 32) | cell + row += (value,) + yield row + return fn + + # Returns a list of (child-range, parent-range) tuples for *node* + def _ranges(node): + child_cells, size_cells = _cells(node) + parent_cells, _ = _cells(node.parent) + ranges_reader = _reader(child_cells, parent_cells, size_cells) + with (node / 'ranges').open('rb') as f: + return [ + (range(child_base, child_base + size), + range(parent_base, parent_base + size)) + for child_base, parent_base, size in ranges_reader(f) + ] + + if not isinstance(root, Path): + root = Path(root) + node = root / node + child_cells, size_cells = _cells(node.parent) + reg_reader = _reader(child_cells, size_cells) + with (node / 'reg').open('rb') as f: + base, size = list(reg_reader(f))[0] + while node.parent != root: + # Iterate up the hierarchy, resolving the base address as we go + if (node.parent / 'ranges').exists(): + for child_range, parent_range in _ranges(node.parent): + if base in child_range: + base += parent_range.start - child_range.start + break + node = node.parent + return range(base, base + size) + + +class GPIOMemory: GPIO_BASE_OFFSET = 0x200000 PERI_BASE_OFFSET = { 'BCM2835': 0x20000000, 'BCM2836': 0x3f000000, 'BCM2837': 0x3f000000, + 'BCM2711': 0xfe000000, } # From BCM2835 data-sheet, p.91 @@ -85,6 +147,8 @@ class GPIOMemory(object): GPAFEN_OFFSET = 0x88 >> 2 GPPUD_OFFSET = 0x94 >> 2 GPPUDCLK_OFFSET = 0x98 >> 2 + # pull-control registers for BCM2711 + GPPUPPDN_OFFSET = 0xe4 >> 2 def __init__(self, soc): try: @@ -97,37 +161,44 @@ def __init__(self, soc): 'unable to open /dev/gpiomem or /dev/mem; ' 'upgrade your kernel or run as root') else: - offset = self.peripheral_base(soc) + self.GPIO_BASE_OFFSET + offset = self.gpio_base(soc) else: offset = 0 self.mem = mmap.mmap(self.fd, 4096, offset=offset) + # Register reads and writes must be in native format (otherwise + # struct resorts to individual byte reads/writes and you can't hit + # half a register :). For arm64 compat we have to figure out what the + # native unsigned 32-bit type is... + try: + self.reg_fmt = { + struct.calcsize(fmt): fmt + for fmt in ('@I', '@L') + }[4] + except KeyError: + raise RuntimeError('unable to find native unsigned 32-bit type') def close(self): self.mem.close() os.close(self.fd) - def peripheral_base(self, soc): + def gpio_base(self, soc): try: - with io.open('/proc/device-tree/soc/ranges', 'rb') as f: - f.seek(4) - # This is deliberately a big-endian read - return struct.unpack(nstr('>L'), f.read(4))[0] + return dt_peripheral_reg(dt_resolve_alias('gpio')).start except IOError: try: - return self.PERI_BASE_OFFSET[soc] + return self.PERI_BASE_OFFSET[soc] + self.GPIO_BASE_OFFSET except KeyError: pass - raise IOError('unable to determine peripheral base') + raise IOError('unable to determine gpio base') def __getitem__(self, index): - return struct.unpack_from(nstr('> self._func_shift) & 7] @@ -407,43 +473,30 @@ def _set_function(self, value): try: value = self.GPIO_FUNCTIONS[value] except KeyError: - raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) + raise PinInvalidFunction( + f'invalid function "{value}" for pin {self!r}') self.factory.mem[self._func_offset] = ( self.factory.mem[self._func_offset] & ~(7 << self._func_shift) | (value << self._func_shift) - ) + ) def _get_state(self): return bool(self.factory.mem[self._level_offset] & (1 << self._level_shift)) def _set_state(self, value): if self.function == 'input': - raise PinSetInput('cannot set state of pin %r' % self) + raise PinSetInput(f'cannot set state of pin {self!r}') if value: self.factory.mem[self._set_offset] = 1 << self._set_shift else: self.factory.mem[self._clear_offset] = 1 << self._clear_shift def _get_pull(self): - return self.GPIO_PULL_UP_NAMES[self._pull] + raise NotImplementedError def _set_pull(self, value): - if self.function != 'input': - raise PinFixedPull('cannot set pull on non-input pin %r' % self) - if value != 'up' and self.factory.pi_info.pulled_up(repr(self)): - raise PinFixedPull('%r has a physical pull-up resistor' % self) - try: - value = self.GPIO_PULL_UPS[value] - except KeyError: - raise PinInvalidPull('invalid pull direction "%s" for pin %r' % (value, self)) - self.factory.mem[self.factory.mem.GPPUD_OFFSET] = value - sleep(0.000000214) - self.factory.mem[self._pull_offset] = 1 << self._pull_shift - sleep(0.000000214) - self.factory.mem[self.factory.mem.GPPUD_OFFSET] = 0 - self.factory.mem[self._pull_offset] = 0 - self._pull = value + raise NotImplementedError def _get_bounce(self): return self._bounce @@ -453,7 +506,7 @@ def _set_bounce(self, value): def _get_edges(self): try: - with io.open(self.factory.fs.path_edge(self.number), 'r') as f: + with io.open(self.factory.fs.path_edge(self._number), 'r') as f: return f.read().strip() except IOError as e: if e.errno == errno.ENOENT: @@ -463,21 +516,104 @@ def _get_edges(self): def _set_edges(self, value): if value != 'none': - self.factory.fs.export(self.number) + self.factory.fs.export(self._number) try: - with io.open(self.factory.fs.path_edge(self.number), 'w') as f: + with io.open(self.factory.fs.path_edge(self._number), 'w') as f: f.write(value) except IOError as e: if e.errno == errno.ENOENT and value == 'none': pass elif e.errno == errno.EINVAL: - raise PinInvalidEdges('invalid edge specification "%s" for pin %r' % self) + raise PinInvalidEdges( + f'invalid edge specification "{value}" for pin {self!r}') else: raise def _enable_event_detect(self): - self.factory.fs.watch(self.number) + self.factory.fs.watch(self._number) self._last_call = None def _disable_event_detect(self): - self.factory.fs.unwatch(self.number) + self.factory.fs.unwatch(self._number) + + +class Native2835Pin(NativePin): + """ + Extends :class:`NativePin` for Pi hardware prior to the Pi 4 (Pi 0, 1, 2, + 3, and 3+). + """ + GPIO_PULL_UPS = { + 'up': 0b10, + 'down': 0b01, + 'floating': 0b00, + } + + GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} + + def _reg_init(self, factory, number): + super()._reg_init(factory, number) + self._pull_offset = self.factory.mem.GPPUDCLK_OFFSET + (number // 32) + self._pull_shift = number % 32 + self._pull = 'floating' + + def _get_pull(self): + return self.GPIO_PULL_UP_NAMES[self._pull] + + def _set_pull(self, value): + if self.function != 'input': + raise PinFixedPull(f'cannot set pull on non-input pin {self!r}') + if self.info.pull not in (value, ''): + raise PinFixedPull( + f'{self!r} has a physical pull-{self.info.pull} resistor') + try: + value = self.GPIO_PULL_UPS[value] + except KeyError: + raise PinInvalidPull( + f'invalid pull direction "{value}" for pin {self!r}') + self.factory.mem[self.factory.mem.GPPUD_OFFSET] = value + sleep(0.000000214) + self.factory.mem[self._pull_offset] = 1 << self._pull_shift + sleep(0.000000214) + self.factory.mem[self.factory.mem.GPPUD_OFFSET] = 0 + self.factory.mem[self._pull_offset] = 0 + self._pull = value + + +class Native2711Pin(NativePin): + """ + Extends :class:`NativePin` for Pi 4 hardware (Pi 4, CM4, Pi 400 at the time + of writing). + """ + GPIO_PULL_UPS = { + 'up': 0b01, + 'down': 0b10, + 'floating': 0b00, + } + + GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} + + def _reg_init(self, factory, number): + super()._reg_init(factory, number) + self._pull_offset = self.factory.mem.GPPUPPDN_OFFSET + (number // 16) + self._pull_shift = (number % 16) * 2 + + def _get_pull(self): + pull = (self.factory.mem[self._pull_offset] >> self._pull_shift) & 3 + return self.GPIO_PULL_UP_NAMES[pull] + + def _set_pull(self, value): + if self.function != 'input': + raise PinFixedPull(f'cannot set pull on non-input pin {self!r}') + if self.info.pull not in (value, ''): + raise PinFixedPull( + f'{self!r} has a physical pull-{self.info.pull} resistor') + try: + value = self.GPIO_PULL_UPS[value] + except KeyError: + raise PinInvalidPull( + f'invalid pull direction "{value}" for pin {self!r}') + self.factory.mem[self._pull_offset] = ( + self.factory.mem[self._pull_offset] + & ~(3 << self._pull_shift) + | (value << self._pull_shift) + ) diff --git a/gpiozero/pins/pi.py b/gpiozero/pins/pi.py index 6514ad58c..5209b844e 100644 --- a/gpiozero/pins/pi.py +++ b/gpiozero/pins/pi.py @@ -1,49 +1,16 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - -import io -from threading import RLock, Lock -from types import MethodType -from collections import defaultdict -try: - from weakref import ref, WeakMethod -except ImportError: +# SPDX-License-Identifier: BSD-3-Clause - from ..compat import WeakMethod +import re +from threading import RLock +from types import MethodType +from weakref import ref, WeakMethod import warnings try: @@ -51,25 +18,408 @@ except ImportError: SpiDev = None -from . import Factory, Pin -from .data import pi_info +from . import Factory, Pin, BoardInfo, HeaderInfo, PinInfo, data +from .data import SPI_HARDWARE_PINS +from ..compat import frozendict +from ..devices import Device from ..exc import ( + GPIOPinInUse, + PinInvalidPin, PinNoPins, PinNonPhysical, - PinInvalidPin, + PinUnknownPi, SPIBadArgs, SPISoftwareFallback, - ) +) -SPI_HARDWARE_PINS = { - 0: { - 'clock': 11, - 'mosi': 10, - 'miso': 9, - 'select': (8, 7), - }, -} +def spi_port_device(clock_pin, mosi_pin, miso_pin, select_pin): + """ + Convert a mapping of pin definitions, which must contain 'clock_pin', and + 'select_pin' at a minimum, to a hardware SPI port, device tuple. Raises + :exc:`~gpiozero.SPIBadArgs` if the pins do not represent a valid hardware + SPI device. + """ + for port, pins in SPI_HARDWARE_PINS.items(): + if all(( + clock_pin == pins['clock'], + mosi_pin in (None, pins['mosi']), + miso_pin in (None, pins['miso']), + select_pin in pins['select'], + )): + device = pins['select'].index(select_pin) + return (port, device) + raise SPIBadArgs('invalid pin selection for hardware SPI') + + +class PiBoardInfo(BoardInfo): + __slots__ = () # workaround python issue #24931 + + @classmethod + def from_revision(cls, revision): + """ + Construct a :class:`PiBoardInfo` instance from the specified Raspberry + Pi board *revision* which must be specified as an :class:`int` + (typically in hexi-decimal format). + + For example, from an old-style revision code for the model B+:: + + >>> from gpiozero.pins.pi import PiBoardInfo + >>> PiBoardInfo.from_revision(0x0010) + PiBoardInfo(revision='0010', model='B+', pcb_revision='1.2', + released='2014Q3', soc='BCM2835', manufacturer='Sony', memory=512, + storage='MicroSD', usb=4, usb3=0, ethernet=1, eth_speed=100, + wifi=False, bluetooth=False, csi=1, dsi=1, headers=..., board=...) + + Or from a new-style revision code for the Pi Zero 2W:: + + >>> PiBoardInfo.from_revision(0x902120) + PiBoardInfo(revision='902120', model='Zero2W', pcb_revision='1.0', + released='2021Q4', soc='BCM2837', manufacturer='Sony', memory=512, + storage='MicroSD', usb=1, usb3=0, ethernet=0, eth_speed=0, + wifi=True, bluetooth=True, csi=1, dsi=0, headers=..., board=...) + """ + if revision & 0x800000: + # New-style revision, parse information from bit-pattern: + # + # MSB -----------------------> LSB + # NOQuuuWuFMMMCCCCPPPPTTTTTTTTRRRR + # + # N - Overvoltage (0=allowed, 1=disallowed) + # O - OTP programming (0=allowed, 1=disallowed) + # Q - OTP read (0=allowed, 1=disallowed) + # u - Unused + # W - Warranty bit (0=intact, 1=voided by overclocking) + # F - New flag (1=valid new-style revision, 0=old-style) + # MMM - Memory size (see memory dict below) + # CCCC - Manufacturer (see manufacturer dict below) + # PPPP - Processor (see soc dict below) + # TTTTTTTT - Type (see model dict below) + # RRRR - Revision (0, 1, 2, etc.) + revcode_memory = (revision & 0x700000) >> 20 + revcode_manufacturer = (revision & 0xf0000) >> 16 + revcode_processor = (revision & 0xf000) >> 12 + revcode_type = (revision & 0xff0) >> 4 + revcode_revision = (revision & 0x0f) + model = { + 0x0: 'A', + 0x1: 'B', + 0x2: 'A+', + 0x3: 'B+', + 0x4: '2B', + 0x6: 'CM', + 0x8: '3B', + 0x9: 'Zero', + 0xa: 'CM3', + 0xc: 'Zero W', + 0xd: '3B+', + 0xe: '3A+', + 0x10: 'CM3+', + 0x11: '4B', + 0x12: 'Zero2W', + 0x13: '400', + 0x14: 'CM4', + 0x15: 'CM4S', + 0x17: '5B', + 0x18: 'CM5', + 0x19: '500', + 0x1a: 'CM5Lite', + }.get(revcode_type, '???') + if model in ('A', 'B'): + pcb_revision = { + 0: '1.0', # is this right? + 1: '1.0', + 2: '2.0', + }.get(revcode_revision, 'Unknown') + else: + pcb_revision = f'1.{revcode_revision}' + soc = { + 0: 'BCM2835', + 1: 'BCM2836', + 2: 'BCM2837', + 3: 'BCM2711', + 4: 'BCM2712', + }.get(revcode_processor, 'Unknown') + manufacturer = { + 0: 'Sony', + 1: 'Egoman', + 2: 'Embest', + 3: 'Sony Japan', + 4: 'Embest', + 5: 'Stadium', + }.get(revcode_manufacturer, 'Unknown') + memory = { + 0: 256, + 1: 512, + 2: 1024, + 3: 2048, + 4: 4096, + 5: 8192, + 6: 16384, + }.get(revcode_memory, None) + released = { + 'A': '2013Q1', + 'B': '2012Q1' if pcb_revision == '1.0' else '2012Q4', + 'A+': '2014Q4' if memory == 512 else '2016Q3', + 'B+': '2014Q3', + '2B': '2015Q1' if pcb_revision in ('1.0', '1.1') else '2016Q3', + 'CM': '2014Q2', + '3B': '2016Q1' if manufacturer in ('Sony', 'Embest') else '2016Q4', + 'Zero': '2015Q4' if pcb_revision == '1.2' else '2016Q2', + 'CM3': '2017Q1', + 'Zero W': '2017Q1', + '3B+': '2018Q1', + '3A+': '2018Q4', + 'CM3+': '2019Q1', + '4B': '2020Q2' if memory == 8192 else '2019Q2', + 'CM4': '2020Q4', + 'CM4S': '2022Q1', + '400': '2020Q4', + 'Zero2W': '2021Q4', + '5B': '2025Q1' if memory == 16384 else '2023Q4', + 'CM5': '2024Q4', + 'CM5Lite': '2024Q4', + '500': '2024Q4', + }.get(model, 'Unknown') + storage = { + 'A': 'SD', + 'B': 'SD', + 'CM': 'eMMC', + 'CM3': 'eMMC / off-board', + 'CM3+': 'eMMC / off-board', + 'CM4': 'eMMC / off-board', + 'CM4S': 'eMMC / off-board', + 'CM5': 'eMMC', + 'CM5Lite': 'off-board', + }.get(model, 'MicroSD') + usb = { + 'A': 1, + 'A+': 1, + 'Zero': 1, + 'Zero W': 1, + 'Zero2W': 1, + 'B': 2, + 'CM': 1, + 'CM3': 1, + '3A+': 1, + 'CM3+': 1, + 'CM4': 2, + 'CM4S': 1, + '400': 3, + 'CM5': 2, + 'CM5Lite': 2, + '500': 3, + }.get(model, 4) + usb3 = { + '4B': 2, + '400': 2, + '5B': 2, + 'CM5': 2, + 'CM5Lite': 2, + '500': 2, + }.get(model, 0) + ethernet = { + 'A': 0, + 'A+': 0, + 'Zero': 0, + 'Zero W': 0, + 'Zero2W': 0, + 'CM': 0, + 'CM3': 0, + '3A+': 0, + 'CM3+': 0, + 'CM4S': 0, + }.get(model, 1) + eth_speed = { + 'B': 100, + 'B+': 100, + '2B': 100, + '3B': 100, + '3B+': 300, + '4B': 1000, + '400': 1000, + 'CM4': 1000, + '5B': 1000, + 'CM5': 1000, + 'CM5Lite': 1000, + '500': 1000, + }.get(model, 0) + bluetooth = wifi = { + '3B': True, + 'Zero W': True, + 'Zero2W': True, + '3B+': True, + '3A+': True, + '4B': True, + '400': True, + 'CM4': True, + '5B': True, + 'CM5': True, + 'CM5Lite': True, + '500': True, + }.get(model, False) + csi = { + 'Zero': 0 if pcb_revision == '1.2' else 1, + 'CM': 2, + 'CM3': 2, + 'CM3+': 2, + '400': 0, + 'CM4': 2, + 'CM4S': 2, + '5B': 2, + 'CM5': 2, + 'CM5Lite': 2, + '500': 0, + }.get(model, 1) + dsi = { + 'Zero': 0, + 'Zero W': 0, + 'Zero2W': 0, + }.get(model, csi) + headers = { + 'A': {'P1': data.REV2_P1, 'P5': data.REV2_P5, 'P6': data.REV2_P6, 'P2': data.PI1_P2, 'P3': data.PI1_P3}, + 'B': {'P1': data.REV1_P1, 'P2': data.PI1_P2, 'P3': data.PI1_P3} if pcb_revision == '1.0' else + {'P1': data.REV2_P1, 'P5': data.REV2_P5, 'P6': data.REV2_P6, 'P2': data.PI1_P2, 'P3': data.PI1_P3}, + 'CM': {'SODIMM': data.CM_SODIMM}, + 'CM3': {'SODIMM': data.CM3_SODIMM}, + 'CM3+': {'SODIMM': data.CM3_SODIMM}, + 'Zero': {'J8': data.PLUS_J8, 'RUN': data.ZERO_RUN, 'TV': data.ZERO_TV}, + 'Zero W': {'J8': data.PLUS_J8, 'RUN': data.ZERO_RUN, 'TV': data.ZERO_TV}, + '3A+': {'J8': data.PLUS_J8, 'RUN': data.PLUS_RUN}, + '3B+': {'J8': data.PLUS_J8, 'RUN': data.PLUS_RUN, 'POE': data.PLUS_POE}, + '4B': {'J8': data.PI4_J8, 'J2': data.PI4_J2, 'J14': data.PI4_J14}, + 'Zero2W': {'J8': data.PLUS_J8}, + '400': {'J8': data.PI4_J8}, + 'CM4': {'J8': data.PI4_J8, 'J1': data.CM4_J1, 'J2': data.CM4_J2, 'J3': data.CM4_J3, 'J6': data.CM4_J6, 'J9': data.CM4_J9}, + 'CM4S': {'SODIMM': data.CM3_SODIMM}, + '5B': {'J8': data.PI5_J8, 'J2': data.PI5_J2, 'J7': data.PI5_J7, 'J14': data.PI4_J14}, + 'CM5': {'J8': data.PI5_J8, 'J1': data.CM4_J1, 'J2': data.CM4_J2, 'J3': data.CM4_J3, 'J6': data.CM4_J6, 'J9': data.CM4_J9}, # This is slightly wrong, and will need to be updated when we have a data.CM5_BOARD + 'CM5Lite': {'J8': data.PI5_J8, 'J1': data.CM4_J1, 'J2': data.CM4_J2, 'J3': data.CM4_J3, 'J6': data.CM4_J6, 'J9': data.CM4_J9}, # This is slightly wrong, and will need to be updated when we have a data.CM5_BOARD + '500': {'J8': data.PI5_J8}, + }.get(model, {'J8': data.PLUS_J8, 'RUN': data.ZERO_RUN}) + board = { + 'A': data.A_BOARD, + 'B': data.REV1_BOARD if pcb_revision == '1.0' else data.REV2_BOARD, + 'A+': data.APLUS_BOARD, + 'CM': data.CM_BOARD, + 'CM3': data.CM_BOARD, + 'CM3+': data.CM3PLUS_BOARD, + 'Zero': data.ZERO12_BOARD if pcb_revision == '1.2' else data.ZERO13_BOARD, + 'Zero W': data.ZERO13_BOARD, + 'Zero2W': data.ZERO2_BOARD, + '3A+': data.A3PLUS_BOARD, + '3B+': data.B3PLUS_BOARD, + '4B': data.B4_BOARD, + 'CM4': data.CM4_BOARD, + 'CM4S': data.CM3PLUS_BOARD, + '400': data.P400_BOARD, + '5B': data.B5_BOARD, + 'CM5': data.CM4_BOARD, # This is slightly wrong, but there isn't a data.CM5_BOARD ascii-art yet + 'CM5Lite': data.CM4_BOARD, # This is slightly wrong, but there isn't a data.CM5_BOARD ascii-art yet + '500': data.P400_BOARD, # This is slightly wrong, but there isn't a data.P500_BOARD ascii-art yet + }.get(model, data.BPLUS_BOARD) + else: + # Old-style revision, use the lookup table + try: + ( + model, + pcb_revision, + released, + soc, + manufacturer, + memory, + storage, + usb, + ethernet, + wifi, + bluetooth, + csi, + dsi, + headers, + board, + ) = data.PI_REVISIONS[revision] + usb3 = 0 + eth_speed = ethernet * 100 + except KeyError: + raise PinUnknownPi(f'unknown old-style revision "{revision:x}"') + headers = frozendict({ + header: HeaderInfo( + name=header, rows=rows, columns=columns, + pins=frozendict({ + number: cls._make_pin( + header, number, row + 1, col + 1, functions) + for number, functions in header_data.items() + for row, col in (divmod(number - 1, 2),) + }) + ) + for header, (rows, columns, header_data) in headers.items() + }) + return cls( + f'{revision:04x}', + model, + pcb_revision, + released, + soc, + manufacturer, + memory, + storage, + usb, + usb3, + ethernet, + eth_speed, + wifi, + bluetooth, + csi, + dsi, + headers, + board, + ) + + @staticmethod + def _make_pin(header, number, row, col, interfaces): + pull = 'up' if number in (3, 5) and header in ('P1', 'J8') else '' + phys_name = f'{header}:{number}' + names = {phys_name} + if header in ('P1', 'J8', 'SODIMM'): + names.add(f'BOARD{number}') + try: + name = interfaces['gpio'] + gpio = int(name[4:]) + names.add(name) + names.add(gpio) + names.add(str(gpio)) + names.add(f'BCM{gpio}') + try: + wpi_map = { + 'J8:3': 8, 'J8:5': 9, 'J8:7': 7, 'J8:8': 15, + 'J8:10': 16, 'J8:11': 0, 'J8:12': 1, 'J8:13': 2, + 'J8:15': 3, 'J8:16': 4, 'J8:18': 5, 'J8:19': 12, + 'J8:21': 13, 'J8:22': 6, 'J8:23': 14, 'J8:24': 10, + 'J8:26': 11, 'J8:27': 30, 'J8:28': 31, 'J8:29': 21, + 'J8:31': 22, 'J8:32': 26, 'J8:33': 23, 'J8:35': 24, + 'J8:36': 27, 'J8:37': 25, 'J8:38': 28, 'J8:40': 29, + 'P1:3': 8, 'P1:5': 9, 'P1:7': 7, 'P1:8': 15, + 'P1:10': 16, 'P1:11': 0, 'P1:12': 1, 'P1:13': 2, + 'P1:15': 3, 'P1:16': 4, 'P1:18': 5, 'P1:19': 12, + 'P1:21': 13, 'P1:22': 6, 'P1:23': 14, 'P1:24': 10, + 'P1:26': 11, 'P1:27': 30, 'P1:28': 31, 'P1:29': 21, + 'P1:31': 22, 'P1:32': 26, 'P1:33': 23, 'P1:35': 24, + 'P1:36': 27, 'P1:37': 25, 'P1:38': 28, 'P1:40': 29, + 'P5:3': 17, 'P5:4': 18, 'P5:5': 19, 'P5:6': 20, + } + names.add(f'WPI{wpi_map[phys_name]}') + except KeyError: + pass + except KeyError: + name = interfaces[''] + names.add(name) + return PinInfo( + number=number, name=name, names=frozenset(names), pull=pull, + row=row, col=col, interfaces=frozenset(interfaces)) + + @property + def description(self): + return f"Raspberry Pi {self.model} rev {self.pcb_revision}" class PiFactory(Factory): @@ -79,45 +429,36 @@ class PiFactory(Factory): :class:`~gpiozero.pins.local.LocalPiFactory`. """ def __init__(self): - super(PiFactory, self).__init__() + super().__init__() self._info = None self.pins = {} self.pin_class = None - self.spi_classes = { - ('hardware', 'exclusive'): None, - ('hardware', 'shared'): None, - ('software', 'exclusive'): None, - ('software', 'shared'): None, - } def close(self): for pin in self.pins.values(): pin.close() self.pins.clear() - def reserve_pins(self, requester, *pins): - super(PiFactory, self).reserve_pins( - requester, *(self.pi_info.to_gpio(pin) for pin in pins)) - - def release_pins(self, reserver, *pins): - super(PiFactory, self).release_pins( - reserver, *(self.pi_info.to_gpio(pin) for pin in pins)) - - def pin(self, spec): - n = self.pi_info.to_gpio(spec) - try: - pin = self.pins[n] - except KeyError: - pin = self.pin_class(self, n) - self.pins[n] = pin - return pin + def pin(self, name): + for header, info in self.board_info.find_pin(name): + try: + pin = self.pins[info] + except KeyError: + pin = self.pin_class(self, info) + self.pins[info] = pin + return pin + raise PinInvalidPin(f'{name} is not a valid pin name') def _get_revision(self): + """ + This method must be overridden by descendents to return the Pi's + revision code as an :class:`int`. The default is unimplemented. + """ raise NotImplementedError - def _get_pi_info(self): + def _get_board_info(self): if self._info is None: - self._info = pi_info(self._get_revision()) + self._info = PiBoardInfo.from_revision(self._get_revision()) return self._info def spi(self, **spi_args): @@ -136,33 +477,34 @@ def spi(self, **spi_args): Both interfaces have the same API, support clock polarity and phase attributes, and can handle half and full duplex communications, but the - hardware interface is significantly faster (though for many things this - doesn't matter). + hardware interface is significantly faster (though for many simpler + devices this doesn't matter). """ spi_args, kwargs = self._extract_spi_args(**spi_args) - shared = 'shared' if kwargs.pop('shared', False) else 'exclusive' + shared = bool(kwargs.pop('shared', False)) if kwargs: raise SPIBadArgs( - 'unrecognized keyword argument %s' % kwargs.popitem()[0]) - for port, pins in SPI_HARDWARE_PINS.items(): - if all(( - spi_args['clock_pin'] == pins['clock'], - spi_args['mosi_pin'] == pins['mosi'], - spi_args['miso_pin'] == pins['miso'], - spi_args['select_pin'] in pins['select'], - )): - try: - return self.spi_classes[('hardware', shared)]( - self, port=port, - device=pins['select'].index(spi_args['select_pin']) - ) - except Exception as e: - warnings.warn( - SPISoftwareFallback( - 'failed to initialize hardware SPI, falling back to ' - 'software (error was: %s)' % str(e))) - break - return self.spi_classes[('software', shared)](self, **spi_args) + f'unrecognized keyword argument {kwargs.popitem()[0]}') + try: + port, device = spi_port_device(**spi_args) + except SPIBadArgs: + # Assume request is for a software SPI implementation + pass + else: + try: + return self._get_spi_class(shared, hardware=True)( + pin_factory=self, **spi_args) + except GPIOPinInUse: + # If a pin is already reserved, don't fallback to software SPI + # as it'll just be reserved there too + raise + except Exception as e: + warnings.warn( + SPISoftwareFallback( + f'failed to initialize hardware SPI, falling back to ' + f'software (error was: {e!s})')) + return self._get_spi_class(shared, hardware=False)( + pin_factory=self, **spi_args) def _extract_spi_args(self, **kwargs): """ @@ -196,7 +538,8 @@ def _extract_spi_args(self, **kwargs): spi_args = pin_defaults elif set(spi_args) <= set(pin_defaults): spi_args = { - key: self.pi_info.to_gpio(spi_args.get(key, default)) + key: None if spi_args.get(key, default) is None else + self.board_info.to_gpio(spi_args.get(key, default)) for key, default in pin_defaults.items() } elif set(spi_args) <= set(dev_defaults): @@ -208,13 +551,14 @@ def _extract_spi_args(self, **kwargs): selected_hw = SPI_HARDWARE_PINS[spi_args['port']] except KeyError: raise SPIBadArgs( - 'port %d is not a valid SPI port' % spi_args['port']) + f"port {spi_args['port']} is not a valid SPI port") try: selected_hw['select'][spi_args['device']] except IndexError: raise SPIBadArgs( - 'device must be in the range 0..%d' % - len(selected_hw['select'])) + f"device must be in the range 0.." + f"{len(selected_hw['select'])}") + # XXX: This is incorrect; assumes port == dev_defaults['port'] spi_args = { key: value if key != 'select_pin' else selected_hw['select'][spi_args['device']] for key, value in pin_defaults.items() @@ -226,6 +570,17 @@ def _extract_spi_args(self, **kwargs): 'schemes (e.g. port and clock_pin) are not permitted') return spi_args, kwargs + def _get_spi_class(self, shared, hardware): + """ + Returns a sub-class of the :class:`SPI` which can be constructed with + *clock_pin*, *mosi_pin*, *miso_pin*, and *select_pin* arguments. The + *shared* argument dictates whether the returned class uses the + :class:`SharedMixin` to permit sharing instances between components, + while *hardware* indicates whether the returned class uses the kernel's + SPI device(s) rather than a bit-banged software implementation. + """ + raise NotImplementedError + class PiPin(Pin): """ @@ -256,25 +611,29 @@ class PiPin(Pin): * :meth:`_get_edges` * :meth:`_set_edges` """ - def __init__(self, factory, number): - super(PiPin, self).__init__() + def __init__(self, factory, info): + super().__init__() + if 'gpio' not in info.interfaces: + raise PinInvalidPin(f'{info} is not a GPIO pin') self._factory = factory + self._info = info + self._number = int(info.name[4:]) self._when_changed_lock = RLock() self._when_changed = None - self._number = number - try: - factory.pi_info.physical_pin(repr(self)) - except PinNoPins: - warnings.warn( - PinNonPhysical( - 'no physical pins exist for %s' % repr(self))) + + @property + def info(self): + return self._info @property def number(self): + warnings.warn( + DeprecationWarning( + "PiPin.number is deprecated; please use Pin.info.name instead")) return self._number def __repr__(self): - return 'GPIO%d' % self._number + return self._info.name @property def factory(self): @@ -327,3 +686,42 @@ def _disable_event_detect(self): on pin :attr:`number`. """ raise NotImplementedError + + +def pi_info(revision=None): + """ + Deprecated function for retrieving information about a *revision* of the + Raspberry Pi. If you wish to retrieve information about the board that your + script is running on, please query the :attr:`Factory.board_info` property + like so:: + + >>> from gpiozero import Device + >>> Device.ensure_pin_factory() + >>> Device.pin_factory.board_info + PiBoardInfo(revision='a02082', model='3B', pcb_revision='1.2', + released='2016Q1', soc='BCM2837', manufacturer='Sony', memory=1024, + storage='MicroSD', usb=4, usb3=0, ethernet=1, eth_speed=100, wifi=True, + bluetooth=True, csi=1, dsi=1, headers=..., board=...) + + To obtain information for a specific Raspberry Pi board revision, use the + :meth:`PiBoardInfo.from_revision` constructor. + + :param str revision: + The revision of the Pi to return information about. If this is omitted + or :data:`None` (the default), then the library will attempt to + determine the model of Pi it is running on and return information about + that. + """ + if revision is None: + if Device.pin_factory is None: + Device.pin_factory = Device._default_pin_factory() + return Device.pin_factory.board_info + else: + if isinstance(revision, bytes): + revision = revision.decode('ascii') + if isinstance(revision, str): + revision = int(revision, base=16) + else: + # be nice to people passing an int (or something numeric anyway) + revision = int(revision) + return PiBoardInfo.from_revision(revision) diff --git a/gpiozero/pins/pigpio.py b/gpiozero/pins/pigpio.py index c9aa2efc2..9485e85f8 100644 --- a/gpiozero/pins/pigpio.py +++ b/gpiozero/pins/pigpio.py @@ -1,50 +1,23 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2021 Kyle Morgan +# Copyright (c) 2020 Ben Nuttall +# Copyright (c) 2019 Maksim Levental +# Copyright (c) 2019 Aaron Rogers # Copyright (c) 2016 BuildTools # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause import os import pigpio from . import SPI -from .pi import PiPin, PiFactory, SPI_HARDWARE_PINS -from .data import pi_info -from ..devices import Device +from .pi import PiPin, PiFactory, spi_port_device from ..mixins import SharedMixin from ..exc import ( PinInvalidFunction, @@ -103,33 +76,27 @@ class PiGPIOFactory(PiFactory): bug in our pin implementation). A workaround for now is simply to restart the :command:`pigpiod` daemon. - .. _pigpio: https://pypi.org/project/pigpio/ + .. _pigpio: http://abyz.me.uk/rpi/pigpio/ """ def __init__(self, host=None, port=None): - super(PiGPIOFactory, self).__init__() + super().__init__() if host is None: host = os.environ.get('PIGPIO_ADDR', 'localhost') if port is None: # XXX Use getservbyname port = int(os.environ.get('PIGPIO_PORT', 8888)) self.pin_class = PiGPIOPin - self.spi_classes = { - ('hardware', 'exclusive'): PiGPIOHardwareSPI, - ('hardware', 'shared'): PiGPIOHardwareSPIShared, - ('software', 'exclusive'): PiGPIOSoftwareSPI, - ('software', 'shared'): PiGPIOSoftwareSPIShared, - } self._connection = pigpio.pi(host, port) # Annoyingly, pigpio doesn't raise an exception when it fails to make a # connection; it returns a valid (but disconnected) pi object if self.connection is None: - raise IOError('failed to connect to %s:%s' % (host, port)) + raise IOError(f'failed to connect to {host}:{port}') self._host = host self._port = port self._spis = [] def close(self): - super(PiGPIOFactory, self).close() + super().close() # We *have* to keep track of SPI interfaces constructed with pigpio; # if we fail to close them they prevent future interfaces from using # the same pins @@ -162,8 +129,16 @@ def port(self): def _get_revision(self): return self.connection.get_hardware_revision() + def _get_spi_class(self, shared, hardware): + return { + (False, True): PiGPIOHardwareSPI, + (True, True): PiGPIOHardwareSPIShared, + (False, False): PiGPIOSoftwareSPI, + (True, False): PiGPIOSoftwareSPIShared, + }[shared, hardware] + def spi(self, **spi_args): - intf = super(PiGPIOFactory, self).spi(**spi_args) + intf = super().spi(**spi_args) self._spis.append(intf) return intf @@ -185,9 +160,8 @@ class PiGPIOPin(PiPin): Extends :class:`~gpiozero.pins.pi.PiPin`. Pin implementation for the `pigpio`_ library. See :class:`PiGPIOFactory` for more information. - .. _pigpio: http://abyz.co.uk/rpi/pigpio/ + .. _pigpio: http://abyz.me.uk/rpi/pigpio/ """ - _CONNECTIONS = {} # maps (host, port) to (connection, pi_info) GPIO_FUNCTIONS = { 'input': pigpio.INPUT, 'output': pigpio.OUTPUT, @@ -215,98 +189,104 @@ class PiGPIOPin(PiPin): GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} - def __init__(self, factory, number): - super(PiGPIOPin, self).__init__(factory, number) - self._pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating' + def __init__(self, factory, info): + super().__init__(factory, info) + self._pull = info.pull or 'floating' self._pwm = False self._bounce = None self._callback = None self._edges = pigpio.EITHER_EDGE try: - self.factory.connection.set_mode(self.number, pigpio.INPUT) + self.factory.connection.set_mode(self._number, pigpio.INPUT) except pigpio.error as e: raise ValueError(e) - self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[self._pull]) - self.factory.connection.set_glitch_filter(self.number, 0) + self.factory.connection.set_pull_up_down( + self._number, self.GPIO_PULL_UPS[self._pull]) + self.factory.connection.set_glitch_filter(self._number, 0) def close(self): if self.factory.connection: self.frequency = None self.when_changed = None self.function = 'input' - self.pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating' + self.pull = self.info.pull or 'floating' def _get_function(self): - return self.GPIO_FUNCTION_NAMES[self.factory.connection.get_mode(self.number)] + return self.GPIO_FUNCTION_NAMES[ + self.factory.connection.get_mode(self._number)] def _set_function(self, value): if value != 'input': self._pull = 'floating' try: - self.factory.connection.set_mode(self.number, self.GPIO_FUNCTIONS[value]) + self.factory.connection.set_mode( + self._number, self.GPIO_FUNCTIONS[value]) except KeyError: - raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) + raise PinInvalidFunction( + f'invalid function "{value}" for pin {self!r}') def _get_state(self): if self._pwm: return ( - self.factory.connection.get_PWM_dutycycle(self.number) / - self.factory.connection.get_PWM_range(self.number) + self.factory.connection.get_PWM_dutycycle(self._number) / + self.factory.connection.get_PWM_range(self._number) ) else: - return bool(self.factory.connection.read(self.number)) + return bool(self.factory.connection.read(self._number)) def _set_state(self, value): if self._pwm: try: - value = int(value * self.factory.connection.get_PWM_range(self.number)) - if value != self.factory.connection.get_PWM_dutycycle(self.number): - self.factory.connection.set_PWM_dutycycle(self.number, value) + value = int(value * self.factory.connection.get_PWM_range(self._number)) + if value != self.factory.connection.get_PWM_dutycycle(self._number): + self.factory.connection.set_PWM_dutycycle(self._number, value) except pigpio.error: - raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) + raise PinInvalidState( + f'invalid state "{value}" for pin {self!r}') elif self.function == 'input': - raise PinSetInput('cannot set state of pin %r' % self) + raise PinSetInput(f'cannot set state of pin {self!r}') else: # write forces pin to OUTPUT, hence the check above - self.factory.connection.write(self.number, bool(value)) + self.factory.connection.write(self._number, bool(value)) def _get_pull(self): return self._pull def _set_pull(self, value): if self.function != 'input': - raise PinFixedPull('cannot set pull on non-input pin %r' % self) - if value != 'up' and self.factory.pi_info.pulled_up(repr(self)): - raise PinFixedPull('%r has a physical pull-up resistor' % self) + raise PinFixedPull(f'cannot set pull on non-input pin {self!r}') + if self.info.pull and value != self.info.pull: + raise PinFixedPull(f'{self!r} has a fixed pull resistor') try: - self.factory.connection.set_pull_up_down(self.number, self.GPIO_PULL_UPS[value]) + self.factory.connection.set_pull_up_down( + self._number, self.GPIO_PULL_UPS[value]) self._pull = value except KeyError: - raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self)) + raise PinInvalidPull(f'invalid pull "{value}" for pin {self!r}') def _get_frequency(self): if self._pwm: - return self.factory.connection.get_PWM_frequency(self.number) + return self.factory.connection.get_PWM_frequency(self._number) return None def _set_frequency(self, value): if not self._pwm and value is not None: if self.function != 'output': - raise PinPWMFixedValue('cannot start PWM on pin %r' % self) + raise PinPWMFixedValue(f'cannot start PWM on pin {self!r}') # NOTE: the pin's state *must* be set to zero; if it's currently # high, starting PWM and setting a 0 duty-cycle *doesn't* bring # the pin low; it stays high! - self.factory.connection.write(self.number, 0) - self.factory.connection.set_PWM_frequency(self.number, value) - self.factory.connection.set_PWM_range(self.number, 10000) - self.factory.connection.set_PWM_dutycycle(self.number, 0) + self.factory.connection.write(self._number, 0) + self.factory.connection.set_PWM_frequency(self._number, int(value)) + self.factory.connection.set_PWM_range(self._number, 10000) + self.factory.connection.set_PWM_dutycycle(self._number, 0) self._pwm = True elif self._pwm and value is not None: - if value != self.factory.connection.get_PWM_frequency(self.number): - self.factory.connection.set_PWM_frequency(self.number, value) - self.factory.connection.set_PWM_range(self.number, 10000) + if value != self.factory.connection.get_PWM_frequency(self._number): + self.factory.connection.set_PWM_frequency(self._number, int(value)) + self.factory.connection.set_PWM_range(self._number, 10000) elif self._pwm and value is None: - self.factory.connection.write(self.number, 0) + self.factory.connection.write(self._number, 0) self._pwm = False def _get_bounce(self): @@ -315,9 +295,10 @@ def _get_bounce(self): def _set_bounce(self, value): if value is None: value = 0 - elif value < 0: - raise PinInvalidBounce('bounce must be 0 or greater') - self.factory.connection.set_glitch_filter(self.number, int(value * 1000000)) + elif not 0 <= value <= 0.3: + raise PinInvalidBounce('bounce must be between 0 and 0.3') + self.factory.connection.set_glitch_filter( + self._number, int(value * 1000000)) def _get_edges(self): return self.GPIO_EDGES_NAMES[self._edges] @@ -331,11 +312,11 @@ def _set_edges(self, value): self.when_changed = f def _call_when_changed(self, gpio, level, ticks): - super(PiGPIOPin, self)._call_when_changed(ticks, level) + super()._call_when_changed(ticks, level) def _enable_event_detect(self): self._callback = self.factory.connection.callback( - self.number, self._edges, self._call_when_changed) + self._number, self._edges, self._call_when_changed) def _disable_event_detect(self): if self._callback is not None: @@ -343,63 +324,59 @@ def _disable_event_detect(self): self._callback = None -class PiGPIOHardwareSPI(SPI, Device): +class PiGPIOHardwareSPI(SPI): """ Hardware SPI implementation for the `pigpio`_ library. Uses the ``spi_*`` functions from the pigpio API. - .. _pigpio: http://abyz.co.uk/rpi/pigpio/ + .. _pigpio: http://abyz.me.uk/rpi/pigpio/ """ - def __init__(self, factory, port, device): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): + port, device = spi_port_device( + clock_pin, mosi_pin, miso_pin, select_pin) self._port = port self._device = device - self._factory = factory self._handle = None - super(PiGPIOHardwareSPI, self).__init__() - pins = SPI_HARDWARE_PINS[port] - self._factory.reserve_pins( - self, - pins['clock'], - pins['mosi'], - pins['miso'], - pins['select'][device] - ) - self._spi_flags = 8 << 16 + super().__init__(pin_factory=pin_factory) + to_reserve = {clock_pin, select_pin} + if mosi_pin is not None: + to_reserve.add(mosi_pin) + if miso_pin is not None: + to_reserve.add(miso_pin) + self.pin_factory.reserve_pins(self, *to_reserve) + self._spi_flags = (8 << 16) | (port << 8) self._baud = 500000 - self._handle = self._factory.connection.spi_open( + self._handle = self.pin_factory.connection.spi_open( device, self._baud, self._spi_flags) def _conflicts_with(self, other): return not ( isinstance(other, PiGPIOHardwareSPI) and - (self._port, self._device) != (other._port, other._device) + (self.pin_factory.host, self._port, self._device) != + (other.pin_factory.host, other._port, other._device) ) def close(self): try: - self._factory._spis.remove(self) + self.pin_factory._spis.remove(self) except (ReferenceError, ValueError): # If the factory has died already or we're not present in its # internal list, ignore the error pass if not self.closed: - self._factory.connection.spi_close(self._handle) + self.pin_factory.connection.spi_close(self._handle) self._handle = None - self._factory.release_all(self) - super(PiGPIOHardwareSPI, self).close() + self.pin_factory.release_all(self) + super().close() @property def closed(self): - return self._handle is None or self._factory.connection is None - - @property - def factory(self): - return self._factory + return self._handle is None or self.pin_factory.connection is None def __repr__(self): try: self._check_open() - return 'SPI(port=%d, device=%d)' % (self._port, self._device) + return f'SPI(port={self._port:d}, device={self._device:d})' except DeviceClosed: return 'SPI(closed)' @@ -409,10 +386,10 @@ def _get_clock_mode(self): def _set_clock_mode(self, value): self._check_open() if not 0 <= value < 4: - raise SPIInvalidClockMode("%d is not a valid SPI clock mode" % value) - self._factory.connection.spi_close(self._handle) + raise SPIInvalidClockMode(f"{value} is not a valid SPI clock mode") + self.pin_factory.connection.spi_close(self._handle) self._spi_flags = (self._spi_flags & ~0x3) | value - self._handle = self._factory.connection.spi_open( + self._handle = self.pin_factory.connection.spi_open( self._device, self._baud, self._spi_flags) def _get_select_high(self): @@ -420,9 +397,9 @@ def _get_select_high(self): def _set_select_high(self, value): self._check_open() - self._factory.connection.spi_close(self._handle) + self.pin_factory.connection.spi_close(self._handle) self._spi_flags = (self._spi_flags & ~0x1c) | (bool(value) << (2 + self._device)) - self._handle = self._factory.connection.spi_open( + self._handle = self.pin_factory.connection.spi_open( self._device, self._baud, self._spi_flags) def _get_bits_per_word(self): @@ -430,37 +407,66 @@ def _get_bits_per_word(self): def _set_bits_per_word(self, value): self._check_open() - self._factory.connection.spi_close(self._handle) + self.pin_factory.connection.spi_close(self._handle) self._spi_flags = (self._spi_flags & ~0x3f0000) | ((value & 0x3f) << 16) - self._handle = self._factory.connection.spi_open( + self._handle = self.pin_factory.connection.spi_open( + self._device, self._baud, self._spi_flags) + + def _get_rate(self): + return self._baud + + def _set_rate(self, value): + self._check_open() + value = int(value) + self.pin_factory.connection.spi_close(self._handle) + self._baud = value + self._handle = self.pin_factory.connection.spi_open( self._device, self._baud, self._spi_flags) + def _get_lsb_first(self): + return bool((self._spi_flags >> 14) & 0x1) if self._port else False + + def _set_lsb_first(self, value): + if self._port: + self._check_open() + self.pin_factory.connection.spi_close(self._handle) + self._spi_flags = ( + (self._spi_flags & ~0xc000) + | (bool(value) << 14) + | (bool(value) << 15) + ) + self._handle = self.pin_factory.connection.spi_open( + self._device, self._baud, self._spi_flags) + else: + super()._set_lsb_first(value) + def transfer(self, data): self._check_open() - count, data = self._factory.connection.spi_xfer(self._handle, data) + count, data = self.pin_factory.connection.spi_xfer(self._handle, data) if count < 0: - raise IOError('SPI transfer error %d' % count) - # Convert returned bytearray to list of ints. XXX Not sure how non-byte - # sized words (aux intf only) are returned ... padded to 16/32-bits? + raise IOError(f'SPI transfer error {count}') + # Convert returned bytearray to list of ints. + # XXX Not sure how non-byte sized words (aux intf only) are returned + # ... padded to 16/32-bits? return [int(b) for b in data] -class PiGPIOSoftwareSPI(SPI, Device): +class PiGPIOSoftwareSPI(SPI): """ Software SPI implementation for the `pigpio`_ library. Uses the ``bb_spi_*`` functions from the pigpio API. - .. _pigpio: http://abyz.co.uk/rpi/pigpio/ + .. _pigpio: http://abyz.me.uk/rpi/pigpio/ """ - def __init__(self, factory, clock_pin, mosi_pin, miso_pin, select_pin): + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): self._closed = True self._select_pin = select_pin self._clock_pin = clock_pin self._mosi_pin = mosi_pin self._miso_pin = miso_pin - self._factory = factory - super(PiGPIOSoftwareSPI, self).__init__() - self._factory.reserve_pins( + super().__init__(pin_factory=pin_factory) + # Can't "unreserve" MOSI/MISO on this implementation + self.pin_factory.reserve_pins( self, clock_pin, mosi_pin, @@ -470,7 +476,7 @@ def __init__(self, factory, clock_pin, mosi_pin, miso_pin, select_pin): self._spi_flags = 0 self._baud = 100000 try: - self._factory.connection.bb_spi_open( + self.pin_factory.connection.bb_spi_open( select_pin, miso_pin, mosi_pin, clock_pin, self._baud, self._spi_flags) # Only set after opening bb_spi; if that fails then close() will @@ -488,16 +494,16 @@ def _conflicts_with(self, other): def close(self): try: - self._factory._spis.remove(self) + self.pin_factory._spis.remove(self) except (ReferenceError, ValueError): # If the factory has died already or we're not present in its # internal list, ignore the error pass - if not self.closed: + if not self._closed and self.pin_factory.connection: self._closed = True - self._factory.connection.bb_spi_close(self._select_pin) - self.factory.release_all(self) - super(PiGPIOSoftwareSPI, self).close() + self.pin_factory.connection.bb_spi_close(self._select_pin) + self.pin_factory.release_all(self) + super().close() @property def closed(self): @@ -507,9 +513,8 @@ def __repr__(self): try: self._check_open() return ( - 'SPI(clock_pin=%d, mosi_pin=%d, miso_pin=%d, select_pin=%d)' % ( - self._clock_pin, self._mosi_pin, self._miso_pin, self._select_pin - )) + f'SPI(clock_pin={self._clock_pin}, mosi_pin={self._mosi_pin}, ' + f'miso_pin={self._miso_pin}, select_pin={self._select_pin})') except DeviceClosed: return 'SPI(closed)' @@ -527,10 +532,10 @@ def _get_clock_mode(self): def _set_clock_mode(self, value): self._check_open() if not 0 <= value < 4: - raise SPIInvalidClockMode("%d is not a valid SPI clock mode" % value) - self._factory.connection.bb_spi_close(self._select_pin) + raise SPIInvalidClockMode(f"{value} is not a valid SPI clock mode") + self.pin_factory.connection.bb_spi_close(self._select_pin) self._spi_flags = (self._spi_flags & ~0x3) | value - self._factory.connection.bb_spi_open( + self.pin_factory.connection.bb_spi_open( self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, self._baud, self._spi_flags) @@ -539,9 +544,9 @@ def _get_select_high(self): def _set_select_high(self, value): self._check_open() - self._factory.connection.bb_spi_close(self._select_pin) + self.pin_factory.connection.bb_spi_close(self._select_pin) self._spi_flags = (self._spi_flags & ~0x4) | (bool(value) << 2) - self._factory.connection.bb_spi_open( + self.pin_factory.connection.bb_spi_open( self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, self._baud, self._spi_flags) @@ -550,21 +555,34 @@ def _get_lsb_first(self): def _set_lsb_first(self, value): self._check_open() - self._factory.connection.bb_spi_close(self._select_pin) + self.pin_factory.connection.bb_spi_close(self._select_pin) self._spi_flags = ( (self._spi_flags & ~0xc000) | (bool(value) << 14) | (bool(value) << 15) ) - self._factory.connection.bb_spi_open( + self.pin_factory.connection.bb_spi_open( + self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, + self._baud, self._spi_flags) + + def _get_rate(self): + return self._baud + + def _set_rate(self, value): + self._check_open() + value = int(value) + self.pin_factory.connection.bb_spi_close(self._select_pin) + self._baud = value + self.pin_factory.connection.bb_spi_open( self._select_pin, self._miso_pin, self._mosi_pin, self._clock_pin, self._baud, self._spi_flags) def transfer(self, data): self._check_open() - count, data = self._factory.connection.bb_spi_xfer(self._select_pin, data) + count, data = self.pin_factory.connection.bb_spi_xfer( + self._select_pin, data) if count < 0: - raise IOError('SPI transfer error %d' % count) + raise IOError(f'SPI transfer error {count}') # Convert returned bytearray to list of ints. bb_spi only supports # byte-sized words so no issues here return [int(b) for b in data] @@ -572,11 +590,11 @@ def transfer(self, data): class PiGPIOHardwareSPIShared(SharedMixin, PiGPIOHardwareSPI): @classmethod - def _shared_key(cls, factory, port, device): - return (factory, port, device) + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): + return (pin_factory.host, clock_pin, select_pin) class PiGPIOSoftwareSPIShared(SharedMixin, PiGPIOSoftwareSPI): @classmethod - def _shared_key(cls, factory, clock_pin, mosi_pin, miso_pin, select_pin): - return (factory, select_pin) + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, select_pin, pin_factory): + return (pin_factory.host, clock_pin, select_pin) diff --git a/gpiozero/pins/rpigpio.py b/gpiozero/pins/rpigpio.py index 6028df6e7..386fc2231 100644 --- a/gpiozero/pins/rpigpio.py +++ b/gpiozero/pins/rpigpio.py @@ -1,42 +1,11 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2015-2023 Dave Jones +# Copyright (c) 2016 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - -import warnings +# SPDX-License-Identifier: BSD-3-Clause from RPi import GPIO @@ -49,7 +18,7 @@ PinInvalidState, PinInvalidBounce, PinPWMFixedValue, - ) +) class RPiGPIOFactory(LocalPiFactory): @@ -78,13 +47,13 @@ class RPiGPIOFactory(LocalPiFactory): """ def __init__(self): - super(RPiGPIOFactory, self).__init__() + super().__init__() GPIO.setmode(GPIO.BCM) GPIO.setwarnings(False) self.pin_class = RPiGPIOPin def close(self): - super(RPiGPIOFactory, self).close() + super().close() GPIO.cleanup() @@ -121,79 +90,83 @@ class RPiGPIOPin(LocalPiPin): GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()} - def __init__(self, factory, number): - super(RPiGPIOPin, self).__init__(factory, number) - self._pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating' + def __init__(self, factory, info): + super().__init__(factory, info) + self._pull = info.pull or 'floating' self._pwm = None self._frequency = None self._duty_cycle = None self._bounce = -666 self._edges = GPIO.BOTH - GPIO.setup(self.number, GPIO.IN, self.GPIO_PULL_UPS[self._pull]) + GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[self._pull]) def close(self): self.frequency = None self.when_changed = None - GPIO.cleanup(self.number) + GPIO.cleanup(self._number) def output_with_state(self, state): self._pull = 'floating' - GPIO.setup(self.number, GPIO.OUT, initial=state) + GPIO.setup(self._number, GPIO.OUT, initial=state) def input_with_pull(self, pull): - if pull != 'up' and self.factory.pi_info.pulled_up(repr(self)): - raise PinFixedPull('%r has a physical pull-up resistor' % self) + if self.info.pull and pull != self.info.pull: + raise PinFixedPull(f'{self!r} has a fixed pull resistor') try: - GPIO.setup(self.number, GPIO.IN, self.GPIO_PULL_UPS[pull]) + GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[pull]) self._pull = pull except KeyError: - raise PinInvalidPull('invalid pull "%s" for pin %r' % (pull, self)) + raise PinInvalidPull(f'invalid pull "{pull}" for pin {self!r}') def _get_function(self): - return self.GPIO_FUNCTION_NAMES[GPIO.gpio_function(self.number)] + return self.GPIO_FUNCTION_NAMES[GPIO.gpio_function(self._number)] def _set_function(self, value): if value != 'input': self._pull = 'floating' if value in ('input', 'output') and value in self.GPIO_FUNCTIONS: - GPIO.setup(self.number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull]) + GPIO.setup(self._number, self.GPIO_FUNCTIONS[value], + self.GPIO_PULL_UPS[self._pull]) else: - raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) + raise PinInvalidFunction( + f'invalid function "{value}" for pin {self!r}') def _get_state(self): if self._pwm: return self._duty_cycle else: - return GPIO.input(self.number) + return GPIO.input(self._number) def _set_state(self, value): if self._pwm: try: self._pwm.ChangeDutyCycle(value * 100) except ValueError: - raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) + raise PinInvalidState( + f'invalid state "{value}" for pin {self!r}') self._duty_cycle = value else: try: - GPIO.output(self.number, value) + GPIO.output(self._number, value) except ValueError: - raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) + raise PinInvalidState( + f'invalid state "{value}" for pin {self!r}') except RuntimeError: - raise PinSetInput('cannot set state of pin %r' % self) + raise PinSetInput(f'cannot set state of pin {self!r}') def _get_pull(self): return self._pull def _set_pull(self, value): if self.function != 'input': - raise PinFixedPull('cannot set pull on non-input pin %r' % self) - if value != 'up' and self.factory.pi_info.pulled_up(repr(self)): - raise PinFixedPull('%r has a physical pull-up resistor' % self) + raise PinFixedPull(f'cannot set pull on non-input pin {self!r}') + if self.info.pull and value != self.info.pull: + raise PinFixedPull(f'{self!r} has a fixed pull resistor') try: - GPIO.setup(self.number, GPIO.IN, self.GPIO_PULL_UPS[value]) + GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[value]) self._pull = value except KeyError: - raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self)) + raise PinInvalidPull(f'invalid pull "{value}" for pin {self!r}') def _get_frequency(self): return self._frequency @@ -201,9 +174,9 @@ def _get_frequency(self): def _set_frequency(self, value): if self._frequency is None and value is not None: try: - self._pwm = GPIO.PWM(self.number, value) + self._pwm = GPIO.PWM(self._number, value) except RuntimeError: - raise PinPWMFixedValue('cannot start PWM on pin %r' % self) + raise PinPWMFixedValue(f'cannot start PWM on pin {self!r}') self._pwm.start(0) self._duty_cycle = 0 self._frequency = value @@ -241,13 +214,13 @@ def _set_edges(self, value): self.when_changed = f def _call_when_changed(self, channel): - super(RPiGPIOPin, self)._call_when_changed() + super()._call_when_changed() def _enable_event_detect(self): GPIO.add_event_detect( - self.number, self._edges, + self._number, self._edges, callback=self._call_when_changed, bouncetime=self._bounce) def _disable_event_detect(self): - GPIO.remove_event_detect(self.number) + GPIO.remove_event_detect(self._number) diff --git a/gpiozero/pins/rpio.py b/gpiozero/pins/rpio.py deleted file mode 100644 index 7d1f37b21..000000000 --- a/gpiozero/pins/rpio.py +++ /dev/null @@ -1,247 +0,0 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2015-2019 Dave Jones -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - - -import warnings - -import RPIO -import RPIO.PWM -from RPIO.Exceptions import InvalidChannelException - -from .local import LocalPiPin, LocalPiFactory -from .data import pi_info -from ..exc import ( - PinInvalidFunction, - PinSetInput, - PinFixedPull, - PinInvalidPull, - PinInvalidBounce, - PinInvalidState, - PinPWMError, - ) - - -class RPIOFactory(LocalPiFactory): - """ - Extends :class:`~gpiozero.pins.local.LocalPiFactory`. Uses the `RPIO`_ - library to interface to the Pi's GPIO pins. This is the default pin - implementation if the RPi.GPIO library is not installed, but RPIO is. - Supports all features including PWM (hardware via DMA). - - .. note:: - - Please note that at the time of writing, RPIO is only compatible with - Pi 1's; the Raspberry Pi 2 Model B is *not* supported. Also note that - root access is required so scripts must typically be run with ``sudo``. - - You can construct RPIO pins manually like so:: - - from gpiozero.pins.rpio import RPIOFactory - from gpiozero import LED - - factory = RPIOFactory() - led = LED(12, pin_factory=factory) - - .. _RPIO: https://pythonhosted.org/RPIO/ - """ - def __init__(self): - super(RPIOFactory, self).__init__() - RPIO.setmode(RPIO.BCM) - RPIO.setwarnings(False) - RPIO.wait_for_interrupts(threaded=True) - RPIO.PWM.setup() - RPIO.PWM.init_channel(0, 10000) - self.pin_class = RPIOPin - - def close(self): - RPIO.PWM.cleanup() - RPIO.stop_waiting_for_interrupts() - RPIO.cleanup() - - -class RPIOPin(LocalPiPin): - """ - Extends :class:`~gpiozero.pins.local.LocalPiPin`. Pin implementation for - the `RPIO`_ library. See :class:`RPIOFactory` for more information. - - .. _RPIO: https://pythonhosted.org/RPIO/ - """ - GPIO_FUNCTIONS = { - 'input': RPIO.IN, - 'output': RPIO.OUT, - 'alt0': RPIO.ALT0, - } - - GPIO_PULL_UPS = { - 'up': RPIO.PUD_UP, - 'down': RPIO.PUD_DOWN, - 'floating': RPIO.PUD_OFF, - } - - GPIO_FUNCTION_NAMES = {v: k for (k, v) in GPIO_FUNCTIONS.items()} - GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()} - - def __init__(self, factory, number): - super(RPIOPin, self).__init__(factory, number) - self._pull = 'up' if self.factory.pi_info.pulled_up(repr(self)) else 'floating' - self._pwm = False - self._duty_cycle = None - self._bounce = None - self._edges = 'both' - try: - RPIO.setup(self.number, RPIO.IN, self.GPIO_PULL_UPS[self._pull]) - except InvalidChannelException as e: - raise ValueError(e) - - def close(self): - self.frequency = None - self.when_changed = None - RPIO.setup(self.number, RPIO.IN, RPIO.PUD_OFF) - - def _get_function(self): - return self.GPIO_FUNCTION_NAMES[RPIO.gpio_function(self.number)] - - def _set_function(self, value): - if value != 'input': - self._pull = 'floating' - try: - RPIO.setup(self.number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull]) - except KeyError: - raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self)) - - def _get_state(self): - if self._pwm: - return self._duty_cycle - else: - return RPIO.input(self.number) - - def _set_state(self, value): - if not 0 <= value <= 1: - raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) - if self._pwm: - RPIO.PWM.clear_channel_gpio(0, self.number) - if value == 0: - RPIO.output(self.number, False) - elif value == 1: - RPIO.output(self.number, True) - else: - RPIO.PWM.add_channel_pulse(0, self.number, start=0, width=int(1000 * value)) - self._duty_cycle = value - else: - try: - RPIO.output(self.number, value) - except ValueError: - raise PinInvalidState('invalid state "%s" for pin %r' % (value, self)) - except RuntimeError: - raise PinSetInput('cannot set state of pin %r' % self) - - def _get_pull(self): - return self._pull - - def _set_pull(self, value): - if self.function != 'input': - raise PinFixedPull('cannot set pull on non-input pin %r' % self) - if value != 'up' and self.factory.pi_info.pulled_up(repr(self)): - raise PinFixedPull('%r has a physical pull-up resistor' % self) - try: - RPIO.setup(self.number, RPIO.IN, self.GPIO_PULL_UPS[value]) - self._pull = value - except KeyError: - raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self)) - - def _get_frequency(self): - if self._pwm: - return 100 - else: - return None - - def _set_frequency(self, value): - if value is not None and value != 100: - raise PinPWMError( - 'RPIOPin implementation is currently limited to ' - '100Hz sub-cycles') - if not self._pwm and value is not None: - self._pwm = True - # Dirty hack to get RPIO's PWM support to setup, but do nothing, - # for a given GPIO pin - RPIO.PWM.add_channel_pulse(0, self.number, start=0, width=0) - RPIO.PWM.clear_channel_gpio(0, self.number) - elif self._pwm and value is None: - RPIO.PWM.clear_channel_gpio(0, self.number) - self._pwm = False - - def _get_bounce(self): - return None if self._bounce is None else (self._bounce / 1000) - - def _set_bounce(self, value): - if value is not None and value < 0: - raise PinInvalidBounce('bounce must be 0 or greater') - f = self.when_changed - self.when_changed = None - try: - self._bounce = None if value is None else int(value * 1000) - finally: - self.when_changed = f - - def _get_edges(self): - return self._edges - - def _set_edges(self, value): - f = self.when_changed - self.when_changed = None - try: - self._edges = value - finally: - self.when_changed = f - - def _call_when_changed(self, channel, value): - super(RPIOPin, self)._call_when_changed() - - def _enable_event_detect(self): - RPIO.add_interrupt_callback( - self.number, self._call_when_changed, self._edges, - self.GPIO_PULL_UPS[self._pull], self._bounce) - - def _disable_event_detect(self): - try: - RPIO.del_interrupt_callback(self.number) - except KeyError: - # Ignore this exception which occurs during shutdown; this - # simply means RPIO's built-in cleanup has already run and - # removed the handler - pass diff --git a/gpiozero/pins/spi.py b/gpiozero/pins/spi.py index 568ccb865..ab8240853 100644 --- a/gpiozero/pins/spi.py +++ b/gpiozero/pins/spi.py @@ -1,69 +1,163 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import operator from threading import RLock +from . import SPI from ..devices import Device, SharedMixin from ..input_devices import InputDevice from ..output_devices import OutputDevice +from ..exc import DeviceClosed, SPIInvalidClockMode + + +class SPISoftware(SPI): + """ + A software bit-banged implementation of the :class:`gpiozero.pins.SPI` + interface. + + This is a reasonable basis for a *local* SPI software implementation, but + be aware that it's unlikely to be usable for remote operation (a dedicated + daemon that locally handles SPI transactions should be used for such + operations). Instances will happily share their clock, mosi, and miso pins + with other instances provided each has a distinct select pin. + + See :class:`~gpiozero.pins.spi.SPISoftwareBus` for the actual SPI + transfer logic. + """ + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin, *, + pin_factory): + self._bus = None + self._select = None + super().__init__(pin_factory=pin_factory) + try: + # XXX We *should* be storing clock_mode locally, not clock_phase; + # after all different users of the bus can disagree about the + # clock's polarity and even select pin polarity + self._clock_phase = False + self._lsb_first = False + self._bits_per_word = 8 + self._bus = SPISoftwareBus( + clock_pin, mosi_pin, miso_pin, pin_factory=pin_factory) + self._select = OutputDevice( + select_pin, active_high=False, pin_factory=pin_factory) + except: + self.close() + raise + + def _conflicts_with(self, other): + if isinstance(other, SoftwareSPI): + return self._select.pin.info.name == other._select.pin.info.name + else: + return True + + def close(self): + if self._select: + self._select.close() + self._select = None + if self._bus is not None: + self._bus.close() + self._bus = None + super().close() + + @property + def closed(self): + return self._bus is None + + def __repr__(self): + try: + self._check_open() + return ( + f'SPI(clock_pin={self._bus.clock.pin.info.name!r}, ' + f'mosi_pin={self._bus.mosi.pin.info.name!r}, ' + f'miso_pin={self._bus.miso.pin.info.name!r}, ' + f'select_pin={self._select.pin.info.name!r})') + except DeviceClosed: + return 'SPI(closed)' + + def transfer(self, data): + with self._bus.lock: + self._select.on() + try: + return self._bus.transfer( + data, self._clock_phase, self._lsb_first, + self._bits_per_word) + finally: + self._select.off() + + def _get_clock_mode(self): + with self._bus.lock: + return (not self._bus.clock.active_high) << 1 | self._clock_phase + + def _set_clock_mode(self, value): + if not (0 <= value < 4): + raise SPIInvalidClockMode(f"{value} is not a valid clock mode") + with self._bus.lock: + self._bus.clock.active_high = not (value & 2) + self._clock_phase = bool(value & 1) + + def _get_lsb_first(self): + return self._lsb_first + + def _set_lsb_first(self, value): + self._lsb_first = bool(value) + + def _get_bits_per_word(self): + return self._bits_per_word + + def _set_bits_per_word(self, value): + if value < 1: + raise ValueError('bits_per_word must be positive') + self._bits_per_word = int(value) + + def _get_select_high(self): + return self._select.active_high + + def _set_select_high(self, value): + with self._bus.lock: + self._select.active_high = value + self._select.off() class SPISoftwareBus(SharedMixin, Device): - def __init__(self, clock_pin, mosi_pin, miso_pin): + """ + A software bit-banged SPI bus implementation, used by + :class:`~gpiozero.pins.spi.SPISoftware` to implement shared SPI interfaces. + + .. warning:: + + This implementation has no rate control; it simply clocks out data as + fast as it can as Python isn't terribly quick on a Pi anyway, and the + extra logic required for rate control is liable to reduce the maximum + achievable data rate quite substantially. + """ + def __init__(self, clock_pin, mosi_pin, miso_pin, *, pin_factory): self.lock = None self.clock = None self.mosi = None self.miso = None - super(SPISoftwareBus, self).__init__() + super().__init__() + # XXX Should probably just use CompositeDevice for this; would make + # close() a bit cleaner - any implications with the RLock? self.lock = RLock() try: - self.clock = OutputDevice(clock_pin, active_high=True) + self.clock = OutputDevice( + clock_pin, active_high=True, pin_factory=pin_factory) if mosi_pin is not None: - self.mosi = OutputDevice(mosi_pin) + self.mosi = OutputDevice(mosi_pin, pin_factory=pin_factory) if miso_pin is not None: - self.miso = InputDevice(miso_pin) + self.miso = InputDevice(miso_pin, pin_factory=pin_factory) except: self.close() raise def close(self): - super(SPISoftwareBus, self).close() + super().close() if getattr(self, 'lock', None): with self.lock: if self.miso is not None: @@ -82,7 +176,7 @@ def closed(self): return self.lock is None @classmethod - def _shared_key(cls, clock_pin, mosi_pin, miso_pin): + def _shared_key(cls, clock_pin, mosi_pin, miso_pin, *, pin_factory=None): return (clock_pin, mosi_pin, miso_pin) def transfer(self, data, clock_phase=False, lsb_first=False, bits_per_word=8): diff --git a/gpiozero/pins/style.py b/gpiozero/pins/style.py new file mode 100644 index 000000000..7f48c6b42 --- /dev/null +++ b/gpiozero/pins/style.py @@ -0,0 +1,92 @@ +# vim: set fileencoding=utf-8: +# +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2021-2023 Dave Jones +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import sys + + +# ANSI color codes, for the pretty printers (nothing comprehensive, just enough +# for our purposes) + +class Style: + def __init__(self, color=None): + self.color = self._term_supports_color() if color is None else bool(color) + self.effects = { + 'reset': 0, + 'bold': 1, + 'normal': 22, + } + self.colors = { + 'black': 0, + 'red': 1, + 'green': 2, + 'yellow': 3, + 'blue': 4, + 'magenta': 5, + 'cyan': 6, + 'white': 7, + 'default': 9, + } + + @staticmethod + def _term_supports_color(): + try: + stdout_fd = sys.stdout.fileno() + except IOError: + return False + else: + is_a_tty = os.isatty(stdout_fd) + is_windows = sys.platform.startswith('win') + return is_a_tty and not is_windows + + @classmethod + def from_style_content(cls, format_spec): + specs = set(format_spec.split()) + style = specs & {'mono', 'color'} + content = specs - style + if len(style) > 1: + raise ValueError('cannot specify both mono and color styles') + try: + style = style.pop() + except KeyError: + style = 'color' if cls._term_supports_color() else 'mono' + if not content: + content = 'full' + else: + content = ' '.join(content) + return cls(style == 'color'), content + + def __call__(self, format_spec): + specs = format_spec.split() + codes = [] + fore = True + for spec in specs: + if spec == 'on': + fore = False + else: + try: + codes.append(self.effects[spec]) + except KeyError: + try: + if fore: + codes.append(30 + self.colors[spec]) + else: + codes.append(40 + self.colors[spec]) + except KeyError: + raise ValueError(f'invalid format specification "{spec}"') + if self.color: + codes = ';'.join(str(code) for code in codes) + return f'\x1b[{codes}m' + else: + return '' + + def __format__(self, format_spec): + if format_spec == '': + return 'color' if self.color else 'mono' + else: + return self(format_spec) diff --git a/gpiozero/spi_devices.py b/gpiozero/spi_devices.py index ab185d21c..34bfb55ed 100644 --- a/gpiozero/spi_devices.py +++ b/gpiozero/spi_devices.py @@ -1,49 +1,18 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Grzegorz Szymaszek +# Copyright (c) 2020 Fangchen Li # Copyright (c) 2016-2019 Andrew Scheller # Copyright (c) 2016-2018 Ben Nuttall # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause from math import log, ceil from operator import or_ -try: - from functools import reduce -except ImportError: - pass # py2's reduce is built-in +from functools import reduce from .exc import DeviceClosed, SPIBadChannel, InputDeviceError from .devices import Device @@ -59,16 +28,14 @@ class SPIDevice(Device): """ def __init__(self, **spi_args): self._spi = None - super(SPIDevice, self).__init__( - pin_factory=spi_args.pop('pin_factory', None) - ) + super().__init__(pin_factory=spi_args.pop('pin_factory', None)) self._spi = self.pin_factory.spi(**spi_args) def close(self): if getattr(self, '_spi', None): self._spi.close() self._spi = None - super(SPIDevice, self).close() + super().close() @property def closed(self): @@ -109,9 +76,11 @@ def _words_to_int(self, words, expected_bits=None): def __repr__(self): try: self._check_open() - return "" % (self.__class__.__name__, self._spi) + return ( + f"") except DeviceClosed: - return "" % self.__class__.__name__ + return f"" class AnalogInputDevice(SPIDevice): @@ -149,14 +118,15 @@ class AnalogInputDevice(SPIDevice): def __init__(self, bits, max_voltage=3.3, **spi_args): if bits is None: - raise InputDeviceError('you must specify the bit resolution of the device') + raise InputDeviceError( + 'you must specify the bit resolution of the device') self._bits = bits self._min_value = -(2 ** bits) self._range = 2 ** (bits + 1) - 1 if max_voltage <= 0: raise InputDeviceError('max_voltage must be positive') self._max_voltage = float(max_voltage) - super(AnalogInputDevice, self).__init__(shared=True, **spi_args) + super().__init__(shared=True, **spi_args) @property def bits(self): @@ -209,7 +179,7 @@ def __init__(self, channel=0, bits=10, differential=False, max_voltage=3.3, **spi_args): self._channel = channel self._differential = bool(differential) - super(MCP3xxx, self).__init__(bits, max_voltage, **spi_args) + super().__init__(bits, max_voltage, **spi_args) @property def channel(self): @@ -294,7 +264,7 @@ def _send(self): # the result with the final byte of output. A start "1" bit is then # transmitted, followed by the single/differential bit (M); 1 for # single-ended read, 0 for differential read. Next comes a single bit - # for channel (M) then the MSBF bit (L) which selects whether the data + # for channel (C) then the MSBF bit (L) which selects whether the data # will be read out in MSB form only (1) or whether LSB read-out will # occur after MSB read-out (0). # @@ -313,8 +283,7 @@ class MCP30xx(MCP3xxx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): - super(MCP30xx, self).__init__(channel, 10, differential, max_voltage, - **spi_args) + super().__init__(channel, 10, differential, max_voltage, **spi_args) class MCP32xx(MCP3xxx): @@ -324,8 +293,7 @@ class MCP32xx(MCP3xxx): """ def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): - super(MCP32xx, self).__init__(channel, 12, differential, max_voltage, - **spi_args) + super().__init__(channel, 12, differential, max_voltage, **spi_args) class MCP33xx(MCP3xxx): @@ -336,8 +304,7 @@ class MCP33xx(MCP3xxx): """ def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): - super(MCP33xx, self).__init__(channel, 12, differential, max_voltage, - **spi_args) + super().__init__(channel, 12, differential, max_voltage, **spi_args) def _read(self): if self.differential: @@ -349,7 +316,7 @@ def _read(self): else: return result else: - return super(MCP33xx, self)._read() + return super()._read() def _send(self): # MCP3302/04 protocol looks like the following: @@ -391,7 +358,7 @@ def differential(self): :class:`MCP3304` in differential mode, channel 0 is read relative to channel 1). """ - return super(MCP33xx, self).differential + return super().differential @property def value(self): @@ -399,7 +366,7 @@ def value(self): The current value read from the device, scaled to a value between 0 and 1 (or -1 to +1 for devices operating in differential mode). """ - return super(MCP33xx, self).value + return super().value class MCP3001(MCP30xx): @@ -411,7 +378,7 @@ class MCP3001(MCP30xx): .. _MCP3001: http://www.farnell.com/datasheets/630400.pdf """ def __init__(self, max_voltage=3.3, **spi_args): - super(MCP3001, self).__init__(0, True, max_voltage, **spi_args) + super().__init__(0, True, max_voltage, **spi_args) def _read(self): # MCP3001 protocol looks like the following: @@ -432,7 +399,7 @@ class MCP3002(MCP30xx, MCP3xx2): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 2: raise SPIBadChannel('channel must be 0 or 1') - super(MCP3002, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3004(MCP30xx): @@ -445,7 +412,7 @@ class MCP3004(MCP30xx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 4: raise SPIBadChannel('channel must be between 0 and 3') - super(MCP3004, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3008(MCP30xx): @@ -458,7 +425,7 @@ class MCP3008(MCP30xx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 8: raise SPIBadChannel('channel must be between 0 and 7') - super(MCP3008, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3201(MCP32xx): @@ -470,7 +437,7 @@ class MCP3201(MCP32xx): .. _MCP3201: http://www.farnell.com/datasheets/1669366.pdf """ def __init__(self, max_voltage=3.3, **spi_args): - super(MCP3201, self).__init__(0, True, max_voltage, **spi_args) + super().__init__(0, True, max_voltage, **spi_args) def _read(self): # MCP3201 protocol looks like the following: @@ -491,7 +458,7 @@ class MCP3202(MCP32xx, MCP3xx2): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 2: raise SPIBadChannel('channel must be 0 or 1') - super(MCP3202, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3204(MCP32xx): @@ -504,7 +471,7 @@ class MCP3204(MCP32xx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 4: raise SPIBadChannel('channel must be between 0 and 3') - super(MCP3204, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3208(MCP32xx): @@ -517,7 +484,7 @@ class MCP3208(MCP32xx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 8: raise SPIBadChannel('channel must be between 0 and 7') - super(MCP3208, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3301(MCP33xx): @@ -529,7 +496,7 @@ class MCP3301(MCP33xx): .. _MCP3301: http://www.farnell.com/datasheets/1669397.pdf """ def __init__(self, max_voltage=3.3, **spi_args): - super(MCP3301, self).__init__(0, True, max_voltage, **spi_args) + super().__init__(0, True, max_voltage, **spi_args) def _read(self): # MCP3301 protocol looks like the following: @@ -558,7 +525,7 @@ class MCP3302(MCP33xx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 4: raise SPIBadChannel('channel must be between 0 and 4') - super(MCP3302, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) class MCP3304(MCP33xx): @@ -574,4 +541,4 @@ class MCP3304(MCP33xx): def __init__(self, channel=0, differential=False, max_voltage=3.3, **spi_args): if not 0 <= channel < 8: raise SPIBadChannel('channel must be between 0 and 7') - super(MCP3304, self).__init__(channel, differential, max_voltage, **spi_args) + super().__init__(channel, differential, max_voltage, **spi_args) diff --git a/gpiozero/threads.py b/gpiozero/threads.py index 95604d4d3..a45507aff 100644 --- a/gpiozero/threads.py +++ b/gpiozero/threads.py @@ -1,40 +1,12 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2016 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause from threading import Thread, Event @@ -42,6 +14,8 @@ _THREADS = set() + + def _threads_shutdown(): while _THREADS: threads = _THREADS.copy() @@ -55,29 +29,28 @@ def _threads_shutdown(): class GPIOThread(Thread): - def __init__(self, group=None, target=None, name=None, args=(), kwargs=None): + def __init__(self, target, args=(), kwargs=None, name=None): if kwargs is None: kwargs = {} self.stopping = Event() - super(GPIOThread, self).__init__(group, target, name, args, kwargs) + super().__init__(None, target, name, args, kwargs) self.daemon = True def start(self): self.stopping.clear() _THREADS.add(self) - super(GPIOThread, self).start() + super().start() def stop(self, timeout=10): self.stopping.set() self.join(timeout) def join(self, timeout=None): - super(GPIOThread, self).join(timeout) + super().join(timeout) if self.is_alive(): assert timeout is not None # timeout can't be None here because if it was, then join() # wouldn't return until the thread was dead - raise ZombieThread( - "Thread failed to die within %d seconds" % timeout) + raise ZombieThread(f"Thread failed to die within {timeout} seconds") else: _THREADS.discard(self) diff --git a/gpiozero/tones.py b/gpiozero/tones.py index 1fe5f8c7c..1331ad011 100644 --- a/gpiozero/tones.py +++ b/gpiozero/tones.py @@ -1,50 +1,16 @@ # vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2019 Dave Jones -# Copyright (c) 2019 Ben Nuttall -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2019-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2019 Ben Nuttall # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, -) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause import re import warnings -from collections import namedtuple -try: - from math import log2 -except ImportError: - from .compat import log2 +from math import log2 from .exc import AmbiguousTone @@ -107,26 +73,22 @@ class to easily represent musical tones. The class can be constructed in a '#': 1, } regex = re.compile( - r'(?P[A-G])' - r'(?P[%s]?)' - r'(?P[0-9])' % ''.join(semitones.keys())) - - def __new__(cls, value=None, **kwargs): - if value is None: - if len(kwargs) != 1: - raise TypeError('expected precisely one keyword argument') - key, value = kwargs.popitem() - try: - return { - 'frequency': cls.from_frequency, - 'midi': cls.from_midi, - 'note': cls.from_note, - }[key](value) - except KeyError: - raise TypeError('unexpected keyword argument %r' % key) + rf'(?P[A-G])' + rf'(?P[{"".join(semitones.keys())}]?)' + rf'(?P[0-9])') + + def __new__(cls, value=None, *, frequency=None, midi=None, note=None): + n = sum(1 for arg in (value, frequency, midi, note) if arg is not None) + if n != 1: + raise TypeError('must specify a value, frequency, midi number, ' + 'or note') + if note is not None: + return cls.from_note(note) + elif midi is not None: + return cls.from_midi(midi) + elif frequency is not None: + return cls.from_frequency(frequency) else: - if kwargs: - raise TypeError('cannot specify keywords with a value') if isinstance(value, (int, float)): if 0 <= value < 128: if value > 0: @@ -154,14 +116,14 @@ def __repr__(self): except ValueError: midi = '' else: - midi = ' midi=%r' % midi + midi = f' midi={midi!r}' try: note = self.note except ValueError: note = '' else: - note = ' note=%r' % note - return "" % (note, midi, self.frequency) + note = f' note={note!r}' + return f"" @classmethod def from_midi(cls, midi_note): @@ -177,7 +139,7 @@ def from_midi(cls, midi_note): A4_midi = 69 A4_freq = 440 return cls.from_frequency(A4_freq * 2 ** ((midi - A4_midi) / 12)) - raise ValueError('invalid MIDI note: %r' % midi) + raise ValueError(f'invalid MIDI note: {midi!r}') @classmethod def from_note(cls, note): @@ -191,6 +153,8 @@ def from_note(cls, note): represented as "A4". One semi-tone above this would be "A#4" or alternatively "Bb4". Unicode representations of sharp and flat are also accepted. + + .. _concert A: https://en.wikipedia.org/wiki/Concert_pitch """ if isinstance(note, bytes): note = note.decode('ascii') @@ -202,7 +166,7 @@ def from_note(cls, note): Tone.tones.index(match.group('note')) + Tone.semitones[match.group('semi')] + octave * 12) - raise ValueError('invalid note specification: %r' % note) + raise ValueError(f'invalid note specification: {note!r}') @classmethod def from_frequency(cls, freq): @@ -213,8 +177,8 @@ def from_frequency(cls, freq): .. _Hz: https://en.wikipedia.org/wiki/Hertz """ if 0 < freq <= 20000: - return super(Tone, cls).__new__(cls, freq) - raise ValueError('invalid frequency: %.2f' % freq) + return super().__new__(cls, freq) + raise ValueError(f'invalid frequency: {freq:.2f}') @property def frequency(self): @@ -236,7 +200,7 @@ def midi(self): result = int(round(12 * log2(self.frequency / 440) + 69)) if 0 <= result < 128: return result - raise ValueError('%f is outside the MIDI note range' % self.frequency) + raise ValueError(f'{self.frequency:f} is outside the MIDI note range') @property def note(self): @@ -256,7 +220,7 @@ def note(self): ('#' if Tone.tones[index] == Tone.tones[index - 1] else '') + str(octave) ) - raise ValueError('%f is outside the notation range' % self.frequency) + raise ValueError(f'{self.frequency:f} is outside the notation range') def up(self, n=1): """ diff --git a/gpiozero/tools.py b/gpiozero/tools.py index 482aaaa30..dd5db2592 100644 --- a/gpiozero/tools.py +++ b/gpiozero/tools.py @@ -1,62 +1,21 @@ # vim: set fileencoding=utf-8: # # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li # Copyright (c) 2016-2019 Ben Nuttall # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, -) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause from random import random from time import sleep -from .mixins import ValuesMixin -try: - from itertools import izip as zip -except ImportError: - pass from itertools import cycle -from math import sin, cos, pi -try: - from statistics import mean -except ImportError: - from .compat import mean -try: - from math import isclose -except ImportError: - from .compat import isclose +from math import sin, cos, pi, isclose +from statistics import mean + +from .mixins import ValuesMixin def _normalize(values): @@ -160,6 +119,28 @@ def scaled(values, output_min, output_max, input_min=0, input_max=1): yield (((v - input_min) / input_size) * output_size) + output_min +def scaled_full(values): + """ + A convenience function that builds on :func:`scaled`. It converts a + "half-range" value (0..1) to a "full-range" value (-1..1). This is + equivalent to calling:: + + scaled(values, -1, 1, 0, 1) + """ + return scaled(values, -1, 1, 0, 1) + + +def scaled_half(values): + """ + A convenience function that builds on :func:`scaled`. It converts a + "full-range" value (-1..1) to a "half-range" value (0..1). This is + equivalent to calling:: + + scaled(values, 0, 1, -1, 1) + """ + return scaled(values, 0, 1, -1, 1) + + def clamped(values, output_min=0, output_max=1): """ Returns *values* clamped from *output_min* to *output_max*, i.e. any items @@ -322,7 +303,6 @@ def all_values(*values): .. _logical conjunction: https://en.wikipedia.org/wiki/Logical_conjunction """ - print("here") values = [_normalize(v) for v in values] for v in zip(*values): yield all(v) @@ -629,7 +609,7 @@ def sin_values(period=360): effect with a couple of LEDs that repeats once a second:: from gpiozero import PWMLED - from gpiozero.tools import sin_values, scaled, inverted + from gpiozero.tools import sin_values, scaled_half, inverted from signal import pause red = PWMLED(2) @@ -637,7 +617,7 @@ def sin_values(period=360): red.source_delay = 0.01 blue.source_delay = red.source_delay - red.source = scaled(sin_values(100), 0, 1, -1, 1) + red.source = scaled_half(sin_values(100)) blue.source = inverted(red) pause() @@ -656,7 +636,7 @@ def cos_values(period=360): "siren" effect with a couple of LEDs that repeats once a second:: from gpiozero import PWMLED - from gpiozero.tools import cos_values, scaled, inverted + from gpiozero.tools import cos_values, scaled_half, inverted from signal import pause red = PWMLED(2) @@ -664,7 +644,7 @@ def cos_values(period=360): red.source_delay = 0.01 blue.source_delay = red.source_delay - red.source = scaled(cos_values(100), 0, 1, -1, 1) + red.source = scaled_half(cos_values(100)) blue.source = inverted(red) pause() @@ -733,6 +713,7 @@ def ramping_values(period=360): step *= -1 value += step + def zip_values(*devices): """ Provides a source constructed from the values of each item, for example:: diff --git a/gpiozerocli/__init__.py b/gpiozerocli/__init__.py index e69de29bb..0f77f623c 100644 --- a/gpiozerocli/__init__.py +++ b/gpiozerocli/__init__.py @@ -0,0 +1,71 @@ +# vim: set fileencoding=utf-8: +# +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2021-2024 Dave Jones +# +# SPDX-License-Identifier: BSD-3-Clause + +import os +import sys +import argparse + +# Remove the try clause when 3.7 support is no longer trivial +try: + from importlib_metadata import version +except ImportError: + from importlib.metadata import version + + +class CliTool: + """ + Base class for simple command line utilities. + + The doc-string of the class forms the basis for the utility's help text. + Instances are constructed with a :attr:`parser` which you can customize. + The :meth:`main` method will be called with the parsed command line + arguments and should return an appropriate exit code. + """ + + def __init__(self): + self.parser = argparse.ArgumentParser(description=self.__class__.__doc__) + self.parser.add_argument( + '--version', + action='version', + version=version('gpiozero')) + + def get_formatter(self): + return self.parser._get_formatter() + + def get_gpiozero_help(self): + fmt = self.get_formatter() + fmt.add_text( + """ + Unable to initialize GPIO Zero. This usually means that you are not + running %(prog)s on a Raspberry Pi. If you still wish to run + %(prog)s, set the GPIOZERO_PIN_FACTORY environment variable to + 'mock' and retry, or refer to the Remote GPIO section of the + manual* to configure your environment to remotely access your + Pi. + """) + fmt.add_text( + "* https://gpiozero.readthedocs.io/en/stable/remote_gpio.html") + return fmt.format_help() + + def __call__(self, args=None): + if args is None: + args = sys.argv[1:] + try: + return self.main(self.parser.parse_args(args)) or 0 + except argparse.ArgumentError as e: + # argparse errors are already nicely formatted, print to stderr and + # exit with code 2 + raise e + except Exception as e: + # Output anything else nicely formatted on stderr and exit code 1 + if int(os.environ.get('DEBUG', '0')): + raise + self.parser.exit(1, f'{self.parser.prog}: error: {e}\n') + + def main(self, args): + raise NotImplementedError diff --git a/gpiozerocli/pinout.py b/gpiozerocli/pinout.py index 8dd3019d1..8ba06fa7d 100755 --- a/gpiozerocli/pinout.py +++ b/gpiozerocli/pinout.py @@ -1,134 +1,82 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2017-2019 Dave Jones -# Copyright (c) 2017 Ben Nuttall -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2017-2023 Dave Jones +# Copyright (c) 2017 Ben Nuttall # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -""" -A utility for querying Raspberry Pi GPIO pin-out information. -""" - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, -) +# SPDX-License-Identifier: BSD-3-Clause import argparse import sys -import textwrap import warnings import webbrowser -from gpiozero import pi_info +from . import CliTool +from gpiozero import Device +from gpiozero.pins.pi import PiBoardInfo +from gpiozero.pins.style import Style -class PinoutTool(object): +class PinoutTool(CliTool): + """ + A utility for querying GPIO pin-out information. + """ def __init__(self): - self.parser = argparse.ArgumentParser( - description=__doc__ - ) + super().__init__() self.parser.add_argument( '-r', '--revision', dest='revision', - default='', - help='RPi revision. Default is to autodetect revision of current device' - ) + type=lambda s: int(s, base=16), + default=None, + help='Board revision. Default is to autodetect revision of ' + 'current device') self.parser.add_argument( '-c', '--color', action="store_true", default=None, - help='Force colored output (by default, the output will include ANSI' - 'color codes if run in a color-capable terminal). See also --monochrome' - ) + help='Force colored output (by default, the output will include ' + 'ANSI color codes if run in a color-capable terminal). See also ' + '--monochrome') self.parser.add_argument( '-m', '--monochrome', dest='color', action='store_false', - help='Force monochrome output. See also --color' - ) + help='Force monochrome output. See also --color') self.parser.add_argument( '-x', '--xyz', dest='xyz', action='store_true', - help='Open pinout.xyz in the default web browser' - ) - - def __call__(self, args=None): - if args is None: - args = sys.argv[1:] - try: - return self.main(self.parser.parse_args(args)) or 0 - except argparse.ArgumentError as e: - # argparse errors are already nicely formatted, print to stderr and - # exit with code 2 - raise e - except Exception as e: - raise - # Output anything else nicely formatted on stderr and exit code 1 - self.parser.exit(1, '{prog}: error: {message}\n'.format( - prog=self.parser.prog, message=e)) + help='Open pinout.xyz in the default web browser') def main(self, args): warnings.simplefilter('ignore') if args.xyz: webbrowser.open('https://pinout.xyz') else: - if args.revision == '': + if args.revision is None: try: - pi_info().pprint(color=args.color) + Device.ensure_pin_factory() + board_info = Device.pin_factory.board_info except ImportError: - formatter = self.parser._get_formatter() - formatter.add_text( - "Unable to initialize GPIO Zero. This usually means " - "that you are not running %(prog)s on a Raspberry Pi. " - "If you still wish to run %(prog)s, set the " - "GPIOZERO_PIN_FACTORY environment variable to 'mock' " - "and retry, or refer to the Remote GPIO section of " - "the manual* to configure your environment to " - "remotely access your Pi." - ) - formatter.add_text( - "* https://gpiozero.readthedocs.io/en/stable/" - "remote_gpio.html" - ) - sys.stderr.write(formatter.format_help()) + sys.stderr.write(self.get_gpiozero_help()) + return 1 except IOError: - raise IOError('This device is not a Raspberry Pi') + sys.stderr.write('Unrecognized board') + return 1 else: - pi_info(args.revision).pprint(color=args.color) - formatter = self.parser._get_formatter() + board_info = PiBoardInfo.from_revision(args.revision) + style = Style(color=args.color) + sys.stdout.write(f'{board_info:{style} full}') + formatter = self.get_formatter() formatter.add_text( "For further information, please refer to " - "https://pinout.xyz/" - ) - sys.stdout.write('\n') + "https://pinout.xyz/") + sys.stdout.write('\n\n') sys.stdout.write(formatter.format_help()) + def output(self, board): + return + main = PinoutTool() diff --git a/gpiozerocli/pintest.py b/gpiozerocli/pintest.py new file mode 100644 index 000000000..9f8bb89af --- /dev/null +++ b/gpiozerocli/pintest.py @@ -0,0 +1,154 @@ +# vim: set fileencoding=utf-8: +# +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2021-2023 Dave Jones +# +# SPDX-License-Identifier: BSD-3-Clause + +import argparse +import sys +import warnings + +from . import CliTool +from gpiozero import Device +from gpiozero.pins.pi import PiBoardInfo + + +class PintestTool(CliTool): + """ + A utility for testing the GPIO pins on a Raspberry Pi, inspired by pigpio's + gpiotest example script, and wiringPi's pintest utility. + """ + def __init__(self): + super().__init__() + self.parser.add_argument( + '-p', '--pins', + dest='pins', default='', + help="The pin(s) to test. Can be specified as a comma-separated " + "list of pins. Pin numbers can be given in any form accepted by " + "gpiozero, e.g. 14, GPIO14, BOARD8. The default is to test all " + "pins") + self.parser.add_argument( + '-s', '--skip', + dest='skip', default='', + help="The pin(s) to skip testing. Can be specified as comma-" + "separated list of pins. Pin numbers can be given in any form " + "accepted by gpiozero, e.g. 14, GPIO14, BOARD8. The default is " + "to skip no pins") + self.parser.add_argument( + '-y', '--yes', + dest='prompt', action='store_false', + help="Proceed without prompting") + self.parser.add_argument( + '-r', '--revision', + dest='revision', type=lambda s: int(s, base=16), default=None, + help="Force board revision. Default is to autodetect revision of " + "current device. You should avoid this option unless you are " + "very sure the detection is incorrect") + + def main(self, args): + if args.revision is None: + try: + Device.ensure_pin_factory() + board_info = Device.pin_factory.board_info + except ImportError: + sys.stderr.write(self.get_gpiozero_help()) + return 1 + except IOError: + sys.stderr.write('Unrecognized board') + return 1 + + pins = self.get_pins( + board_info, + include=args.pins.split(',') if args.pins else (), + exclude=args.skip.split(',') if args.skip else ()) + fmt = self.get_formatter() + fmt.add_text( + f""" + This program checks the board's user-accessible GPIO pins. The + board's model is: {board_info.description}. The following pins are + selected for testing: + """) + fmt.add_text(', '.join(pin.name for pin in pins)) + fmt.add_text( + """ + Please ensure that nothing is connected to any of the pins listed + above for the test duration. + """) + print(fmt.format_help()) + if args.prompt: + s = input('Proceed with test? [y/N] ').strip().lower() + if s != 'y': + return 2 + + failed = [] + for pin in pins: + try: + print(f'Testing {pin.name}...', end='') + self.test_pin(pin) + except ValueError as e: + print(e) + failed.append(pin) + else: + print('ok') + + return 1 if failed else 0 + + def get_pins(self, board, include, exclude): + if not include: + pins = { + pin + for header in board.headers.values() + for pin in header.pins.values() + if 'gpio' in pin.interfaces + } + else: + pins = { + pin + for name in include + for header, pin in board.find_pin(name) + } + skip = { + pin + for name in exclude + for header, pin in board.find_pin(name) + } + pins -= skip + for pin in pins: + if 'gpio' not in pin.interfaces: + raise ValueError(f'{pin.spec} is not a GPIO pin') + return pins + + def test_pin(self, pin_info): + with Device.pin_factory.pin(pin_info.name) as pin: + save_function = pin.function + save_state = pin.state + try: + pin.function = 'output' + pin.state = 0 + if pin.state != 0: + raise ValueError(f'Write 0 to {pin_info.function} failed') + pin.state = 1 + if pin.state != 1: + raise ValueError(f'Write 1 to {pin_info.function} failed') + pin.function = 'input' + if pin_info.pull != 'up': + pin.pull = 'down' + if pin.state != 0: + raise ValueError( + f'Pull down on {pin_info.function} failed') + if pin_info.pull != 'down': + pin.pull = 'up' + if pin.state != 1: + raise ValueError( + f'Pull up on {pin_info.function} failed') + if pin_info.pull == '': + pin.pull = 'floating' + finally: + pin.function = save_function + if save_function == 'output': + pin.state = save_state + + +main = PintestTool() diff --git a/rtd_requirements.txt b/rtd_requirements.txt new file mode 100644 index 000000000..fa2610e52 --- /dev/null +++ b/rtd_requirements.txt @@ -0,0 +1,10 @@ +. +Sphinx==8.2.3 +sphinx-rtd-theme==3.0.2 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 diff --git a/scripts/class_graph b/scripts/class_graph new file mode 100755 index 000000000..d122206e8 --- /dev/null +++ b/scripts/class_graph @@ -0,0 +1,229 @@ +#!/usr/bin/python3 + +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script generates Graphviz-compatible dot scripts from the class +definitions of the containing project. Specify the root class to generate with +the -i (multiple roots can be specified). Specify parts of the hierarchy to +exclude with -x. Default configurations can be specified in the containing +project's setup.cfg under [{SETUP_SECTION}] +""" + +from __future__ import annotations + +import re +import sys +assert sys.version_info >= (3, 6), 'Script requires Python 3.6+' +import typing as t +from pathlib import Path +from configparser import ConfigParser +from argparse import ArgumentParser, Namespace, FileType + + +PROJECT_ROOT = (Path(__file__).parent / '..').resolve() +SETUP_SECTION = str(Path(__file__).name) + ':settings' + + +def main(args: t.List[str] = None): + if args is None: + args = sys.argv[1:] + config = get_config(args) + + m = make_class_map(config.source, config.omit) + if config.include or config.exclude: + m = filter_map(m, include_roots=config.include, + exclude_roots=config.exclude) + config.output.write(render_map(m, config.abstract)) + + +def get_config(args: t.List[str]) -> Namespace: + config = ConfigParser( + defaults={ + 'source': '', + 'include': '', + 'exclude': '', + 'abstract': '', + 'omit': '', + 'output': '-', + }, + delimiters=('=',), default_section=SETUP_SECTION, + empty_lines_in_values=False, interpolation=None, + converters={'list': lambda s: s.strip().splitlines()}) + config.read(PROJECT_ROOT / 'setup.cfg') + sect = config[SETUP_SECTION] + # Resolve source and output defaults relative to setup.cfg + if sect['source']: + sect['source'] = '\n'.join( + str(PROJECT_ROOT / source) + for source in sect.getlist('source') + ) + if sect['output'] and sect['output'] != '-': + sect['output'] = str(PROJECT_ROOT / sect['output']) + + parser = ArgumentParser(description=__doc__.format(**globals())) + parser.add_argument( + '-s', '--source', action='append', metavar='PATH', + default=sect.getlist('source'), + help="the pattern(s) of files to search for classes; can be specified " + "multiple times. Default: %(default)r") + parser.add_argument( + '-i', '--include', action='append', metavar='CLASS', + default=sect.getlist('exclude'), + help="only include classes which have BASE somewhere in their " + "ancestry; can be specified multiple times. Default: %(default)r") + parser.add_argument( + '-x', '--exclude', action='append', metavar='CLASS', + default=sect.getlist('exclude'), + help="exclude any classes which have BASE somewhere in their " + "ancestry; can be specified multiple times. Default: %(default)r") + parser.add_argument( + '-o', '--omit', action='append', metavar='CLASS', + default=sect.getlist('omit'), + help="omit the specified class, but not its descendents from the " + "chart; can be specified multiple times. Default: %(default)r") + parser.add_argument( + '-a', '--abstract', action='append', metavar='CLASS', + default=sect.getlist('abstract'), + help="mark the specified class as abstract, rendering it in a " + "different color; can be specified multiple times. Default: " + "%(default)r") + parser.add_argument( + 'output', nargs='?', type=FileType('w'), + default=sect['output'], + help="the file to write the output to; defaults to stdout") + ns = parser.parse_args(args) + ns.abstract = set(ns.abstract) + ns.include = set(ns.include) + ns.exclude = set(ns.exclude) + ns.omit = set(ns.omit) + if not ns.source: + ns.source = [str(PROJECT_ROOT)] + ns.source = set(ns.source) + return ns + + +def make_class_map(search_paths: t.List[str], omit: t.Set[str])\ + -> t.Dict[str, t.Set[str]]: + """ + Find all Python source files under *search_paths*, extract (via a crude + regex) all class definitions and return a mapping of class-name to the list + of base classes. + + All classes listed in *omit* will be excluded from the result, but not + their descendents (useful for excluding "object" etc.) + """ + def find_classes() -> t.Iterator[t.Tuple[str, t.Set[str]]]: + class_re = re.compile( + r'^class\s+(?P\w+)\s*(?:\((?P.*)\))?:', re.MULTILINE) + for path in search_paths: + p = Path(path) + for py_file in p.parent.glob(p.name): + with py_file.open() as f: + for match in class_re.finditer(f.read()): + if match.group('name') not in omit: + yield match.group('name'), { + base.strip() + for base in ( + match.group('bases') or 'object' + ).split(',') + if base.strip() not in omit + } + return { + name: bases + for name, bases in find_classes() + } + + +def filter_map(class_map: t.Dict[str, t.Set[str]], include_roots: t.Set[str], + exclude_roots: t.Set[str]) -> t.Dict[str, t.Set[str]]: + """ + Returns *class_map* (which is a mapping such as that returned by + :func:`make_class_map`), with only those classes which have at least one + of the *include_roots* in their ancestry, and none of the *exclude_roots*. + """ + def has_parent(cls: str, parent: str) -> bool: + return cls == parent or any( + has_parent(base, parent) for base in class_map.get(cls, ())) + + filtered = { + name: bases + for name, bases in class_map.items() + if (not include_roots or + any(has_parent(name, root) for root in include_roots)) + and not any(has_parent(name, root) for root in exclude_roots) + } + pure_bases = { + base for name, bases in filtered.items() for base in bases + } - set(filtered) + # Make a second pass to fill in missing links between classes that are + # only included as bases of other classes + for base in pure_bases: + filtered[base] = pure_bases & class_map[base] + return filtered + + +def render_map(class_map: t.Dict[str, t.Set[str]], abstract: t.Set[str]) -> str: + """ + Renders *class_map* (which is a mapping such as that returned by + :func:`make_class_map`) to graphviz's dot language. + + The *abstract* sequence determines which classes will be rendered lighter + to indicate their abstract nature. All classes with names ending "Mixin" + will be implicitly rendered in a different style. + """ + def all_names(class_map: t.Dict[str, t.Set[str]]) -> t.Iterator[str]: + for name, bases in class_map.items(): + yield name + for base in bases: + yield base + + template = """\ +digraph classes {{ + graph [rankdir=RL]; + node [shape=rect, style=filled, fontname=Sans, fontsize=10]; + edge []; + + /* Mixin classes */ + node [color="#c69ee0", fontcolor="#000000"] + {mixin_nodes} + + /* Abstract classes */ + node [color="#9ec6e0", fontcolor="#000000"] + {abstract_nodes} + + /* Concrete classes */ + node [color="#2980b9", fontcolor="#ffffff"]; + {concrete_nodes} + + /* Edges */ + {edges} +}} +""" + + return template.format( + mixin_nodes='\n '.join( + '{name};'.format(name=name) + for name in sorted(set(all_names(class_map))) + if name.endswith('Mixin') + ), + abstract_nodes='\n '.join( + '{name};'.format(name=name) + for name in sorted(abstract & set(all_names(class_map))) + ), + concrete_nodes='\n '.join( + '{name};'.format(name=name) + for name in sorted(set(all_names(class_map))) + if not name.endswith('Mixin') + and not name in abstract + ), + edges='\n '.join( + '{name}->{base};'.format(name=name, base=base) + for name, bases in sorted(class_map.items()) + for base in sorted(bases) + ), + ) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/copyrights b/scripts/copyrights new file mode 100755 index 000000000..10ce4f486 --- /dev/null +++ b/scripts/copyrights @@ -0,0 +1,403 @@ +#!/usr/bin/python3 + +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script updates the copyright headers on all files project-wide. It derives +the authorship and copyright years information from the git history of the +project; hence, this script must be run within a git clone of the project's +repository. Options are available to specify the license text file, the files +to edit, and files to exclude. Default options can be specified in the +containing project's setup.cfg under [{SETUP_SECTION}] +""" + +from __future__ import annotations + +import os +import sys +assert sys.version_info >= (3, 6), 'Script requires Python 3.6+' +import tempfile +import typing as t +from argparse import ArgumentParser, Namespace +from configparser import ConfigParser +from operator import attrgetter +from itertools import groupby +from datetime import datetime +from subprocess import Popen, PIPE, DEVNULL +from pathlib import Path +from fnmatch import fnmatch + + +PROJECT_ROOT = (Path(__file__).parent / '..').resolve() +SETUP_SECTION = str(Path(__file__).name) + ':settings' +SPDX_PREFIX = 'SPDX-License-Identifier:' +COPYRIGHT_PREFIX = 'Copyright (c)' + + +def main(args: t.List[str] = None): + if args is None: + args = sys.argv[1:] + config = get_config(args) + + writer = CopyWriter.from_config(config) + for path, copyrights in get_copyrights(config.include, config.exclude): + print(f'Re-writing {path}...') + copyrights = sorted( + copyrights, reverse=True, key=lambda c: (max(c.years), c.author)) + with AtomicReplaceFile(path, encoding='utf-8') as target: + with path.open('r') as source: + for chunk in writer.transform(source, copyrights): + target.write(chunk) + + +def get_config(args: t.List[str]) -> Namespace: + config = ConfigParser( + defaults={ + 'include': '**/*', + 'exclude': '', + 'license': 'LICENSE.txt', + 'preamble': '', + 'strip_preamble': 'false', + 'spdx_prefix': SPDX_PREFIX, + 'copy_prefix': COPYRIGHT_PREFIX, + }, + delimiters=('=',), default_section=SETUP_SECTION, + empty_lines_in_values=False, interpolation=None, + converters={'list': lambda s: s.strip().splitlines()}) + config.read(PROJECT_ROOT / 'setup.cfg') + sect = config[SETUP_SECTION] + # Resolve license default relative to setup.cfg + if sect['license']: + sect['license'] = str(PROJECT_ROOT / sect['license']) + + parser = ArgumentParser(description=__doc__.format(**globals())) + parser.add_argument( + '-i', '--include', action='append', metavar='GLOB', + default=sect.getlist('include'), + help="The set of patterns that a file must match to be included in " + "the set of files to re-write. Can be specified multiple times to " + "add several patterns. Default: %(default)r") + parser.add_argument( + '-e', '--exclude', action='append', metavar='GLOB', + default=sect.getlist('exclude'), + help="The set of patterns that a file must *not* match to be included " + "in the set of files to re-write. Can be specified multiple times to " + "add several patterns. Default: %(default)r") + parser.add_argument( + '-l', '--license', action='store', type=Path, metavar='PATH', + default=sect['license'], + help="The file containing the project's license text. If this file " + "contains a SPDX-License-Identifier line (in addition to the license " + "text itself), then matching license text found in source files will " + "be replaced by the SPDX-License-Identifier line (appropriately " + "commented). Default: %(default)s") + parser.add_argument( + '-p', '--preamble', action='append', metavar='STR', + default=sect.getlist('preamble'), + help="The line(s) of text to insert before the copyright attributions " + "in source files. This is typically a brief description of the " + "project. Can be specified multiple times to add several lines. " + "Default: %(default)r") + parser.add_argument( + '-S', '--spdx-prefix', action='store', metavar='STR', + default=sect['spdx_prefix'], + help="The prefix on the line in the license file, and within comments " + "of source files that identifies the appropriate license from the " + "SPDX list. Default: %(default)r") + parser.add_argument( + '-C', '--copy-prefix', action='store', metavar='STR', + default=sect['copy_prefix'], + help="The prefix before copyright attributions in source files. " + "Default: %(default)r") + parser.add_argument( + '--no-strip-preamble', action='store_false', dest='strip_preamble') + parser.add_argument( + '--strip-preamble', action='store_true', + default=sect.getboolean('strip-preamble'), + help="If enabled, any existing preamble matching that specified " + "by --preamble will be removed. This can be used to change the " + "preamble text in files by first specifying the old preamble with " + "this option, then running a second time with the new preamble") + + ns = parser.parse_args(args) + ns.include = set(ns.include) + ns.exclude = set(ns.exclude) + return ns + + +class Copyright(t.NamedTuple): + author: str + email: str + years: t.Set[int] + + def __str__(self): + if len(self.years) > 1: + years = f'{min(self.years)}-{max(self.years)}' + else: + years = f'{min(self.years)}' + return f'{years} {self.author} <{self.email}>' + + +def get_copyrights(include: t.Set[str], exclude: t.Set[str])\ + -> t.Iterator[t.Tuple[Path, t.Iterable[Copyright]]]: + sorted_blame = sorted( + get_contributions(include, exclude), + key=lambda c: (c.path, c.author, c.email) + ) + blame_by_file = { + path: list(file_contributions) + for path, file_contributions in groupby( + sorted_blame, key=attrgetter('path') + ) + } + for path, file_contributors in blame_by_file.items(): + it = groupby(file_contributors, key=lambda c: (c.author, c.email)) + copyrights = [ + Copyright(author, email, {y.year for y in years}) + for (author, email), years in it + ] + yield path, copyrights + + +class Contribution(t.NamedTuple): + author: str + email: str + year: int + path: Path + + +def get_contributions(include: t.Set[str], exclude: t.Set[str])\ + -> t.Iterator[Contribution]: + for path in get_source_paths(include, exclude): + blame = Popen( + ['git', 'blame', '--line-porcelain', 'HEAD', '--', str(path)], + stdout=PIPE, + stderr=PIPE, + universal_newlines=True + ) + author = email = year = None + if blame.stdout is not None: + for line in blame.stdout: + if line.startswith('author '): + author = line.split(' ', 1)[1].rstrip() + elif line.startswith('author-mail '): + email = line.split(' ', 1)[1].rstrip() + email = email.lstrip('<').rstrip('>') + elif line.startswith('author-time '): + # Forget the timezone; we only want the year anyway + timestamp = int(line.split(' ', 1)[1].strip()) + year = datetime.fromtimestamp(timestamp).year + elif line.startswith('filename '): + assert author is not None + assert email is not None + assert year is not None + yield Contribution( + author=author, email=email, year=year, path=path) + author = email = year = None + blame.wait() + assert blame.returncode == 0 + + +def get_source_paths(include: t.Set[str], exclude: t.Set[str])\ + -> t.Iterator[Path]: + ls_tree = Popen( + ['git', 'ls-tree', '-r', '--name-only', 'HEAD'], + stdout=PIPE, stderr=DEVNULL, universal_newlines=True) + if not include: + include = {'*'} + if ls_tree.stdout is not None: + for filename in ls_tree.stdout: + filename = filename.strip() + if any(fnmatch(filename, pattern) for pattern in exclude): + continue + if any(fnmatch(filename, pattern) for pattern in include): + yield Path(filename) + ls_tree.wait() + assert ls_tree.returncode == 0 + + +class License(t.NamedTuple): + ident: t.Optional[str] + text: t.List[str] + + +def get_license(path: Path, *, spdx_prefix: str = SPDX_PREFIX) -> License: + with open(path, 'r') as f: + lines = f.read().splitlines() + + idents = [ + line.rstrip() for line in lines + if line.startswith(spdx_prefix) + ] + ident = None + if len(idents) > 1: + raise RuntimeError(f'More than one {spdx_prefix} line in {path}!') + elif len(idents) == 1: + ident = idents[0] + + body = [ + line.rstrip() for line in lines + if not line.startswith(spdx_prefix) + ] + while not body[0]: + del body[0] + while not body[-1]: + del body[-1] + return License(ident, body) + + +class CopyWriter: + """ + Transformer for the copyright header in source files. The :meth:`transform` + method can be called with a file-like object as the *source* and will + yield chunks of replacement data to be written to the replacement. + """ + + # The script's kinda dumb at this point - only handles straight-forward + # line-based comments, not multi-line delimited styles like /*..*/ + COMMENTS = { + '': '#', + '.c': '//', + '.cpp': '//', + '.js': '//', + '.py': '#', + '.rst': '..', + '.sh': '#', + '.sql': '--', + } + + def __init__(self, license: Path=Path('LICENSE.txt'), + preamble: t.List[str]=None, + spdx_prefix: str=SPDX_PREFIX, + copy_prefix: str=COPYRIGHT_PREFIX): + if preamble is None: + preamble = [] + self.license = get_license(license, spdx_prefix=spdx_prefix) + self.preamble = preamble + self.spdx_prefix = spdx_prefix + self.copy_prefix = copy_prefix + + @classmethod + def from_config(cls, config: Namespace) -> CopyWriter: + return cls( + config.license, config.preamble, + config.spdx_prefix, config.copy_prefix) + + def transform(self, source: t.TextIO, + copyrights: t.List[Copyright], *, + comment_prefix: str=None) -> t.Iterator[str]: + if comment_prefix is None: + comment_prefix = self.COMMENTS[Path(source.name).suffix] + license_start = self.license.text[0] + license_end = self.license.text[-1] + state = 'header' + empty = True + for linenum, line in enumerate(source, start=1): + if state == 'header': + if linenum == 1 and line.startswith('#!'): + yield line + empty = False + elif linenum < 3 and ( + 'fileencoding=' in line or '-*- coding:' in line): + yield line + empty = False + elif line.rstrip() == comment_prefix: + pass # skip blank comment lines + elif line.startswith(f'{comment_prefix} {self.spdx_prefix}'): + pass # skip existing SPDX ident + elif line.startswith(f'{comment_prefix} {self.copy_prefix}'): + pass # skip existing copyright lines + elif any(line.startswith(f'{comment_prefix} {pre_line}') + for pre_line in self.preamble): + pass # skip existing preamble + elif line.startswith(f'{comment_prefix} {license_start}'): + state = 'license' # skip existing license lines + else: + yield from self._generate_header( + copyrights, comment_prefix, empty) + state = 'blank' + elif state == 'license': + if line.startswith(f'{comment_prefix} {license_end}'): + yield from self._generate_header( + copyrights, comment_prefix, empty) + state = 'blank' + continue + if state == 'blank': + # Ensure there's a blank line between license and start of the + # source body + if line.strip(): + yield '\n' + yield line + state = 'body' + elif state == 'body': + yield line + + def _generate_header(self, copyrights: t.Iterable[Copyright], + comment_prefix: str, empty: bool) -> t.Iterator[str]: + if not empty: + yield comment_prefix + '\n' + for line in self.preamble: + yield f'{comment_prefix} {line}\n' + if self.preamble: + yield comment_prefix + '\n' + for copyright in copyrights: + yield f'{comment_prefix} {self.copy_prefix} {copyright!s}\n' + yield comment_prefix + '\n' + if self.license.ident: + yield f'{comment_prefix} {self.license.ident}\n' + else: + for line in self.license.text: + if line: + yield f'{comment_prefix} {line}\n' + else: + yield comment_prefix + '\n' + + +class AtomicReplaceFile: + """ + A context manager for atomically replacing a target file. + + Uses :class:`tempfile.NamedTemporaryFile` to construct a temporary file in + the same directory as the target file. The associated file-like object is + returned as the context manager's variable; you should write the content + you wish to this object. + + When the context manager exits, if no exception has occurred, the temporary + file will be renamed over the target file atomically (after copying + permissions from the target file). If an exception occurs during the + context manager's block, the temporary file will be deleted leaving the + original target file unaffected and the exception will be re-raised. + + :param pathlib.Path path: + The full path and filename of the target file. This is expected to be + an absolute path. + + :param str encoding: + If ``None`` (the default), the temporary file will be opened in binary + mode. Otherwise, this specifies the encoding to use with text mode. + """ + def __init__(self, path: t.Union[str, Path], encoding: str = None): + if isinstance(path, str): + path = Path(path) + self._path = path + self._tempfile = tempfile.NamedTemporaryFile( + mode='wb' if encoding is None else 'w', + dir=str(self._path.parent), encoding=encoding, delete=False) + self._withfile = None + + def __enter__(self): + self._withfile = self._tempfile.__enter__() + return self._withfile + + def __exit__(self, exc_type, exc_value, exc_tb): + os.fchmod(self._withfile.file.fileno(), self._path.stat().st_mode) + result = self._tempfile.__exit__(exc_type, exc_value, exc_tb) + if exc_type is None: + os.rename(self._withfile.name, str(self._path)) + else: + os.unlink(self._withfile.name) + return result + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/previewer b/scripts/previewer new file mode 100755 index 000000000..dbd502e5a --- /dev/null +++ b/scripts/previewer @@ -0,0 +1,211 @@ +#!/usr/bin/python3 + +# SPDX-License-Identifier: BSD-3-Clause + +""" +This script builds the HTML documentation of the containing project, and serves +it from a trivial built-in web-server. It then watches the project source code +for changes, and rebuilds the documentation as necessary. Options are available +to specify the build output directory, the build command, and the paths to +watch for changes. Default options can be specified in the containing project's +setup.cfg under [{SETUP_SECTION}] +""" + +from __future__ import annotations + +import os +import sys +assert sys.version_info >= (3, 6), 'Script requires Python 3.6+' +import time +import shlex +import socket +import traceback +import typing as t +import subprocess as sp +import multiprocessing as mp +from pathlib import Path +from functools import partial +from configparser import ConfigParser +from argparse import ArgumentParser, Namespace +from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler + + +PROJECT_ROOT = Path(__file__).parent / '..' +SETUP_SECTION = str(Path(__file__).name) + ':settings' + + +def main(args: t.List[str] = None): + if args is None: + args = sys.argv[1:] + config = get_config(args) + + queue: mp.Queue = mp.Queue() + builder_proc = mp.Process(target=builder, args=(config, queue), daemon=True) + server_proc = mp.Process(target=server, args=(config, queue), daemon=True) + builder_proc.start() + server_proc.start() + exc, value, tb = queue.get() + server_proc.terminate() + builder_proc.terminate() + traceback.print_exception(exc, value, tb) + + +def get_config(args: t.List[str]) -> Namespace: + config = ConfigParser( + defaults={ + 'command': 'make doc', + 'html': 'build/html', + 'watch': '', + 'ignore': '\n'.join(['*.swp', '*.bak', '*~', '.*']), + 'bind': '0.0.0.0', + 'port': '8000', + }, + delimiters=('=',), default_section=SETUP_SECTION, + empty_lines_in_values=False, interpolation=None, + converters={'list': lambda s: s.strip().splitlines()}) + config.read(PROJECT_ROOT / 'setup.cfg') + sect = config[SETUP_SECTION] + # Resolve html and watch defaults relative to setup.cfg + if sect['html']: + sect['html'] = str(PROJECT_ROOT / sect['html']) + if sect['watch']: + sect['watch'] = '\n'.join( + str(PROJECT_ROOT / watch) + for watch in sect.getlist('watch') + ) + + parser = ArgumentParser(description=__doc__.format(**globals())) + parser.add_argument( + 'html', default=sect['html'], type=Path, nargs='?', + help="The base directory (relative to the project's root) which you " + "wish to server over HTTP. Default: %(default)s") + parser.add_argument( + '-c', '--command', default=sect['command'], + help="The command to run (relative to the project root) to regenerate " + "the HTML documentation. Default: %(default)s") + parser.add_argument( + '-w', '--watch', action='append', default=sect.getlist('watch'), + help="Can be specified multiple times to append to the list of source " + "patterns (relative to the project's root) to watch for changes. " + "Default: %(default)s") + parser.add_argument( + '-i', '--ignore', action='append', default=sect.getlist('ignore'), + help="Can be specified multiple times to append to the list of " + "patterns to ignore. Default: %(default)s") + parser.add_argument( + '--bind', metavar='ADDR', default=sect['bind'], + help="The address to listen on. Default: %(default)s") + parser.add_argument( + '--port', metavar='PORT', default=sect['port'], + help="The port to listen on. Default: %(default)s") + ns = parser.parse_args(args) + ns.command = shlex.split(ns.command) + if not ns.watch: + parser.error('You must specify at least one --watch') + ns.watch = [ + str(Path(watch).relative_to(Path.cwd())) + for watch in ns.watch + ] + return ns + + +class DevRequestHandler(SimpleHTTPRequestHandler): + server_version = 'DocsPreview/1.0' + protocol_version = 'HTTP/1.0' + + +class DevServer(ThreadingHTTPServer): + allow_reuse_address = True + base_path = None + + +def get_best_family(host: t.Union[str, None], port: t.Union[str, int, None])\ + -> t.Tuple[ + socket.AddressFamily, + t.Union[t.Tuple[str, int], t.Tuple[str, int, int, int]] + ]: + infos = socket.getaddrinfo( + host, port, + type=socket.SOCK_STREAM, + flags=socket.AI_PASSIVE) + for family, type, proto, canonname, sockaddr in infos: + return family, sockaddr + + +def server(config: Namespace, queue: mp.Queue = None): + try: + DevServer.address_family, addr = get_best_family(config.bind, config.port) + handler = partial(DevRequestHandler, directory=str(config.html)) + with DevServer(addr[:2], handler) as httpd: + host, port = httpd.socket.getsockname()[:2] + hostname = socket.gethostname() + print(f'Serving {config.html} HTTP on {host} port {port}') + print(f'http://{hostname}:{port}/ ...') + # XXX Wait for queue message to indicate time to start? + httpd.serve_forever() + except: + if queue is not None: + queue.put(sys.exc_info()) + raise + + +def get_stats(config: Namespace) -> t.Dict[Path, os.stat_result]: + return { + path: path.stat() + for watch_pattern in config.watch + for path in Path('.').glob(watch_pattern) + if not any(path.match(ignore_pattern) + for ignore_pattern in config.ignore) + } + + +def get_changes(old_stats: t.Dict[Path, os.stat_result], + new_stats: t.Dict[Path, os.stat_result])\ + -> t.Tuple[t.Set[Path], t.Set[Path], t.Set[Path]]: + # Yes, this is crude and could be more efficient but it's fast enough on a + # Pi so it'll be fast enough on anything else + return ( + new_stats.keys() - old_stats.keys(), # new + old_stats.keys() - new_stats.keys(), # deleted + { # modified + filepath + for filepath in old_stats.keys() & new_stats.keys() + if new_stats[filepath].st_mtime > old_stats[filepath].st_mtime + } + ) + + +def rebuild(config: Namespace) -> t.Dict[Path, os.stat_result]: + print('Rebuilding...') + sp.run(config.command, cwd=PROJECT_ROOT) + return get_stats(config) + + +def builder(config: Namespace, queue: mp.Queue = None): + try: + old_stats = rebuild(config) + print('Watching for changes in:') + print('\n'.join(config.watch)) + # XXX Add some message to the queue to indicate first build done and + # webserver can start? And maybe launch webbrowser too? + while True: + new_stats = get_stats(config) + created, deleted, modified = get_changes(old_stats, new_stats) + if created or deleted or modified: + for filepath in created: + print(f'New file, {filepath}') + for filepath in deleted: + print(f'Deleted file, {filepath}') + for filepath in modified: + print(f'Changed detected in {filepath}') + old_stats = rebuild(config) + else: + time.sleep(0.5) # make sure we're not a busy loop + except: + if queue is not None: + queue.put(sys.exc_info()) + raise + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..890e94609 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,128 @@ +[metadata] +name = gpiozero +version = 2.0.1 +description = A simple interface to GPIO devices with Raspberry Pi +long_description = file:README.rst +author = Ben Nuttall +author_email = ben@bennuttall.com +url = https://gpiozero.readthedocs.io/ +project_urls = + Documentation = https://gpiozero.readthedocs.io/ + Source Code = https://github.com/gpiozero/gpiozero + Issue Tracker = https://github.com/gpiozero/gpiozero/issues +keywords = raspberrypi gpio +license = BSD-3-Clause +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Education + Intended Audience :: Developers + Topic :: Education + Topic :: System :: Hardware + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: Implementation :: PyPy + +[options] +packages = find: +python_requires = >=3.9 +install_requires = + colorzero + importlib_resources~=5.0;python_version<'3.10' + importlib_metadata~=4.6;python_version<'3.10' + +[options.package_data] +gpiozero = + fonts/*.txt + +[options.extras_require] +test = + pytest + pytest-cov +doc = + sphinx>=4.0 + sphinx-rtd-theme>=1.0 + +[options.entry_points] +console_scripts = + pinout = gpiozerocli.pinout:main + pintest = gpiozerocli.pintest:main +gpiozero_pin_factories = + pigpio = gpiozero.pins.pigpio:PiGPIOFactory + lgpio = gpiozero.pins.lgpio:LGPIOFactory + rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory + native = gpiozero.pins.native:NativeFactory + mock = gpiozero.pins.mock:MockFactory +gpiozero_mock_pin_classes = + mockpin = gpiozero.pins.mock:MockPin + mockpwmpin = gpiozero.pins.mock:MockPWMPin + mockchargingpin = gpiozero.pins.mock:MockChargingPin + mocktriggerpin = gpiozero.pins.mock:MockTriggerPin + +[tool:pytest] +addopts = -rsx --cov --tb=short +testpaths = tests + +[coverage:run] +source = gpiozero +branch = true + +[coverage:report] +ignore_errors = true +show_missing = true +exclude_lines = + pragma: no cover + assert False + raise AssertionError + raise NotImplementedError + pass + if __name__ == .__main__.: + +[copyrights:settings] +include = + **/*.py + **/*.rst +exclude = + docs/examples/*.py + docs/license.rst +license = LICENSE.rst +preamble = + GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +strip-preamble = false + +[class_graph:settings] +abstract = + Device + GPIODevice + SmoothedInputDevice + AnalogInputDevice + MCP3xxx + MCP3xx2 + MCP30xx + MCP32xx + MCP33xx + SPIDevice + CompositeDevice + CompositeOutputDevice + LEDCollection + InternalDevice +omit = + object + GPIOBase + GPIOMeta + frozendict + WeakMethod + _EnergenieMaster +source = + gpiozero/**/*.py + +[previewer:settings] +command = make -C docs html +html = build/html +watch = + gpiozero/**/*.py + gpiozerocli/**/*.py + docs/*.rst + docs/examples/*.py + docs/_static/* diff --git a/setup.py b/setup.py index 2fb0c256f..606849326 100644 --- a/setup.py +++ b/setup.py @@ -1,143 +1,3 @@ -"A simple interface to GPIO devices with Raspberry Pi." +from setuptools import setup -import io -import os -import sys -import errno -from setuptools import setup, find_packages - -if sys.version_info[0] == 2: - if not sys.version_info >= (2, 7): - raise ValueError('This package requires Python 2.7 or above') -elif sys.version_info[0] == 3: - if not sys.version_info >= (3, 2): - raise ValueError('This package requires Python 3.2 or above') -else: - raise ValueError('Unrecognized major version of Python') - -HERE = os.path.abspath(os.path.dirname(__file__)) - -# Workaround -try: - import multiprocessing -except ImportError: - pass - -__project__ = 'gpiozero' -__version__ = '1.5.0' -__author__ = 'Ben Nuttall' -__author_email__ = 'ben@raspberrypi.org' -__url__ = 'https://github.com/RPi-Distro/python-gpiozero' -__platforms__ = 'ALL' - -__classifiers__ = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Education", - "Intended Audience :: Developers", - "Topic :: Education", - "Topic :: System :: Hardware", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: Implementation :: PyPy", -] - -__keywords__ = [ - 'raspberrypi', - 'gpio', -] - -__requires__ = [ - 'colorzero', -] - -__extra_requires__ = { - 'doc': ['sphinx'], - 'test': ['pytest', 'coverage', 'mock'], -} - -if sys.version_info[:2] == (3, 2): - # Particular versions are required for Python 3.2 compatibility - __extra_requires__['doc'].extend([ - 'Jinja2<2.7', - 'MarkupSafe<0.16', - ]) - __extra_requires__['test'][0] = 'pytest<3.0dev' - __extra_requires__['test'][1] = 'coverage<4.0dev' -elif sys.version_info[:2] == (3, 3): - __extra_requires__['test'][0] = 'pytest<3.3dev' -elif sys.version_info[:2] == (3, 4): - __extra_requires__['test'][0] = 'pytest<5.0dev' - -try: - # If we're executing on a Raspberry Pi, install all GPIO libraries for - # testing (except RPIO which doesn't work on the multi-core models yet) - with io.open('/proc/device-tree/model', 'r') as f: - if f.read().startswith('Raspberry Pi'): - __extra_requires__['test'].append('RPi.GPIO') - __extra_requires__['test'].append('pigpio') -except IOError as e: - if e.errno != errno.ENOENT: - raise - -__entry_points__ = { - 'gpiozero_pin_factories': [ - 'pigpio = gpiozero.pins.pigpio:PiGPIOFactory', - 'rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory', - 'rpio = gpiozero.pins.rpio:RPIOFactory', - 'native = gpiozero.pins.native:NativeFactory', - 'mock = gpiozero.pins.mock:MockFactory', - # Backwards compatible names - 'PiGPIOPin = gpiozero.pins.pigpio:PiGPIOFactory', - 'RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOFactory', - 'RPIOPin = gpiozero.pins.rpio:RPIOFactory', - 'NativePin = gpiozero.pins.native:NativeFactory', - ], - 'gpiozero_mock_pin_classes': [ - 'mockpin = gpiozero.pins.mock:MockPin', - 'mockpwmpin = gpiozero.pins.mock:MockPWMPin', - 'mockchargingpin = gpiozero.pins.mock:MockChargingPin', - 'mocktriggerpin = gpiozero.pins.mock:MockTriggerPin', - ], - 'console_scripts': [ - 'pinout = gpiozerocli.pinout:main', - ] -} - - -def main(): - import io - with io.open(os.path.join(HERE, 'README.rst'), 'r') as readme: - setup( - name = __project__, - version = __version__, - description = __doc__, - long_description = readme.read(), - classifiers = __classifiers__, - author = __author__, - author_email = __author_email__, - url = __url__, - license = [ - c.rsplit('::', 1)[1].strip() - for c in __classifiers__ - if c.startswith('License ::') - ][0], - keywords = __keywords__, - packages = find_packages(), - include_package_data = True, - platforms = __platforms__, - install_requires = __requires__, - extras_require = __extra_requires__, - entry_points = __entry_points__, - ) - - -if __name__ == '__main__': - main() +setup() diff --git a/tests/cli/test_pinout.py b/tests/cli/test_pinout.py index 7fcae0e35..af1df9145 100644 --- a/tests/cli/test_pinout.py +++ b/tests/cli/test_pinout.py @@ -1,41 +1,9 @@ # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2017-2019 Dave Jones -# Copyright (c) 2016 Stewart -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2017-2023 Dave Jones +# Copyright (c) 2016 Stewart # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import os @@ -58,7 +26,7 @@ def test_args_color(): def test_args_revision(): args = main.parser.parse_args(['--revision', '000d']) - assert args.revision == '000d' + assert args.revision == 13 def test_help(capsys): with pytest.raises(SystemExit) as ex: diff --git a/tests/conftest.py b/tests/conftest.py index e226be588..e6d961efd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,44 +1,15 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2019-2023 Dave Jones # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - print_function, - absolute_import, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import sys import pytest import warnings +from threading import Event, Thread from gpiozero import Device from gpiozero.pins.mock import MockFactory, MockPWMPin @@ -54,25 +25,50 @@ if sys.version_info[:2] < (3, 4): warnings.simplefilter('always') -@pytest.yield_fixture() +@pytest.fixture() def no_default_factory(request): save_pin_factory = Device.pin_factory Device.pin_factory = None - yield None - Device.pin_factory = save_pin_factory + try: + yield None + finally: + Device.pin_factory = save_pin_factory -@pytest.yield_fixture(scope='function') +@pytest.fixture(scope='function') def mock_factory(request): save_factory = Device.pin_factory Device.pin_factory = MockFactory() - yield Device.pin_factory - # This reset() may seem redundant given we're re-constructing the factory - # for each function that requires it but MockFactory (via LocalFactory) - # stores some info at the class level which reset() clears. - if Device.pin_factory is not None: - Device.pin_factory.reset() - Device.pin_factory = save_factory + try: + yield Device.pin_factory + # This reset() may seem redundant given we're re-constructing the + # factory for each function that requires it but MockFactory (via + # LocalFactory) stores some info at the class level which reset() + # clears. + finally: + if Device.pin_factory is not None: + Device.pin_factory.reset() + Device.pin_factory = save_factory @pytest.fixture() def pwm(request, mock_factory): mock_factory.pin_class = MockPWMPin + +class ThreadedTest(Thread): + def __init__(self, test_fn, *args, **kwargs): + self._fn = test_fn + self._args = args + self._kwargs = kwargs + self._result = None + super().__init__(daemon=True) + self.start() + + def run(self): + self._result = self._fn(*self._args, **self._kwargs) + + @property + def result(self): + self.join(10) + if not self.is_alive(): + return self._result + else: + raise RuntimeError('test thread did not finish') diff --git a/tests/test_boards.py b/tests/test_boards.py index 45faccb3d..f937baa40 100644 --- a/tests/test_boards.py +++ b/tests/test_boards.py @@ -1,53 +1,27 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2017-2019 Ben Nuttall -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Jack Wearden +# Copyright (c) 2017-2020 Ben Nuttall # Copyright (c) 2016-2019 Andrew Scheller # Copyright (c) 2018 SteveAmor # Copyright (c) 2018 Claire Pollard -# Copyright (c) 2016 Ian Harcombe +# Copyright (c) 2016 Martin OHanlon # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import sys import pytest +import warnings from time import sleep from threading import Event +from unittest import mock from gpiozero import * +from gpiozero.fonts import * def test_composite_output_on_off(mock_factory): @@ -102,6 +76,10 @@ def test_button_board_init(mock_factory): assert isinstance(board[0], Button) assert isinstance(board[1], Button) assert isinstance(board[2], Button) + assert board.pull_up + assert board[0].pull_up + assert board[1].pull_up + assert board[2].pull_up assert [b.pin for b in board] == pins def test_button_board_pressed_released(mock_factory): @@ -109,13 +87,6 @@ def test_button_board_pressed_released(mock_factory): pin2 = mock_factory.pin(5) pin3 = mock_factory.pin(6) with ButtonBoard(4, 5, foo=6) as board: - assert isinstance(board[0], Button) - assert isinstance(board[1], Button) - assert isinstance(board[2], Button) - assert board.pull_up - assert board[0].pull_up - assert board[1].pull_up - assert board[2].pull_up assert board.value == (False, False, False) assert board.wait_for_release(1) assert not board.is_active @@ -156,6 +127,14 @@ def test_button_board_when_pressed(mock_factory): assert evt2.wait(1) assert not evt1.wait(0) assert not evt3.wait(0) + # Fake a pin event with no changes and make sure nothing gets set + evt.clear() + evt2.clear() + pin2._call_when_changed() + assert not evt.wait(0) + assert not evt1.wait(0) + assert not evt2.wait(0) + assert not evt3.wait(0) def test_led_collection_bad_init(mock_factory): with pytest.raises(GPIOPinMissing): @@ -189,12 +168,6 @@ def test_led_board_pwm_init(mock_factory, pwm): assert isinstance(board[2], PWMLED) assert [b.pin for b in board] == pins -def test_led_bar_graph_bad_init(mock_factory): - with pytest.raises(GPIOPinMissing): - LEDBarGraph() - with pytest.raises(GPIOPinMissing): - LEDBarGraph(pwm=True) - def test_led_board_on_off(mock_factory): pin1 = mock_factory.pin(2) pin2 = mock_factory.pin(3) @@ -678,12 +651,18 @@ def test_led_bar_graph_bad_init(mock_factory): pin1 = mock_factory.pin(2) pin2 = mock_factory.pin(3) pin3 = mock_factory.pin(4) + with pytest.raises(GPIOPinMissing): + LEDBarGraph() + with pytest.raises(GPIOPinMissing): + LEDBarGraph(pwm=True) with pytest.raises(TypeError): LEDBarGraph(2, 3, foo=4) with pytest.raises(ValueError): LEDBarGraph(2, 3, 4, initial_value=-2) with pytest.raises(ValueError): LEDBarGraph(2, 3, 4, initial_value=2) + with pytest.raises(ValueError): + LEDBarGraph(2, LEDBoard(3, 4)) def test_led_bar_graph_initial_value(mock_factory): pin1 = mock_factory.pin(2) @@ -913,23 +892,29 @@ def test_traffic_hat(mock_factory): assert ([led.pin for led in board.lights] + [board.buzzer.pin, board.button.pin]) == pins -def test_robot_bad_init(mock_factory): - with pytest.raises(GPIOPinMissing): +def test_traffic_phat(mock_factory): + pins = [mock_factory.pin(n) for n in (25, 24, 23)] + with TrafficpHat() as board: + assert repr(board).startswith('' assert repr(device2) == '' + assert repr(device3) == '' assert device1.value assert not device2.value + assert device3.value is None assert device1.socket == 1 assert device2.socket == 2 + assert device3.socket == 3 [pin.clear_states() for pin in pins] device1.on() assert device1.value @@ -1289,6 +1223,23 @@ def test_energenie(mock_factory): pins[3].assert_states_and_times([(0.0, True), (0.0, True)]) pins[4].assert_states_and_times([(0.0, False)]) pins[5].assert_states_and_times([(0.0, False), (0.1, True), (0.25, False)]) + [pin.clear_states() for pin in pins] + device3.on() + assert device3.value + pins[0].assert_states_and_times([(0.0, False), (0.0, True)]) + pins[1].assert_states_and_times([(0.0, True), (0.0, False)]) + pins[2].assert_states_and_times([(0.0, True), (0.0, False)]) + pins[3].assert_states_and_times([(0.0, True), (0.0, False)]) + pins[4].assert_states_and_times([(0.0, False)]) + pins[5].assert_states_and_times([(0.0, False), (0.1, True), (0.25, False)]) + + device3.value = False + assert not device3.value + device3.value = True + assert device3.value + with pytest.raises(TypeError): + device3.value = None + device1.close() assert repr(device1) == '' @@ -1531,3 +1482,202 @@ def test_jamhat_pwm(mock_factory, pwm): False, False, # buttons None # buzzer ) + +def test_pibrella(mock_factory, pwm): + with Pibrella() as pb: + assert repr(pb).startswith(' -# Copyright (c) 2016 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') +# SPDX-License-Identifier: BSD-3-Clause - -import gc -import sys import pytest -import random -import weakref -from gpiozero.compat import * +from gpiozero.compat import frozendict def test_frozendict(): @@ -64,306 +30,4 @@ def test_frozendict(): h = hash(f) assert h is not None assert hash(f) == h - -# ported from the official test cases; see -# https://github.com/python/cpython/blob/master/Lib/test/test_math.py for original - -NAN = float('nan') -INF = float('inf') -NINF = float('-inf') - -def test_isclose_negative_tolerances(): - with pytest.raises(ValueError): - isclose(1, 1, rel_tol=-1e-100) - with pytest.raises(ValueError): - isclose(1, 1, rel_tol=1e-100, abs_tol=-1e10) - -def test_isclose_identical(): - examples = [ - (2.0, 2.0), - (0.1e200, 0.1e200), - (1.123e-300, 1.123e-300), - (12345, 12345.0), - (0.0, -0.0), - (345678, 345678), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=0.0, abs_tol=0.0) - -def test_isclose_eight_decimals(): - examples = [ - (1e8, 1e8 + 1), - (-1e-8, -1.000000009e-8), - (1.12345678, 1.12345679), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=1e-8) - assert not isclose(a, b, rel_tol=1e-9) - -def test_isclose_near_zero(): - examples = [1e-9, -1e-9, -1e-150] - for a in examples: - assert not isclose(a, 0.0, rel_tol=0.9) - assert isclose(a, 0.0, abs_tol=1e-8) - -def test_isclose_inf(): - assert isclose(INF, INF) - assert isclose(INF, INF, abs_tol=0.0) - assert isclose(NINF, NINF) - assert isclose(NINF, NINF, abs_tol=0.0) - -def test_isclose_inf_ninf_nan(): - examples = [ - (NAN, NAN), - (NAN, 1e-100), - (1e-100, NAN), - (INF, NAN), - (NAN, INF), - (INF, NINF), - (INF, 1.0), - (1.0, INF), - (INF, 1e308), - (1e308, INF), - ] - for a, b in examples: - assert not isclose(a, b, abs_tol=0.999999999999999) - -def test_isclose_zero_tolerance(): - examples = [ - (1.0, 1.0), - (-3.4, -3.4), - (-1e-300, -1e-300), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=0.0) - examples = [ - (1.0, 1.000000000000001), - (0.99999999999999, 1.0), - (1.0e200, .999999999999999e200), - ] - for a, b in examples: - assert not isclose(a, b, rel_tol=0.0) - -def test_isclose_assymetry(): - assert isclose(9, 10, rel_tol=0.1) - assert isclose(10, 9, rel_tol=0.1) - -def test_isclose_integers(): - examples = [ - (100000001, 100000000), - (123456789, 123456788), - ] - for a, b in examples: - assert isclose(a, b, rel_tol=1e-8) - assert not isclose(a, b, rel_tol=1e-9) - -# ported from the official test cases; see -# https://github.com/python/cpython/blob/master/Lib/test/test_statistics.py for -# original - -def test_mean(): - examples = [ - (4.8125, (0, 1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 7, 7, 7, 8, 9)), - (22.015625, (17.25, 19.75, 20.0, 21.5, 21.75, 23.25, 25.125, 27.5)), - (INF, (1, 3, 5, 7, 9, INF)), - (NINF, (1, 3, 5, 7, 9, NINF)), - ] - for result, values in examples: - values = list(values) - random.shuffle(values) - assert mean(values) == result - assert mean(iter(values)) == result - -def test_mean_big_data(): - c = 1e9 - data = [3.4, 4.5, 4.9, 6.7, 6.8, 7.2, 8.0, 8.1, 9.4] - expected = mean(data) + c - assert expected != c - assert mean([x + c for x in data]) == expected - -def test_mean_doubled_data(): - data = [random.uniform(-3, 5) for _ in range(1000)] - expected = mean(data) - actual = mean(data * 2) - assert isclose(expected, actual) - -def test_mean_empty(): - with pytest.raises(ValueError): - mean(()) - -def test_median(): - assert median([1, 2, 3, 4, 5, 6]) == 3.5 - assert median([1, 2, 3, 4, 5, 6, 9]) == 4 - -def test_median_empty(): - with pytest.raises(ValueError): - median(()) - -# ported from the official test cases; see -# https://github.com/python/cpython/blob/master/Lib/test/test_weakref.py for -# original - -class Object(object): - def __init__(self, arg): - self.arg = arg - def __repr__(self): - return "" % self.arg - def __eq__(self, other): - if isinstance(other, Object): - return self.arg == other.arg - return NotImplemented - def __ne__(self, other): - if isinstance(other, Object): - return self.arg != other.arg - return NotImplemented - def __lt__(self, other): - if isinstance(other, Object): - return self.arg < other.arg - return NotImplemented - def __hash__(self): - return hash(self.arg) - def some_method(self): - return 4 - def other_method(self): - return 5 - -@pytest.fixture() -def subclass(request): - class C(Object): - def some_method(self): - return 6 - return C - - -def test_weakmethod_alive(): - o = Object(1) - r = WeakMethod(o.some_method) - assert isinstance(r, weakref.ReferenceType) - assert isinstance(r(), type(o.some_method)) - assert r().__self__ is o - assert r().__func__ is o.some_method.__func__ - assert r()() == 4 - -def test_weakmethod_object_dead(): - o = Object(1) - r = WeakMethod(o.some_method) - del o - gc.collect() - assert r() is None - -@pytest.mark.xfail((3, 2) <= sys.version_info[:2] <= (3, 3), - reason='intermittent failure on py3.2 and py3.3') -def test_weakmethod_method_dead(subclass): - o = subclass(1) - r = WeakMethod(o.some_method) - del subclass.some_method - gc.collect() - assert r() is None - -def test_weakmethod_callback_when_object_dead(subclass): - calls = [] - def cb(arg): - calls.append(arg) - o = subclass(1) - r = WeakMethod(o.some_method, cb) - del o - gc.collect() - assert calls == [r] - # Callback is only called once - subclass.some_method = Object.some_method - gc.collect() - assert calls == [r] - -@pytest.mark.xfail((3, 2) <= sys.version_info[:2] <= (3, 3), - reason='intermittent failure on py3.2 and py3.3') -def test_weakmethod_callback_when_method_dead(subclass): - calls = [] - def cb(arg): - calls.append(arg) - o = subclass(1) - r = WeakMethod(o.some_method, cb) - del subclass.some_method - gc.collect() - assert calls == [r] - # Callback is only called once - del o - gc.collect() - assert calls == [r] - -@pytest.mark.xfail(hasattr(sys, 'pypy_version_info'), - reason='pypy memory management is different') -def test_weakmethod_no_cycles(): - o = Object(1) - def cb(_): - pass - r = WeakMethod(o.some_method, cb) - wr = weakref.ref(r) - del r - assert wr() is None - -def test_weakmethod_equality(): - def _eq(a, b): - assert a == b - assert not (a != b) - def _ne(a, b): - assert not (a == b) - assert a != b - x = Object(1) - y = Object(1) - a = WeakMethod(x.some_method) - b = WeakMethod(y.some_method) - c = WeakMethod(x.other_method) - d = WeakMethod(y.other_method) - # Objects equal, same method - _eq(a, b) - _eq(c, d) - # Objects equal, different method - _ne(a, c) - _ne(a, d) - _ne(b, c) - _ne(b, d) - # Objects unequal, same or different method - z = Object(2) - e = WeakMethod(z.some_method) - f = WeakMethod(z.other_method) - _ne(a, e) - _ne(a, f) - _ne(b, e) - _ne(b, f) - del x, y, z - gc.collect() - # Dead WeakMethods compare by identity - refs = a, b, c, d, e, f - for q in refs: - for r in refs: - assert (q == r) == (q is r) - assert (q != r) == (q is not r) - -def test_weakmethod_hashing(): - x = Object(1) - y = Object(1) - a = WeakMethod(x.some_method) - b = WeakMethod(y.some_method) - c = WeakMethod(y.other_method) - # Since WeakMethod objects are equal, the hashes should be equal - assert hash(a) == hash(b) - ha = hash(a) - # Dead WeakMethods retain their old hash value - del x, y - gc.collect() - assert hash(a) == ha - assert hash(b) == ha - # If it wasn't hashed when alive, a dead WeakMethod cannot be hashed - with pytest.raises(TypeError): - hash(c) - -def test_weakmethod_bad_method(): - with pytest.raises(TypeError): - WeakMethod('foo') - -def test_weakmethod_other_equality(): - x = Object(1) - a = WeakMethod(x.some_method) - b = WeakMethod(x.other_method) - assert not a == 1 - assert a != 1 + assert repr(f) == f"" diff --git a/tests/test_devices.py b/tests/test_devices.py index b816345ac..8c5229822 100644 --- a/tests/test_devices.py +++ b/tests/test_devices.py @@ -1,47 +1,17 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones # Copyright (c) 2019 Ben Nuttall # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - -import os +# SPDX-License-Identifier: BSD-3-Clause + import warnings -from mock import patch import pytest import errno +from unittest import mock from gpiozero import * from gpiozero.pins.mock import MockFactory @@ -51,9 +21,9 @@ def test_default_pin_factory_order(): - with patch('sys.path') as path, \ - patch('io.open') as io, \ - patch('os.environ.get') as get: + with mock.patch('sys.path') as path, \ + mock.patch('io.open') as io, \ + mock.patch('os.environ.get') as get: # ensure no pin libraries can be imported path.return_value = [] # ensure /proc/device-tree... is not found when trying native @@ -66,8 +36,8 @@ def test_default_pin_factory_order(): device = GPIODevice(2) assert len(ws) == 4 assert all(w.category == PinFactoryFallback for w in ws) - assert ws[0].message.args[0].startswith('Falling back from rpigpio:') - assert ws[1].message.args[0].startswith('Falling back from rpio:') + assert ws[0].message.args[0].startswith('Falling back from lgpio:') + assert ws[1].message.args[0].startswith('Falling back from rpigpio:') assert ws[2].message.args[0].startswith('Falling back from pigpio:') assert ws[3].message.args[0].startswith('Falling back from native:') @@ -76,6 +46,8 @@ def test_device_bad_pin(mock_factory): device = GPIODevice() with pytest.raises(PinInvalidPin): device = GPIODevice(60) + with pytest.raises(PinInvalidPin): + device = GPIODevice('60') with pytest.raises(PinInvalidPin): device = GPIODevice('BCM60') with pytest.raises(PinInvalidPin): @@ -86,22 +58,18 @@ def test_device_bad_pin(mock_factory): device = GPIODevice('J8:42') with pytest.raises(PinInvalidPin): device = GPIODevice('J8:1') + with pytest.raises(PinInvalidPin): + device = GPIODevice('J8:A') with pytest.raises(PinInvalidPin): device = GPIODevice('foo') -def test_device_non_physical(mock_factory): - with warnings.catch_warnings(record=True) as w: - warnings.resetwarnings() - device = GPIODevice('GPIO37') - assert len(w) == 1 - assert w[0].category == PinNonPhysical - def test_device_init(mock_factory): pin = mock_factory.pin(2) with GPIODevice(2) as device: assert repr(device).startswith('' with pytest.raises(TypeError): GPIODevice(2, foo='bar') @@ -138,8 +106,10 @@ def test_device_reopen_same_pin(mock_factory): assert device.pin is None def test_device_pin_parsing(mock_factory): - # MockFactory defaults to a Pi 2B layout + # MockFactory defaults to a Pi 3B layout pin = mock_factory.pin(2) + with GPIODevice('2') as device: + assert device.pin is pin with GPIODevice('GPIO2') as device: assert device.pin is pin with GPIODevice('BCM2') as device: @@ -154,8 +124,8 @@ def test_device_pin_parsing(mock_factory): def test_device_repr(mock_factory): with GPIODevice(4) as device: assert repr(device) == ( - '' % device.pin) + f'') def test_device_repr_after_close(mock_factory): with GPIODevice(2) as device: @@ -182,12 +152,13 @@ def test_composite_device_sequence(mock_factory): with CompositeDevice(InputDevice(4), InputDevice(5)) as device: assert repr(device).startswith('' assert device.value == (0, 0) assert not device.is_active device[0].pin.drive_high() @@ -200,10 +171,21 @@ def test_composite_device_named(mock_factory): bar=InputDevice(5), _order=('foo', 'bar') ) as device: + assert repr(device) == '' assert device.namedtuple._fields == ('foo', 'bar') assert device.value == (0, 0) assert not device.is_active +def test_composite_device_some_named(mock_factory): + with CompositeDevice( + InputDevice(4), + foobar=InputDevice(5), + ) as device: + assert repr(device) == '' + assert device.namedtuple._fields == ('device_0', 'foobar') + assert device.value == (0, 0) + assert not device.is_active + def test_composite_device_bad_init(mock_factory): with pytest.raises(ValueError): CompositeDevice(foo=1, bar=2, _order=('foo',)) @@ -219,6 +201,7 @@ def test_composite_device_read_only(mock_factory): with pytest.raises(AttributeError): device.foo = 1 +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_shutdown(mock_factory): from gpiozero.devices import _shutdown ds = DistanceSensor(17, 19) diff --git a/tests/test_fonts.py b/tests/test_fonts.py new file mode 100644 index 000000000..c87ecc86c --- /dev/null +++ b/tests/test_fonts.py @@ -0,0 +1,86 @@ +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2021 Dave Jones +# +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, +) +str = type('') + +import io + +import pytest + +from gpiozero.fonts import * + + +@pytest.fixture() +def font_text(request): + return """\ +# A comment + + . 0_ 1. 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_ +... |.| ..| ._| ._| |_| |_. |_. ..| |_| |_| +... |_| ..| |_. ._| ..| ._| |_| ..| |_| ._| +""" + + +@pytest.fixture() +def font_data(request): + return { + ' ': (0, 0, 0, 0, 0, 0, 0), + '0': (1, 1, 1, 1, 1, 1, 0), + '1': (0, 1, 1, 0, 0, 0, 0), + '2': (1, 1, 0, 1, 1, 0, 1), + '3': (1, 1, 1, 1, 0, 0, 1), + '4': (0, 1, 1, 0, 0, 1, 1), + '5': (1, 0, 1, 1, 0, 1, 1), + '6': (1, 0, 1, 1, 1, 1, 1), + '7': (1, 1, 1, 0, 0, 0, 0), + '8': (1, 1, 1, 1, 1, 1, 1), + '9': (1, 1, 1, 1, 0, 1, 1), + } + + +def test_fonts_load_types(tmpdir, font_text, font_data): + text_stream = io.StringIO(font_text) + bin_stream = io.BytesIO(font_text.encode('utf-8')) + tmpfile = tmpdir.join('test.font') + tmpfile.write_text(font_text, 'utf-8') + + assert load_font_7seg(text_stream) == font_data + assert load_font_7seg(bin_stream) == font_data + assert load_font_7seg(str(tmpfile)) == font_data + assert load_font_7seg(str(tmpfile).encode('utf-8')) == font_data + + +def test_fonts_bad_data(): + data = """\ + . 0_ 1. 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_ +... |.| ..| ._| ._| |_| |_. |_. ..| |_| |_| +""" + with pytest.raises(ValueError): + load_font_7seg(io.StringIO(data)) + + data = """\ + . 0_ 1 2_ 3_ 4. 5_ 6_ 7_ 8_ 9_ +... |.| .| ._| ._| |_| |_. |_. ..| |_| |_| +... |_| .| |_. ._| ..| ._| |_| ..| |_| ._| +""" + with pytest.raises(ValueError): + load_font_7seg(io.StringIO(data)) + + data = """\ + .... 0--- 2--- 2--- 3--- 4 5--- 6--- 7---. 8--- 9--- +..... | /| | | | | | | | / | | | | +..... | / | --- --- -- ---| --- |--- | --- ---| +..... |/ | | | | | | | | | | | | +..... --- --- --- --- --- --- --- +""" + with pytest.raises(ValueError): + load_font_14seg(io.StringIO(data)) diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 78bdf762b..807ff4ae3 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -1,43 +1,13 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016-2019 Andrew Scheller +# +# Copyright (c) 2016-2023 Dave Jones # Copyright (c) 2019 Ben Nuttall +# Copyright (c) 2016-2019 Andrew Scheller # Copyright (c) 2018 Philippe Muller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import sys import pytest @@ -45,9 +15,9 @@ from time import sleep from threading import Event from functools import partial +from unittest import mock -import mock - +from conftest import ThreadedTest from gpiozero.pins.mock import MockChargingPin, MockTriggerPin from gpiozero.threads import GPIOThread from gpiozero import * @@ -61,6 +31,7 @@ def test_input_initial_values(mock_factory): assert pin.function == 'input' assert pin.pull == 'up' assert device.pull_up + assert repr(device) == '' with InputDevice(4, pull_up=False) as device: assert pin.pull == 'down' assert not device.pull_up @@ -113,12 +84,12 @@ def test_input_is_active_high_externally_pulled_down(mock_factory): def test_input_invalid_pull_up(mock_factory): with pytest.raises(PinInvalidState) as exc: InputDevice(4, pull_up=None) - assert str(exc.value) == 'Pin 4 is defined as floating, but "active_state" is not defined' + assert str(exc.value) == 'Pin GPIO4 is defined as floating, but "active_state" is not defined' def test_input_invalid_active_state(mock_factory): with pytest.raises(PinInvalidState) as exc: InputDevice(4, active_state=True) - assert str(exc.value) == 'Pin 4 is not floating, but "active_state" is not None' + assert str(exc.value) == 'Pin GPIO4 is not floating, but "active_state" is not None' def test_input_event_activated(mock_factory): event = Event() @@ -200,8 +171,10 @@ def test_input_smoothed_attrib(mock_factory): assert not device.partial device._queue.start() assert not device.is_active + assert repr(device) == '' with pytest.raises(InputDeviceError): device.threshold = 1 + assert repr(device) == '' with pytest.raises(BadQueueLen): SmoothedInputDevice(4, queue_len=-1) with pytest.raises(BadWaitTime): @@ -296,6 +269,7 @@ def test_input_light_sensor(mock_factory): @pytest.mark.skipif(hasattr(sys, 'pypy_version_info'), reason='timing is too random on pypy') +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_input_distance_sensor(mock_factory): echo_pin = mock_factory.pin(4) trig_pin = mock_factory.pin(5, pin_class=MockTriggerPin, @@ -349,3 +323,180 @@ def test_input_distance_sensor_edge_cases(mock_factory): break else: assert False + +def rotate_cw(a_pin, b_pin): + a_pin.drive_low() + b_pin.drive_low() + a_pin.drive_high() + b_pin.drive_high() + +def rotate_ccw(a_pin, b_pin): + b_pin.drive_low() + a_pin.drive_low() + b_pin.drive_high() + a_pin.drive_high() + +def test_input_rotary_encoder(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with pytest.raises(ValueError): + RotaryEncoder(20, 21, threshold_steps=(2, 0)) + with RotaryEncoder(20, 21) as encoder: + assert repr(encoder).startswith('' + +def test_input_rotary_encoder_jiggle(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21) as encoder: + # Check the FSM permits "jiggle" in the sequence + a_pin.drive_low() + a_pin.drive_high() + a_pin.drive_low() + b_pin.drive_low() + b_pin.drive_high() + b_pin.drive_low() + a_pin.drive_high() + a_pin.drive_low() + a_pin.drive_high() + b_pin.drive_high() + assert encoder.steps == 1 + +def test_input_rotary_encoder_limits(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21, max_steps=4) as encoder: + assert not encoder.wrap + for expected in [1, 2, 3, 4, 4, 4]: + rotate_cw(a_pin, b_pin) + assert encoder.steps == expected + +def test_input_rotary_encoder_threshold(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21, max_steps=4, threshold_steps=(2, 4)) as encoder: + assert encoder.threshold_steps == (2, 4) + for expected in [1, 2, 3, 4]: + rotate_cw(a_pin, b_pin) + assert encoder.is_active == (2 <= encoder.steps <= 4) + +def test_input_rotary_encoder_settable(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21, max_steps=4) as encoder: + assert encoder.max_steps == 4 + assert encoder.steps == 0 + assert encoder.value == 0 + rotate_cw(a_pin, b_pin) + assert encoder.steps == 1 + assert encoder.value == 0.25 + encoder.steps = 0 + assert encoder.steps == 0 + assert encoder.value == 0 + rotate_ccw(a_pin, b_pin) + assert encoder.steps == -1 + assert encoder.value == -0.25 + encoder.value = 0 + assert encoder.steps == 0 + assert encoder.value == 0 + with RotaryEncoder(20, 21, max_steps=0) as encoder: + assert encoder.max_steps == 0 + assert encoder.steps == 0 + assert encoder.value == 0 + rotate_cw(a_pin, b_pin) + assert encoder.steps == 1 + # value is perpetually 0 when max_steps is 0 + assert encoder.value == 0 + encoder.steps = 0 + assert encoder.steps == 0 + assert encoder.value == 0 + +def test_input_rotary_encoder_wrap(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21, max_steps=4, wrap=True) as encoder: + assert encoder.wrap + for expected in [1, 2, 3, 4, -4, -3, -2, -1, 0]: + rotate_cw(a_pin, b_pin) + assert encoder.steps == expected + +def test_input_rotary_encoder_when(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21) as encoder: + rotated = Event() + rotated_cw = Event() + rotated_ccw = Event() + assert encoder.when_rotated is None + assert encoder.when_rotated_clockwise is None + assert encoder.when_rotated_counter_clockwise is None + encoder.when_rotated = rotated.set + encoder.when_rotated_clockwise = rotated_cw.set + encoder.when_rotated_counter_clockwise = rotated_ccw.set + assert encoder.when_rotated is not None + assert encoder.when_rotated_clockwise is not None + assert encoder.when_rotated_counter_clockwise is not None + assert callable(encoder.when_rotated) + assert callable(encoder.when_rotated_clockwise) + assert callable(encoder.when_rotated_counter_clockwise) + assert not rotated.wait(0) + assert not rotated_cw.wait(0) + assert not rotated_ccw.wait(0) + rotate_cw(a_pin, b_pin) + assert rotated.wait(0) + assert rotated_cw.wait(0) + assert not rotated_ccw.wait(0) + rotated.clear() + rotated_cw.clear() + rotate_ccw(a_pin, b_pin) + assert rotated.wait(0) + assert not rotated_cw.wait(0) + assert rotated_ccw.wait(0) + +def test_input_rotary_encoder_wait(mock_factory): + a_pin = mock_factory.pin(20) + b_pin = mock_factory.pin(21) + with RotaryEncoder(20, 21) as encoder: + # The rotary encoder waits are "pulsed", i.e. they act like edge waits + # rather than level waits hence the need for a background thread here + # that actively attempts to wait on rotation while it happens. It's + # not enough to rotate and *then* attempt to wait + test_rotate = ThreadedTest(lambda: encoder.wait_for_rotate(0)) + test_rotate_cw = ThreadedTest(lambda: encoder.wait_for_rotate_clockwise(0)) + test_rotate_ccw = ThreadedTest(lambda: encoder.wait_for_rotate_counter_clockwise(0)) + assert not test_rotate.result + assert not test_rotate_cw.result + assert not test_rotate_ccw.result + test_thread = ThreadedTest(lambda: encoder.wait_for_rotate(1)) + test_thread_cw = ThreadedTest(lambda: encoder.wait_for_rotate_clockwise(1)) + test_thread_ccw = ThreadedTest(lambda: encoder.wait_for_rotate_counter_clockwise(1)) + rotate_cw(a_pin, b_pin) + assert test_thread.result + assert test_thread_cw.result + assert not test_thread_ccw.result + test_rotate = ThreadedTest(lambda: encoder.wait_for_rotate(0)) + test_rotate_cw = ThreadedTest(lambda: encoder.wait_for_rotate_clockwise(0)) + test_rotate_ccw = ThreadedTest(lambda: encoder.wait_for_rotate_counter_clockwise(0)) + assert not test_rotate.result + assert not test_rotate_cw.result + assert not test_rotate_ccw.result + test_thread = ThreadedTest(lambda: encoder.wait_for_rotate(1)) + test_thread_cw = ThreadedTest(lambda: encoder.wait_for_rotate_clockwise(1)) + test_thread_ccw = ThreadedTest(lambda: encoder.wait_for_rotate_counter_clockwise(1)) + rotate_ccw(a_pin, b_pin) + assert test_thread.result + assert not test_thread_cw.result + assert test_thread_ccw.result diff --git a/tests/test_internal_devices.py b/tests/test_internal_devices.py index faff7d52f..0b66f1c47 100644 --- a/tests/test_internal_devices.py +++ b/tests/test_internal_devices.py @@ -1,51 +1,22 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2019-2021 Dave Jones # Copyright (c) 2019 Jeevan M R <14.jeevan@gmail.com> -# Copyright (c) 2019 Dave Jones # Copyright (c) 2019 Ben Nuttall # Copyright (c) 2018 SteveAmor # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# SPDX-License-Identifier: BSD-3-Clause -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - -import io import errno import warnings from posix import statvfs_result from subprocess import CalledProcessError +from threading import Event +from unittest import mock import pytest -from mock import patch from gpiozero import * from datetime import datetime, time @@ -53,9 +24,15 @@ file_not_found = IOError(errno.ENOENT, 'File not found') bad_ping = CalledProcessError(1, 'returned non-zero exit status 1') + +def test_polled_event_delay(mock_factory): + with TimeOfDay(time(7), time(8)) as tod: + tod.event_delay = 1.0 + assert tod.event_delay == 1.0 + def test_timeofday_bad_init(mock_factory): with pytest.raises(TypeError): - TimeOfDay() + TimeOfDay() with pytest.raises(ValueError): TimeOfDay(7, 12) with pytest.raises(TypeError): @@ -88,7 +65,7 @@ def test_timeofday_value(mock_factory): assert tod.start_time == time(7) assert tod.end_time == time(8) assert not tod.utc - with patch('gpiozero.internal_devices.datetime') as dt: + with mock.patch('gpiozero.internal_devices.datetime') as dt: dt.now.return_value = datetime(2018, 1, 1, 6, 59, 0) assert not tod.is_active dt.now.return_value = datetime(2018, 1, 1, 7, 0, 0) @@ -97,12 +74,13 @@ def test_timeofday_value(mock_factory): assert tod.is_active dt.now.return_value = datetime(2018, 1, 2, 8, 1, 0) assert not tod.is_active + assert repr(tod) == '' with TimeOfDay(time(1, 30), time(23, 30)) as tod: assert tod.start_time == time(1, 30) assert tod.end_time == time(23, 30) assert tod.utc - with patch('gpiozero.internal_devices.datetime') as dt: + with mock.patch('gpiozero.internal_devices.datetime') as dt: dt.utcnow.return_value = datetime(2018, 1, 1, 1, 29, 0) assert not tod.is_active dt.utcnow.return_value = datetime(2018, 1, 1, 1, 30, 0) @@ -115,7 +93,7 @@ def test_timeofday_value(mock_factory): assert not tod.is_active with TimeOfDay(time(23), time(1)) as tod: - with patch('gpiozero.internal_devices.datetime') as dt: + with mock.patch('gpiozero.internal_devices.datetime') as dt: dt.utcnow.return_value = datetime(2018, 1, 1, 22, 59, 0) assert not tod.is_active dt.utcnow.return_value = datetime(2018, 1, 1, 23, 0, 0) @@ -128,7 +106,7 @@ def test_timeofday_value(mock_factory): assert not tod.is_active with TimeOfDay(time(6), time(5)) as tod: - with patch('gpiozero.internal_devices.datetime') as dt: + with mock.patch('gpiozero.internal_devices.datetime') as dt: dt.utcnow.return_value = datetime(2018, 1, 1, 5, 30, 0) assert not tod.is_active dt.utcnow.return_value = datetime(2018, 1, 1, 5, 59, 0) @@ -148,16 +126,51 @@ def test_timeofday_value(mock_factory): dt.utcnow.return_value = datetime(2018, 1, 2, 6, 0, 0) assert tod.is_active +def test_polled_events(mock_factory): + with TimeOfDay(time(7), time(8)) as tod: + tod.event_delay = 0.1 + activated = Event() + deactivated = Event() + with mock.patch('gpiozero.internal_devices.datetime') as dt: + dt.utcnow.return_value = datetime(2018, 1, 1, 0, 0, 0) + tod._fire_events(tod.pin_factory.ticks(), tod.is_active) + tod.when_activated = activated.set + tod.when_deactivated = deactivated.set + assert not activated.wait(0) + assert not deactivated.wait(0) + dt.utcnow.return_value = datetime(2018, 1, 1, 7, 1, 0) + assert activated.wait(1) + activated.clear() + assert not deactivated.wait(0) + dt.utcnow.return_value = datetime(2018, 1, 1, 8, 1, 0) + assert deactivated.wait(1) + assert not activated.wait(0) + tod.when_activated = None + tod.when_deactivated = None + +def test_polled_event_start_stop(mock_factory): + with TimeOfDay(time(7), time(8)) as tod: + assert not tod._event_thread + tod.when_activated = lambda: True + assert tod._event_thread + tod.when_deactivated = lambda: True + assert tod._event_thread + tod.when_activated = None + assert tod._event_thread + tod.when_deactivated = None + assert not tod._event_thread + def test_pingserver_bad_init(mock_factory): with pytest.raises(TypeError): PingServer() def test_pingserver_init(mock_factory): - with patch('gpiozero.internal_devices.subprocess') as sp: + with mock.patch('gpiozero.internal_devices.subprocess') as sp: sp.check_call.return_value = True with PingServer('example.com') as server: assert repr(server).startswith('' with PingServer('192.168.1.10') as server: assert server.host == '192.168.1.10' with PingServer('8.8.8.8') as server: @@ -166,7 +179,7 @@ def test_pingserver_init(mock_factory): assert server.host == '2001:4860:4860::8888' def test_pingserver_value(mock_factory): - with patch('gpiozero.internal_devices.subprocess.check_call') as check_call: + with mock.patch('gpiozero.internal_devices.subprocess.check_call') as check_call: with PingServer('example.com') as server: assert server.is_active check_call.side_effect = bad_ping @@ -175,8 +188,8 @@ def test_pingserver_value(mock_factory): assert server.is_active def test_cputemperature_bad_init(mock_factory): - with patch('io.open') as m: - m.return_value.__enter__.side_effect = file_not_found + with mock.patch('io.open', mock.mock_open()) as m: + m.side_effect = file_not_found with pytest.raises(IOError): with CPUTemperature('') as temp: temp.value @@ -192,12 +205,12 @@ def test_cputemperature_bad_init(mock_factory): CPUTemperature(min_temp=20, max_temp=10) def test_cputemperature(mock_factory): - with patch('io.open') as m: - m.return_value.__enter__.return_value.readline.return_value = '37000' + with mock.patch('io.open', mock.mock_open(read_data='37000')) as m: with CPUTemperature() as cpu: assert repr(cpu).startswith('' with warnings.catch_warnings(record=True) as w: warnings.resetwarnings() with CPUTemperature(min_temp=30, max_temp=40) as cpu: @@ -210,16 +223,15 @@ def test_cputemperature(mock_factory): assert cpu.is_active def test_loadaverage_bad_init(mock_factory): - with patch('io.open') as m: - foo = m.return_value.__enter__ - foo.side_effect = file_not_found + with mock.patch('io.open', mock.mock_open()) as m: + m.side_effect = file_not_found with pytest.raises(IOError): with LoadAverage('') as load: load.value with pytest.raises(IOError): with LoadAverage('badfile') as load: load.value - foo.return_value.readline.return_value = '0.09 0.10 0.09 1/292 20758' + with mock.patch('io.open', mock.mock_open(read_data='0.09 0.10 0.09 1/292 20758')): with pytest.raises(ValueError): LoadAverage(min_load_average=1) with pytest.raises(ValueError): @@ -232,9 +244,7 @@ def test_loadaverage_bad_init(mock_factory): LoadAverage(minutes=10) def test_loadaverage(mock_factory): - with patch('io.open') as m: - foo = m.return_value.__enter__ - foo.return_value.readline.return_value = '0.09 0.10 0.09 1/292 20758' + with mock.patch('io.open', mock.mock_open(read_data='0.09 0.10 0.09 1/292 20758')): with LoadAverage() as la: assert repr(la).startswith('' + with mock.patch('io.open', mock.mock_open(read_data='1.72 1.40 1.31 3/457 23102')): with LoadAverage(min_load_average=0.5, max_load_average=2, threshold=1, minutes=5) as la: assert la.min_load_average == 0.5 @@ -265,7 +276,7 @@ def test_diskusage_bad_init(mock_factory): DiskUsage(filesystem='badfilesystem') def test_diskusage(mock_factory): - with patch('os.statvfs') as statvfs: + with mock.patch('os.statvfs') as statvfs: statvfs.return_value = statvfs_result(( 4096, 4096, 100000, 48000, 48000, 0, 0, 0, 0, 255)) with DiskUsage() as disk: @@ -274,6 +285,7 @@ def test_diskusage(mock_factory): assert disk.usage == 52.0 assert disk.is_active == False assert disk.value == 0.52 + assert repr(disk) == '' with DiskUsage(threshold=50.0) as disk: assert disk.is_active == True with warnings.catch_warnings(record=True) as w: diff --git a/tests/test_mixins.py b/tests/test_mixins.py index 804ce8721..736e29e4b 100644 --- a/tests/test_mixins.py +++ b/tests/test_mixins.py @@ -1,40 +1,10 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2019 Dave Jones -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2019-2021 Dave Jones # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import gc import sys @@ -133,3 +103,22 @@ def test_bad_callback(mock_factory): dev.when_activated = 100 with pytest.raises(BadEventHandler): dev.when_activated = lambda x, y: x + y + + +def test_shared_key(mock_factory): + class SharedDevice(SharedMixin, GPIODevice): + def __init__(self, pin, pin_factory=None): + super().__init__(pin, pin_factory=pin_factory) + + @classmethod + def _shared_key(cls, pin, pin_factory=None): + return pin + + def _conflicts_with(self, other): + return not isinstance(other, SharedDevice) + + with SharedDevice(4) as dev: + with SharedDevice(4) as another_dev: + pass + with pytest.raises(GPIOPinInUse): + GPIODevice(4) diff --git a/tests/test_mock_pin.py b/tests/test_mock_pin.py index 1c50643ec..5ab2b0298 100644 --- a/tests/test_mock_pin.py +++ b/tests/test_mock_pin.py @@ -1,54 +1,26 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2016-2019 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2016-2019 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause from threading import Event +from time import sleep import pytest -from gpiozero.pins.mock import MockPWMPin, MockPin from gpiozero import * +from gpiozero.pins.mock import * def test_mock_pin_init(mock_factory): with pytest.raises(ValueError): Device.pin_factory.pin(60) - assert Device.pin_factory.pin(2).number == 2 + assert Device.pin_factory.pin(2).info.name == 'GPIO2' + def test_mock_pin_defaults(mock_factory): pin = Device.pin_factory.pin(4) @@ -63,21 +35,25 @@ def test_mock_pin_defaults(mock_factory): pin = Device.pin_factory.pin(2) assert pin.pull == 'up' + def test_mock_pin_open_close(mock_factory): pin = Device.pin_factory.pin(2) pin.close() + def test_mock_pin_init_twice_same_pin(mock_factory): pin1 = Device.pin_factory.pin(2) - pin2 = Device.pin_factory.pin(pin1.number) + pin2 = Device.pin_factory.pin(pin1.info.name) assert pin1 is pin2 + def test_mock_pin_init_twice_different_pin(mock_factory): pin1 = Device.pin_factory.pin(2) - pin2 = Device.pin_factory.pin(pin1.number+1) + pin2 = Device.pin_factory.pin(3) assert pin1 != pin2 - assert pin1.number == 2 - assert pin2.number == pin1.number+1 + assert pin1.info.name == 'GPIO2' + assert pin2.info.name == 'GPIO3' + def test_mock_pwm_pin_defaults(mock_factory): pin = Device.pin_factory.pin(4, pin_class=MockPWMPin) @@ -92,30 +68,35 @@ def test_mock_pwm_pin_defaults(mock_factory): pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) assert pin.pull == 'up' + def test_mock_pwm_pin_open_close(mock_factory): pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) pin.close() + def test_mock_pwm_pin_init_twice_same_pin(mock_factory): pin1 = Device.pin_factory.pin(2, pin_class=MockPWMPin) - pin2 = Device.pin_factory.pin(pin1.number, pin_class=MockPWMPin) + pin2 = Device.pin_factory.pin(pin1.info.name, pin_class=MockPWMPin) assert pin1 is pin2 + def test_mock_pwm_pin_init_twice_different_pin(mock_factory): pin1 = Device.pin_factory.pin(2, pin_class=MockPWMPin) - pin2 = Device.pin_factory.pin(pin1.number + 1, pin_class=MockPWMPin) + pin2 = Device.pin_factory.pin(3, pin_class=MockPWMPin) assert pin1 != pin2 - assert pin1.number == 2 - assert pin2.number == pin1.number+1 + assert pin1.info.name == 'GPIO2' + assert pin2.info.name == 'GPIO3' + def test_mock_pin_init_twice_different_modes(mock_factory): pin1 = Device.pin_factory.pin(2, pin_class=MockPin) - pin2 = Device.pin_factory.pin(pin1.number + 1, pin_class=MockPWMPin) + pin2 = Device.pin_factory.pin(3, pin_class=MockPWMPin) assert pin1 != pin2 with pytest.raises(ValueError): - Device.pin_factory.pin(pin1.number, pin_class=MockPWMPin) + Device.pin_factory.pin(pin1.info.name, pin_class=MockPWMPin) with pytest.raises(ValueError): - Device.pin_factory.pin(pin2.number, pin_class=MockPin) + Device.pin_factory.pin(pin2.info.name, pin_class=MockPin) + def test_mock_pin_frequency_unsupported(mock_factory): pin = Device.pin_factory.pin(2) @@ -123,6 +104,7 @@ def test_mock_pin_frequency_unsupported(mock_factory): with pytest.raises(PinPWMUnsupported): pin.frequency = 100 + def test_mock_pin_frequency_supported(mock_factory): pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) pin.function = 'output' @@ -132,6 +114,7 @@ def test_mock_pin_frequency_supported(mock_factory): pin.frequency = None assert not pin.state + def test_mock_pin_pull(mock_factory): pin = Device.pin_factory.pin(4) pin.function = 'input' @@ -152,6 +135,7 @@ def test_mock_pin_pull(mock_factory): with pytest.raises(PinFixedPull): pin.pull = 'floating' + def test_mock_pin_state(mock_factory): pin = Device.pin_factory.pin(2) with pytest.raises(PinSetInput): @@ -164,6 +148,35 @@ def test_mock_pin_state(mock_factory): pin.state = 0.5 assert pin.state == 1 + +def test_mock_pin_connected_state(mock_factory): + input_pin = Device.pin_factory.pin(4) + pin2 = Device.pin_factory.pin(5, pin_class=MockConnectedPin, input_pin=input_pin) + pin3 = Device.pin_factory.pin(6, pin_class=MockConnectedPin) + input_pin.function = 'input' + pin2.function = 'output' + pin3.function = 'output' + pin2.state = 0 + assert input_pin.state == 0 + pin2.state = 1 + assert input_pin.state == 1 + pin3.state = 0 + assert input_pin.state == 1 + pin3.state = 1 + assert input_pin.state == 1 + + +def test_mock_pin_functions(mock_factory): + pin = Device.pin_factory.pin(2) + assert pin.function == 'input' + pin.function = 'output' + assert pin.function == 'output' + pin.function = 'input' + assert pin.function == 'input' + with pytest.raises(ValueError): + pin.function = 'foo' + + def test_mock_pwm_pin_state(mock_factory): pin = Device.pin_factory.pin(2, pin_class=MockPWMPin) with pytest.raises(PinSetInput): @@ -176,6 +189,7 @@ def test_mock_pwm_pin_state(mock_factory): pin.state = 0.5 assert pin.state == 0.5 + def test_mock_pin_edges(mock_factory): pin = Device.pin_factory.pin(2) assert pin.when_changed is None @@ -202,3 +216,23 @@ def changed(ticks, state): assert not fired.is_set() assert pin.edges == 'falling' + +def test_mock_charging_pin(mock_factory): + pin = Device.pin_factory.pin(4, pin_class=MockChargingPin, charge_time=1) + pin.function = 'input' + assert pin.state == 0 + pin.function = 'output' + assert pin.state == 0 + pin.close() + mock_factory.reset() + pin = Device.pin_factory.pin(4, pin_class=MockChargingPin, charge_time=0.01) + pin.function = 'input' + sleep(0.1) + assert pin.state == 1 + pin.function = 'output' + pin.state = 0 + pin.function = 'output' + assert pin.state == 0 + pin.function = 'input' + sleep(0.1) + assert pin.state == 1 diff --git a/tests/test_outputs.py b/tests/test_outputs.py index 9a70992cb..22d23979a 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -1,54 +1,23 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li # Copyright (c) 2018-2019 Ben Nuttall -# Copyright (c) 2016-2019 Dave Jones # Copyright (c) 2016 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import sys from time import sleep, time -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose +from math import isclose import pytest from colorzero import Color, Red, Green, Blue from gpiozero import * +from gpiozero.tones import Tone def test_output_initial_values(mock_factory, pwm): @@ -57,6 +26,7 @@ def test_output_initial_values(mock_factory, pwm): assert repr(device).startswith('' with OutputDevice(2, initial_value=True) as device: assert pin.state state = pin.state @@ -986,18 +956,6 @@ def test_rgbled_close_nonpwm(mock_factory): led.close() assert led.closed -def test_motor_bad_init(mock_factory): - with pytest.raises(GPIOPinMissing): - Motor() - with pytest.raises(GPIOPinMissing): - Motor(2) - with pytest.raises(GPIOPinMissing): - Motor(forward=2) - with pytest.raises(GPIOPinMissing): - Motor(backward=2) - with pytest.raises(TypeError): - Motor(a=2, b=3) - def test_motor_pins(mock_factory, pwm): f = mock_factory.pin(1) b = mock_factory.pin(2) @@ -1141,16 +1099,6 @@ def test_motor_reverse_nonpwm(mock_factory): assert motor.value == -1 assert b.state == 1 and f.state == 0 -def test_motor_enable_pin_bad_init(mock_factory, pwm): - with pytest.raises(GPIOPinMissing): - Motor(enable=1) - with pytest.raises(GPIOPinMissing): - Motor(forward=1, enable=2) - with pytest.raises(GPIOPinMissing): - Motor(backward=1, enable=2) - with pytest.raises(GPIOPinMissing): - Motor(backward=1, enable=2, pwm=True) - def test_motor_enable_pin_init(mock_factory, pwm): f = mock_factory.pin(1) b = mock_factory.pin(2) @@ -1163,7 +1111,7 @@ def test_motor_enable_pin_init(mock_factory, pwm): assert motor.enable_device.pin is e assert isinstance(motor.enable_device, DigitalOutputDevice) assert e.state - with Motor(1, 2, 3) as motor: + with Motor(1, 2, enable=3) as motor: assert motor.forward_device.pin is f assert isinstance(motor.forward_device, PWMOutputDevice) assert motor.backward_device.pin is b @@ -1328,6 +1276,7 @@ def test_phaseenable_motor_reverse_nonpwm(mock_factory): assert motor.value == -1 assert p.state == 1 and e.state == 1 +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_servo_pins(mock_factory, pwm): p = mock_factory.pin(1) with Servo(1) as servo: @@ -1335,6 +1284,7 @@ def test_servo_pins(mock_factory, pwm): assert servo.pwm_device.pin is p assert isinstance(servo.pwm_device, PWMOutputDevice) +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_servo_bad_value(mock_factory, pwm): p = mock_factory.pin(1) with pytest.raises(ValueError): @@ -1349,6 +1299,7 @@ def test_servo_pins_nonpwm(mock_factory): with pytest.raises(PinPWMUnsupported): Servo(1) +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_servo_close(mock_factory, pwm): p = mock_factory.pin(2) with Servo(2) as servo: @@ -1358,6 +1309,7 @@ def test_servo_close(mock_factory, pwm): servo.close() assert servo.closed +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_servo_pulse_width(mock_factory, pwm): p = mock_factory.pin(2) with Servo(2, min_pulse_width=5/10000, max_pulse_width=25/10000) as servo: @@ -1369,9 +1321,12 @@ def test_servo_pulse_width(mock_factory, pwm): assert isclose(servo.pulse_width, 5/10000) servo.value = 1 assert isclose(servo.pulse_width, 25/10000) + servo.pulse_width = 20/10000 + assert isclose(servo.pulse_width, 20/10000) servo.value = None assert servo.pulse_width is None +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_servo_initial_values(mock_factory, pwm): p = mock_factory.pin(2) with Servo(2) as servo: @@ -1392,6 +1347,7 @@ def test_servo_initial_values(mock_factory, pwm): assert not servo.is_active assert servo.value is None +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_servo_values(mock_factory, pwm): p = mock_factory.pin(1) with Servo(1) as servo: @@ -1419,12 +1375,21 @@ def test_servo_values(mock_factory, pwm): servo.value = None assert servo.value is None +def test_angular_servo_bad_value(mock_factory, pwm): + p = mock_factory.pin(1) + with pytest.raises(ValueError): + AngularServo(1, initial_angle=-100) + with pytest.raises(ValueError): + AngularServo(1, min_angle=95) + +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PWMSoftwareFallback') def test_angular_servo_range(mock_factory, pwm): with AngularServo(1, initial_angle=15, min_angle=0, max_angle=90) as servo: assert repr(servo).startswith('' with TonalBuzzer(2, mid_tone='C4') as tb: assert tb.pwm_device.frequency is None with TonalBuzzer(2, mid_tone='C4', initial_value=0) as tb: @@ -1518,6 +1494,7 @@ def test_tonalbuzzer_init(mock_factory, pwm): with TonalBuzzer(2, octaves=2, initial_value=1) as tb: assert isclose(tb.pwm_device.frequency, 1760) +@pytest.mark.filterwarnings('ignore::gpiozero.exc.AmbiguousTone') def test_tonalbuzzer_play(mock_factory, pwm): with TonalBuzzer(2) as tb: tb.play(60) @@ -1527,15 +1504,15 @@ def test_tonalbuzzer_play(mock_factory, pwm): assert tb.pwm_device.frequency is None tb.play('C5') assert isclose(tb.pwm_device.frequency, 523.25, abs_tol=1/100) - tb.play('A#4') + tb.play(Tone('A#4')) assert isclose(tb.pwm_device.frequency, 466.16, abs_tol=1/100) tb.stop() assert tb.value is None assert tb.pwm_device.frequency is None with pytest.raises(ValueError): - tb.play('GS3') + tb.play('G#3') with pytest.raises(ValueError): - tb.play('AS5') + tb.play('A#5') def test_tonalbuzzer_set_value(mock_factory, pwm): with TonalBuzzer(2) as tb: @@ -1572,3 +1549,15 @@ def test_tonalbuzzer_read_value(mock_factory, pwm): assert isclose(tb.value, 0.5) tb.play('A6') assert isclose(tb.value, 1) + +def test_tonalbuzzer_tone(mock_factory, pwm): + with TonalBuzzer(2) as tb: + assert tb.tone is None + tb.play('A3') + assert tb.tone == Tone('A3') + tb.play('A4') + assert tb.tone == Tone('A4') + tb.play('A5') + assert tb.tone == Tone('A5') + tb.tone = None + assert tb.tone is None diff --git a/tests/test_pins_data.py b/tests/test_pins_data.py index 05dd91f93..adffe0d12 100644 --- a/tests/test_pins_data.py +++ b/tests/test_pins_data.py @@ -1,64 +1,36 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones +# +# Copyright (c) 2016-2023 Dave Jones # Copyright (c) 2019 Ben Nuttall # Copyright (c) 2018 Martchus # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import io import re import errno import pytest -from mock import patch, MagicMock +from unittest import mock import gpiozero.pins.data import gpiozero.pins.local from gpiozero.pins.local import LocalPiFactory -from gpiozero.pins.data import Style, HeaderInfo, PinInfo +from gpiozero.pins.pi import PiBoardInfo +from gpiozero.pins.style import Style +from gpiozero.compat import frozendict from gpiozero import * def test_pi_revision(): - with patch('gpiozero.devices.Device.pin_factory', LocalPiFactory()): + with mock.patch('gpiozero.devices.Device.pin_factory', LocalPiFactory()): # Can't use MockPin for this as we want something that'll actually try # and read /proc/device-tree/system/linux,revision and /proc/cpuinfo # (MockPin simply parrots the 3B's data); LocalPiFactory is used as we # can definitely instantiate it (strictly speaking it's abstract but - # we're only interested in the pi_info stuff) - with patch('io.open') as m: + # we're only interested in the BoardInfo stuff) + with mock.patch('io.open') as m: m.return_value.__enter__.side_effect = [ # Pretend /proc/device-tree/system/linux,revision doesn't # exist, and that /proc/cpuinfo contains the Revision: 0002 @@ -66,7 +38,7 @@ def test_pi_revision(): IOError(errno.ENOENT, 'File not found'), ['lots of irrelevant', 'lines', 'followed by', 'Revision: 0002', 'Serial: xxxxxxxxxxx'] ] - assert pi_info().revision == '0002' + assert Device.pin_factory.board_info.revision == '0002' # LocalPiFactory caches the revision (because realistically it # isn't going to change at runtime); we need to wipe it here though Device.pin_factory._info = None @@ -74,7 +46,7 @@ def test_pi_revision(): IOError(errno.ENOENT, 'File not found'), ['Revision: a21042'] ] - assert pi_info().revision == 'a21042' + assert Device.pin_factory.board_info.revision == 'a21042' # Check over-volting result (some argument over whether this is 7 # or 8 character result; make sure both work) Device.pin_factory._info = None @@ -82,20 +54,31 @@ def test_pi_revision(): IOError(errno.ENOENT, 'File not found'), ['Revision: 1000003'] ] - assert pi_info().revision == '0003' + assert Device.pin_factory.board_info.revision == '0003' Device.pin_factory._info = None m.return_value.__enter__.side_effect = [ IOError(errno.ENOENT, 'File not found'), ['Revision: 100003'] ] - assert pi_info().revision == '0003' + assert Device.pin_factory.board_info.revision == '0003' + # Check we complain loudly if we can't access linux,revision + Device.pin_factory._info = None + m.return_value.__enter__.side_effect = [ + # Pretend /proc/device-tree/system/linux,revision doesn't + # exist, and that /proc/cpuinfo contains the Revision: 0002 + # after some filler + IOError(errno.EACCES, 'Permission denied'), + ['Revision: 100003'] + ] + with pytest.raises(IOError): + Device.pin_factory.board_info # Check that parsing /proc/device-tree/system/linux,revision also # works properly Device.pin_factory._info = None m.return_value.__enter__.side_effect = None m.return_value.__enter__.return_value = io.BytesIO(b'\x00\xa2\x20\xd3') - assert pi_info().revision == 'a220d3' - # Finally, check that if everything's a bust we raise PinUnknownPi + assert Device.pin_factory.board_info.revision == 'a220d3' + # Check that if everything's a bust we raise PinUnknownPi with pytest.raises(PinUnknownPi): Device.pin_factory._info = None m.return_value.__enter__.return_value = None @@ -103,10 +86,11 @@ def test_pi_revision(): IOError(errno.ENOENT, 'File not found'), ['nothing', 'relevant'] ] - pi_info() + Device.pin_factory.board_info with pytest.raises(PinUnknownPi): - pi_info('0fff') + PiBoardInfo.from_revision(0xfff) +@pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_pi_info(): r = pi_info('900011') assert r.model == 'B' @@ -132,151 +116,182 @@ def test_pi_info(): assert not r.bluetooth assert r.csi == 1 assert r.dsi == 1 + assert repr(r).startswith('PiBoardInfo(revision=') + assert 'headers=...' in repr(r) +@pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_pi_info_other_types(): assert pi_info(b'9000f1') == pi_info(0x9000f1) +def test_find_pin(): + board_info = PiBoardInfo.from_revision(0xa21041) + assert {('J8', 1), ('J8', 17)} == { + (head.name, pin.number) + for (head, pin) in board_info.find_pin('3V3') + } + assert {('J8', 3)} == { + (head.name, pin.number) + for (head, pin) in board_info.find_pin('GPIO2') + } + assert set() == { + (head.name, pin.number) + for (head, pin) in board_info.find_pin('GPIO47') + } + +@pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_physical_pins(): # Assert physical pins for some well-known Pi's; a21041 is a Pi2B - assert pi_info('a21041').physical_pins('3V3') == {('J8', 1), ('J8', 17)} - assert pi_info('a21041').physical_pins('GPIO2') == {('J8', 3)} - assert pi_info('a21041').physical_pins('GPIO47') == set() + board_info = PiBoardInfo.from_revision(0xa21041) + assert board_info.physical_pins('3V3') == {('J8', 1), ('J8', 17)} + assert board_info.physical_pins('GPIO2') == {('J8', 3)} + assert board_info.physical_pins('GPIO47') == set() +@pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_physical_pin(): + board_info = PiBoardInfo.from_revision(0xa21041) with pytest.raises(PinMultiplePins): - assert pi_info('a21041').physical_pin('GND') - assert pi_info('a21041').physical_pin('GPIO3') == ('J8', 5) + assert board_info.physical_pin('GND') + assert board_info.physical_pin('GPIO3') == ('J8', 5) with pytest.raises(PinNoPins): - assert pi_info('a21041').physical_pin('GPIO47') + assert board_info.physical_pin('GPIO47') +@pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_pulled_up(): - assert pi_info('a21041').pulled_up('GPIO2') - assert not pi_info('a21041').pulled_up('GPIO4') - assert not pi_info('a21041').pulled_up('GPIO47') + board_info = PiBoardInfo.from_revision(0xa21041) + assert board_info.pulled_up('GPIO2') + assert not board_info.pulled_up('GPIO4') + assert not board_info.pulled_up('GPIO47') -def test_pprint_content(): - with patch('sys.stdout') as stdout: - stdout.output = [] - stdout.write = lambda buf: stdout.output.append(buf) - pi_info('900092').pprint(color=False) - s = ''.join(stdout.output) - assert ('o' * 20 + ' ') in s # first header row - assert ('1' + 'o' * 19 + ' ') in s # second header row - assert 'PiZero' in s - assert 'V1.2' in s # PCB revision - assert '900092' in s # Pi revision - assert 'BCM2835' in s # SOC name - stdout.output = [] - pi_info('0002').pprint(color=False) - s = ''.join(stdout.output) - assert ('o' * 13 + ' ') in s # first header row - assert ('1' + 'o' * 12 + ' ') in s # second header row - assert 'Pi Model' in s - assert 'B V1.0' in s # PCB revision - assert '0002' in s # Pi revision - assert 'BCM2835' in s # SOC name - stdout.output = [] - pi_info('0014').headers['SODIMM'].pprint(color=False) - assert len(''.join(stdout.output).splitlines()) == 100 +def test_pull(): + board_info = PiBoardInfo.from_revision(0xa21041) + for header, pin in board_info.find_pin('GPIO2'): + assert pin.pull == 'up' + for header, pin in board_info.find_pin('GPIO4'): + assert pin.pull == '' + for header, pin in board_info.find_pin('GPIO47'): + assert pin.pull == '' -def test_format_content(): - with patch('sys.stdout') as stdout: - stdout.output = [] - stdout.write = lambda buf: stdout.output.append(buf) - pi_info('900092').pprint(color=False) - s = ''.join(stdout.output) - assert '{0:mono}\n'.format(pi_info('900092')) == s - stdout.output = [] - pi_info('900092').pprint(color=True) - s = ''.join(stdout.output) - assert '{0:color full}\n'.format(pi_info('900092')) == s +def test_pprint_content(capsys): + PiBoardInfo.from_revision(0x900092).pprint(color=False) + cap = capsys.readouterr() + assert ('-' + 'o' * 20) in cap.out # first header row + assert (' ' + '1' + 'o' * 19) in cap.out # second header row + assert 'PiZero' in cap.out + assert 'V1.2' in cap.out # PCB revision + assert '900092' in cap.out # Pi revision + assert 'BCM2835' in cap.out # SoC name + PiBoardInfo.from_revision(0x0002).pprint(color=False) + cap = capsys.readouterr() + assert ('o' * 13 + ' ') in cap.out # first header row + assert ('1' + 'o' * 12 + ' ') in cap.out # second header row + assert 'Pi Model' in cap.out + assert 'B V1.0' in cap.out # PCB revision + assert '0002' in cap.out # Pi revision + assert 'BCM2835' in cap.out # SOC name + PiBoardInfo.from_revision(0x0014).headers['SODIMM'].pprint(color=False) + cap = capsys.readouterr() + assert len(cap.out.splitlines()) == 100 -def test_pprint_headers(): - assert len(pi_info('0002').headers) == 1 - assert len(pi_info('000e').headers) == 2 - assert len(pi_info('900092').headers) == 1 - with patch('sys.stdout') as stdout: - stdout.output = [] - stdout.write = lambda buf: stdout.output.append(buf) - pi_info('0002').pprint() - s = ''.join(stdout.output) - assert 'P1:\n' in s - assert 'P5:\n' not in s - stdout.output = [] - pi_info('000e').pprint() - s = ''.join(stdout.output) - assert 'P1:\n' in s - assert 'P5:\n' in s - stdout.output = [] - pi_info('900092').pprint() - s = ''.join(stdout.output) - assert 'J8:\n' in s - assert 'P1:\n' not in s - assert 'P5:\n' not in s +def test_format_content(capsys): + board_info = PiBoardInfo.from_revision(0x900092) + board_info.pprint(color=False) + cap = capsys.readouterr() + assert f'{board_info:mono}\n' == cap.out + board_info.pprint(color=True) + cap = capsys.readouterr() + assert f'{board_info:color full}\n' == cap.out + with pytest.raises(ValueError): + f'{board_info:color foo}' + +def test_pprint_headers(capsys): + assert len(PiBoardInfo.from_revision(0x0002).headers) == 3 + assert len(PiBoardInfo.from_revision(0x000e).headers) == 5 + assert len(PiBoardInfo.from_revision(0x900092).headers) == 3 + PiBoardInfo.from_revision(0x0002).pprint() + cap = capsys.readouterr() + assert 'P1:\n' in cap.out + assert 'P5:\n' not in cap.out + PiBoardInfo.from_revision(0x000e).pprint() + cap = capsys.readouterr() + assert 'P1:\n' in cap.out + assert 'P5:\n' in cap.out + PiBoardInfo.from_revision(0x900092).pprint() + cap = capsys.readouterr() + assert 'J8:\n' in cap.out + assert 'P1:\n' not in cap.out + assert 'P5:\n' not in cap.out -def test_pprint_color(): - with patch('sys.stdout') as stdout: +def test_format_headers(capsys): + board_info = PiBoardInfo.from_revision(0xc03131) + board_info.headers['J8'].pprint(color=False) + cap = capsys.readouterr() + assert f'{board_info.headers["J8"]:mono}\n' == cap.out + board_info.headers['J8'].pprint(color=True) + cap = capsys.readouterr() + assert f'{board_info.headers["J8"]:color}\n' == cap.out + with pytest.raises(ValueError): + f'{board_info.headers["J8"]:mono foo}' + +def test_pprint_color(capsys): + board_info = PiBoardInfo.from_revision(0x900092) + board_info.pprint(color=False) + cap = capsys.readouterr() + assert '\x1b[0m' not in cap.out + board_info.pprint(color=True) + cap = capsys.readouterr() + assert '\x1b[0m' in cap.out + +def test_pprint_style_detect(): + board_info = PiBoardInfo.from_revision(0x900092) + with mock.patch('sys.stdout') as stdout: stdout.output = [] stdout.write = lambda buf: stdout.output.append(buf) - pi_info('900092').pprint(color=False) - s = ''.join(stdout.output) - assert '\x1b[0m' not in s # make sure ANSI reset code isn't in there - stdout.output = [] - pi_info('900092').pprint(color=True) - s = ''.join(stdout.output) - assert '\x1b[0m' in s # check the ANSI reset code *is* in there (can't guarantee much else!) - stdout.output = [] stdout.fileno.side_effect = IOError('not a real file') - pi_info('900092').pprint() + board_info.pprint() s = ''.join(stdout.output) assert '\x1b[0m' not in s # default should output mono - with patch('os.isatty') as isatty: + with mock.patch('os.isatty') as isatty: isatty.return_value = True stdout.fileno.side_effect = None + stdout.fileno.return_value = 1 stdout.output = [] - pi_info('900092').pprint() + board_info.pprint() s = ''.join(stdout.output) assert '\x1b[0m' in s # default should now output color -def test_pprint_styles(): +def test_style_parser(): with pytest.raises(ValueError): Style.from_style_content('mono color full') with pytest.raises(ValueError): - Style.from_style_content('full specs') - with patch('sys.stdout') as stdout: - s = '{0:full}'.format(pi_info('900092')) - assert '\x1b[0m' not in s # ensure default is mono when stdout is not a tty - with pytest.raises(ValueError): - '{0:foo on bar}'.format(Style()) + f'{Style():foo on bar}' -def test_pprint_missing_pin(): +def test_pprint_missing_pin(capsys): header = HeaderInfo('FOO', 4, 2, { - 1: PinInfo(1, '5V', False, 1, 1), - 2: PinInfo(2, 'GND', False, 1, 2), + 1: PinInfo(1, '5V', {'5V'}, '', 1, 1, set()), + 2: PinInfo(2, 'GND', {'GND'}, '', 1, 2, set()), # Pin 3 is deliberately missing - 4: PinInfo(4, 'GPIO1', False, 2, 2), - 5: PinInfo(5, 'GPIO2', False, 3, 1), - 6: PinInfo(6, 'GPIO3', False, 3, 2), - 7: PinInfo(7, '3V3', False, 4, 1), - 8: PinInfo(8, 'GND', False, 4, 2), + 4: PinInfo(4, 'GPIO1', {'GPIO1'}, '', 2, 2, {'gpio'}), + 5: PinInfo(5, 'GPIO2', {'GPIO2'}, 'up', 3, 1, {'gpio'}), + 6: PinInfo(6, 'GPIO3', {'GPIO3'}, 'up', 3, 2, {'gpio'}), + 7: PinInfo(7, '3V3', {'3V3'}, '', 4, 1, set()), + 8: PinInfo(8, 'GND', {'GND'}, '', 4, 2, set()), }) - with patch('sys.stdout') as stdout: - stdout.output = [] - stdout.write = lambda buf: stdout.output.append(buf) - s = ''.join(stdout.output) - header.pprint() - for i in range(1, 9): - if i == 3: - assert '(3)' not in s - else: - assert ('(%d)' % i) + header.pprint() + cap = capsys.readouterr() + for i in range(1, 9): + if i == 3: + assert '(3)' not in cap.out + else: + assert f'({i:d})' in cap.out def test_pprint_rows_cols(): - assert '{0:row1}'.format(pi_info('900092').headers['J8']) == '1o' - assert '{0:row2}'.format(pi_info('900092').headers['J8']) == 'oo' - assert '{0:col1}'.format(pi_info('0002').headers['P1']) == '1oooooooooooo' - assert '{0:col2}'.format(pi_info('0002').headers['P1']) == 'ooooooooooooo' + board_info = PiBoardInfo.from_revision(0x900092) + assert f'{board_info.headers["J8"]:row1}' == '1o' + assert f'{board_info.headers["J8"]:row2}' == 'oo' + board_info = PiBoardInfo.from_revision(0x0002) + assert f'{board_info.headers["P1"]:col1}' == '1oooooooooooo' + assert f'{board_info.headers["P1"]:col2}' == 'ooooooooooooo' with pytest.raises(ValueError): - '{0:row16}'.format(pi_info('0002').headers['P1']) + f'{board_info.headers["P1"]:row16}' with pytest.raises(ValueError): - '{0:col3}'.format(pi_info('0002').headers['P1']) + f'{board_info.headers["P1"]:col3}' diff --git a/tests/test_real_pins.py b/tests/test_real_pins.py index ec93d007d..7fff6d48c 100644 --- a/tests/test_real_pins.py +++ b/tests/test_real_pins.py @@ -1,105 +1,111 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2024 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2020 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') -try: - range = xrange -except NameError: - pass +# SPDX-License-Identifier: BSD-3-Clause -import io import os import errno +import warnings from time import time, sleep +from math import isclose +from unittest import mock + +# NOTE: Remove try when compatibility moves beyond Python 3.10 +try: + from importlib_metadata import entry_points +except ImportError: + from importlib.metadata import entry_points import pytest -import pkg_resources from gpiozero import * -from gpiozero.pins.mock import MockConnectedPin, MockFactory +from gpiozero.pins.mock import MockConnectedPin, MockFactory, MockSPIDevice from gpiozero.pins.native import NativeFactory -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose +from gpiozero.pins.local import LocalPiFactory, LocalPiHardwareSPI # This module assumes you've wired the following GPIO pins together. The pins # can be re-configured via the listed environment variables (useful for when # your testing rig requires different pins because the defaults interfere with -# attached hardware). -TEST_PIN = int(os.environ.get('GPIOZERO_TEST_PIN', '22')) -INPUT_PIN = int(os.environ.get('GPIOZERO_TEST_INPUT_PIN', '27')) +# attached hardware). Please note that the name specified *must* be the primary +# name of the pin, e.g. GPIO22 rather than an alias like BCM22 or 22 (several +# tests rely upon this). +TEST_PIN = os.environ.get('GPIOZERO_TEST_PIN', 'GPIO22') +INPUT_PIN = os.environ.get('GPIOZERO_TEST_INPUT_PIN', 'GPIO27') + +# The lock path is intended to prevent parallel runs of the "real pins" test +# suite. For example, if you are testing multiple Python versions under tox, +# the mocked devices will all happily test in parallel. However, the real pins +# test must not or you risk one run trying to set a pin to an output while +# another simultaneously demands it's an input. The path specified here must be +# visible and accessible to all simultaneous runs. TEST_LOCK = os.environ.get('GPIOZERO_TEST_LOCK', '/tmp/real_pins_lock') -@pytest.fixture( - scope='module', - params=[ - name - for name in pkg_resources.\ - get_distribution('gpiozero').\ - get_entry_map('gpiozero_pin_factories').keys() - if not name.endswith('Pin') # leave out compatibility names - ]) -def pin_factory_name(request): - return request.param +def local_only(): + if not isinstance(Device.pin_factory, LocalPiFactory): + pytest.skip("Test cannot run with non-local pin factories") + +def local_hardware_spi_only(intf): + if not isinstance(intf, LocalPiHardwareSPI): + pytest.skip("Test assumes LocalPiHardwareSPI descendant") -@pytest.yield_fixture() + +with warnings.catch_warnings(): + @pytest.fixture( + scope='module', + params=[ep.name for ep in entry_points(group='gpiozero_pin_factories')]) + def pin_factory_name(request): + return request.param + + +@pytest.fixture() def pin_factory(request, pin_factory_name): try: - factory = pkg_resources.load_entry_point( - 'gpiozero', 'gpiozero_pin_factories', pin_factory_name)() + with warnings.catch_warnings(): + eps = entry_points(group='gpiozero_pin_factories') + for ep in eps: + if ep.name == pin_factory_name: + factory = ep.load()() + break + else: + assert False, 'internal error' except Exception as e: - pytest.skip("skipped factory %s: %s" % (pin_factory_name, str(e))) + pytest.skip("skipped factory {pin_factory_name}: {e!s}".format( + pin_factory_name=pin_factory_name, e=e)) else: yield factory factory.close() -@pytest.yield_fixture() + +@pytest.fixture() def default_factory(request, pin_factory): save_pin_factory = Device.pin_factory Device.pin_factory = pin_factory yield pin_factory Device.pin_factory = save_pin_factory -@pytest.yield_fixture(scope='function') + +@pytest.fixture(scope='function') def pins(request, pin_factory): # Why return both pins in a single fixture? If we defined one fixture for # each pin then pytest will (correctly) test RPiGPIOPin(22) against # NativePin(27) and so on. This isn't supported, so we don't test it + assert not ( + {INPUT_PIN, TEST_PIN} & { + 'GPIO2', + 'GPIO3', + 'GPIO7', + 'GPIO8', + 'GPIO9', + 'GPIO10', + 'GPIO11', + }), 'Cannot use SPI (7-11) or I2C (2-3) pins for tests' input_pin = pin_factory.pin(INPUT_PIN) input_pin.function = 'input' input_pin.pull = 'down' @@ -113,40 +119,37 @@ def pins(request, pin_factory): def setup_module(module): - # Python 2.7 compatible method of exclusive-open - flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY start = time() while True: if time() - start > 300: # 5 minute timeout raise RuntimeError('timed out waiting for real pins lock') try: - fd = os.open(TEST_LOCK, flags) - except OSError as e: - if e.errno == errno.EEXIST: - print('Waiting for lock before testing real-pins') - sleep(0.1) - else: - raise - else: - with os.fdopen(fd, 'w') as f: + with open(TEST_LOCK, 'x') as f: f.write('Lock file for gpiozero real-pin tests; delete ' 'this if the test suite is not currently running\n') + except FileExistsError: + print('Waiting for lock before testing real-pins') + sleep(1) + else: break + def teardown_module(module): os.unlink(TEST_LOCK) -def test_pin_numbers(pins): +def test_pin_names(pins): test_pin, input_pin = pins - assert test_pin.number == TEST_PIN - assert input_pin.number == INPUT_PIN + assert test_pin.info.name == TEST_PIN + assert input_pin.info.name == INPUT_PIN + def test_function_bad(pins): test_pin, input_pin = pins with pytest.raises(PinInvalidFunction): test_pin.function = 'foo' + def test_output(pins): test_pin, input_pin = pins test_pin.function = 'output' @@ -155,6 +158,7 @@ def test_output(pins): test_pin.state = 1 assert input_pin.state == 1 + def test_output_with_state(pins): test_pin, input_pin = pins test_pin.output_with_state(0) @@ -162,15 +166,19 @@ def test_output_with_state(pins): test_pin.output_with_state(1) assert input_pin.state == 1 + def test_pull(pins): test_pin, input_pin = pins + input_pin.pull = 'floating' test_pin.function = 'input' test_pin.pull = 'up' + assert test_pin.state == 1 assert input_pin.state == 1 test_pin.pull = 'down' - test_pin, input_pin = pins + assert test_pin.state == 0 assert input_pin.state == 0 + def test_pull_bad(pins): test_pin, input_pin = pins test_pin.function = 'input' @@ -179,26 +187,39 @@ def test_pull_bad(pins): with pytest.raises(PinInvalidPull): test_pin.input_with_pull('foo') + def test_pull_down_warning(pin_factory): - if pin_factory.pi_info.pulled_up('GPIO2'): - pin = pin_factory.pin(2) - try: - with pytest.raises(PinFixedPull): - pin.pull = 'down' - with pytest.raises(PinFixedPull): - pin.input_with_pull('down') - finally: - pin.close() - else: - pytest.skip("GPIO2 isn't pulled up on this pi") + with pin_factory.pin('GPIO2') as pin: + if pin.info.pull != 'up': + pytest.skip("GPIO2 isn't pulled up on this pi") + with pytest.raises(PinFixedPull): + pin.pull = 'down' + with pytest.raises(PinFixedPull): + pin.input_with_pull('down') + def test_input_with_pull(pins): test_pin, input_pin = pins + input_pin.pull = 'floating' test_pin.input_with_pull('up') + assert test_pin.state == 1 assert input_pin.state == 1 test_pin.input_with_pull('down') + assert test_pin.state == 0 assert input_pin.state == 0 + +def test_pulls_are_weak(pins): + test_pin, input_pin = pins + test_pin.function = 'output' + for pull in ('floating', 'down', 'up'): + input_pin.pull = pull + test_pin.state = 0 + assert input_pin.state == 0 + test_pin.state = 1 + assert input_pin.state == 1 + + def test_bad_duty_cycle(pins): test_pin, input_pin = pins test_pin.function = 'output' @@ -210,7 +231,8 @@ def test_bad_duty_cycle(pins): sleep(0.1) test_pin.frequency = 100 except PinPWMUnsupported: - pytest.skip("%r doesn't support PWM" % test_pin.factory) + pytest.skip("{test_pin.factory!r} doesn't support PWM".format( + test_pin=test_pin)) else: try: with pytest.raises(ValueError): @@ -218,6 +240,7 @@ def test_bad_duty_cycle(pins): finally: test_pin.frequency = None + def test_duty_cycles(pins): test_pin, input_pin = pins test_pin.function = 'output' @@ -227,7 +250,8 @@ def test_duty_cycles(pins): sleep(0.1) test_pin.frequency = 100 except PinPWMUnsupported: - pytest.skip("%r doesn't support PWM" % test_pin.factory) + pytest.skip("{test_pin.factory!r} doesn't support PWM".format( + test_pin=test_pin)) else: try: for duty_cycle in (0.0, 0.1, 0.5, 1.0): @@ -238,46 +262,57 @@ def test_duty_cycles(pins): finally: test_pin.frequency = None + def test_explicit_factory(no_default_factory, pin_factory): with GPIODevice(TEST_PIN, pin_factory=pin_factory) as device: assert Device.pin_factory is None assert device.pin_factory is pin_factory - assert device.pin.number == TEST_PIN + assert device.pin.info.name == TEST_PIN + +@pytest.mark.filterwarnings('ignore::DeprecationWarning') def test_envvar_factory(no_default_factory, pin_factory_name): os.environ['GPIOZERO_PIN_FACTORY'] = pin_factory_name assert Device.pin_factory is None try: device = GPIODevice(TEST_PIN) except Exception as e: - pytest.skip("skipped factory %s: %s" % (pin_factory_name, str(e))) + pytest.skip("skipped factory {pin_factory_name}: {e!s}".format( + pin_factory_name=pin_factory_name, e=e)) else: try: - group = 'gpiozero_pin_factories' - for factory in pkg_resources.iter_entry_points(group, pin_factory_name): - factory_class = factory.load() + group = entry_points(group='gpiozero_pin_factories') + for ep in group: + if ep.name == pin_factory_name: + factory_class = ep.load() + break + else: + assert False, 'internal error' assert isinstance(Device.pin_factory, factory_class) assert device.pin_factory is Device.pin_factory - assert device.pin.number == TEST_PIN + assert device.pin.info.name == TEST_PIN finally: device.close() Device.pin_factory.close() + def test_compatibility_names(no_default_factory): os.environ['GPIOZERO_PIN_FACTORY'] = 'NATIVE' try: device = GPIODevice(TEST_PIN) except Exception as e: - pytest.skip("skipped factory %s: %s" % (pin_factory_name, str(e))) + pytest.skip("skipped factory {pin_factory_name}: {e!s}".format( + pin_factory_name=pin_factory_name, e=e)) else: try: assert isinstance(Device.pin_factory, NativeFactory) assert device.pin_factory is Device.pin_factory - assert device.pin.number == TEST_PIN + assert device.pin.info.name == TEST_PIN finally: device.close() Device.pin_factory.close() + def test_bad_factory(no_default_factory): os.environ['GPIOZERO_PIN_FACTORY'] = 'foobarbaz' # Waits for someone to implement the foobarbaz pin factory just to @@ -285,6 +320,8 @@ def test_bad_factory(no_default_factory): with pytest.raises(BadPinFactory): GPIODevice(TEST_PIN) + +@pytest.mark.filterwarnings('ignore::gpiozero.exc.PinFactoryFallback') def test_default_factory(no_default_factory): assert Device.pin_factory is None os.environ.pop('GPIOZERO_PIN_FACTORY', None) @@ -295,7 +332,133 @@ def test_default_factory(no_default_factory): else: try: assert device.pin_factory is Device.pin_factory - assert device.pin.number == TEST_PIN + assert device.pin.info.name == TEST_PIN finally: device.close() Device.pin_factory.close() + + +def test_spi_init(pin_factory): + with pin_factory.spi() as intf: + assert isinstance(intf, SPI) + assert repr(intf) in ( + "SPI(clock_pin='GPIO11', mosi_pin='GPIO10', miso_pin='GPIO9', " + "select_pin='GPIO8')", + "SPI(port=0, device=0)" + ) + intf.close() + assert intf.closed + assert repr(intf) == 'SPI(closed)' + with pin_factory.spi(port=0, device=1) as intf: + assert repr(intf) in ( + "SPI(clock_pin='GPIO11', mosi_pin='GPIO10', miso_pin='GPIO9', " + "select_pin='GPIO7')", + "SPI(port=0, device=1)" + ) + with pin_factory.spi(clock_pin=11, mosi_pin=10, select_pin=8) as intf: + assert repr(intf) in ( + "SPI(clock_pin='GPIO11', mosi_pin='GPIO10', miso_pin='GPIO9', " + "select_pin='GPIO8')", + "SPI(port=0, device=0)" + ) + # Ensure we support "partial" SPI where we don't reserve a pin because + # the device wants it for general IO (see SPI screens which use a pin + # for data/commands) + with pin_factory.spi(clock_pin=11, mosi_pin=10, miso_pin=None, select_pin=7) as intf: + assert isinstance(intf, SPI) + with pin_factory.spi(clock_pin=11, mosi_pin=None, miso_pin=9, select_pin=7) as intf: + assert isinstance(intf, SPI) + with pin_factory.spi(shared=True) as intf: + assert isinstance(intf, SPI) + with pytest.raises(ValueError): + pin_factory.spi(port=1) + with pytest.raises(ValueError): + pin_factory.spi(device=2) + with pytest.raises(ValueError): + pin_factory.spi(port=0, clock_pin=12) + with pytest.raises(ValueError): + pin_factory.spi(foo='bar') + + +def test_spi_hardware_conflict(default_factory): + with LED(11) as led: + with pytest.raises(GPIOPinInUse): + Device.pin_factory.spi(port=0, device=0) + with Device.pin_factory.spi(port=0, device=0) as spi: + with pytest.raises(GPIOPinInUse): + LED(11) + + +def test_spi_hardware_same_port(default_factory): + with Device.pin_factory.spi(device=0) as intf: + local_hardware_spi_only(intf) + with pytest.raises(GPIOPinInUse): + Device.pin_factory.spi(device=0) + with Device.pin_factory.spi(device=1) as another_intf: + assert intf._port == another_intf._port + + +def test_spi_hardware_shared_bus(default_factory): + with Device.pin_factory.spi(device=0, shared=True) as intf: + with Device.pin_factory.spi(device=0, shared=True) as another_intf: + assert intf is another_intf + + +def test_spi_hardware_read(default_factory): + local_only() + with mock.patch('gpiozero.pins.local.SpiDev') as spidev: + spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] + with Device.pin_factory.spi() as intf: + local_hardware_spi_only(intf) + assert intf.read(3) == [0, 1, 2] + assert intf.read(6) == list(range(6)) + + +def test_spi_hardware_write(default_factory): + local_only() + with mock.patch('gpiozero.pins.local.SpiDev') as spidev: + spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] + with Device.pin_factory.spi() as intf: + local_hardware_spi_only(intf) + assert intf.write([0, 1, 2]) == 3 + assert spidev.return_value.xfer2.called_with([0, 1, 2]) + assert intf.write(list(range(6))) == 6 + assert spidev.return_value.xfer2.called_with(list(range(6))) + + +def test_spi_hardware_modes(default_factory): + local_only() + with mock.patch('gpiozero.pins.local.SpiDev') as spidev: + spidev.return_value.mode = 0 + spidev.return_value.lsbfirst = False + spidev.return_value.cshigh = True + spidev.return_value.bits_per_word = 8 + with Device.pin_factory.spi() as intf: + local_hardware_spi_only(intf) + assert intf.clock_mode == 0 + assert not intf.clock_polarity + assert not intf.clock_phase + intf.clock_polarity = False + assert intf.clock_mode == 0 + intf.clock_polarity = True + assert intf.clock_mode == 2 + intf.clock_phase = True + assert intf.clock_mode == 3 + assert not intf.lsb_first + assert intf.select_high + assert intf.bits_per_word == 8 + intf.select_high = False + intf.lsb_first = True + intf.bits_per_word = 12 + assert not spidev.return_value.cshigh + assert spidev.return_value.lsbfirst + assert spidev.return_value.bits_per_word == 12 + intf.rate = 1000000 + assert intf.rate == 1000000 + intf.rate = 500000 + assert intf.rate == 500000 + + +# XXX Test two simultaneous SPI devices sharing clock, MOSI, and MISO, with +# separate select pins (including threaded tests which attempt simultaneous +# reading/writing) diff --git a/tests/test_spi.py b/tests/test_spi.py deleted file mode 100644 index 0a96936a3..000000000 --- a/tests/test_spi.py +++ /dev/null @@ -1,248 +0,0 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2019 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -nstr = str -str = type('') - - -import io -import sys -import pytest -from array import array -from mock import patch -from collections import namedtuple - -from gpiozero.pins.native import NativeFactory -from gpiozero.pins.local import ( - LocalPiHardwareSPI, - LocalPiSoftwareSPI, - LocalPiHardwareSPIShared, - LocalPiSoftwareSPIShared, - ) -from gpiozero.pins.mock import MockSPIDevice -from gpiozero import * - - -def test_spi_hardware_params(mock_factory): - with patch('os.open'), patch('mmap.mmap') as mmap_mmap, patch('io.open') as io_open: - mmap_mmap.return_value = array(nstr('B'), (0,) * 4096) - io_open.return_value.__enter__.return_value = io.BytesIO(b'\x00\xa2\x10\x42') - factory = NativeFactory() - with patch('gpiozero.pins.local.SpiDev'): - with factory.spi() as device: - assert isinstance(device, LocalPiHardwareSPI) - device.close() - assert device.closed - with factory.spi(port=0, device=0) as device: - assert isinstance(device, LocalPiHardwareSPI) - with factory.spi(port=0, device=1) as device: - assert isinstance(device, LocalPiHardwareSPI) - with factory.spi(clock_pin=11) as device: - assert isinstance(device, LocalPiHardwareSPI) - with factory.spi(clock_pin=11, mosi_pin=10, select_pin=8) as device: - assert isinstance(device, LocalPiHardwareSPI) - with factory.spi(clock_pin=11, mosi_pin=10, select_pin=7) as device: - assert isinstance(device, LocalPiHardwareSPI) - with factory.spi(shared=True) as device: - assert isinstance(device, LocalPiHardwareSPIShared) - with pytest.raises(ValueError): - factory.spi(port=1) - with pytest.raises(ValueError): - factory.spi(device=2) - with pytest.raises(ValueError): - factory.spi(port=0, clock_pin=12) - with pytest.raises(ValueError): - factory.spi(foo='bar') - -def test_spi_software_params(mock_factory): - with patch('os.open'), patch('mmap.mmap') as mmap_mmap, patch('io.open') as io_open: - mmap_mmap.return_value = array(nstr('B'), (0,) * 4096) - io_open.return_value.__enter__.return_value = io.BytesIO(b'\x00\xa2\x10\x42') - factory = NativeFactory() - with patch('gpiozero.pins.local.SpiDev'): - with factory.spi(select_pin=6) as device: - assert isinstance(device, LocalPiSoftwareSPI) - device.close() - assert device.closed - with factory.spi(clock_pin=11, mosi_pin=9, miso_pin=10) as device: - assert isinstance(device, LocalPiSoftwareSPI) - device._bus.close() - assert device._bus.closed - device.close() - assert device.closed - with factory.spi(select_pin=6, shared=True) as device: - assert isinstance(device, LocalPiSoftwareSPIShared) - with patch('gpiozero.pins.local.SpiDev', None): - # Clear out the old factory's caches (this is only necessary - # because we're being naughty switching out patches) - factory.pins.clear() - factory._reservations.clear() - # Ensure software fallback works when SpiDev isn't present - with factory.spi() as device: - assert isinstance(device, LocalPiSoftwareSPI) - -def test_spi_hardware_conflict(mock_factory): - with patch('gpiozero.pins.local.SpiDev') as spidev: - with LED(11) as led: - with pytest.raises(GPIOPinInUse): - mock_factory.spi(port=0, device=0) - with patch('gpiozero.pins.local.SpiDev') as spidev: - with mock_factory.spi(port=0, device=0) as spi: - with pytest.raises(GPIOPinInUse): - LED(11) - -def test_spi_hardware_read(mock_factory): - with patch('gpiozero.pins.local.SpiDev') as spidev: - spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] - with mock_factory.spi() as device: - assert device.read(3) == [0, 1, 2] - assert device.read(6) == list(range(6)) - -def test_spi_hardware_write(mock_factory): - with patch('gpiozero.pins.local.SpiDev') as spidev: - spidev.return_value.xfer2.side_effect = lambda data: list(range(10))[:len(data)] - with mock_factory.spi() as device: - assert device.write([0, 1, 2]) == 3 - assert spidev.return_value.xfer2.called_with([0, 1, 2]) - assert device.write(list(range(6))) == 6 - assert spidev.return_value.xfer2.called_with(list(range(6))) - -def test_spi_hardware_modes(mock_factory): - with patch('gpiozero.pins.local.SpiDev') as spidev: - spidev.return_value.mode = 0 - spidev.return_value.lsbfirst = False - spidev.return_value.cshigh = True - spidev.return_value.bits_per_word = 8 - with mock_factory.spi() as device: - assert device.clock_mode == 0 - assert not device.clock_polarity - assert not device.clock_phase - device.clock_polarity = False - assert device.clock_mode == 0 - device.clock_polarity = True - assert device.clock_mode == 2 - device.clock_phase = True - assert device.clock_mode == 3 - assert not device.lsb_first - assert device.select_high - assert device.bits_per_word == 8 - device.select_high = False - device.lsb_first = True - device.bits_per_word = 12 - assert not spidev.return_value.cshigh - assert spidev.return_value.lsbfirst - assert spidev.return_value.bits_per_word == 12 - -def test_spi_software_read(mock_factory): - class SPISlave(MockSPIDevice): - def on_start(self): - super(SPISlave, self).on_start() - for i in range(10): - self.tx_word(i) - with patch('gpiozero.pins.local.SpiDev', None), \ - SPISlave(11, 10, 9, 8) as slave, \ - mock_factory.spi() as master: - assert master.read(3) == [0, 1, 2] - assert master.read(6) == [0, 1, 2, 3, 4, 5] - slave.clock_phase = True - master.clock_phase = True - assert master.read(3) == [0, 1, 2] - assert master.read(6) == [0, 1, 2, 3, 4, 5] - -def test_spi_software_write(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None), \ - MockSPIDevice(11, 10, 9, 8) as test_device, \ - mock_factory.spi() as master: - master.write([0]) - assert test_device.rx_word() == 0 - master.write([2, 0]) - # 0b 0000_0010 0000_0000 - assert test_device.rx_word() == 512 - master.write([0, 1, 1]) - # 0b 0000_0000 0000_0001 0000_0001 - assert test_device.rx_word() == 257 - -def test_spi_software_write_lsb_first(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None), \ - MockSPIDevice(11, 10, 9, 8, lsb_first=True) as test_device, \ - mock_factory.spi() as master: - # lsb_first means the bit-strings above get reversed - master.write([0]) - assert test_device.rx_word() == 0 - master.write([2, 0]) - # 0b 0000_0000 0100_0000 - assert test_device.rx_word() == 64 - master.write([0, 1, 1]) - # 0b 1000_0000 1000_0000 0000_0000 - assert test_device.rx_word() == 8421376 - -def test_spi_software_clock_mode(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None), \ - mock_factory.spi() as master: - assert master.clock_mode == 0 - assert not master.clock_polarity - assert not master.clock_phase - master.clock_polarity = False - assert master.clock_mode == 0 - master.clock_polarity = True - assert master.clock_mode == 2 - master.clock_phase = True - assert master.clock_mode == 3 - master.clock_mode = 0 - assert not master.clock_polarity - assert not master.clock_phase - with pytest.raises(ValueError): - master.clock_mode = 5 - -def test_spi_software_attr(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None), \ - mock_factory.spi() as master: - assert not master.lsb_first - assert not master.select_high - assert master.bits_per_word == 8 - master.bits_per_word = 12 - assert master.bits_per_word == 12 - master.lsb_first = True - assert master.lsb_first - master.select_high = True - assert master.select_high - with pytest.raises(ValueError): - master.bits_per_word = 0 - - -# XXX Test two simultaneous SPI devices sharing clock, MOSI, and MISO, with -# separate select pins (including threaded tests which attempt simultaneous -# reading/writing) diff --git a/tests/test_spi_devices.py b/tests/test_spi_devices.py index 2af0c1d5a..35b4d8fad 100644 --- a/tests/test_spi_devices.py +++ b/tests/test_spi_devices.py @@ -1,51 +1,16 @@ -# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins -# Copyright (c) 2016-2019 Dave Jones -# Copyright (c) 2019 Ben Nuttall -# Copyright (c) 2019 Andrew Scheller -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. +# vim: set fileencoding=utf-8: # -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li +# Copyright (c) 2019 Andrew Scheller # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - - -import sys +# SPDX-License-Identifier: BSD-3-Clause + import pytest -from mock import patch from collections import namedtuple -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose +from math import isclose from gpiozero.pins.mock import MockSPIDevice, MockPin from gpiozero import * @@ -63,11 +28,10 @@ def scale(v, ref, bits): class MockMCP3xxx(MockSPIDevice): - def __init__( - self, clock_pin, mosi_pin, miso_pin, select_pin=None, - channels=8, bits=10): - super(MockMCP3xxx, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + channels=8, bits=10, pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, + pin_factory=pin_factory) self.vref = 3.3 self.channels = [0.0] * channels self.channel_bits = 3 @@ -75,7 +39,7 @@ def __init__( self.state = 'idle' def on_start(self): - super(MockMCP3xxx, self).on_start() + super().on_start() self.state = 'idle' def on_bit(self): @@ -113,12 +77,13 @@ def on_result(self, differential, channel): class MockMCP3xx1(MockMCP3xxx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, bits=10): - super(MockMCP3xx1, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=2, bits=bits) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + bits=10, pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=2, + bits=bits, pin_factory=pin_factory) def on_start(self): - super(MockMCP3xx1, self).on_start() + super().on_start() result = self.channels[0] - self.channels[1] result = clamp(result, 0, self.vref) result = scale(result, self.vref, self.bits) @@ -129,20 +94,18 @@ def on_bit(self): class MockMCP3xx2(MockMCP3xxx): - def __init__( - self, clock_pin, mosi_pin, miso_pin, select_pin=None, - bits=10): - super(MockMCP3xx2, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=2, bits=bits) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + bits=10, pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=2, + bits=bits, pin_factory=pin_factory) self.channel_bits = 1 class MockMCP33xx(MockMCP3xxx): - def __init__( - self, clock_pin, mosi_pin, miso_pin, select_pin=None, - channels=8): - super(MockMCP33xx, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels, 12) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + channels=8, pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, + channels=channels, bits=12, pin_factory=pin_factory) def on_result(self, differential, channel): if differential: @@ -159,60 +122,69 @@ def on_result(self, differential, channel): class MockMCP3001(MockMCP3xx1): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3001, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, bits=10) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, bits=10, + pin_factory=pin_factory) class MockMCP3002(MockMCP3xx2): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3002, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, bits=10) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, bits=10, + pin_factory=pin_factory) class MockMCP3004(MockMCP3xxx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3004, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=4, bits=10) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=4, + bits=10, pin_factory=pin_factory) class MockMCP3008(MockMCP3xxx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3008, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=8, bits=10) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=8, + bits=10, pin_factory=pin_factory) class MockMCP3201(MockMCP3xx1): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3201, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, bits=12) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, bits=12, + pin_factory=pin_factory) class MockMCP3202(MockMCP3xx2): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3202, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, bits=12) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, bits=12, + pin_factory=pin_factory) class MockMCP3204(MockMCP3xxx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3204, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=4, bits=12) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=4, + bits=12, pin_factory=pin_factory) class MockMCP3208(MockMCP3xxx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3208, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=8, bits=12) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=8, + bits=12, pin_factory=pin_factory) class MockMCP3301(MockMCP3xxx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3301, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=2, bits=12) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=2, + bits=12, pin_factory=pin_factory) def on_start(self): - super(MockMCP3301, self).on_start() + super().on_start() result = self.channels[0] - self.channels[1] result = clamp(result, -self.vref, self.vref) result = scale(result, self.vref, self.bits) @@ -222,15 +194,17 @@ def on_start(self): class MockMCP3302(MockMCP33xx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3302, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=4) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=4, + pin_factory=pin_factory) class MockMCP3304(MockMCP33xx): - def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None): - super(MockMCP3304, self).__init__( - clock_pin, mosi_pin, miso_pin, select_pin, channels=8) + def __init__(self, clock_pin, mosi_pin, miso_pin, select_pin=None, *, + pin_factory=None): + super().__init__(clock_pin, mosi_pin, miso_pin, select_pin, channels=8, + pin_factory=pin_factory) def single_mcp_test(mock, pot, channel, bits): @@ -250,6 +224,7 @@ def single_mcp_test(mock, pot, channel, bits): assert isclose(pot.value, 1.0, abs_tol=tolerance) assert isclose(pot.voltage, pot.max_voltage, abs_tol=voltage_tolerance) + def differential_mcp_test(mock, pot, pos_channel, neg_channel, bits, full=False): scale = 2**bits tolerance = 1 / scale @@ -287,6 +262,80 @@ def differential_mcp_test(mock, pot, pos_channel, neg_channel, bits, full=False) assert isclose(pot.voltage, 0.0, abs_tol=voltage_tolerance) +def test_spi_software_read(mock_factory): + class SPISlave(MockSPIDevice): + def on_start(self): + super().on_start() + for i in range(10): + self.tx_word(i) + with SPISlave(11, 10, 9, 8) as slave, mock_factory.spi() as master: + assert master.read(3) == [0, 1, 2] + assert master.read(6) == [0, 1, 2, 3, 4, 5] + slave.clock_phase = True + master.clock_phase = True + assert master.read(3) == [0, 1, 2] + assert master.read(6) == [0, 1, 2, 3, 4, 5] + + +def test_spi_software_write(mock_factory): + with MockSPIDevice(11, 10, 9, 8) as test_device, mock_factory.spi() as master: + master.write([0]) + assert test_device.rx_word() == 0 + master.write([2, 0]) + # 0b 0000_0010 0000_0000 + assert test_device.rx_word() == 512 + master.write([0, 1, 1]) + # 0b 0000_0000 0000_0001 0000_0001 + assert test_device.rx_word() == 257 + + +def test_spi_software_write_lsb_first(mock_factory): + with MockSPIDevice(11, 10, 9, 8, lsb_first=True) as test_device, \ + mock_factory.spi() as master: + # lsb_first means the bit-strings above get reversed + master.write([0]) + assert test_device.rx_word() == 0 + master.write([2, 0]) + # 0b 0000_0000 0100_0000 + assert test_device.rx_word() == 64 + master.write([0, 1, 1]) + # 0b 1000_0000 1000_0000 0000_0000 + assert test_device.rx_word() == 8421376 + + +def test_spi_software_clock_mode(mock_factory): + with mock_factory.spi() as master: + assert master.clock_mode == 0 + assert not master.clock_polarity + assert not master.clock_phase + master.clock_polarity = False + assert master.clock_mode == 0 + master.clock_polarity = True + assert master.clock_mode == 2 + master.clock_phase = True + assert master.clock_mode == 3 + master.clock_mode = 0 + assert not master.clock_polarity + assert not master.clock_phase + with pytest.raises(ValueError): + master.clock_mode = 5 + + +def test_spi_software_attr(mock_factory): + with mock_factory.spi() as master: + assert not master.lsb_first + assert not master.select_high + assert master.bits_per_word == 8 + master.bits_per_word = 12 + assert master.bits_per_word == 12 + master.lsb_first = True + assert master.lsb_first + master.select_high = True + assert master.select_high + with pytest.raises(ValueError): + master.bits_per_word = 0 + + def test_analog_input_device_bad_init(mock_factory): with pytest.raises(InputDeviceError): AnalogInputDevice(None) @@ -297,136 +346,138 @@ def test_analog_input_device_bad_init(mock_factory): with pytest.raises(InputDeviceError): AnalogInputDevice(bits=8, max_voltage=-1) + def test_MCP3001(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None): - mock = MockMCP3001(11, 10, 9, 8) - with MCP3001() as pot: - assert repr(pot).startswith('' + with MCP3001(max_voltage=5.0) as pot: + differential_mcp_test(mock, pot, 0, 1, 10) + def test_MCP3002(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None): - mock = MockMCP3002(11, 10, 9, 8) - with pytest.raises(ValueError): - MCP3002(channel=5) - with MCP3002(channel=1) as pot: - assert repr(pot).startswith('' + with MCP3002(channel=1, max_voltage=5.0) as pot: + single_mcp_test(mock, pot, 1, 10) + with MCP3002(channel=1, differential=True) as pot: + differential_mcp_test(mock, pot, 1, 0, 10) + def test_MCP3004(mock_factory): - with patch('gpiozero.pins.local.SpiDev', None): - mock = MockMCP3004(11, 10, 9, 8) - with pytest.raises(ValueError): - MCP3004(channel=5) - with MCP3004(channel=3) as pot: - assert repr(pot).startswith(' -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: +# vim: set fileencoding=utf-8: # -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. +# GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins # -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. +# Copyright (c) 2019-2021 Dave Jones # -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import ( + unicode_literals, + absolute_import, + print_function, + division, +) +str = type('') import warnings +from fractions import Fraction import pytest @@ -44,9 +32,12 @@ def test_tone_init(A4): warnings.resetwarnings() assert Tone(440) == A4 assert Tone("A4") == A4 + assert Tone(Fraction(880, 2)) == A4 assert len(w) == 0 assert Tone(69) == A4 assert len(w) == 1 + assert Tone(0) != A4 + assert len(w) == 1 assert isinstance(w[0].message, AmbiguousTone) assert Tone(frequency=440) == A4 assert Tone(note="A4") == A4 @@ -57,11 +48,15 @@ def test_tone_init(A4): Tone(foo=1) with pytest.raises(TypeError): Tone(frequency=440, midi=69) + with pytest.raises(TypeError): + Tone(440, midi=69) def test_tone_str(A4): assert str(A4) == "A4" assert str(A4.up()) == "A#4" assert str(A4.down(12)) == "A3" + assert repr(A4) == "".format(note='A4') + assert repr(Tone(13000)) == '' def test_tone_from_frequency(A4): assert Tone.from_frequency(440) == A4 diff --git a/tests/test_tools.py b/tests/test_tools.py index 26c4b668c..b1c2eccf3 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1,62 +1,22 @@ +# vim: set fileencoding=utf-8: +# # GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins +# +# Copyright (c) 2016-2023 Dave Jones +# Copyright (c) 2020 Fangchen Li # Copyright (c) 2018-2019 Ben Nuttall -# Copyright (c) 2016-2019 Dave Jones # Copyright (c) 2016-2019 Andrew Scheller # -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of the copyright holder nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import ( - unicode_literals, - absolute_import, - print_function, - division, - ) -str = type('') - +# SPDX-License-Identifier: BSD-3-Clause import pytest -from math import sin, cos, radians +from math import sin, cos, radians, isclose +from statistics import mean, median from time import time, sleep from itertools import islice from gpiozero import Device, LED, Button, Robot from gpiozero.tools import * -try: - from math import isclose -except ImportError: - from gpiozero.compat import isclose -try: - from statistics import mean -except ImportError: - from gpiozero.compat import mean -try: - from statistics import median -except ImportError: - from gpiozero.compat import median epsilon = 0.01 # time to sleep after setting source before checking value @@ -164,6 +124,12 @@ def test_scaled(): # scale -1->+1 to 0->1 assert list(scaled((-1, 1, 0.0, -0.5), 0, 1, -1, 1)) == [0, 1, 0.5, 0.25] +def test_scaled_full(): + assert list(scaled_full((0, 0.5, 1, 0.5))) == [-1, 0, 1, 0] + +def test_scaled_half(): + assert list(scaled_half((-1, 0, 1, 0))) == [0, 0.5, 1, 0.5] + def test_clamped(): with pytest.raises(ValueError): list(clamped((), 0, 0)) diff --git a/tox.ini b/tox.ini index c90b45b26..071773dcb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py32,py33,py34,py35,py36,py37} +envlist = {py39,py310,py311,py312} [testenv] deps = .[test] @@ -10,58 +10,3 @@ setenv = COVERAGE_FILE=.coverage.{envname} GPIOZERO_TEST_LOCK={toxworkdir}/real_pins_lock passenv = GPIOZERO_* COVERAGE_* - -[testenv:py32] -# All this shenanigans is to get tox (and everything else) working with python -# 3.2. We stop venv for downloading or installing anything in the venv, because -# all the local stuff almost certainly doesn't work on py3.2. -basepython = python3.2 -setenv = - VIRTUALENV_NO_DOWNLOAD=1 - VIRTUALENV_NO_PIP=1 - VIRTUALENV_NO_WHEEL=1 - VIRTUALENV_NO_SETUPTOOLS=1 - COVERAGE_FILE=.coverage.{envname} - GPIOZERO_TEST_LOCK={toxworkdir}/real_pins_lock -# The following lines are needed to stop tox trying to install dependencies (or -# anything else) itself because pip won't exist in the venv yet. -whitelist_externals = - echo - curl - pip - make -deps = -list_dependencies_command = echo -skip_install = true -# Now do everything manually... -commands = - curl https://bootstrap.pypa.io/3.2/get-pip.py -o {envdir}/get-pip32.py - python {envdir}/get-pip32.py - pip install -e .[test] - make test -passenv = GPIOZERO_* COVERAGE_* - -[testenv:py33] -# Same story as above -basepython = python3.3 -setenv = - VIRTUALENV_NO_DOWNLOAD=1 - VIRTUALENV_NO_PIP=1 - VIRTUALENV_NO_WHEEL=1 - VIRTUALENV_NO_SETUPTOOLS=1 - COVERAGE_FILE=.coverage.{envname} - GPIOZERO_TEST_LOCK={toxworkdir}/real_pins_lock -whitelist_externals = - echo - curl - pip - make -deps = -list_dependencies_command = echo -skip_install = true -commands = - curl https://bootstrap.pypa.io/3.3/get-pip.py -o {envdir}/get-pip33.py - python {envdir}/get-pip33.py - pip install -e .[test] - make test -passenv = GPIOZERO_* COVERAGE_* pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy