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 + 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 89% rename from library/CHANGELOG.txt rename to CHANGELOG.md index 61c6c34..bc7a213 100644 --- a/library/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +1.0.0 +----- + +* Repackage to Pi 5 / Bookworm compatibility +* Switch from smbus to smbus2 + 0.0.6 ----- 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/Makefile b/Makefile new file mode 100644 index 0000000..c08fa86 --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +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: +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/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 99% rename from neopixel-blinkt/blinkt.py rename to contrib/neopixel-blinkt/blinkt.py index 483102b..6097a18 100644 --- a/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/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 diff --git a/examples/neopixel/grbw.py b/examples/neopixel/grbw.py index 23f7cfa..7f2ee41 100755 --- a/examples/neopixel/grbw.py +++ b/examples/neopixel/grbw.py @@ -1,12 +1,10 @@ #!/usr/bin/env python -import colorsys import math import time 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 9d819f3..732b656 100755 --- a/examples/pantiltweb/pantiltweb.py +++ b/examples/pantiltweb/pantiltweb.py @@ -1,12 +1,13 @@ #!/usr/bin/env python -import pantilthat from sys import exit +import pantilthat + 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 +24,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/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..aa95ae9 100755 --- a/examples/smooth.py +++ b/examples/smooth.py @@ -5,14 +5,13 @@ import pantilthat - while True: # Get the time in seconds t = time.time() # 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/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/MANIFEST.in b/library/MANIFEST.in deleted file mode 100644 index 61cfb1e..0000000 --- a/library/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include CHANGELOG.txt -include LICENSE.txt -include README.txt -include setup.py -include pantilthat/*.py 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/test.py b/library/test.py deleted file mode 100644 index 2804268..0000000 --- a/library/test.py +++ /dev/null @@ -1,286 +0,0 @@ -import sys -import time -import mock - - -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("Writing {data} to {name}: {result}".format(data=data, addr=addr, reg=reg, name=name, result=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) - - - -import pantilthat -import sys -import atexit -import threading - -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 - -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, "Config reg incorrect!: {}".format(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/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/library/pantilthat/__init__.py b/pantilthat/__init__.py similarity index 87% rename from library/pantilthat/__init__.py rename to pantilthat/__init__.py index 720ab59..ad9058a 100644 --- a/library/pantilthat/__init__.py +++ b/pantilthat/__init__.py @@ -1,6 +1,6 @@ -from .pantilt import PanTilt, WS2812, PWM, RGB, GRB, RGBW, GRBW +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/library/pantilthat/pantilt.py b/pantilthat/pantilt.py similarity index 94% rename from library/pantilthat/pantilt.py rename to pantilthat/pantilt.py index 5cb25bd..78d18ef 100644 --- a/library/pantilthat/pantilt.py +++ b/pantilthat/pantilt.py @@ -1,8 +1,8 @@ -from threading import Timer -import time import atexit -from sys import version_info +import time +from threading import Timer +from smbus2 import SMBus PWM = 0 WS2812 = 1 @@ -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..02ce128 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,122 @@ +[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 = [ + "smbus2" +] + +[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 = [ + "printf \"Setting up i2c...\n\"", + "sudo raspi-config nonint do_i2c 0" +] 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/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 5ed0316..0000000 Binary files a/sphinx/favicon.png and /dev/null differ 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 8fd0cda..0000000 Binary files a/sphinx/shop-logo.png and /dev/null differ diff --git a/terminal.jpg b/terminal.jpg deleted file mode 100644 index f964199..0000000 Binary files a/terminal.jpg and /dev/null differ 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'" 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"