From 2fbb3c4d7c215d087d15ab1ab497ebf27a518ebb Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 12:49:27 +0000 Subject: [PATCH 01/13] Remove deprecated packaging and docs directories. --- packaging/CHANGELOG | 43 ---- packaging/debian/README | 8 - packaging/debian/changelog | 42 --- packaging/debian/clean | 1 - packaging/debian/compat | 1 - packaging/debian/control | 31 --- packaging/debian/copyright | 26 -- packaging/debian/docs | 0 packaging/debian/rules | 12 - packaging/debian/source/format | 1 - packaging/debian/source/options | 1 - packaging/makeall.sh | 175 ------------- packaging/makedeb.sh | 38 --- packaging/makedoc.sh | 37 --- packaging/makelog.sh | 82 ------ sphinx/_static/custom.css | 41 --- sphinx/_templates/breadcrumbs.html | 0 sphinx/_templates/layout.html | 43 ---- sphinx/conf.py | 394 ----------------------------- sphinx/favicon.png | Bin 26049 -> 0 bytes sphinx/index.rst | 107 -------- sphinx/shop-logo.png | Bin 19652 -> 0 bytes 22 files changed, 1083 deletions(-) delete mode 100644 packaging/CHANGELOG delete mode 100644 packaging/debian/README delete mode 100644 packaging/debian/changelog delete mode 100644 packaging/debian/clean delete mode 100644 packaging/debian/compat delete mode 100644 packaging/debian/control delete mode 100644 packaging/debian/copyright delete mode 100644 packaging/debian/docs delete mode 100755 packaging/debian/rules delete mode 100644 packaging/debian/source/format delete mode 100644 packaging/debian/source/options delete mode 100755 packaging/makeall.sh delete mode 100755 packaging/makedeb.sh delete mode 100755 packaging/makedoc.sh delete mode 100755 packaging/makelog.sh delete mode 100644 sphinx/_static/custom.css delete mode 100644 sphinx/_templates/breadcrumbs.html delete mode 100644 sphinx/_templates/layout.html delete mode 100644 sphinx/conf.py delete mode 100644 sphinx/favicon.png delete mode 100644 sphinx/index.rst delete mode 100644 sphinx/shop-logo.png diff --git a/packaging/CHANGELOG b/packaging/CHANGELOG deleted file mode 100644 index 2d7445e..0000000 --- a/packaging/CHANGELOG +++ /dev/null @@ -1,43 +0,0 @@ -pantilthat (0.0.6) stable; urgency=low - - * Bugfix: return 0 if get_servo_1/2 values are out of range - * Bugfix: import sys.version_info for missing module warnings - - -- Phil Howard Saturday, 20 Oct 2018 00:00:00 +0000 - -pantilthat (0.0.5) stable; urgency=low - - * Bugfix: fix to set_pixel_rgbw - * Bugfix: fix so that get_servo_two returns correct value - * Bugfix: deferred setup to prevent side-effects upon import - - -- Phil Howard Fri, 2 Feb 2018 00:00:00 +0000 - -pantilthat (0.0.4) stable; urgency=low - - * New: get_pan and get_tilt methods to read back servo position - * Bugfix: Brightness does not set value unless light_mode == PWM - * Bugfix: Idle timeout Timer() daemonized to prevent blocking on exit - - -- Phil Howard Wed, 3 May 2017 00:00:00 +0000 - -pantilthat (0.0.3) stable; urgency=low - - * Added idle_timeout functionality to prevent idle servo jitter - * Added options for RGB, GRB, RGBW and GRBW pixel types - RGBW and GRBW support up to 18 pixels - - -- Phil Howard Tue, 24 Jan 2017 00:00:00 +0000 - -pantilthat (0.0.2) stable; urgency=low - - * Corrected package dependencies - - -- Phil Howard Mon, 21 Nov 2016 00:00:00 +0000 - -pantilthat (0.0.1) stable; urgency=low - - * Initial Release - - -- Phil Howard Wed, 26 Oct 2016 00:00:00 +0000 - - diff --git a/packaging/debian/README b/packaging/debian/README deleted file mode 100644 index 5ac1801..0000000 --- a/packaging/debian/README +++ /dev/null @@ -1,8 +0,0 @@ -README - -Pan-Tilt HAT controls two servos and WS2812 or PWM-dimmed LEDs. - -Ideal for adding a Pan/Tilt camera to your Pi. - -Learn more: https://shop.pimoroni.com/products/pan-tilt-hat -For examples run: `curl -sS https://get.pimoroni.com/pantilthat | bash` diff --git a/packaging/debian/changelog b/packaging/debian/changelog deleted file mode 100644 index c8dd7bc..0000000 --- a/packaging/debian/changelog +++ /dev/null @@ -1,42 +0,0 @@ -pantilthat (0.0.6) stable; urgency=low - - * Bugfix: return 0 if get_servo_1/2 values are out of range - - -- Phil Howard Fri, 6 Jul 2018 00:00:00 +0000 - -pantilthat (0.0.5) stable; urgency=low - - * Bugfix: fix to set_pixel_rgbw - * Bugfix: fix so that get_servo_two returns correct value - * Bugfix: deferred setup to prevent side-effects upon import - - -- Phil Howard Fri, 2 Feb 2018 00:00:00 +0000 - -pantilthat (0.0.4) stable; urgency=low - - * New: get_pan and get_tilt methods to read back servo position - * Bugfix: Brightness does not set value unless light_mode == PWM - * Bugfix: Idle timeout Timer() daemonized to prevent blocking on exit - - -- Phil Howard Wed, 3 May 2017 00:00:00 +0000 - -pantilthat (0.0.3) stable; urgency=low - - * Added idle_timeout functionality to prevent idle servo jitter - * Added options for RGB, GRB, RGBW and GRBW pixel types - RGBW and GRBW support up to 18 pixels - - -- Phil Howard Tue, 24 Jan 2017 00:00:00 +0000 - -pantilthat (0.0.2) stable; urgency=low - - * Corrected package dependencies - - -- Phil Howard Mon, 21 Nov 2016 00:00:00 +0000 - -pantilthat (0.0.1) stable; urgency=low - - * Initial Release - - -- Phil Howard Wed, 26 Oct 2016 00:00:00 +0000 - - diff --git a/packaging/debian/clean b/packaging/debian/clean deleted file mode 100644 index 45149aa..0000000 --- a/packaging/debian/clean +++ /dev/null @@ -1 +0,0 @@ -*.egg-info/* diff --git a/packaging/debian/compat b/packaging/debian/compat deleted file mode 100644 index ec63514..0000000 --- a/packaging/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/packaging/debian/control b/packaging/debian/control deleted file mode 100644 index c153e30..0000000 --- a/packaging/debian/control +++ /dev/null @@ -1,31 +0,0 @@ -Source: pantilthat -Maintainer: Phil Howard -Homepage: https://github.com/pimoroni/pantilt-hat -Section: python -Priority: extra -Build-Depends: debhelper (>= 9.0.0), dh-python, python-all (>= 2.7), python-setuptools, python3-all (>= 3.4), python3-setuptools -Standards-Version: 3.9.6 -X-Python-Version: >= 2.7 -X-Python3-Version: >= 3.4 - -Package: python-pantilthat -Architecture: all -Section: python -Depends: ${misc:Depends}, ${python:Depends}, python-smbus -Suggests: python-picamera -Description: Python library for the Pimoroni Pan-Tilt HAT - Pan-Tilt HAT controls two servos and WS2812 or PWM-dimmed LEDs. - Ideal for adding a Pan/Tilt camera to your Pi. - . - This is the Python 2 version of the package. - -Package: python3-pantilthat -Architecture: all -Section: python -Depends: ${misc:Depends}, ${python3:Depends}, python3-smbus -Suggests: python3-picamera -Description: Python library for the Pimoroni Pan-Tilt HAT - Pan-Tilt HAT controls two servos and WS2812 or PWM-dimmed LEDs. - Ideal for adding a Pan/Tilt camera to your Pi. - . - This is the Python 3 version of the package. diff --git a/packaging/debian/copyright b/packaging/debian/copyright deleted file mode 100644 index b4588ec..0000000 --- a/packaging/debian/copyright +++ /dev/null @@ -1,26 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: pantilthat -Source: https://github.com/pimoroni/pantilt-hat - -Files: * -Copyright: 2016 Pimoroni Ltd -License: MIT - -License: MIT - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. diff --git a/packaging/debian/docs b/packaging/debian/docs deleted file mode 100644 index e69de29..0000000 diff --git a/packaging/debian/rules b/packaging/debian/rules deleted file mode 100755 index a649e05..0000000 --- a/packaging/debian/rules +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/make -f -# -*- makefile -*- - -#export DH_VERBOSE=1 -export DH_OPTIONS - -%: - dh $@ --with python2,python3 --buildsystem=python_distutils - -override_dh_auto_install: - python setup.py install --root debian/python-pantilthat --install-layout=deb - python3 setup.py install --root debian/python3-pantilthat --install-layout=deb diff --git a/packaging/debian/source/format b/packaging/debian/source/format deleted file mode 100644 index 89ae9db..0000000 --- a/packaging/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/packaging/debian/source/options b/packaging/debian/source/options deleted file mode 100644 index 8f82c91..0000000 --- a/packaging/debian/source/options +++ /dev/null @@ -1 +0,0 @@ -extend-diff-ignore = "^[^/]+\.egg-info/" diff --git a/packaging/makeall.sh b/packaging/makeall.sh deleted file mode 100755 index ddaa02b..0000000 --- a/packaging/makeall.sh +++ /dev/null @@ -1,175 +0,0 @@ -#!/bin/bash - -# script control variables - -reponame="" # leave this blank for auto-detection -libname="" # leave this blank for auto-detection -packagename="" # leave this blank for auto-selection - -rpigpio="no" # set to 'no' to turn off warning -smbus="yes" # set to 'no' to turn off warning - -debianlog="debian/changelog" -debcontrol="debian/control" -debcopyright="debian/copyright" -debrules="debian/rules" -debreadme="debian/README" - -debdir="$(pwd)" -rootdir="$(dirname $debdir)" -libdir="$rootdir/library" - -FLAG=false - -# function define - -success() { - echo "$(tput setaf 2)$1$(tput sgr0)" -} - -inform() { - echo "$(tput setaf 6)$1$(tput sgr0)" -} - -warning() { - echo "$(tput setaf 1)$1$(tput sgr0)" -} - -newline() { - echo "" -} - -# assessing repo and library variables - -if [ -z "$reponame" ] || [ -z "$libname" ]; then - inform "detecting reponame and libname..." -else - inform "using reponame and libname overrides" -fi - -if [ -z "$reponame" ]; then - if [[ $rootdir == *"python"* ]]; then - repodir="$(dirname $rootdir)" - reponame="$(basename $repodir)" - else - repodir="$rootdir" - reponame="$(basename $repodir)" - fi - reponame=$(echo "$reponame" | tr "[A-Z]" "[a-z]") -fi - -if [ -z "$libname" ]; then - cd "$libdir" - libname=$(grep "name" setup.py | tr -d "[:space:]" | cut -c 7- | rev | cut -c 3- | rev) - libname=$(echo "$libname" | tr "[A-Z]" "[a-z]") && cd "$debdir" -fi - -if [ -z "$packagename" ]; then - packagename="$libname" -fi - -echo "reponame is $reponame and libname is $libname" -echo "output packages will be python-$packagename and python3-$packagename" - -# checking generating changelog file - -./makelog.sh -version=$(head -n 1 "$libdir/CHANGELOG.txt") -echo "building $libname version $version" - -# checking debian/changelog file - -inform "checking debian/changelog file..." - -if ! head -n 1 $debianlog | grep "$libname" &> /dev/null; then - warning "library not mentioned in header!" && FLAG=true -elif head -n 1 $debianlog | grep "UNRELEASED"; then - warning "this changelog is not going to generate a release!" - warning "change distribution to 'stable'" && FLAG=true -fi - -# checking debian/copyright file - -inform "checking debian/copyright file..." - -if ! grep "^Source" $debcopyright | grep "$reponame" &> /dev/null; then - warning "$(grep "^Source" $debcopyright)" && FLAG=true -fi - -if ! grep "^Upstream-Name" $debcopyright | grep "$libname" &> /dev/null; then - warning "$(grep "^Upstream-Name" $debcopyright)" && FLAG=true -fi - -# checking debian/control file - -inform "checking debian/control file..." - -if ! grep "^Source" $debcontrol | grep "$libname" &> /dev/null; then - warning "$(grep "^Source" $debcontrol)" && FLAG=true -fi - -if ! grep "^Homepage" $debcontrol | grep "$reponame" &> /dev/null; then - warning "$(grep "^Homepage" $debcontrol)" && FLAG=true -fi - -if ! grep "^Package: python-$packagename" $debcontrol &> /dev/null; then - warning "$(grep "^Package: python-" $debcontrol)" && FLAG=true -fi - -if ! grep "^Package: python3-$packagename" $debcontrol &> /dev/null; then - warning "$(grep "^Package: python3-" $debcontrol)" && FLAG=true -fi - -if ! grep "^Priority: extra" $debcontrol &> /dev/null; then - warning "$(grep "^Priority" $debcontrol)" && FLAG=true -fi - -if [ $rpigpio == "yes" ] && ! grep "rpi.gpio" $debcontrol &> /dev/null; then - warning "if this library does not depend on rpi.gpio change 'rpigpio' variable!" && FLAG=true -fi - -if [ $smbus == "yes" ] && ! grep "smbus" $debcontrol &> /dev/null; then - warning "if this library does not depend on smbus change the 'smbus' variable!" && FLAG=true -fi - -# checking debian/rules file - -inform "checking debian/rules file..." - -if ! grep "debian/python-$packagename" $debrules &> /dev/null; then - warning "$(grep "debian/python-" $debrules)" && FLAG=true -fi - -if ! grep "debian/python3-$packagename" $debrules &> /dev/null; then - warning "$(grep "debian/python3-" $debrules)" && FLAG=true -fi - -# checking debian/README file - -inform "checking debian/readme file..." - -if ! grep -e "$libname" -e "$reponame" $debreadme &> /dev/null; then - warning "README does not seem to mention product, repo or lib!" && FLAG=true -fi - -# summary of checks pre build - -if $FLAG; then - warning "Check all of the above and correct!" && exit 1 -else - inform "we're good to go... bulding!" -fi - -# building deb and final checks - -./makedeb.sh - -inform "running lintian..." -lintian -v $(find -name "python*$version*.deb") -lintian -v $(find -name "python3*$version*.deb") - -inform "checking signatures..." -gpg --verify $(find -name "*$version*changes") -gpg --verify $(find -name "*$version*dsc") - -exit 0 diff --git a/packaging/makedeb.sh b/packaging/makedeb.sh deleted file mode 100755 index 03ebac7..0000000 --- a/packaging/makedeb.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -gettools="no" # if set to yes downloads the tools required -setup="yes" # if set to yes populates library folder -buildeb="yes" # if set to yes builds the deb files -cleanup="yes" # if set to yes cleans up build files -pkgfiles=( "build" "changes" "deb" "dsc" "tar.xz" ) - -if [ $gettools == "yes" ]; then - sudo apt-get update && sudo apt-get install build-essential debhelper devscripts dh-make dh-python dput gnupg - sudo apt-get install python-all python-setuptools python3-all python3-setuptools - sudo apt-get install python-mock python-sphinx python-sphinx-rtd-theme - sudo pip install Sphinx --upgrade && sudo pip install sphinx_rtd_theme --upgrade -fi - -if [ $setup == "yes" ]; then - rm -R ../library/build ../library/debian &> /dev/null - cp -R ./debian ../library/ && cp -R ../sphinx ../library/doc -fi - -cd ../library - -if [ $buildeb == "yes" ]; then - debuild -aarmhf - for file in ${pkgfiles[@]}; do - rm ../packaging/*.$file &> /dev/null - mv ../*.$file ../packaging - done - rm -R ../documentation/html &> /dev/null - cp -R ./build/sphinx/html ../documentation -fi - -if [ $cleanup == "yes" ]; then - debuild clean - rm -R ./build ./debian ./doc &> /dev/null -fi - -exit 0 diff --git a/packaging/makedoc.sh b/packaging/makedoc.sh deleted file mode 100755 index 244e992..0000000 --- a/packaging/makedoc.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -gettools="no" # if set to yes downloads the tools required -setup="yes" # if set to yes populates library folder -buildoc="yes" # if set to yes builds the deb files -cleanup="yes" # if set to yes cleans up build files -pkgfiles=( "build" "changes" "deb" "dsc" "tar.xz" ) - -if [ $gettools == "yes" ]; then - sudo apt-get update && sudo apt-get install build-essential debhelper devscripts dh-make dh-python - sudo apt-get install python-all python-setuptools python3-all python3-setuptools - sudo apt-get install python-mock python-sphinx python-sphinx-rtd-theme - sudo pip install Sphinx --upgrade && sudo pip install sphinx_rtd_theme --upgrade -fi - -if [ $setup == "yes" ]; then - rm -R ../library/build ../library/debian &> /dev/null - cp -R ./debian ../library/ && cp -R ../sphinx ../library/doc -fi - -cd ../library - -if [ $buildoc == "yes" ]; then - debuild - for file in ${pkgfiles[@]}; do - rm ../*.$file &> /dev/null - done - rm -R ../documentation/html &> /dev/null - cp -R ./build/sphinx/html ../documentation -fi - -if [ $cleanup == "yes" ]; then - debuild clean - rm -R ./build ./debian ./doc &> /dev/null -fi - -exit 0 diff --git a/packaging/makelog.sh b/packaging/makelog.sh deleted file mode 100755 index 1055987..0000000 --- a/packaging/makelog.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/bin/bash - -# script control variables - -libname="" # leave this blank for auto-detection -sibname=() # name of sibling in packages list -versionwarn="yes" # set to anything but 'yes' to turn off warning - -debdir="$(pwd)" -rootdir="$(dirname $debdir)" -libdir="$rootdir/library" - -mainlog="CHANGELOG" -debianlog="debian/changelog" -pypilog="$libdir/CHANGELOG.txt" - -# function define - -success() { - echo "$(tput setaf 2)$1$(tput sgr0)" -} - -inform() { - echo "$(tput setaf 6)$1$(tput sgr0)" -} - -warning() { - echo "$(tput setaf 1)$1$(tput sgr0)" -} - -newline() { - echo "" -} - -# generate debian changelog - -cat $mainlog > $debianlog -inform "seeded debian changelog" - -# generate pypi changelog - -sed -e "/--/d" -e "s/ \*/\*/" \ - -e "s/.*\([0-9].[0-9].[0-9]\).*/\1/" \ - -e '/[0-9].[0-9].[0-9]/ a\ ------' $mainlog | cat -s > $pypilog - -version=$(head -n 1 $pypilog) -inform "pypi changelog generated" - -# touch up version in setup.py file - -if [ -n $(grep version "$libdir/setup.py" &> /dev/null) ]; then - inform "touched up version in setup.py" - sed -i "s/'[0-9].[0-9].[0-9]'/'$version'/" "$libdir/setup.py" -else - warning "couldn't touch up version in setup, no match found" -fi - -# touch up version in lib or package siblings - -if [ -z "$libname" ]; then - cd "$libdir" - libname=$(grep "name" setup.py | tr -d "[:space:]" | cut -c 7- | rev | cut -c 3- | rev) - libname=$(echo "$libname" | tr "[A-Z]" "[a-z]") && cd "$debdir" - sibname+=( "$libname" ) -elif [ "$libname" != "package" ]; then - sibname+=( "$libname" ) -fi - -for sibling in ${sibname[@]}; do - if grep -e "__version__" "$libdir/$sibling.py" &> /dev/null; then - sed -i "s/__version__ = '[0-9].[0-9].[0-9]'/__version__ = '$version'/" "$libdir/$sibling.py" - inform "touched up version in $sibling.py" - elif grep -e "__version__" "$libdir/$sibling/__init__.py" &> /dev/null; then - sed -i "s/__version__ = '[0-9].[0-9].[0-9]'/__version__ = '$version'/" "$libdir/$sibling/__init__.py" - inform "touched up version in $sibling/__init__.py" - elif [ "$versionwarn" == "yes" ]; then - warning "couldn't touch up __version__ in $sibling, no match found" - fi -done - -exit 0 diff --git a/sphinx/_static/custom.css b/sphinx/_static/custom.css deleted file mode 100644 index 7338b08..0000000 --- a/sphinx/_static/custom.css +++ /dev/null @@ -1,41 +0,0 @@ -.rst-content a, .rst-content a:focus { - color:#13c0d7; -} -.rst-content a:visited, .rst-content a:active { - color:#87319a; -} -.rst-content .highlighted { - background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAJElEQVQIW2P8//9/PSMjYyMDEmAEsdElwILoEnBBZAkUQZgEABMWE4Kzp1KUAAAAAElFTkSuQmCC),rgba(246,167,4,0.2); - margin:0 -6px; -} -.wy-side-nav-search { - background:#333333; -} -.wy-nav-side { - background:#444444; -} -.rst-content dl:not(.docutils) dt { - background:#e7fafd; - border-top:solid 3px #13c0d7; - color:rgba(0,0,0,0.5); -} -.rst-content .viewcode-link, .rst-content .viewcode-back { - color:#00b09b; -} -code.literal { - color:#e63c2e; -} -.rst-content #at-a-glance { - margin-bottom:24px; -} -.rst-content #at-a-glance dt, -.rst-content #at-a-glance dd dl:not(.docutils) dt { - border:none; - background:#f0f0f0; -} -.rst-content #at-a-glance dd dl:not(.docutils) dd { - display:none; -} -.rst-content #at-a-glance dd dl:not(.docutils) { - margin-bottom:0; -} \ No newline at end of file diff --git a/sphinx/_templates/breadcrumbs.html b/sphinx/_templates/breadcrumbs.html deleted file mode 100644 index e69de29..0000000 diff --git a/sphinx/_templates/layout.html b/sphinx/_templates/layout.html deleted file mode 100644 index a2bd1c5..0000000 --- a/sphinx/_templates/layout.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "!layout.html" %} -{% block extrahead %} - -{% endblock %} -{% block footer %} - -{% endblock %} \ No newline at end of file diff --git a/sphinx/conf.py b/sphinx/conf.py deleted file mode 100644 index fcf9829..0000000 --- a/sphinx/conf.py +++ /dev/null @@ -1,394 +0,0 @@ -#-*- coding: utf-8 -*- - -import sys -import site - -import mock - -# Prompte /usr/local/lib to the front of sys.path -#sys.path.insert(0,site.getsitepackages()[0]) - -import sphinx_rtd_theme - -sys.modules['smbus'] = mock.Mock() - -sys.path.insert(0, '../library/') - - -from sphinx.ext import autodoc - - -class OutlineMethodDocumenter(autodoc.MethodDocumenter): - objtype = 'method' - - def add_content(self, more_content, no_docstring=False): - return - -class OutlineFunctionDocumenter(autodoc.FunctionDocumenter): - objtype = 'function' - - def add_content(self, more_content, no_docstring=False): - return - -class ClassOutlineDocumenter(autodoc.ClassDocumenter): - objtype = 'classoutline' - - def add_content(self, more_content, no_docstring=False): - return - - def __init__(self, directive, name, indent=u''): - # Monkey path the Method and Function documenters - sphinx_app.add_autodocumenter(OutlineMethodDocumenter) - sphinx_app.add_autodocumenter(OutlineFunctionDocumenter) - autodoc.ClassDocumenter.__init__(self, directive, name, indent) - - def __del__(self): - # Return the Method and Function documenters to normal - sphinx_app.add_autodocumenter(autodoc.MethodDocumenter) - sphinx_app.add_autodocumenter(autodoc.FunctionDocumenter) - - -def setup(app): - global sphinx_app - sphinx_app = app - app.add_autodocumenter(ClassOutlineDocumenter) - - ClassOutlineDocumenter.objtype = 'class' - - -import pantilthat - -PACKAGE_NAME = u"PanTiltHAT" -PACKAGE_HANDLE = "PanTiltHAT" -PACKAGE_MODULE = "pantilthat" -PACKAGE_VERSION = pantilthat.__version__ - -suppress_warnings = ["app.add_directive"] - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.viewcode', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -# -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = PACKAGE_NAME -copyright = u'2016, Pimoroni Ltd' -author = u'Phil Howard' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'{}'.format(PACKAGE_VERSION) -# The full version, including alpha/beta/rc tags. -release = u'{}'.format(PACKAGE_VERSION) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# -# today = '' -# -# Else, today_fmt is used as the format for a strftime call. -# -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' -#html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - 'collapse_navigation': False, - 'display_version': True -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [ - '_themes', - sphinx_rtd_theme.get_html_theme_path() -] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -# -# html_title = PACKAGE_NAME + u' v0.1.2' - -# A shorter title for the navigation bar. Default is the same as html_title. -# -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# -html_logo = 'shop-logo.png' - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# -html_favicon = 'favicon.png' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# -# html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -# -# html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# -# html_additional_pages = {} - -# If false, no module index is generated. -# -# html_domain_indices = True - -# If false, no index is generated. -# -html_use_index = False - -# If true, the index is split into individual pages for each letter. -# -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# -html_show_sourcelink = False - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -# -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -# -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# -# html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = PACKAGE_HANDLE + 'doc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, PACKAGE_HANDLE + '.tex', PACKAGE_NAME + u' Documentation', - u'Phil Howard', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# -# latex_use_parts = False - -# If true, show page references after internal links. -# -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# -# latex_appendices = [] - -# It false, will not define \strong, \code, itleref, \crossref ... but only -# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added -# packages. -# -# latex_keep_old_macro_names = True - -# If false, no module index is generated. -# -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, PACKAGE_MODULE, PACKAGE_NAME + u' Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -# -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, PACKAGE_HANDLE, PACKAGE_NAME + u' Documentation', - author, PACKAGE_HANDLE, 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# -# texinfo_appendices = [] - -# If false, no module index is generated. -# -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# -# texinfo_no_detailmenu = False diff --git a/sphinx/favicon.png b/sphinx/favicon.png deleted file mode 100644 index 5ed0316c76a3c90cc6c8844cdd3c1385ad5cf09d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26049 zcmeI42UOEbx9Af(NJpeeFDgN@h`552WJ|#V|4JR{{f4zP`#W@tf#$HO$PHvJ*o(O=3lT*sJT)9eQc#@hT zQ`Mk3Y>-Gb>+1Bav(@%;QCCf)4a`N~GmjNKy5zsMy1F;KSF@10u;1g?OBO;w$(1ml z>j)6jy&@_8nmJBOwYj4<1dCdA5{HF+zO&IDw*>%L$M*9LLRHA@1MeXLSOB*{J`T#Z zsjruxYI7L@SP}q}Y?KQpfbqJ3c0T>~lYn+wK!x5zTOz|3Dz`pF_Iw=31EKtMxhQg_P5e!-K~NiXSr zKCVwrvracU-Q$zZ2aUBFiFAqD{Gcl(|nh|0t-%+F5E+buY+ZMNhd zIISbuq**^q9MoLL!U2$L_O6Xs&CLyAyR`u;yy~5b%m5>X0EdZP3&BysTc$)W{dc4N zME7NxvTtj*OER7&I&or^)8=+9XeL_vtKbxJGCJrJI`?4Ie;weSE}K@?kaziuJJi*? zTsL;16GtBGy1l@D&~_#GOYFJ6X`%b(Jk(JijIUK|vuU*XJ<&~2WbH_rrd$jb_`sa} zh46+C37|AWt3mTI{-aLkhM@OAtY}i>es6$m&<5{y`V$ENa(f{Gzij~EM*VJ_?0F!d zJLS#{08sOe*1Yg0je08q0HAm)l)FL>pZNL7(gxDZ=ZrNCG+0nPMOk8w21#;xyxEY` zM7L>^<>>Sq#9!k)s=%hdMUg&5Uilm>O0qCXX&$=lMuTTfo83rT6G4b0*G@nMWfi|l zVH5kBt%ranEf$9jsw$Kia))K{>Pa28mUfRjSVi)V2^OmCA9+1aip7}AevTh)XAy|n zkE&4t-wb`AgZ&E2OZG`2GksL+Ezdrb5Sc|ta;{S!eI=7r$m86CzKid?P=HHOv9ORn zFsa@3H&FBIkxJIrjOn@BPQm7kA%<}f7^AU>H#I{;Hgt`E11ma&YK6cuoV?LFEIwYv zSlw8mkaLB-lJY5m5#i-O{+|3b?PrFsD(_NJPilF$ac#f%>%Pnthe!a*dDj~0Q z&hCs;3Y~X&^9En0BkU2|4*ZL#r-W}WGbyFKOzC%ehrhV6czM%@m?VPaa+|8nSvn`W z0J;L&Dmwl&^?clcl(cqz(69hQntdu+D$>B1`xAE{b;1kEB=ID}q@bjQVtoTo1DQd; z;uHEM24p3chL`e{4IXOG6cN5URWew_Xz;8^xV%T#M>kHpSH~)^&gx6pXBf$Yh%3ss zB2@GAVO6bkF|4Or1>hfy<%c{Bv6p$AQZ;PM9_+5Dd*kjgc=HFS>nV5^+m+fS*k#ST zcLPaV?@z9ay~(`L!`egA6TpkjE5mz#$g#|>ET~Lm#DAo2#AelF_0;O}$eNd1mTVSJ z)|R-7__i0}Q{ksNPp>>3_3B-c9)KT2g1D}7zsu@WmbnCX-at$R~5x*v1pu2N>p zg=pdEw&aoIz)sv`jpXgpR6}V)Kf^7<;Zn6B^JSi8mmwo(Z3d$Z;tcVO&c}si8D-7h z87>nir%%S6>p!=p=Q;vj*AuC$`tPu8aTQ=)%cYV%4ZaM94vQwKHM!08ZR2V8u9>WYjDp6E_yvATZ%Y%)y;>%%hnZa2 zLm-#-(Pp#B+RgxLacL>|=blAu=~ij19RHliy8Jq}38^~vprN3&ApHI8J&DhqU*dM^ zr~E#i`k;wx4SWuq1J;Jphd#u1!e+)v!?}wO!HvOvhR07VO)XDMNMge5aRb>v^O0c} zCOBr#lio-Df*_09l;Ja|i%Xqph$1$kmtcZC2${dDzS#Ac;OeA`^VMo*1YH`zXZC(C+r&Ed6yRq zHzHqb&$91&Zj4epUs3PNtJE#mCOxIjUwGcvjMmJzTD4Zy^p)U*-NDi>`-!kqfwl1! z+SL^gUswB@e3)dbS9*4eJz9^(hYxI_`_$GKvEF~F)1mW0@)zxCV_k!fj(%ewV|6e0 z-@U7FKFmlcspw`$r)&A9=@S?=G|ARr%`s)r%rKanCdsPQt+cRwr$V`Ue%eUWYA|ri z&t+s}{6f`>)n(P?JIk)6+a~q*F4Q{84Ze(6Cn;n25HrVM<#J&hb=%)@E4I6``$ac4 z1PWO->v~)|`ti1K-Y5U8V2e(}FJ&cFb#J&vhU(w?1Z_f~&R&mR0@E_lwtimGb7f+>*Lo zyY}vSUKXPzqe@-X*Pu1<49u$y5@(csE9HrcjqMmei4mPwUMsRX%wnNq!HhFpekcdi;q zmZLT`mL>Yu27*63HVk?W?+o)<*~q=-N{csif~Vu2f_`7K)SLV`i0scy--qYY=;H?di`jZi<^eV zucBN%f6zm7_sqbAaiLRT5hPWaU=5+`` zO;P&$K(PJCpuAV5UD-4b<2(8SAKo?>wBnT1_6%gZv+d@IS0)j$NUNIO{lvfBWXd?y{ zh6o6P;XhqtnE#7wEe~`U=?ZnmXbXB?fk4XgATf}#kSIt1q^ztgDxxS45e6$N3CM|w z{dD^W(|>WVitt1sJ^V1(p$UGQn0j#czo-7pa7KKaU~W(kPx#?rmty-l+JDW+UlZ=o z=G&wZhr$l0rnD#28*VGf|F^WiD)r0mhY{jmxx$2P1z{p!J6@OwOqf>)A|lLd1A~b0 z3PD9-g0`YCFj(l9e+=_~F2lzG?kbI5t024}AzlG7eK1%YBq}Z-a2|vfKtHScP3xbX zcp~jkK2Q(1oIQHDNq=7)wSP`97WA3_a4}>#s{w@}Tn}#o&i(ZJcf)@!0OkO7wTIhE z^Zzvbnee-bB>&;+=ZyHjvF}8Mp`rpJc2HiJkO+tuBp`&IZnh9GFVs%hRzLs-6BQPO z{A}NUw(rp0C!!AqiGxMOg+%{X^qmpH)fVaF`Tu0k!H}-ra1YdfV&#7@BGXAd6Fx4$-@? zKf1mBX8yMY^M`kIp!zd%Jv!R^ULg#VKY2v{SK`R;p8sI_R|!7}EB*x%BlM88g^AlC zJzStD>F+zpw>{&pL<~QFMdj*ndnn9LOj1uHXTOqTq_?_wOO6h&cFH$oZS*Z$8k~M%ejb`1l$6 z{B1%X`oPfq-3R(OZwp01|3h9db$|2otvyH&eI(NPpQGNtBBx&s`(c9N?eO>~u67t@ z7$Iy?4$^`Ukf=UH{iL{=9m9YyUshSn^x$w_^r6RP#W<)LhX4o0|u^zrLNd z1b)!?XN}+4pihkG^UHT@^oaiEA2aQD{qGPDacU!c;m(JX>6^{(>A%|`A3 zF#nSLyUDkOL=Sxl2bcb-j*;}c_BVLOiU{;w#kWGh0_cJEt&HDu{%C3PkCqrYKPl*OyljB!-^xrJ})ujJs$C&Pp3&Esuqzjzom1aEuF+#*y+dt|Jj)Djeg&q;aHtjO$25mqtbH3dguGX&fmZ z<2n)%rou5UOd3bZ$GDC}gsE_h3zNo?@-eO>5n(DE-p zTgl$|`?@caf!sxd25%R}9u8LxJKcMjT*%GX0Pq1I@jd}1LStkJueMjjI{>VusjXu@+Y6L5gDBjn~$M_56>L%!ZA%V)OC8N;>iH(3?B zu^|`Y^Vk>?U$*+7kzk#nK|%1%$6d<6_RV&I7Nzpz)t2%n1A$Fud zB(h}&%Z=Ub{ROIZiL~`#j+bPCI~PbQaD9OU54X|?iQ3^-DV#Qw{*ybXz#XR(yn(ni zNH(rvQ)y1WrO|g&XVh9L18K=iqpv*{>pCUJz~loq&?#mSxRj*X_q2q_ka(8sIi2O{ z^Md-lg6Xprrfgkg@tdoPdsP*}WC6}Gx!qP=&OnU@eCrh6=%(Z@=931H25`UR*RX37 z60DCG(__BoX!ZNP^AyUF3a10E>A~*c5*W#$BxWZ_Te>GBo}ilh7PIwCadZjHR1AV4 zIB~>HCmylQNav;r5xhzi#5G6%m;5E<-b-KCSfFI@@|lEqY+S+$r!&Gd=@Z>msx=oc zAusp$zCvW5QYXH;mWks?u#v5tIFr}wGK*9Z?xzahR3Y);0vaeXax=M;ywGb)y-6bZ zV6IB6?tQ`SAR_b2-E#sy+XmG>3Tb;o+ms{vBpm7Fsug3Z>#*EnGAw2b5S9ZrKSjqZ zs7dA-&DT}Wldkj)kx`^7zoar9hH$)RML+Cdl9Jw~9V%Uwigac~RUfMojv zmUTwF4<8C$fkX|T%2JfY*@}`=RV19zdGRQ<&3oTIsCq7lNUzBpMPkm2M=bIwVt@HefyoJin2NX}tg)a?9}|hs z9C|HYrDVjHFHw}7S0WueUBA^dGxoWgeSg?KPA57e<^6INA3oNKU_<$p+DOoct=^gX zP0=g0C2r|NUlW_HI^9GG3?X1W(5FFy8J_TwPqn-(xV_J(h$ciu+GFmz*Ic~H>EFE< z0L;74)1>dp7+Usl?OnmS<{6c{h3yufL z!rpY&Um9B(Vmo^GrFEDpp2L}T)tR1!L|SJ}l7_2Gm%l#vv|UdDwD*12rR=eE_ClBl zP#}7m4ie?{!nZo+`4UjrO0+@0B39PoqS-l1DeNa9TbFP()m!%aVFiABJ~{K|u+ws~ zZ*{pE{IPGM`Nk|9B*HrRr*kS4ch z{=S}|_3U_l`HRWdKI@{Od4syESe8=D8^C35&V!G$q9R{c!3X~DB-;~XsXHTZOV@R7 z?KaDK(8xAQYL0xpv_+&7Qj^)HMQAMOq@qn-u|7S^U3{MtM98*!>FUjg7i^+84RQ)z zRm(cxv;6dqB0iO*ggevyg;{+-cwD{P=Ru0Te)%3A)W^0=DSVQO5w#sIS_+TWsaCA? zK>$fZr6()df`zOePi}t#K{G=->ue&Ij(HQEXUwhP$*c#rZ{7ST6}Wzls%9IXzcO2S zKD8@7I2E_Px@1aYA+GGKBg==VR+ecU#F|2Lom!(grB8Zd+bf+Ja)@A%U4cBOwtv9K z!1qrm4&L7AeI#J|F;4*i2n(dQ>79CRfa^rfUc@q1x~1}T>N8e5BBS|UapW>cuYguw zM47!R!M_@B+3{hOEK!2a=NIsU;VB9xNtBJ6AI=Fa00jr2ZLEp)Qj)PfLnP6&04HZ| zO^^Eskjs~YO-4pB`G^45yG#+rh&7WMd0Q&~05<7QO;mwXJ8zOzZk^8o7%MdjJ?r;c zJ~Ihx-xeKz6`YV@N6HWEHxzZen!b=qTm^bnmBaGnwT?pp!IhWf%DCg6>$JVs4b}7} zpFSXrzuII%Kwzo%$eY5F+Gc8;WZ^Yc_T1*p7UNK-bPg9>ay)j8M&HCHr?mz7Ud>!z ztihhHG@?=xcJetZsa;B;feL*i0pN8aKNi!} z+9Ei!w>D!Yav(w}sH6oW?i4fG=`Q|jQ0uI6v)p$UvV=whdwP?-6bFZyV!#3Vy@Au|ONkU9>6K9iXemd0z71QGG{SRm1ZH%er89>ZTYh zD!#~KxO~8lB??<>eoeV^^cJ{P88PXSn(vMZYOFF&3iXIx%P#*oc$;GUKGaReG5V=x zV{S1mBvpG|Eb?7fzzeMyD7Og3*zP6un?VkXf!LHcPPMFJf0{B)2Gvf7N7CSC(}iN& z$8N+V;6?TfvxIx?QaXlhhHEX96M9TH%g5r`&ncaB>K;{vKjhJ&CvTa3(Ksshzf0^LqSw<!JInuqCmhp|OnXDt;7M2Cy`{Lu@;%MDcfn_M##PCY!39=hi;0-8p$q!Jwrl0gIc+ z@+=L}{ulJ%j?qNDJ<(3^GGYxYHiy1+eTSiHz-lwIn0u&4>aH31!kGF_|GLt=w{tRu zH4qXOV%l*3+3U_c8F%a}XYC3tk@uvhiaS_YXg+Ku^p&T?!*Lu29L|S{zc%38pz(2u zhyYC?p7p=;&}z@yjG)l6Q1$gkakDp2+R1)yw5a69Zu>+>B}MGptim8 zwYy*wm1a~RBu<{z&7t(#Mt3>o_m2(&f?n5u=o_NlGEjeay)Ew zz~ms`tNqN>)%F$YwK^Gkr*lMJ81tOI_pH+|fx=pBG6wrf@=Nhx&aQk{+=4UDQV81# zK-=x)A)`iHluE2#p_-jmG58#IXUi?$JJ{kFzeqjxNkhu-@h9Kb861feJt_9FT& z2`~-p72$u?(Cs2uOGMk7ALvHMDuBiFK{DiavXJjimtSL`tERk@|J9F;UM+;S_ z@?7qjmGPDC>i|Z4=c2ZQ2`i@QH&fB2=V%j2%^C^AjPOTnt*m^^dhBrC`A%Vx#>IWe z=*_xq%mZ!d-jAJe5(_@tgHP@sB#9mwRiBG)W_gZzPQqWAd{$^O_qQSdElG}^uif7% z&b|(~Xts4iZ|U{dEcP175nBt^J4Hd7!w++Jitx6HPZEZ`*PJI2Na(4UF)-tW4HTU! zSkkN6>SLGmVzskC0{ONi*6xtxClWM|%f71)b}3r!rQu1kfUFW*UDV)HW{w%61vaeD z%`EI_X_0}=T^`g`#eZT>SE$Y>2sKjXqH;AenmmKQ*Eh7h^wwXF$JqIK$yrTGOFDWt z>N9E$Qws%}2Z>#qx)VK4{Eyw%S8%2MQJFF_V#KWvZdbkfkoHx^40uEM{^z6^hW_`k zadu_HVm#lw&Zefd9%w9pU({<>Xhqb>_8j-@e%jQCm;E zT$ra}*2=J=7$)g`(@vkYD6W?;@X3l)Oy$Un3h|Sz>De;UGB0G* zuY>wQ;bqKQcvuBDJ(R|4>_Sqks;;DXlf-A}t4^OkCnYkV(zuuo+p|uvdixakD$mBy zChivMITq|%M)GH4*;L*Pe1THON{~+~9{U*OgVdOCba!>&%-kQbD9q@6Yz0 z0WWixys??<-z3Mydel69R)g66ZB-8l^fczO&kAcXZn)a@p3Y_Q3j>k9GA+z2CR%63 zGM93jXDwJlni#G!00sC`?}VCqrcKCfEbDs?Tu0Cp*9C^^-GDHOx?cwutZ<*y&S6@x zboI*=RSYlk;E%b};i7d~i9pP4nB-=glwA+6d4FqK9t?6Y_t>DOY>=k?D$j+R57)Eg z%W(0x9JB2D8(uHsA>V?07u(~RoC*mV zUByvR*De5+BwRhxKV9(3Eaxocp4;i_&fvRdecx2cTW;AHMbxN#wd=NN{>EE=MV*G> zNPf3M@s=TZ#g#8SW2cn#5jd8D7W}|sN z;UwUm3GqQ>81e^E_)pUlwd%QWNP4(mPfU6|UafHntB{5*Dv#!k`zFUD`S8b2tf-$) zss=XfC?-veJ&UL!`_y~xTBS3=ElqIjP<;D)c4pJ;dzv1Su|?sm<9m-Esj@_YVdmm#BznulBXr*pLs6Na&wf1lxfk<77K55S>2z2wluW`5 zSG&RKd5a1?v0Ce`B%Dl_2D~razC8uwd0=Z7$s5T1x}7a*;7nP{N2gleg&yDC85dw(gc(wYX1PI!jA`uisgJ_g!7LBx=-^JX$=bPVnqSC)dd^Cjgb0 zI?gn%b4ujhB7w!z&pJ81ar|P1=u5ASY^yb2rf6GFA8c+-+}^sF-;yD8g`pUp8b1Gy zR^oGX;MCA^bwI^hA}i&yN}LHP5^L7$vV^8>^%e%eSD6L23ge;`QSK?X>T0oRkl}^n zwlxJ^(WAnYt>DMhLtL#Cz8AaCpWz>JXeDHREzv$8%zLl984^QdCl`S4_j`6T4sh_mGp@+vBq1suw#`TL^h!yAXhKTCkyfyJ-)85K%_houramBz;wC_5h znMvrdK*u;i?u(_qbC%<^q4qv5cae>K&2IS=f6FOnJXy-~_?KT|jDh!~|(EF)S-JSUvO9cZ)cDjm3F zlI^Nq!Fg7@5}_AibXuBxtw`P1oJaE0)78L*t>LD3I5%#PwBwJNp8MeGbbhSrN!RlE zBqQmJ^wUqTwIltzKKQg%C)5BUgDr8efu@s(aoJ`uXZKv97PR=`;}qT zQ>B@(fYg~AB|{r62`40l#e1&ZN#i0`B8Q|l-N>k0v;2f_eayVW-5$yB zv%P)4z8)VuwOIi3pMUA`X3K)J;V$YrQFTaj2tTA_4p)ts??WNpy(f9NZcJeYe*1OH z1RqN(9yyv_p^LI1_rRi2efJP|JkCv+Y2lh5Amd1}MRj~n`=-n8#+uC@zAUt0-F&a=({z-VO_%@J!IqrLlP7qyBF2U7mgV@T_tYV^%CH6jqHtnHobtch6om zR4|&MlW1f$ni8*v+2kg%Oe0QvJOzt_atJXUP{8)zeW(Z{jL*X5Gs9}%*O7&=H+ry# zaCuJDB6Nz{^3u9ZU2#BiAz9^5vlZTyQZ(SD;Tdb7h@(;hy-*${=F4yLrZtukMYB< z8s&f`jdNaDaTE3J&HR9c!Mk06hH#)JKL_wS^~be9<^%JQsKJW@=UIueIrKN*;mnuU z;`mGm_Ub6%O;!R)`BCJl0mH$~b$$|457}uurn=SZ^RCXupU88d;wB5^&TzgD5_Jw) z<1b`?cQf7D*gm&1C@{lu8 z99{@o56HJ()|5PT`JM*&<2ACxz}0BCnkb`}fa}<&U*bP!^cmO)jU#{im|69imtB7F zxI5)*?lL=)`V)a}P*1EEr~&%BWUDsfu0m_5TuNqwY zu2Uy?)7VqCmQCIg5gOSqKg@M!dOi60bMeEFjRvIc^26y5%=uS>Ie~yWtUzJ2JnHCP zVDRVSnoa}L$8#a~u(s`%ALXut)=A4n_XY1wu^u>0A|F7^z&j<$>KR6!&G#bCO)dy} zR(=i!`)4m!+K41X^Jo#2Y5s3&)c(h%ii&PFew~uqDi9-vJxdPo0RO sEqojv7``d`buF~*_LrdjufYH%t*lq&#AY$*dwGDGlBQz0ymi?B07VsPKL7v# diff --git a/sphinx/index.rst b/sphinx/index.rst deleted file mode 100644 index 2e0ae53..0000000 --- a/sphinx/index.rst +++ /dev/null @@ -1,107 +0,0 @@ -.. role:: python(code) - :language: python - -.. currentmodule:: pantilthat - -Welcome -------- - -This documentation will guide you through the methods available in the Pan Tilt HAT python library. - -Pan-Tilt HAT lets you mount and control one of our pan-tilt modules right on top of your Raspberry Pi. The HAT and its on-board microcontroller let you independently drive the two servos (pan and tilt), as well as driving up to 24 regular LED (with PWM control) or NeoPixel RGB (or RGBW) LEDs - -* More information - https://shop.pimoroni.com/products/pan-tilt-hat -* Get the code - https://github.com/pimoroni/pantilt-hat -* Get help - http://forums.pimoroni.com/c/support - -At A Glance ------------ - -.. autoclassoutline:: PanTilt - :members: - -Set Brightness --------------- - -.. automethod:: pantilthat.brightness - -Clear ------ - -.. automethod:: pantilthat.clear - -Set Light Mode & Type ---------------------- - -.. automethod:: pantilthat.light_mode - -.. automethod:: pantilthat.light_type - -Pan ---- - -.. automethod:: pantilthat.pan - -.. automethod:: pantilthat.servo_one - -.. automethod:: pantilthat.get_pan - -Tilt ----- - -.. automethod:: pantilthat.tilt - -.. automethod:: pantilthat.servo_two - -.. automethod:: pantilthat.get_tilt - -Servo Enable ------------- - -.. automethod:: pantilthat.servo_enable - -Servo Idle Timeout ------------------- - -.. automethod:: pantilthat.idle_timeout - -Servo Pulse Min ---------------- - -.. automethod:: pantilthat.servo_pulse_min - -Servo Pulse Max ---------------- - -.. automethod:: pantilthat.servo_pulse_max - -Set All LEDs ------------- - -.. automethod:: pantilthat.set_all - -Set A LED ---------- - -.. automethod:: pantilthat.set_pixel - -Set A LED (RGBW) ----------------- - -.. automethod:: pantilthat.set_pixel_rgbw - -Show ----- - -.. automethod:: pantilthat.show - -Constants ---------- - -* :python:`WS2812 = 1` - used with :python:`pantilthat.light_mode` to set WS2812/SK6812 LEDs -* :python:`PWM = 0` - used with :python:`pantilthat.light_mode` to set PWM dimmed LEDs - -* :python:`RGB = 0` - used with :python:`pantilthat.light_type` to set RGB WS2812 LEDs -* :python:`GRB = 1` - used with :python:`pantilthat.light_type` to set GRB WS2812 LEDs -* :python:`RGBW = 2` - used with :python:`pantilthat.light_type` to set RGBW SK6812 LEDs -* :python:`GRBW = 3` - used with :python:`pantilthat.light_type` to set GRBW SK6812 LEDs diff --git a/sphinx/shop-logo.png b/sphinx/shop-logo.png deleted file mode 100644 index 8fd0cda225cb6add7db0f7137919d92322c89f06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19652 zcmeI4c|6qJ+sD76O(@y2Z%ML@nX%1e-x~WG#y-Yivdl1pAt6b!Wi7flqJ$LwBB=f&vQSo-}Ahle`a2o?{%*8xvuklpL3n>`RD7Tx#B5#7%!FXX2p#d1H zP*ZDks5ctsE~cT*tV)7Y0{CKxC=rscj~@X}QWIN`3#VMKHG{-N*1HhB)x@;c5{lTF zn2YG(0x%+qa!RsjFc>VN43h&Zsz4!%G9nOpup&qv1_H~=%7fuxc{oH~ljcD%m6 zzfDab>IG2{d_($MCBZrbj{#X?2)MuiG)6Coax3D$yL%$mlRZ0~^E&4g* zKjUF^QACV7Wko8RLAMlFeiP1Z39zBuhjs?T^00~)}O=rq5LMp z{$>LI73Ht-f06%lj$kj0-#=8)*C^{9{#j4Jc@Tq90T?Y$%5wO3mHjpDUxm~OR6%RI zy$|;PU=|3dp#N|V{#6nEXLIm<8~Xp8gP-dc?S=C5#JH=2zAxSH4gYT1zE0*}YwG(} z@b?;2U3>PxDfLTPP^k5~0jq-k*7h^=*Zy{Eb?6hg-d`CaPx(+zD3s4P>91~TDU!DI zS2wlvr}Cg5mZ(nI;MNC9)%=TU?E(39E4QMYii%K0RSN)nRU&^XmA_|O7yjb-Cj2^AzZ|>PEAVS88?IxgCLsJwJsR6uB(bX>IgP9}a~q4LskQ30WC(s9w|L*=F8q5?wOq~oH^hssOGMFoVmNykN-50#gWiwX#Bla7lv zA1W^$7ZniNCLI@TK2%;hE-E0jO*$^xe5ky1TvR}4n{-^X`A~W3xTt{8HtD!%^P%$6 zaZv%GZPIbk=0oMB7&4A0wX&^Bq}V*dGxCdQBQUS=@m z1oYDggkI#{gjI3+4MY00hbdz@#exz%v1W2bb#7tPcP?0}OPv ztVsiJooX%lO?Kk;j!VASmMv2o>G3@#yWA}Ir+f{@Gmd!d$A<7A-TU5=KmgyG*$2qiEzez$Qm&g-N zLP_UaNKaBg2G|d;c;ty)71m{gXaR54Y98 c29iqXq3)xp_>0_3<6H>b_N?3w_6T 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 From 80178106633842a8a0efd90c5b5117e1d1f56e35 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 12:49:50 +0000 Subject: [PATCH 02/13] neopixel-blinkt: move to contrib. --- {neopixel-blinkt => contrib/neopixel-blinkt}/CHANGELOG.txt | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/LICENSE.txt | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/MANIFEST.in | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/README.md | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/README.rst | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/blinkt.py | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/setup.cfg | 0 {neopixel-blinkt => contrib/neopixel-blinkt}/setup.py | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {neopixel-blinkt => contrib/neopixel-blinkt}/CHANGELOG.txt (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/LICENSE.txt (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/MANIFEST.in (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/README.md (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/README.rst (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/blinkt.py (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/setup.cfg (100%) rename {neopixel-blinkt => contrib/neopixel-blinkt}/setup.py (100%) diff --git a/neopixel-blinkt/CHANGELOG.txt b/contrib/neopixel-blinkt/CHANGELOG.txt similarity index 100% rename from neopixel-blinkt/CHANGELOG.txt rename to contrib/neopixel-blinkt/CHANGELOG.txt diff --git a/neopixel-blinkt/LICENSE.txt b/contrib/neopixel-blinkt/LICENSE.txt similarity index 100% rename from neopixel-blinkt/LICENSE.txt rename to contrib/neopixel-blinkt/LICENSE.txt diff --git a/neopixel-blinkt/MANIFEST.in b/contrib/neopixel-blinkt/MANIFEST.in similarity index 100% rename from neopixel-blinkt/MANIFEST.in rename to contrib/neopixel-blinkt/MANIFEST.in diff --git a/neopixel-blinkt/README.md b/contrib/neopixel-blinkt/README.md similarity index 100% rename from neopixel-blinkt/README.md rename to contrib/neopixel-blinkt/README.md diff --git a/neopixel-blinkt/README.rst b/contrib/neopixel-blinkt/README.rst similarity index 100% rename from neopixel-blinkt/README.rst rename to contrib/neopixel-blinkt/README.rst diff --git a/neopixel-blinkt/blinkt.py b/contrib/neopixel-blinkt/blinkt.py similarity index 100% rename from neopixel-blinkt/blinkt.py rename to contrib/neopixel-blinkt/blinkt.py diff --git a/neopixel-blinkt/setup.cfg b/contrib/neopixel-blinkt/setup.cfg similarity index 100% rename from neopixel-blinkt/setup.cfg rename to contrib/neopixel-blinkt/setup.cfg diff --git a/neopixel-blinkt/setup.py b/contrib/neopixel-blinkt/setup.py similarity index 100% rename from neopixel-blinkt/setup.py rename to contrib/neopixel-blinkt/setup.py From 16d766cb60b5a2303de463ef067ccd2dfcbcbe1b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 12:59:56 +0000 Subject: [PATCH 03/13] Repackage to latest boilerplate. Drop Python 2.7 support. Use f-strings instead of format. Switch from smbus to Python smbus2. --- .gitignore | 3 + library/CHANGELOG.txt => CHANGELOG.md | 0 LICENSE | 2 +- library/MANIFEST.in => MANIFEST.in | 0 Makefile | 66 +++ README.md | 59 ++- check.sh | 84 ++++ examples/pantiltweb/pantiltweb.py | 6 +- install.sh | 385 ++++++++++++++++++ library/LICENSE.txt | 21 - library/README.txt | 2 - library/setup.py | 54 --- .../pantilthat => pantilthat}/__init__.py | 0 {library/pantilthat => pantilthat}/pantilt.py | 21 +- pyproject.toml | 117 ++++++ requirements-dev.txt | 10 + requirements-examples.txt | 1 + library/test.py => test.py | 0 tox.ini | 27 ++ uninstall.sh | 72 ++++ 20 files changed, 812 insertions(+), 118 deletions(-) rename library/CHANGELOG.txt => CHANGELOG.md (100%) rename library/MANIFEST.in => MANIFEST.in (100%) create mode 100644 Makefile create mode 100755 check.sh create mode 100755 install.sh delete mode 100644 library/LICENSE.txt delete mode 100644 library/README.txt delete mode 100755 library/setup.py rename {library/pantilthat => pantilthat}/__init__.py (100%) rename {library/pantilthat => pantilthat}/pantilt.py (94%) create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt create mode 100644 requirements-examples.txt rename library/test.py => test.py (100%) create mode 100644 tox.ini create mode 100755 uninstall.sh diff --git a/.gitignore b/.gitignore index a76c4d3..fa45562 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ __pycache__ *.orig.* packaging/*tar.xz library/debian/ +.coverage +.pytest_cache +.tox diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md diff --git a/LICENSE b/LICENSE index d7a44a9..edd3445 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Pimoroni Ltd. +Copyright (c) 2023 Pimoroni Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/library/MANIFEST.in b/MANIFEST.in similarity index 100% rename from library/MANIFEST.in rename to MANIFEST.in diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..56cf0df --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) +LIBRARY_VERSION := $(shell hatch version 2> /dev/null) + +.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy +usage: +ifdef LIBRARY_NAME + @echo "Library: ${LIBRARY_NAME}" + @echo "Version: ${LIBRARY_VERSION}\n" +else + @echo "WARNING: You should 'make dev-deps'\n" +endif + @echo "Usage: make , where target is one of:\n" + @echo "install: install the library locally from source" + @echo "uninstall: uninstall the local library" + @echo "dev-deps: install Python dev dependencies" + @echo "check: perform basic integrity checks on the codebase" + @echo "qa: run linting and package QA" + @echo "pytest: run Python test fixtures" + @echo "clean: clean Python build and dist directories" + @echo "build: build Python distribution files" + @echo "testdeploy: build and upload to test PyPi" + @echo "deploy: build and upload to PyPi" + @echo "tag: tag the repository with the current version\n" + +version: + @hatch version + +install: + ./install.sh --unstable + +uninstall: + ./uninstall.sh + +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix shellcheck + +check: + @bash check.sh + +shellcheck: + shellcheck *.sh + +qa: + tox -e qa + +pytest: + tox -e py + +nopost: + @bash check.sh --nopost + +tag: version + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" + +build: check + @hatch build + +clean: + -rm -r dist + +testdeploy: build + twine upload --repository testpypi dist/* + +deploy: nopost build + twine upload dist/* diff --git a/README.md b/README.md index 922b856..7927dc0 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,69 @@ # Pan-Tilt HAT + +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/pantilthat-python/test.yml?branch=main)](https://github.com/pimoroni/pantilthat-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/pantilthat-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/pantilthat-python?branch=main) +[![PyPi Package](https://img.shields.io/pypi/v/pantilthat.svg)](https://pypi.python.org/pypi/pantilthat) +[![Python Versions](https://img.shields.io/pypi/pyversions/pantilthat.svg)](https://pypi.python.org/pypi/pantilthat) + + https://shop.pimoroni.com/products/pan-tilt-hat Pan-Tilt HAT is a two-channel servo driver designed to control a tiny servo-powered Pan/Tilt assembly. It also controls either PWM-dimmed lights or WS2812 pixels; up to 24 RGB or 18 RGBW. -## Installing +# Installing + +We'd recommend using this library with Raspberry Pi OS Bookworm or later. It requires Python ≥3.7. -### Full install (recommended): +## Full install (recommended): -We've created an easy installation script that will install all pre-requisites and get your Pan-Tilt HAT -up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal +We've created an easy installation script that will install all pre-requisites and get you up and running with minimal efforts. To run it, fire up Terminal which you'll find in Menu -> Accessories -> Terminal on your Raspberry Pi desktop, as illustrated below: ![Finding the terminal](http://get.pimoroni.com/resources/github-repo-terminal.png) -In the new terminal window type the command exactly as it appears below (check for typos) and follow the on-screen instructions: +In the new terminal window type the commands exactly as it appears below (check for typos) and follow the on-screen instructions: ```bash -curl https://get.pimoroni.com/pantilthat | bash +git clone https://github.com/pimoroni/pantilthat-python +cd pantilthat-python +./install.sh ``` -### Manual install: +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: -Enable i2c: - -```bash -sudo raspi-config nonint do_i2c 0 +``` +source ~/.virtualenvs/pimoroni/bin/activate ``` -Install the library: +## Development: + +If you want to contribute, or like living on the edge of your seat by having the latest code, you can install the development version like so: ```bash -python3 -m pip install pantilthat +git clone https://github.com/pimoroni/pantilthat-python +cd pantilthat-python +./install.sh --unstable ``` -ℹ️ Depending on your system, you might need to use `sudo` for the above command. +## Install stable library from PyPi and configure manually + +* Set up a virtual environment: `python3 -m venv --system-site-packages $HOME/.virtualenvs/pimoroni` +* Switch to the virtual environment: `source ~/.virtualenvs/pimoroni/bin/activate` +* Install the library: `pip install pantilthat` + +In some cases you may need to us `sudo` or install pip with: `sudo apt install python3-pip`. -### Development: +This will not make any configuration changes, so you may also need to enable: -If you want to contribute, or like living on the edge of your seat by having the latest code, you should clone this repository, `cd` to the library directory, and run: +* i2c: `sudo raspi-config nonint do_i2c 0` + +You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. + +Some of the examples have additional dependencies. You can install them with: ```bash -sudo python3 setup.py install +pip install -r requirements-examples.txt ``` -(or `sudo python setup.py install` whichever your primary Python environment may be) - -In all cases you will have to enable the i2c bus. ## Breakout Header Pinout diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..38dfc3a --- /dev/null +++ b/check.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# This script handles some basic QA checks on the source + +NOPOST=$1 +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') +TERM=${TERM:="xterm-256color"} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -p|--nopost) + NOPOST=true + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then + warning "Trailing whitespace found!" + exit 1 +else + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then + warning "DOS line-endings found!" + exit 1 +else + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then + warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." + exit 1 +else + success "Changes found for version ${LIBRARY_VERSION}." +fi +printf "\n" + +inform "Checking for git tag ${LIBRARY_VERSION}..." +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then + warning "Missing git tag for version ${LIBRARY_VERSION}" +fi +printf "\n" + +if [[ $NOPOST ]]; then + inform "Checking for .postN on library version..." + if [[ "$POST_VERSION" != "" ]]; then + warning "Found .$POST_VERSION on library version." + inform "Please only use these for testpypi releases." + exit 1 + else + success "OK" + fi +fi diff --git a/examples/pantiltweb/pantiltweb.py b/examples/pantiltweb/pantiltweb.py index 9d819f3..b1048c3 100755 --- a/examples/pantiltweb/pantiltweb.py +++ b/examples/pantiltweb/pantiltweb.py @@ -6,7 +6,7 @@ try: from flask import Flask, render_template except ImportError: - exit("This script requires the flask module\nInstall with: sudo pip install flask") + exit("This script requires the flask module\nInstall with: pip install flask") app = Flask(__name__) @@ -23,11 +23,11 @@ def api(direction, angle): if direction == 'pan': pantilthat.pan(angle) - return "{{'pan':{}}}".format(angle) + return f"{{'pan':{angle}}}" elif direction == 'tilt': pantilthat.tilt(angle) - return "{{'tilt':{}}}".format(angle) + return f"{{'tilt':{angle}}}" return "{'error':'invalid direction'}" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..61f1a4a --- /dev/null +++ b/install.sh @@ -0,0 +1,385 @@ +#!/bin/bash +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") +CONFIG_BACKUP=false +APT_HAS_UPDATED=false +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" +USAGE="./install.sh (--unstable)" +POSITIONAL_ARGS=() +FORCE=false +UNSTABLE=false +PYTHON="python" +CMD_ERRORS=false + + +user_check() { + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" + fi +} + +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 +} + +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + +venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" + if [ ! -f "$VENV_BASH_SNIPPET" ]; then + inform "Creating $VENV_BASH_SNIPPET\n" + mkdir -p "$RESOURCES_TOP_DIR" + cat << EOF > "$VENV_BASH_SNIPPET" +# Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +VENV_DIR="$VENV_DIR" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$VENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + PYTHON_BIN=$(which "$PYTHON") + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + if confirm "Would you like us to create and/or use a default one?"; then + printf "\n" + if [ ! -f "$VENV_DIR/bin/activate" ]; then + inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages + venv_bash_snippet + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + else + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + fi + else + printf "\n" + fatal "Please create and/or activate a virtual Python environment and try again!\n" + fi + fi + printf "\n" +} + +check_for_error() { + if [ $? -ne 0 ]; then + CMD_ERRORS=true + warning "^^^ 😬 previous command did not exit cleanly!" + fi +} + +function do_config_backup { + if [ ! $CONFIG_BACKUP == true ]; then + CONFIG_BACKUP=true + FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" + inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" + sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" + mkdir -p "$RESOURCES_TOP_DIR/config-backups/" + cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" + if [ -f "$UNINSTALLER" ]; then + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" + fi + fi +} + +function apt_pkg_install { + PACKAGES_NEEDED=() + PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to + for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do + PACKAGE="${PACKAGES_IN[$i]}" + if [ "$PACKAGE" == "" ]; then continue; fi + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 + if [ "$?" == "1" ]; then + PACKAGES_NEEDED+=("$PACKAGE") + fi + done + PACKAGES="${PACKAGES_NEEDED[*]}" + if ! [ "$PACKAGES" == "" ]; then + printf "\n" + inform "Installing missing packages: $PACKAGES" + if [ ! $APT_HAS_UPDATED ]; then + sudo apt update + APT_HAS_UPDATED=true + fi + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES + check_for_error + if [ -f "$UNINSTALLER" ]; then + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" + fi + fi +} + +function pip_pkg_install { + # A null Keyring prevents pip stalling in the background + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + check_for_error +} + +function pip_requirements_install { + # A null Keyring prevents pip stalling in the background + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install -r "$@" + check_for_error +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -u|--unstable) + UNSTABLE=true + shift + ;; + -f|--force) + FORCE=true + shift + ;; + -p|--python) + PYTHON=$2 + shift + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +printf "Installing %s...\n\n" "$LIBRARY_NAME" + +user_check +venv_check + +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" +fi + +PYTHON_VER=$($PYTHON --version) + +inform "Checking Dependencies. Please wait..." + +# Install toml and try to read pyproject.toml into bash variables + +pip_pkg_install toml + +CONFIG_VARS=$( + $PYTHON - < "$UNINSTALLER" +printf "It's recommended you run these steps manually.\n" +printf "If you want to run the full script, open it in\n" +printf "an editor and remove 'exit 1' from below.\n" +exit 1 +source $VIRTUAL_ENV/bin/activate +EOF + +printf "\n" + +inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages +apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" + +if $UNSTABLE; then + warning "Installing unstable library from source.\n" + pip_pkg_install . +else + inform "Installing stable library from pypi.\n" + pip_pkg_install "$LIBRARY_NAME" +fi + +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" +fi + +find_config + +printf "\n" + +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + +inform "Running setup commands...\n" +for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do + CMD="${SETUP_CMDS[$i]}" + # Attempt to catch anything that touches config.txt and trigger a backup + if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then + do_config_backup + fi + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi + eval "$CMD" + check_for_error +done + +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + +for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do + CONFIG_LINE="${CONFIG_TXT[$i]}" + if ! [ "$CONFIG_LINE" == "" ]; then + do_config_backup + inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE + if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + fi + fi +done + +printf "\n" + +# Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples + +if [ -d "examples" ]; then + if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then + inform "Copying examples to $RESOURCES_DIR" + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" + success "Done!" + fi +fi + +printf "\n" + +if [ -f "requirements-examples.txt" ]; then + if confirm "Would you like to install example dependencies?"; then + inform "Installing dependencies from requirements-examples.txt..." + pip_requirements_install requirements-examples.txt + fi +fi + +printf "\n" + +# Use pdoc to generate basic documentation from the installed module + +if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." + pip_pkg_install pdoc + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then + inform "Documentation saved to $RESOURCES_DIR/docs" + success "Done!" + else + warning "Error: Failed to generate documentation." + fi +fi + +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi diff --git a/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index 843e68a..0000000 --- a/library/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Pimoroni Ltd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/library/README.txt b/library/README.txt deleted file mode 100644 index 06cec8b..0000000 --- a/library/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -Learn more: https://shop.pimoroni.com/products/pan-tilt-hat - diff --git a/library/setup.py b/library/setup.py deleted file mode 100755 index ec918b3..0000000 --- a/library/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2016 Pimoroni - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -classifiers = ['Development Status :: 5 - Production/Stable', - 'Operating System :: POSIX :: Linux', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development', - 'Topic :: System :: Hardware'] - -setup( - name = 'pantilthat', - version = '0.0.6', - author = 'Philip Howard', - author_email = 'phil@pimoroni.com', - description = """Python library for driving Pimoroni PanTiltHAT!""", - long_description= open('README.txt').read() + open('CHANGELOG.txt').read(), - license = 'MIT', - keywords = 'Raspberry Pi', - url = 'http://www.pimoroni.com', - classifiers = classifiers, - py_modules = [], - packages = ['pantilthat'], - install_requires= [] -) diff --git a/library/pantilthat/__init__.py b/pantilthat/__init__.py similarity index 100% rename from library/pantilthat/__init__.py rename to pantilthat/__init__.py diff --git a/library/pantilthat/pantilt.py b/pantilthat/pantilt.py similarity index 94% rename from library/pantilthat/pantilt.py rename to pantilthat/pantilt.py index 5cb25bd..570fe20 100644 --- a/library/pantilthat/pantilt.py +++ b/pantilthat/pantilt.py @@ -1,7 +1,7 @@ from threading import Timer import time import atexit -from sys import version_info +from smbus2 import SMBus PWM = 0 @@ -67,14 +67,7 @@ def setup(self): return True if self._i2c is None: - try: - from smbus import SMBus - self._i2c = SMBus(1) - except ImportError: - if version_info[0] < 3: - raise ImportError("This library requires python-smbus\nInstall with: sudo apt-get install python-smbus") - elif version_info[0] == 3: - raise ImportError("This library requires python3-smbus\nInstall with: sudo apt-get install python3-smbus") + self._i2c = SMBus(1) self.clear() self._set_config() @@ -123,19 +116,13 @@ def _check_int_range(self, value, value_min, value_max): if type(value) is not int: raise TypeError("Value should be an integer") if value < value_min or value > value_max: - raise ValueError("Value {value} should be between {min} and {max}".format( - value=value, - min=value_min, - max=value_max)) + raise ValueError(f"Value {value} should be between {value_min} and {value_max}") def _check_range(self, value, value_min, value_max): """Check the type and bounds check an expected int value.""" if value < value_min or value > value_max: - raise ValueError("Value {value} should be between {min} and {max}".format( - value=value, - min=value_min, - max=value_max)) + raise ValueError(f"Value {value} should be between {value_min} and {value_max}") def _servo_us_to_degrees(self, us, us_min, us_max): """Converts pulse time in microseconds to degrees diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9314227 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,117 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme", "hatch-requirements-txt"] +build-backend = "hatchling.build" + +[project] +name = "pantilthat" +dynamic = ["version", "readme", "optional-dependencies"] +description = "Python library for driving Pimoroni PanTilt HAT!" +license = {file = "LICENSE"} +requires-python = ">= 3.7" +authors = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +maintainers = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +keywords = [ + "Pi", + "Raspberry", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", +] +dependencies = [] + +[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] +example-depends = ["requirements-examples.txt"] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/pantilthat-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "pantilthat/__init__.py" + +[tool.hatch.build] +include = [ + "pantilthat", + "README.md", + "CHANGELOG.md", + "LICENSE", + "requirements-examples.txt" +] + +[tool.hatch.build.targets.sdist] +include = [ + "*" +] +exclude = [ + ".*", + "dist" +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, + { text = "\n" }, + { path = "CHANGELOG.md" } +] + +[tool.ruff] +exclude = [ + '.tox', + '.egg', + '.git', + '__pycache__', + 'build', + 'dist' +] +line-length = 200 + +[tool.codespell] +skip = """ +./.tox,\ +./.egg,\ +./.git,\ +./__pycache__,\ +./build,\ +./dist.\ +""" + +[tool.isort] +line_length = 200 + +[tool.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[tool.pimoroni] +apt_packages = [] +configtxt = [] +commands = [] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..d392e8f --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,10 @@ +check-manifest +ruff +codespell +isort +twine +hatch +hatch-fancy-pypi-readme +hatch-requirements-txt +tox +pdoc diff --git a/requirements-examples.txt b/requirements-examples.txt new file mode 100644 index 0000000..8ab6294 --- /dev/null +++ b/requirements-examples.txt @@ -0,0 +1 @@ +flask \ No newline at end of file diff --git a/library/test.py b/test.py similarity index 100% rename from library/test.py rename to test.py diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..2b6d87b --- /dev/null +++ b/tox.ini @@ -0,0 +1,27 @@ +[tox] +envlist = py,qa +skip_missing_interpreters = True +isolated_build = true +minversion = 4.0.0 + +[testenv] +commands = + coverage run -m pytest -v -r wsx + coverage report +deps = + mock + pytest>=3.1 + pytest-cov + build + +[testenv:qa] +commands = + check-manifest + python -m build --no-isolation + python -m twine check dist/* + isort --check . + ruff check . + codespell . +deps = + -r{toxinidir}/requirements-dev.txt + diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..3314b7f --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +FORCE=false +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME +PYTHON="python" + + +venv_check() { + PYTHON_BIN=$(which $PYTHON) + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + exit 1 + fi +} + +user_check() { + if [ "$(id -u)" -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" + exit 1 + fi +} + +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} + +prompt() { + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" + +user_check +venv_check + +printf "Uninstalling for Python 3...\n" +$PYTHON -m pip uninstall "$LIBRARY_NAME" + +if [ -d "$RESOURCES_DIR" ]; then + if confirm "Would you like to delete $RESOURCES_DIR?"; then + rm -r "$RESOURCES_DIR" + fi +fi + +printf "Done!\n" From 991019e5996d351db6d562a4012160b702b47644 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 13:02:44 +0000 Subject: [PATCH 04/13] QA: Prune trailing whitespace. --- examples/pantiltweb/templates/gui.html | 2 +- examples/smooth.py | 2 +- terminal.jpg | Bin 64262 -> 0 bytes test.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 terminal.jpg diff --git a/examples/pantiltweb/templates/gui.html b/examples/pantiltweb/templates/gui.html index 247d629..2d94cd4 100644 --- a/examples/pantiltweb/templates/gui.html +++ b/examples/pantiltweb/templates/gui.html @@ -116,7 +116,7 @@

Pan Tilt HAT: Web Interface

angle = current_tilt; } - $.get('/api/' + current_direction + '/' + angle); + $.get('/api/' + current_direction + '/' + angle); } }); diff --git a/examples/smooth.py b/examples/smooth.py index 5209cd5..c1289c9 100755 --- a/examples/smooth.py +++ b/examples/smooth.py @@ -12,7 +12,7 @@ # G enerate an angle using a sine wave (-1 to 1) multiplied by 90 (-90 to 90) a = math.sin(t * 2) * 90 - + # Cast a to int for v0.0.2 a = int(a) diff --git a/terminal.jpg b/terminal.jpg deleted file mode 100644 index f96419931a849f6963570d49b8e808b869dd9dbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64262 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

Ugp{z$O2wQV}_@r9QlX2!5*C$=yf;Ys-H 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 diff --git a/test.py b/test.py index 2804268..98c0006 100644 --- a/test.py +++ b/test.py @@ -181,7 +181,7 @@ def assert_raises(action, expect, message): print("\n=== SERVOS ===") print("\nSetting servo one to 0 degrees...") -i2c_assert(lambda: pt.servo_one(0), +i2c_assert(lambda: pt.servo_one(0), lambda: regs[REG_SERVO1] == 5 and regs[REG_SERVO1 + 1] == 125, "Servo 1 regs contain incorrect value!") print("OK!") From 00b4a21d48f7c88b37c2ae60607306cc6877b9be Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 13:04:03 +0000 Subject: [PATCH 05/13] QA: Apply isort suggestions. --- contrib/neopixel-blinkt/blinkt.py | 5 +++-- examples/neopixel/grbw.py | 1 - examples/neopixel/half.py | 1 - examples/neopixel/max.py | 1 - examples/neopixel/neopixels.py | 1 - examples/neopixel/off.py | 1 - examples/neopixel/setall.py | 1 + examples/neopixel/white.py | 1 - examples/pantiltweb/pantiltweb.py | 3 ++- examples/smooth.py | 1 - pantilthat/__init__.py | 2 +- pantilthat/pantilt.py | 6 +++--- test.py | 7 ++++--- 13 files changed, 14 insertions(+), 17 deletions(-) diff --git a/contrib/neopixel-blinkt/blinkt.py b/contrib/neopixel-blinkt/blinkt.py index 483102b..6097a18 100644 --- a/contrib/neopixel-blinkt/blinkt.py +++ b/contrib/neopixel-blinkt/blinkt.py @@ -1,8 +1,9 @@ """Library for Pimoroni Blinkt! programs compatibility with a Pimoroni Pan-Tilt HAT and an Adafruit Neopixel strip""" -import sys -import pantilthat import atexit import signal +import sys + +import pantilthat _clear_on_exit = True _brightness = 0.2 diff --git a/examples/neopixel/grbw.py b/examples/neopixel/grbw.py index 23f7cfa..6ff8a7a 100755 --- a/examples/neopixel/grbw.py +++ b/examples/neopixel/grbw.py @@ -6,7 +6,6 @@ import pantilthat - pantilthat.light_mode(pantilthat.WS2812) pantilthat.light_type(pantilthat.GRBW) diff --git a/examples/neopixel/half.py b/examples/neopixel/half.py index 0dc123b..34953d9 100755 --- a/examples/neopixel/half.py +++ b/examples/neopixel/half.py @@ -2,7 +2,6 @@ import pantilthat - pantilthat.light_mode(pantilthat.WS2812) pantilthat.light_type(pantilthat.GRBW) pantilthat.set_all(0, 0, 0, 127) diff --git a/examples/neopixel/max.py b/examples/neopixel/max.py index 475e6cc..a4a8014 100755 --- a/examples/neopixel/max.py +++ b/examples/neopixel/max.py @@ -2,7 +2,6 @@ import pantilthat - pantilthat.light_mode(pantilthat.WS2812) pantilthat.light_type(pantilthat.GRBW) pantilthat.set_all(255, 255, 255, 255) diff --git a/examples/neopixel/neopixels.py b/examples/neopixel/neopixels.py index e5013d9..2a86014 100755 --- a/examples/neopixel/neopixels.py +++ b/examples/neopixel/neopixels.py @@ -6,7 +6,6 @@ import pantilthat - pantilthat.light_mode(pantilthat.WS2812) pantilthat.light_type(pantilthat.GRBW) diff --git a/examples/neopixel/off.py b/examples/neopixel/off.py index 2b7edd0..32ac991 100755 --- a/examples/neopixel/off.py +++ b/examples/neopixel/off.py @@ -2,7 +2,6 @@ import pantilthat - pantilthat.light_mode(pantilthat.WS2812) pantilthat.light_type(pantilthat.GRBW) pantilthat.clear() diff --git a/examples/neopixel/setall.py b/examples/neopixel/setall.py index 49a84ce..5745cab 100755 --- a/examples/neopixel/setall.py +++ b/examples/neopixel/setall.py @@ -2,6 +2,7 @@ import sys from sys import argv + import pantilthat if len(argv)<2 or len(argv)>5: diff --git a/examples/neopixel/white.py b/examples/neopixel/white.py index ac1708a..0e9573b 100755 --- a/examples/neopixel/white.py +++ b/examples/neopixel/white.py @@ -2,7 +2,6 @@ import pantilthat - pantilthat.light_mode(pantilthat.WS2812) pantilthat.light_type(pantilthat.GRBW) pantilthat.set_all(0, 0, 0, 255) diff --git a/examples/pantiltweb/pantiltweb.py b/examples/pantiltweb/pantiltweb.py index b1048c3..732b656 100755 --- a/examples/pantiltweb/pantiltweb.py +++ b/examples/pantiltweb/pantiltweb.py @@ -1,8 +1,9 @@ #!/usr/bin/env python -import pantilthat from sys import exit +import pantilthat + try: from flask import Flask, render_template except ImportError: diff --git a/examples/smooth.py b/examples/smooth.py index c1289c9..aa95ae9 100755 --- a/examples/smooth.py +++ b/examples/smooth.py @@ -5,7 +5,6 @@ import pantilthat - while True: # Get the time in seconds t = time.time() diff --git a/pantilthat/__init__.py b/pantilthat/__init__.py index 720ab59..bb77a51 100644 --- a/pantilthat/__init__.py +++ b/pantilthat/__init__.py @@ -1,4 +1,4 @@ -from .pantilt import PanTilt, WS2812, PWM, RGB, GRB, RGBW, GRBW +from .pantilt import GRB, GRBW, PWM, RGB, RGBW, WS2812, PanTilt __version__ = '0.0.6' diff --git a/pantilthat/pantilt.py b/pantilthat/pantilt.py index 570fe20..78d18ef 100644 --- a/pantilthat/pantilt.py +++ b/pantilthat/pantilt.py @@ -1,8 +1,8 @@ -from threading import Timer -import time import atexit -from smbus2 import SMBus +import time +from threading import Timer +from smbus2 import SMBus PWM = 0 WS2812 = 1 diff --git a/test.py b/test.py index 98c0006..50d0c58 100644 --- a/test.py +++ b/test.py @@ -1,7 +1,7 @@ import sys import time -import mock +import mock REG_CONFIG = 0x00 REG_SERVO1 = 0x01 @@ -97,11 +97,12 @@ def assert_raises(action, expect, message): -import pantilthat -import sys import atexit +import sys import threading +import pantilthat + old_path = sys.path sys.path = ['.'] From 64f7db932ca94d3fb4bdd2ea70ab7e56455743a6 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 14:01:05 +0000 Subject: [PATCH 06/13] CI: Fix possible bug parsing package version. --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 56cf0df..c08fa86 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) -LIBRARY_VERSION := $(shell hatch version 2> /dev/null) +LIBRARY_NAME := $(shell hatch project metadata name 2>&1) +LIBRARY_VERSION := $(shell hatch version 2>&1) .PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy usage: From bc6424b8beecd96ebd86495ae28d2a43903587c1 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 14:01:17 +0000 Subject: [PATCH 07/13] QA: Apply ruff suggestions. --- examples/neopixel/grbw.py | 1 - pantilthat/__init__.py | 4 ++-- test.py | 21 ++++----------------- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/examples/neopixel/grbw.py b/examples/neopixel/grbw.py index 6ff8a7a..7f2ee41 100755 --- a/examples/neopixel/grbw.py +++ b/examples/neopixel/grbw.py @@ -1,6 +1,5 @@ #!/usr/bin/env python -import colorsys import math import time diff --git a/pantilthat/__init__.py b/pantilthat/__init__.py index bb77a51..ad9058a 100644 --- a/pantilthat/__init__.py +++ b/pantilthat/__init__.py @@ -1,6 +1,6 @@ -from .pantilt import GRB, GRBW, PWM, RGB, RGBW, WS2812, PanTilt +from .pantilt import GRB, GRBW, PWM, RGB, RGBW, WS2812, PanTilt # noqa F401 -__version__ = '0.0.6' +__version__ = "1.0.0" pantilthat = PanTilt() diff --git a/test.py b/test.py index 50d0c58..1bf8ecb 100644 --- a/test.py +++ b/test.py @@ -1,8 +1,9 @@ import sys -import time import mock +import pantilthat + REG_CONFIG = 0x00 REG_SERVO1 = 0x01 REG_SERVO2 = 0x03 @@ -47,7 +48,7 @@ def _debug(self, addr, reg, data): name = self._watch_regs[reg] length = self._watch_len[reg] result = regs[reg:reg+length] - #print("Writing {data} to {name}: {result}".format(data=data, addr=addr, reg=reg, name=name, result=result)) + print(f"Writing {data} to {name}: {result}") def write_i2c_block_data(self, addr, reg, data): global regs @@ -96,20 +97,6 @@ def assert_raises(action, expect, message): sys.exit(1) - -import atexit -import sys -import threading - -import pantilthat - -old_path = sys.path -sys.path = ['.'] - -assert_raises(lambda: pantilthat.setup(), ImportError, "ImportError not raised by pantilthat.setup() when missing SMbus!") - -sys.path = old_path - smbus = mock.Mock() smbus.SMBus = SMBus @@ -147,7 +134,7 @@ def assert_raises(action, expect, message): # # Library should start up with servo1 and servo2 disabled # and the light mode should default to WS2812, enabled -assert regs[REG_CONFIG] == 0b00001100, "Config reg incorrect!: {}".format(regs[REG_CONFIG]) +assert regs[REG_CONFIG] == 0b00001100, f"Config reg incorrect!: {regs[REG_CONFIG]}" print("OK!") # Check every method we expect to exit, actually exists From 0e1739e1147c4fb057a7b55fea35dd2245658c41 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 14:40:58 +0000 Subject: [PATCH 08/13] QA: Migrate test.py to pytest. --- test.py | 274 ----------------------------------------- tests/conftest.py | 56 +++++++++ tests/test_features.py | 108 ++++++++++++++++ tests/test_setup.py | 22 ++++ tests/test_wrapper.py | 14 +++ 5 files changed, 200 insertions(+), 274 deletions(-) delete mode 100644 test.py create mode 100644 tests/conftest.py create mode 100644 tests/test_features.py create mode 100644 tests/test_setup.py create mode 100644 tests/test_wrapper.py diff --git a/test.py b/test.py deleted file mode 100644 index 1bf8ecb..0000000 --- a/test.py +++ /dev/null @@ -1,274 +0,0 @@ -import sys - -import mock - -import pantilthat - -REG_CONFIG = 0x00 -REG_SERVO1 = 0x01 -REG_SERVO2 = 0x03 -REG_WS2812 = 0x05 -REG_UPDATE = 0x4e - -regs =[0 for x in range(79)] - -class SMBus: - def __init__(self, bus_id): - global regs - - regs[0] = 0 # 0x00: REG_CONFIG - regs[0] = 0 # 0x01: REG_SERVO1 - regs[0] = 0 - regs[0] = 0 # 0x03: REG_SERVO2 - regs[0] = 0 - regs[0] = 0 # 0x05: REG_WS2812 - regs[0] = 0 - regs[78] = 0 #0x4E: REG_UPDATE - - self._watch_regs = { - REG_CONFIG: 'REG_CONFIG', - REG_SERVO1: 'REG_SERVO1', - REG_SERVO2: 'REG_SERVO2', - REG_WS2812: 'REG_WS2812', - REG_UPDATE: 'REG_UPDATE' - } - - self._watch_len = { - REG_CONFIG: 1, - REG_SERVO1: 2, - REG_SERVO2: 2, - REG_WS2812: 72, # 24 LEDs - REG_UPDATE: 1 - } - - def _debug(self, addr, reg, data): - global regs - - if reg in self._watch_regs.keys(): - name = self._watch_regs[reg] - length = self._watch_len[reg] - result = regs[reg:reg+length] - print(f"Writing {data} to {name}: {result}") - - def write_i2c_block_data(self, addr, reg, data): - global regs - - self._debug(addr, reg, data) - - for index, value in enumerate(data): - regs[reg + index] = value - - def write_word_data(self, addr, reg, data): - global regs - - regs[reg] = (data >> 8) & 0xff - regs[reg + 1] = data & 0xff - self._debug(addr, reg, data) - - def write_byte_data(self, addr, reg, data): - global regs - - regs[reg] = data & 0xff - - self._debug(addr, reg, data) - - def read_byte_data(self, addr, reg): - global regs - - return regs[reg] - - def read_word_data(self, addr, reg): - global regs - - return (regs[reg] << 8) | regs[reg + 1] - - -def i2c_assert(action, expect, message): - action() - assert expect(), message - -def assert_raises(action, expect, message): - try: - action() - except expect: - return - - print(message) - sys.exit(1) - - -smbus = mock.Mock() -smbus.SMBus = SMBus - -sys.modules['smbus'] = smbus -sys.path.insert(0, ".") - -pantilthat.setup() - - -#print("Testing help...") -#time.sleep(1) -#help(pantilthat.brightness) -#help(pantilthat.pan) - -print("\nTesting constants...") -assert pantilthat.WS2812 == 1, "pantilthat.WS2812 should equal 1" -assert pantilthat.PWM == 0, "pantilthat.PWM should equal 0" -assert pantilthat.RGB == 0, "pantilthat.RGB should equal 0" -assert pantilthat.GRB == 1, "pantilthat.GRB should equal 1" -assert pantilthat.RGBW == 2, "pantilthat.RGBW should equal 2" -assert pantilthat.GRBW == 3, "pantilthat.GRBW should equal 3" -print("OK!") - -pt = pantilthat - -# Config Register -# Bit 8 - N/A -# Bit 7 - N/A -# Bit 6 - N/A -# Bit 5 - Light On -# Bit 4 - Light Mode: 0 = PWM, 1 = WS2812 -# Bit 3 - Enable Lights -# Bit 2 - Enable Servo 2 -# Bit 1 - Enable Servo 1 -# -# Library should start up with servo1 and servo2 disabled -# and the light mode should default to WS2812, enabled -assert regs[REG_CONFIG] == 0b00001100, f"Config reg incorrect!: {regs[REG_CONFIG]}" -print("OK!") - -# Check every method we expect to exit, actually exists -print("\nTesting for API consistency...") -for method in ["idle_timeout", "servo_enable", "servo_pulse_max", "servo_pulse_min", - "brightness", "clear", "light_mode", "light_type", "set_all", - "set_pixel", "set_pixel_rgbw", "show", - "servo_one", "pan", "get_pan", "get_servo_one", - "servo_two", "tilt", "get_tilt", "get_servo_two"]: - - assert hasattr(pt, method), "Method {method}() should exist!".format(method=method) - assert callable(getattr(pt, method)), "Method {method}() should be callable!".format(method=method) -print("OK!") - -print("\nTesting servo aliases...") -assert pt.pan == pt.servo_one, "Method 'pan' should alias 'servo_one'" -assert pt.tilt == pt.servo_two, "Method 'tilt' should alias 'servo_two'" -assert pt.get_pan == pt.get_servo_one, "Method 'get_pan' should alias 'get_servo_one'" -assert pt.get_tilt == pt.get_servo_two, "Method 'get_tilt' should alias 'get_servo_two'" -print("OK!") - -print("\nSetting known good config...") -pt.servo_enable(1, True) -pt.servo_enable(2, True) - -pt.servo_pulse_min(1, 510) -pt.servo_pulse_max(1, 2300) - -pt.servo_pulse_min(2, 510) -pt.servo_pulse_max(2, 2300) - -print("\n=== SERVOS ===") - -print("\nSetting servo one to 0 degrees...") -i2c_assert(lambda: pt.servo_one(0), - lambda: regs[REG_SERVO1] == 5 and regs[REG_SERVO1 + 1] == 125, - "Servo 1 regs contain incorrect value!") -print("OK!") - -print("\nSetting servo two to 0 degrees...") -i2c_assert(lambda: pt.servo_two(0), - lambda: regs[REG_SERVO2] == 5 and regs[REG_SERVO2 + 1] == 125, - "Servo 2 regs contain incorrect value!") -print("OK!") - -print("\n=== READBACK ===") - -for x in range(-90, 91): - pt.pan(x) - pt.tilt(x) - #print("Pan {}, got {}".format(x, pt.get_pan())) - #print("Tilt {}, got {}".format(x, pt.get_tilt())) - assert pt.get_pan() == x, "get_pan() should return {}, returned {}".format(x, pt.get_pan()) - assert pt.get_tilt() == x, "get_tilt() should return {}, returned {}".format(x, pt.get_tilt()) - -print("\nTesting full sweep...") -# Perform a full sweep to catch any bounds errors -for x in range(-90, 91): - pt.pan(x) - pt.tilt(x) -for x in reversed(range(-90, 91)): - pt.pan(x) - pt.tilt(x) -print("OK!") - -print("\nTesting servo_enable...") -pt.servo_enable(1,False) -pt.servo_enable(2,False) - -assert regs[REG_CONFIG] == 0b00001100, "Config reg {config:08b} incorrect! Should be 00001100".format(config=regs[REG_CONFIG]) -print("OK") - -print("\nTesting value/range checks...") - -assert_raises(lambda: pt.servo_enable(3, True), ValueError, - "ValueError not raised by servo_enable index out of range") -print("OK! - ValueError raised by servo_enable index of out range.") - -assert_raises(lambda: pt.servo_enable(1, "banana"), ValueError, - "ValueError not raised by servo_enable value invalid") -print("OK! - ValueError raised by servo_enable value invalid.") - -assert_raises(lambda: pt.servo_pulse_min(3, 510), ValueError, - "ValueError not raised by servo_pulse_min index out of range") -print("OK! - ValueError raised by servo_pulse_min index of out range.") - -assert_raises(lambda: pt.servo_pulse_max(3, 510), ValueError, - "ValueError not raised by servo_pulse_min index out of range") -print("OK! - ValueError raised by servo_pulse_min index of out range.") - -print("\n=== LIGHTS ===") - -print("\nTesting range checks...") - -assert_raises(lambda: pt.set_pixel(34, 255, 255, 255), ValueError, - "ValueError not raised by set_pixel index out of range") -print("OK! - ValueError raised by set_pixel index of out range.") - -assert_raises(lambda: pt.set_pixel(0, 256, 0, 0), ValueError, - "ValueError not raised by set_pixel colour out of range") -print("OK! - ValueError raised by colour out of range.") - -print("\nTesting set_pixel...") -pt.set_pixel(0, 255, 255, 255) - -i2c_assert(lambda: pt.show(), - lambda: sum(regs[REG_WS2812:REG_WS2812 + 72]) == 255 * 3 and regs[REG_UPDATE] == 1, - "WS2812 regs contain incorrect value!") -print("OK!") - -print("\nTesting set_all...") -pt.set_all(255, 255, 255) - -i2c_assert(lambda: pt.show(), - lambda: sum(regs[REG_WS2812:REG_WS2812 + 72]) == 255 * 3 * 24 and regs[REG_UPDATE] == 1, - "WS2812 regs contain incorrect value!") -print("OK!") - -print("\nChecking brightness ignored in WS2812 mode...") -expected = 255 -pt.brightness(222) -assert regs[REG_WS2812] == 255, "Brightness has affected WS2812 mode. REG_WS2812 is {} should be {}".format(regs[REG_WS2812], expected) -print("OK!") - -print("\nChanging light mode...") -expected = 0b00000100 -pt.light_mode(pantilthat.PWM) -assert regs[REG_CONFIG] == expected, "Failed to change light mode. REG_CONFIG is {0:08b} should be {1:08b}".format(regs[REG_CONFIG], expected) # The servos were disabled above -print("OK!") - -print("\nChanging brightness...") -expected = 123 -pt.brightness(expected) -assert regs[REG_WS2812] == expected, "Failed to change rightness. REG_WS2812 is {} should be {}".format(regs[REG_WS2812], expected) -print("OK!") - -print("\nWell done, you've not broken anything!") # I'll never forgive myself :D diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..4f3a09e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +import sys + +import mock +import pytest + +REG_CONFIG = 0x00 +REG_SERVO1 = 0x01 +REG_SERVO2 = 0x03 +REG_WS2812 = 0x05 +REG_UPDATE = 0x4e + + +class SMBus: + def __init__(self, bus_id): + self.regs = [0 for _ in range(79)] + + self.regs[REG_CONFIG] = 0 + self.regs[REG_SERVO1] = 0 + self.regs[REG_SERVO1 + 1] = 0 + self.regs[REG_SERVO2] = 0 + self.regs[REG_SERVO2 + 1] = 0 + self.regs[REG_WS2812] = 0 + self.regs[REG_UPDATE] = 0 + + def write_i2c_block_data(self, addr, reg, data): + for index, value in enumerate(data): + self.regs[reg + index] = value + + def write_word_data(self, addr, reg, data): + self.regs[reg] = (data >> 8) & 0xff + self.regs[reg + 1] = data & 0xff + + def write_byte_data(self, addr, reg, data): + self.regs[reg] = data & 0xff + + def read_byte_data(self, addr, reg): + return self.regs[reg] + + def read_word_data(self, addr, reg): + return (self.regs[reg] << 8) | self.regs[reg + 1] + + +@pytest.fixture(scope="function", autouse=False) +def smbus2_mock(): + smbus = mock.Mock() + smbus.SMBus = SMBus + sys.modules["smbus2"] = smbus + yield smbus + del sys.modules["smbus2"] + + +@pytest.fixture(scope="function", autouse=False) +def pantilthat(): + import pantilthat + yield pantilthat + del sys.modules["pantilthat"] diff --git a/tests/test_features.py b/tests/test_features.py new file mode 100644 index 0000000..b5393d5 --- /dev/null +++ b/tests/test_features.py @@ -0,0 +1,108 @@ +import pytest +from conftest import REG_SERVO1, REG_SERVO2, REG_UPDATE, REG_WS2812 + + +def test_servos(smbus2_mock, pantilthat): + pantilthat.setup() + + regs = pantilthat.pantilthat._i2c.regs + + pantilthat.servo_enable(1, True) + pantilthat.servo_enable(2, True) + + pantilthat.servo_pulse_min(1, 510) + pantilthat.servo_pulse_max(1, 2300) + + pantilthat.servo_pulse_min(2, 510) + pantilthat.servo_pulse_max(2, 2300) + + pantilthat.servo_one(0) + pantilthat.servo_two(0) + + assert regs[REG_SERVO1] == 5 + assert regs[REG_SERVO1 + 1] == 125 + + assert regs[REG_SERVO2] == 5 + assert regs[REG_SERVO2 + 1] == 125 + + +def test_servo_readback(smbus2_mock, pantilthat): + pantilthat.setup() + + for x in range(-90, 91): + pantilthat.pan(x) + pantilthat.tilt(x) + assert pantilthat.get_pan() == x, f"get_pan() should return {x}, returned {pantilthat.get_pan()}" + assert pantilthat.get_tilt() == x, f"get_tilt() should return {x}, returned {pantilthat.get_tilt()}" + + +def test_servo_full_sweep(smbus2_mock, pantilthat): + pantilthat.setup() + + for x in range(-90, 91): + pantilthat.pan(x) + pantilthat.tilt(x) + + for x in reversed(range(-90, 91)): + pantilthat.pan(x) + pantilthat.tilt(x) + + +def test_set_pixel(smbus2_mock, pantilthat): + pantilthat.setup() + + regs = pantilthat.pantilthat._i2c.regs + + pantilthat.set_pixel(0, 255, 255, 255) + pantilthat.show() + + assert sum(regs[REG_WS2812:REG_WS2812 + 72]) == 255 * 3 + assert regs[REG_UPDATE] == 1 + + pantilthat.set_all(255, 255, 255) + pantilthat.show() + + assert sum(regs[REG_WS2812:REG_WS2812 + 72]) == 255 * 3 * 24 + assert regs[REG_UPDATE] == 1 + + +def test_servo_args(smbus2_mock, pantilthat): + pantilthat.setup() + + # Try to enable a mythical third servo + with pytest.raises(ValueError): + pantilthat.servo_enable(3, True) + + with pytest.raises(ValueError): + pantilthat.servo_enable(1, "banana") + + with pytest.raises(ValueError): + pantilthat.servo_pulse_min(3, 510) + + with pytest.raises(ValueError): + pantilthat.servo_pulse_max(3, 510) + + +def test_light_args(smbus2_mock, pantilthat): + pantilthat.setup() + + # Try an out of range pixel + with pytest.raises(ValueError): + pantilthat.set_pixel(34, 255, 255, 255) + + # Try an out of range colour value + with pytest.raises(ValueError): + pantilthat.set_pixel(0, 256, 0, 0) + + +def test_brightness(smbus2_mock, pantilthat): + pantilthat.setup() + + regs = pantilthat.pantilthat._i2c.regs + + pantilthat.brightness(222) + assert regs[REG_WS2812] != 222 + + pantilthat.light_mode(pantilthat.PWM) + pantilthat.brightness(123) + assert regs[REG_WS2812] == 123 \ No newline at end of file diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..1decfd9 --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,22 @@ +from conftest import REG_CONFIG + + +def test_setup(smbus2_mock, pantilthat): + pantilthat.setup() + + +def test_consts(smbus2_mock, pantilthat): + assert pantilthat.WS2812 == 1, "pantilthat.WS2812 should equal 1" + assert pantilthat.PWM == 0, "pantilthat.PWM should equal 0" + assert pantilthat.RGB == 0, "pantilthat.RGB should equal 0" + assert pantilthat.GRB == 1, "pantilthat.GRB should equal 1" + assert pantilthat.RGBW == 2, "pantilthat.RGBW should equal 2" + assert pantilthat.GRBW == 3, "pantilthat.GRBW should equal 3" + + +def test_default_config(smbus2_mock, pantilthat): + pantilthat.setup() + + regs = pantilthat.pantilthat._i2c.regs + + assert regs[REG_CONFIG] == 0b00001100, f"Config reg incorrect!: {regs[REG_CONFIG]}" \ No newline at end of file diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py new file mode 100644 index 0000000..4310f11 --- /dev/null +++ b/tests/test_wrapper.py @@ -0,0 +1,14 @@ +def test_wrapped_functions(smbus2_mock, pantilthat): + for method in ["idle_timeout", "servo_enable", "servo_pulse_max", "servo_pulse_min", + "brightness", "clear", "light_mode", "light_type", "set_all", + "set_pixel", "set_pixel_rgbw", "show", + "servo_one", "pan", "get_pan", "get_servo_one", + "servo_two", "tilt", "get_tilt", "get_servo_two"]: + assert hasattr(pantilthat, method), "Method {method}() should exist!".format(method=method) + assert callable(getattr(pantilthat, method)), "Method {method}() should be callable!".format(method=method) + +def test_function_alises(smbus2_mock, pantilthat): + assert pantilthat.pan == pantilthat.servo_one, "Method 'pan' should alias 'servo_one'" + assert pantilthat.tilt == pantilthat.servo_two, "Method 'tilt' should alias 'servo_two'" + assert pantilthat.get_pan == pantilthat.get_servo_one, "Method 'get_pan' should alias 'get_servo_one'" + assert pantilthat.get_tilt == pantilthat.get_servo_two, "Method 'get_tilt' should alias 'get_servo_two'" From 3208af005b8b185da1867acb3b52ace93e81d0c0 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 14:43:04 +0000 Subject: [PATCH 09/13] Remove MANIFEST.in --- MANIFEST.in | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 61cfb1e..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include CHANGELOG.txt -include LICENSE.txt -include README.txt -include setup.py -include pantilthat/*.py From 59f6aeb88b24c3c1b4d54750d9ef26eee576d61c Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 14:43:31 +0000 Subject: [PATCH 10/13] QA: Add workflows. --- .github/workflows/build.yml | 42 ++++++++++++++++++++++++++++++++++ .github/workflows/install.yml | 40 ++++++++++++++++++++++++++++++++ .github/workflows/qa.yml | 39 +++++++++++++++++++++++++++++++ .github/workflows/test.yml | 43 +++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/install.yml create mode 100644 .github/workflows/qa.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5365571 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,42 @@ +name: Build + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Build (Python ${{ matrix.python }}) + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + env: + TERM: xterm-256color + RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install Dependencies + run: | + make dev-deps + + - name: Build Packages + run: | + make build + + - name: Upload Packages + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FILE }} + path: dist/ diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 0000000..f3c1a2d --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,40 @@ +name: Install Test + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Install (Python ${{ matrix.python }}) + runs-on: ubuntu-latest + env: + TERM: xterm-256color + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Stub files & Patch install.sh + run: | + mkdir -p boot/firmware + touch boot/firmware/config.txt + sed -i "s|/boot/firmware|`pwd`/boot/firmware|g" install.sh + sed -i "s|sudo raspi-config|raspi-config|g" pyproject.toml + touch raspi-config + chmod +x raspi-config + echo `pwd` >> $GITHUB_PATH + + - name: Run install.sh + run: | + ./install.sh --unstable --force diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..2e166c0 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,39 @@ +name: QA + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Linting & Spelling + runs-on: ubuntu-latest + env: + TERM: xterm-256color + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python '3,11' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + make dev-deps + + - name: Run Quality Assurance + run: | + make qa + + - name: Run Code Checks + run: | + make check + + - name: Run Bash Code Checks + run: | + make shellcheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9e29cb9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Test (Python ${{ matrix.python }}) + runs-on: ubuntu-latest + env: + TERM: xterm-256color + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install Dependencies + run: | + make dev-deps + + - name: Run Tests + run: | + make pytest + + - name: Coverage + if: ${{ matrix.python == '3.9' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python -m pip install coveralls + coveralls --service=github + From 9fdc7ffcc7e3c2fee77096d989c4c2c43f2d08d2 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 15:15:49 +0000 Subject: [PATCH 11/13] Add smbus2 dependency. --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9314227..009cf96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,9 @@ classifiers = [ "Topic :: Software Development :: Libraries", "Topic :: System :: Hardware", ] -dependencies = [] +dependencies = [ + "smbus2" +] [tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] example-depends = ["requirements-examples.txt"] From 13010a7e6574bd41f05aedc72555d0b369e46f5b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 15:16:06 +0000 Subject: [PATCH 12/13] Add release notes for v1.0.0. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c6c34..bc7a213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +1.0.0 +----- + +* Repackage to Pi 5 / Bookworm compatibility +* Switch from smbus to smbus2 + 0.0.6 ----- From b24f2c1126dfc56e40c79a8008b581c89433d908 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 17 Mar 2025 15:29:41 +0000 Subject: [PATCH 13/13] install: Set up i2c. --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 009cf96..02ce128 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -116,4 +116,7 @@ ignore = [ [tool.pimoroni] apt_packages = [] configtxt = [] -commands = [] +commands = [ + "printf \"Setting up i2c...\n\"", + "sudo raspi-config nonint do_i2c 0" +]