diff --git a/library/.coveragerc b/.coveragerc similarity index 100% rename from library/.coveragerc rename to .coveragerc diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..07620e3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + env: + 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/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..ac672a5 --- /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 index 213fdea..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,37 +1,41 @@ -name: Python Tests +name: Tests on: pull_request: push: branches: - - master + - main jobs: test: + name: Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.5, 3.7, 3.9] + python: ['3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - name: Checkout Code + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + - name: Install Dependencies run: | - python -m pip install --upgrade setuptools tox + make dev-deps + - name: Run Tests - working-directory: library run: | - tox -e py + make pytest + - name: Coverage + if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: library run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.9' }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7e3c19a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +1.0.0 +----- + +* Port to gpiod/gpiodevice +* Repackage to hatch/pyproject.toml +* BREAKING: spi_cs_gpio will not auto-detect SPI CS line, use spi_cs=(0, 1) +* BREAKING: Constants `BG_CS_FRONT_BCM` and `BG_CS_BACK_BCM` are now CS lines, not pins + +0.1.0 +----- + +* Add init support for PAA5100JE +* Add frame capture support + +0.0.1 +----- + +* Initial Release 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 index 751430c..56cf0df 100644 --- a/Makefile +++ b/Makefile @@ -1,70 +1,66 @@ -LIBRARY_VERSION=$(shell cat library/setup.py | grep version | awk -F"'" '{print $$2}') -LIBRARY_NAME=$(shell cat library/setup.py | grep name | awk -F"'" '{print $$2}') +LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) +LIBRARY_VERSION := $(shell hatch version 2> /dev/null) -.PHONY: usage install uninstall +.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 "check: peform basic integrity checks on the codebase" - @echo "python-readme: generate library/README.md from README.md and CHANGELOG" - @echo "python-wheels: build python .whl files for distribution" - @echo "python-sdist: build python source distribution" - @echo "python-clean: clean python build and dist directories" - @echo "python-dist: build all python distribution files" - @echo "python-testdeploy: build all and deploy to test PyPi" - @echo "tag: tag the repository with the current version" + @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 + ./install.sh --unstable uninstall: ./uninstall.sh -check: - @echo "Checking for trailing whitespace" - @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO - @echo "Checking for DOS line-endings" - @! grep -IUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile - @echo "Checking library/CHANGELOG.txt" - @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} - @echo "Checking library/${LIBRARY_NAME}/__init__.py" - @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" - -tag: - git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix shellcheck -python-readme: library/README.md +check: + @bash check.sh -python-license: library/LICENSE.txt +shellcheck: + shellcheck *.sh -library/README.md: README.md library/CHANGELOG.txt - cp README.md library/README.md - printf "\n# Changelog\n" >> library/README.md - cat library/CHANGELOG.txt >> library/README.md +qa: + tox -e qa -library/LICENSE.txt: LICENSE - cp LICENSE library/LICENSE.txt +pytest: + tox -e py -python-wheels: python-readme python-license - cd library; python3 setup.py bdist_wheel - cd library; python setup.py bdist_wheel +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python setup.py sdist +tag: version + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-clean: - -rm -r library/dist - -rm -r library/build - -rm -r library/*.egg-info +build: check + @hatch build -python-dist: python-clean python-wheels python-sdist - ls library/dist +clean: + -rm -r dist -python-testdeploy: python-dist - twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* +testdeploy: build + twine upload --repository testpypi dist/* -python-deploy: check python-dist - twine upload library/dist/* +deploy: nopost build + twine upload dist/* diff --git a/README.md b/README.md index ccf0542..30d6444 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PMW3901 / PAA5100JE 2-Dimensional Optical Flow Sensor -[![Build Status](https://travis-ci.com/pimoroni/pmw3901-python.svg?branch=master)](https://travis-ci.com/pimoroni/pmw3901-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/pmw3901-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/pmw3901-python?branch=master) +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/pmw3901-python/test.yml?branch=main)](https://github.com/pimoroni/pmw3901-python/actions/workflows/test.yml) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/pmw3901-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/pmw3901-python?branch=main) [![PyPi Package](https://img.shields.io/pypi/v/pmw3901.svg)](https://pypi.python.org/pypi/pmw3901) [![Python Versions](https://img.shields.io/pypi/pyversions/pmw3901.svg)](https://pypi.python.org/pypi/pmw3901) @@ -10,13 +10,13 @@ Stable library from PyPi: -* Just run `sudo pip install pmw3901` +* Just run `python3 -m pip install pmw3901` Latest/development library from GitHub: * `git clone https://github.com/pimoroni/pmw3901-python` * `cd pmw3901-python` -* `sudo ./install.sh` +* `./install.sh` # Usage 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/frame_capture.py b/examples/frame_capture.py index 9f5dc5a..48d0429 100755 --- a/examples/frame_capture.py +++ b/examples/frame_capture.py @@ -1,7 +1,8 @@ #!/usr/bin/env python -import time import argparse -from pmw3901 import PMW3901, PAA5100, BG_CS_FRONT_BCM, BG_CS_BACK_BCM +import time + +from pmw3901 import BG_CS_BACK_BCM, BG_CS_FRONT_BCM, PAA5100, PMW3901 print("""frame_capture.py - Capture the raw frame data from the PMW3901 @@ -9,23 +10,16 @@ """) parser = argparse.ArgumentParser() -parser.add_argument('--board', type=str, - choices=['pmw3901', 'paa5100'], - required=True, - help='Breakout type.') -parser.add_argument('--rotation', type=int, - default=0, choices=[0, 90, 180, 270], - help='Rotation of sensor in degrees.') -parser.add_argument('--spi-slot', type=str, - default='front', choices=['front', 'back'], - help='Breakout Garden SPI slot.') +parser.add_argument("--board", type=str, choices=["pmw3901", "paa5100"], required=True, help="Breakout type.") +parser.add_argument("--rotation", type=int, default=0, choices=[0, 90, 180, 270], help="Rotation of sensor in degrees.") +parser.add_argument("--spi-slot", type=str, default="front", choices=["front", "back"], help="Breakout Garden SPI slot.") args = parser.parse_args() # Pick the right class for the specified breakout -SensorClass = PMW3901 if args.board == 'pmw3901' else PAA5100 +SensorClass = PMW3901 if args.board == "pmw3901" else PAA5100 -flo = SensorClass(spi_port=0, spi_cs=1, spi_cs_gpio=BG_CS_FRONT_BCM if args.spi_slot == 'front' else BG_CS_BACK_BCM) +flo = SensorClass(spi_port=0, spi_cs=BG_CS_FRONT_BCM if args.spi_slot == "front" else BG_CS_BACK_BCM) flo.set_rotation(args.rotation) diff --git a/examples/motion.py b/examples/motion.py index d5ab31c..69cd711 100755 --- a/examples/motion.py +++ b/examples/motion.py @@ -1,7 +1,8 @@ #!/usr/bin/env python -import time import argparse -from pmw3901 import PMW3901, PAA5100, BG_CS_FRONT_BCM, BG_CS_BACK_BCM +import time + +from pmw3901 import BG_CS_BACK_BCM, BG_CS_FRONT_BCM, PAA5100, PMW3901 print("""motion.py - Detect flow/motion in front of the PMW3901 sensor. @@ -9,23 +10,16 @@ """) parser = argparse.ArgumentParser() -parser.add_argument('--board', type=str, - choices=['pmw3901', 'paa5100'], - required=True, - help='Breakout type.') -parser.add_argument('--rotation', type=int, - default=0, choices=[0, 90, 180, 270], - help='Rotation of sensor in degrees.') -parser.add_argument('--spi-slot', type=str, - default='front', choices=['front', 'back'], - help='Breakout Garden SPI slot.') +parser.add_argument("--board", type=str, choices=["pmw3901", "paa5100"], required=True, help="Breakout type.") +parser.add_argument("--rotation", type=int, default=0, choices=[0, 90, 180, 270], help="Rotation of sensor in degrees.") +parser.add_argument("--spi-slot", type=str, default="front", choices=["front", "back"], help="Breakout Garden SPI slot.") args = parser.parse_args() # Pick the right class for the specified breakout -SensorClass = PMW3901 if args.board == 'pmw3901' else PAA5100 +SensorClass = PMW3901 if args.board == "pmw3901" else PAA5100 -flo = SensorClass(spi_port=0, spi_cs_gpio=BG_CS_FRONT_BCM if args.spi_slot == 'front' else BG_CS_BACK_BCM) +flo = SensorClass(spi_port=0, spi_cs=BG_CS_FRONT_BCM if args.spi_slot == "front" else BG_CS_BACK_BCM) flo.set_rotation(args.rotation) tx = 0 @@ -39,7 +33,7 @@ continue tx += x ty += y - print("Relative: x {:03d} y {:03d} | Absolute: x {:03d} y {:03d}".format(x, y, tx, ty)) + print(f"Relative: x {x:03d} y {y:03d} | Absolute: x {tx:03d} y {ty:03d}") time.sleep(0.01) except KeyboardInterrupt: pass diff --git a/install.sh b/install.sh index 1205c0c..3db90bc 100755 --- a/install.sh +++ b/install.sh @@ -1,25 +1,370 @@ #!/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 -LIBRARY_VERSION=`cat library/setup.py | grep version | awk -F"'" '{print $2}'` -LIBRARY_NAME=`cat library/setup.py | grep name | awk -F"'" '{print $2}'` -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" +user_check() { + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" + fi +} -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./install.sh'\n" +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 +} + +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" -printf "Installing for Python 2..\n" -python setup.py install +# 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 -if [ -f "/usr/bin/python3" ]; then - printf "Installing for Python 3..\n" - python3 setup.py install +# 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 -cd .. +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" -printf "Done!\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" + +# 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/CHANGELOG.txt b/library/CHANGELOG.txt deleted file mode 100644 index c80b797..0000000 --- a/library/CHANGELOG.txt +++ /dev/null @@ -1,10 +0,0 @@ -0.1.0 ------ - -* Add init support for PAA5100JE -* Add frame capture support - -0.0.1 ------ - -* Initial Release diff --git a/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index aed751a..0000000 --- a/library/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 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.md b/library/README.md deleted file mode 100644 index d10115c..0000000 --- a/library/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# PMW3901 / PAA5100JE 2-Dimensional Optical Flow Sensor - -[![Build Status](https://travis-ci.com/pimoroni/pmw3901-python.svg?branch=master)](https://travis-ci.com/pimoroni/pmw3901-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/pmw3901-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/pmw3901-python?branch=master) -[![PyPi Package](https://img.shields.io/pypi/v/pmw3901.svg)](https://pypi.python.org/pypi/pmw3901) -[![Python Versions](https://img.shields.io/pypi/pyversions/pmw3901.svg)](https://pypi.python.org/pypi/pmw3901) - - -# Installing - -Stable library from PyPi: - -* Just run `sudo pip install pmw3901` - -Latest/development library from GitHub: - -* `git clone https://github.com/pimoroni/pmw3901-python` -* `cd pmw3901-python` -* `sudo ./install.sh` - -# Usage - -The PAA5100JE has a slightly different init routine to the PMW3901, you should use the class provided to ensure it's set up correctly: - -``` -from pmw3901 import PAA5100 -``` - -And for the PMW3901, continue using the old class: - -``` -from pmw3901 import PMW3901 -``` - -The example `motion.py` demonstrates setting up either sensor, and accepts a `--board` argument to specify which you'd like to use. - -# Changelog -0.1.0 ------ - -* Add init support for PAA5100JE -* Add frame capture support - -0.0.1 ------ - -* Initial Release diff --git a/library/README.rst b/library/README.rst deleted file mode 100644 index 3c84461..0000000 --- a/library/README.rst +++ /dev/null @@ -1,26 +0,0 @@ -PMW3901 2-Dimensional Optical Flow Sensor -========================================= - -|Build Status| |Coverage Status| |PyPi Package| |Python Versions| - -Installing -========== - -Stable library from PyPi: - -- Just run ``sudo pip install pmw3901`` - -Latest/development library from GitHub: - -- ``git clone https://github.com/pimoroni/pmw3901-python`` -- ``cd pmw3901-python`` -- ``sudo ./install.sh`` - -.. |Build Status| image:: https://travis-ci.com/pimoroni/pmw3901-python.svg?branch=master - :target: https://travis-ci.com/pimoroni/pmw3901-python -.. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/pmw3901-python/badge.svg?branch=master - :target: https://coveralls.io/github/pimoroni/pmw3901-python?branch=master -.. |PyPi Package| image:: https://img.shields.io/pypi/v/pmw3901.svg - :target: https://pypi.python.org/pypi/pmw3901 -.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/pmw3901.svg - :target: https://pypi.python.org/pypi/pmw3901 diff --git a/library/setup.cfg b/library/setup.cfg deleted file mode 100644 index 5c3c3ea..0000000 --- a/library/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -exclude = - test.py - .tox, - .eggs, - .git, - __pycache__, - build, - dist -ignore = - E501 diff --git a/library/setup.py b/library/setup.py deleted file mode 100755 index 9d1c703..0000000 --- a/library/setup.py +++ /dev/null @@ -1,55 +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 :: 4 - Beta', - '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='pmw3901', - version='0.1.0', - author='Philip Howard', - author_email='phil@pimoroni.com', - description="""Python library for the PMW3901 optical flow sensor""", - long_description=open('README.md').read(), - long_description_content_type="text/markdown", - license='MIT', - keywords='Raspberry Pi', - url='http://www.pimoroni.com', - project_urls={'GitHub': 'https://www.github.com/pimoroni/pmw3901-python'}, - classifiers=classifiers, - packages=['pmw3901'], - install_requires=['spidev'] -) diff --git a/library/tests/conftest.py b/library/tests/conftest.py deleted file mode 100644 index a6b5816..0000000 --- a/library/tests/conftest.py +++ /dev/null @@ -1,53 +0,0 @@ -import sys -import mock -import pytest - - -class SPIDevFakeDevice(): - def __init__(self): - self.regs = [0 for _ in range(512)] - self.regs[0x00] = 0x49 # Fake part ID - self.regs[0x01] = 0x00 # Fake revision ID - - def xfer2(self, data): - self.ptr = data[0] - return [self.regs[self.ptr - 1 + x] for x in range(len(data))] - - -@pytest.fixture(scope='function', autouse=False) -def PMW3901(): - from pmw3901 import PMW3901 - yield PMW3901 - del sys.modules['pmw3901'] - - -@pytest.fixture(scope='function', autouse=False) -def PAA5100(): - from pmw3901 import PAA5100 - yield PAA5100 - del sys.modules['pmw3901'] - - -@pytest.fixture(scope='function', autouse=False) -def spidev(): - """Mock spidev module.""" - fakedev = SPIDevFakeDevice() - spidev = mock.MagicMock() - spidev.SpiDev().xfer2.side_effect = fakedev.xfer2 - spidev._fakedev = fakedev - sys.modules['spidev'] = spidev - yield spidev - del sys.modules['spidev'] - - -@pytest.fixture(scope='function', autouse=False) -def GPIO(): - """Mock RPi.GPIO module.""" - GPIO = mock.MagicMock() - # Fudge for Python < 37 (possibly earlier) - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi'].GPIO = GPIO - sys.modules['RPi.GPIO'] = GPIO - yield GPIO - del sys.modules['RPi'] - del sys.modules['RPi.GPIO'] diff --git a/library/tests/test_paa5100.py b/library/tests/test_paa5100.py deleted file mode 100644 index 62e105c..0000000 --- a/library/tests/test_paa5100.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - - -def test_setup(GPIO, spidev, PAA5100): - paa5100 = PAA5100() - - GPIO.setwarnings.assert_called_with(False) - GPIO.setmode.assert_called_with(GPIO.BCM) - GPIO.setup.assert_called_with(paa5100.spi_cs_gpio, GPIO.OUT) - - -def test_setup_invalid_gpio(GPIO, spidev, PAA5100): - paa5100 = PAA5100(spi_cs_gpio=20) - - assert spidev.SpiDev.open.called_with(0, 0) - - del paa5100 - - -def test_setup_bg_front(GPIO, spidev, PAA5100): - PAA5100(spi_cs_gpio=7) - assert spidev.SpiDev.open.called_with(0, 1) - - -def test_setup_bg_back(GPIO, spidev, PAA5100): - PAA5100(spi_cs_gpio=8) - assert spidev.SpiDev.open.called_with(0, 0) - - -def test_setup_not_present(GPIO, spidev, PAA5100): - spidev._fakedev.regs[0x00] = 0x00 - - with pytest.raises(RuntimeError): - PAA5100() diff --git a/library/tests/test_pmw3901.py b/library/tests/test_pmw3901.py deleted file mode 100644 index 3bab3bc..0000000 --- a/library/tests/test_pmw3901.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - - -def test_setup(GPIO, spidev, PMW3901): - pmw3901 = PMW3901() - - GPIO.setwarnings.assert_called_with(False) - GPIO.setmode.assert_called_with(GPIO.BCM) - GPIO.setup.assert_called_with(pmw3901.spi_cs_gpio, GPIO.OUT) - - -def test_setup_invalid_gpio(GPIO, spidev, PMW3901): - pmw3901 = PMW3901(spi_cs_gpio=20) - - assert spidev.SpiDev.open.called_with(0, 0) - - del pmw3901 - - -def test_setup_bg_front(GPIO, spidev, PMW3901): - PMW3901(spi_cs_gpio=7) - assert spidev.SpiDev.open.called_with(0, 1) - - -def test_setup_bg_back(GPIO, spidev, PMW3901): - PMW3901(spi_cs_gpio=8) - assert spidev.SpiDev.open.called_with(0, 0) - - -def test_setup_not_present(GPIO, spidev, PMW3901): - spidev._fakedev.regs[0x00] = 0x00 - - with pytest.raises(RuntimeError): - PMW3901() diff --git a/library/tox.ini b/library/tox.ini deleted file mode 100644 index a8f3249..0000000 --- a/library/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = py{27,35,37,39},qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py install - coverage run -m py.test -v -r wsx - coverage report -deps = - mock - pytest>=3.1 - pytest-cov - -[testenv:qa] -commands = - check-manifest --ignore tox.ini,tests/*,.coveragerc - python setup.py sdist bdist_wheel - twine check dist/* - flake8 --ignore E501 -deps = - check-manifest - flake8 - twine diff --git a/library/pmw3901/__init__.py b/pmw3901/__init__.py similarity index 64% rename from library/pmw3901/__init__.py rename to pmw3901/__init__.py index 8fbfe81..3962695 100644 --- a/library/pmw3901/__init__.py +++ b/pmw3901/__init__.py @@ -1,47 +1,58 @@ -import time import struct +import time + +import gpiod +import gpiodevice import spidev -import RPi.GPIO as GPIO +from gpiod.line import Direction, Value -__version__ = '0.1.0' +__version__ = "1.0.0" WAIT = -1 -BG_CS_FRONT_BCM = 7 -BG_CS_BACK_BCM = 8 +BG_CS_FRONT_BCM = 1 # GPIO 8 +BG_CS_BACK_BCM = 0 # GPIO 7 REG_ID = 0x00 REG_DATA_READY = 0x02 REG_MOTION_BURST = 0x16 -REG_POWER_UP_RESET = 0x3a -REG_ORIENTATION = 0x5b -REG_RESOLUTION = 0x4e # PAA5100 only +REG_POWER_UP_RESET = 0x3A +REG_ORIENTATION = 0x5B +REG_RESOLUTION = 0x4E # PAA5100 only REG_RAWDATA_GRAB = 0x58 REG_RAWDATA_GRAB_STATUS = 0x59 +OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE) + + +class PMW3901: + _device_name = "PMW3901" -class PMW3901(): - def __init__(self, spi_port=0, spi_cs_gpio=BG_CS_FRONT_BCM): - self.spi_cs_gpio = spi_cs_gpio + def __init__(self, spi_port=0, spi_cs=1, spi_cs_gpio=None): self.spi_dev = spidev.SpiDev() - try: - spi_cs = [8, 7].index(spi_cs_gpio) - except ValueError: + self._spi_cs_gpio = None + + if spi_cs_gpio is not None: spi_cs = 0 + self._spi_cs_gpio = gpiodevice.get_pin(spi_cs_gpio, f"{self._device_name}_cs", OUTL) + self.spi_dev.open(spi_port, spi_cs) self.spi_dev.max_speed_hz = 400000 - self.spi_dev.no_cs = True - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) - GPIO.setup(self.spi_cs_gpio, GPIO.OUT) + if spi_cs_gpio is not None: + try: + # TODO: Not sure this does anything but break with an OSError? + self.spi_dev.no_cs = True + except OSError: + pass - GPIO.output(self.spi_cs_gpio, 0) - time.sleep(0.05) - GPIO.output(self.spi_cs_gpio, 1) + if self._spi_cs_gpio: + self.set_pin(self._spi_cs_gpio, 0) + time.sleep(0.05) + self.set_pin(self._spi_cs_gpio, 1) - self._write(REG_POWER_UP_RESET, 0x5a) + self._write(REG_POWER_UP_RESET, 0x5A) time.sleep(0.02) for offset in range(5): self._read(REG_DATA_READY + offset) @@ -50,10 +61,14 @@ def __init__(self, spi_port=0, spi_cs_gpio=BG_CS_FRONT_BCM): product_id, revision = self.get_id() if product_id != 0x49 or revision not in (0x00, 0x01): - raise RuntimeError("Invalid Product ID or Revision for PMW3901: 0x{:02x}/0x{:02x}".format(product_id, revision)) + raise RuntimeError(f"Invalid Product ID or Revision for PMW3901: 0x{product_id:02x}/0x{revision:02x}") # print("Product ID: {}".format(ID.get_product_id())) # print("Revision: {}".format(ID.get_revision_id())) + def set_pin(self, pin, state): + lines, offset = pin + lines.set_value(offset, Value.ACTIVE if state else Value.INACTIVE) + def get_id(self): """Get chip ID and revision from PMW3901.""" return self._read(REG_ID, 2) @@ -108,16 +123,18 @@ def get_motion(self, timeout=5): """ t_start = time.time() while time.time() - t_start < timeout: - GPIO.output(self.spi_cs_gpio, 0) + if self._spi_cs_gpio: + self.set_pin(self._spi_cs_gpio, 0) data = self.spi_dev.xfer2([REG_MOTION_BURST] + [0 for x in range(12)]) - GPIO.output(self.spi_cs_gpio, 1) + if self._spi_cs_gpio: + self.set_pin(self._spi_cs_gpio, 1) (_, dr, obs, x, y, quality, raw_sum, raw_max, raw_min, shutter_upper, shutter_lower) = struct.unpack(" timeout: - raise RuntimeError("Raw data capture timeout, got {} values".format(x)) + raise RuntimeError(f"Raw data capture timeout, got {x} values") return None class PAA5100(PMW3901): + _device_name = "PAA5100" + def _secret_sauce(self): """Write the secret sauce registers for the PAA5100. @@ -376,11 +399,11 @@ def _secret_sauce(self): """ self._bulk_write([ - 0x7f, 0x00, + 0x7F, 0x00, 0x55, 0x01, 0x50, 0x07, - 0x7f, 0x0e, + 0x7F, 0x0E, 0x43, 0x10 ]) if self._read(0x67) & 0b10000000: @@ -388,11 +411,11 @@ def _secret_sauce(self): else: self._write(0x48, 0x02) self._bulk_write([ - 0x7f, 0x00, - 0x51, 0x7b, + 0x7F, 0x00, + 0x51, 0x7B, 0x50, 0x00, 0x55, 0x00, - 0x7f, 0x0e + 0x7F, 0x0E ]) if self._read(0x73) == 0x00: c1 = self._read(0x70) @@ -404,49 +427,49 @@ def _secret_sauce(self): c1 = max(0, min(0x3F, c1)) c2 = (c2 * 45) // 100 self._bulk_write([ - 0x7f, 0x00, - 0x61, 0xad, + 0x7F, 0x00, + 0x61, 0xAD, 0x51, 0x70, - 0x7f, 0x0e + 0x7F, 0x0E ]) self._write(0x70, c1) self._write(0x71, c2) self._bulk_write([ - 0x7f, 0x00, - 0x61, 0xad, + 0x7F, 0x00, + 0x61, 0xAD, - 0x7f, 0x03, + 0x7F, 0x03, 0x40, 0x00, - 0x7f, 0x05, - 0x41, 0xb3, - 0x43, 0xf1, + 0x7F, 0x05, + 0x41, 0xB3, + 0x43, 0xF1, 0x45, 0x14, - 0x5f, 0x34, - 0x7b, 0x08, - 0x5e, 0x34, - 0x5b, 0x11, - 0x6d, 0x11, + 0x5F, 0x34, + 0x7B, 0x08, + 0x5E, 0x34, + 0x5B, 0x11, + 0x6D, 0x11, 0x45, 0x17, - 0x70, 0xe5, - 0x71, 0xe5, + 0x70, 0xE5, + 0x71, 0xE5, - 0x7f, 0x06, - 0x44, 0x1b, - 0x40, 0xbf, - 0x4e, 0x3f, + 0x7F, 0x06, + 0x44, 0x1B, + 0x40, 0xBF, + 0x4E, 0x3F, - 0x7f, 0x08, + 0x7F, 0x08, 0x66, 0x44, 0x65, 0x20, - 0x6a, 0x3a, + 0x6A, 0x3A, 0x61, 0x05, 0x62, 0x05, - 0x7f, 0x09, - 0x4f, 0xaf, - 0x5f, 0x40, + 0x7F, 0x09, + 0x4F, 0xAF, + 0x5F, 0x40, 0x48, 0x80, 0x49, 0x80, 0x57, 0x77, @@ -455,69 +478,69 @@ def _secret_sauce(self): 0x62, 0x08, 0x63, 0x50, - 0x7f, 0x0a, + 0x7F, 0x0A, 0x45, 0x60, - 0x7f, 0x00, - 0x4d, 0x11, + 0x7F, 0x00, + 0x4D, 0x11, 0x55, 0x80, 0x74, 0x21, - 0x75, 0x1f, - 0x4a, 0x78, - 0x4b, 0x78, + 0x75, 0x1F, + 0x4A, 0x78, + 0x4B, 0x78, 0x44, 0x08, 0x45, 0x50, - 0x64, 0xff, - 0x65, 0x1f, + 0x64, 0xFF, + 0x65, 0x1F, - 0x7f, 0x14, + 0x7F, 0x14, 0x65, 0x67, 0x66, 0x08, 0x63, 0x70, - 0x6f, 0x1c, + 0x6F, 0x1C, - 0x7f, 0x15, + 0x7F, 0x15, 0x48, 0x48, - 0x7f, 0x07, - 0x41, 0x0d, + 0x7F, 0x07, + 0x41, 0x0D, 0x43, 0x14, - 0x4b, 0x0e, - 0x45, 0x0f, + 0x4B, 0x0E, + 0x45, 0x0F, 0x44, 0x42, - 0x4c, 0x80, + 0x4C, 0x80, - 0x7f, 0x10, - 0x5b, 0x02, + 0x7F, 0x10, + 0x5B, 0x02, - 0x7f, 0x07, + 0x7F, 0x07, 0x40, 0x41, - WAIT, 0x0a, # Wait 10ms + WAIT, 0x0A, # Wait 10ms - 0x7f, 0x00, + 0x7F, 0x00, 0x32, 0x00, - 0x7f, 0x07, + 0x7F, 0x07, 0x40, 0x40, - 0x7f, 0x06, - 0x68, 0xf0, + 0x7F, 0x06, + 0x68, 0xF0, 0x69, 0x00, - 0x7f, 0x0d, - 0x48, 0xc0, - 0x6f, 0xd5, + 0x7F, 0x0D, + 0x48, 0xC0, + 0x6F, 0xD5, - 0x7f, 0x00, - 0x5b, 0xa0, - 0x4e, 0xa8, - 0x5a, 0x90, + 0x7F, 0x00, + 0x5B, 0xA0, + 0x4E, 0xA8, + 0x5A, 0x90, 0x40, 0x80, - 0x73, 0x1f, + 0x73, 0x1F, - WAIT, 0x0a, # Wait 10ms + WAIT, 0x0A, # Wait 10ms 0x73, 0x00 ]) @@ -526,9 +549,9 @@ def _secret_sauce(self): if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() - parser.add_argument('--rotation', type=int, + parser.add_argument("--rotation", type=int, default=0, choices=[0, 90, 180, 270], - help='Rotation of sensor in degrees.', ) + help="Rotation of sensor in degrees.", ) args = parser.parse_args() flo = PMW3901(spi_port=0, spi_cs_gpio=BG_CS_FRONT_BCM) flo.set_rotation(args.rotation) @@ -542,7 +565,7 @@ def _secret_sauce(self): continue tx += x ty += y - print("Motion: {:03d} {:03d} x: {:03d} y {:03d}".format(x, y, tx, ty)) + print(f"Motion: {x:03d} {y:03d} x: {tx:03d} y {ty:03d}") time.sleep(0.01) except KeyboardInterrupt: pass diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1222826 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,118 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "pmw3901" +dynamic = ["version", "readme"] +description = "Python library for the PMW3901 optical flow sensor" +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 = [ + "spidev" +] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/pmw3901-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "pmw3901/__init__.py" + +[tool.hatch.build] +include = [ + "pmw3901", + "README.md", + "CHANGELOG.md", + "LICENSE" +] + +[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.black] +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..525b042 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +check-manifest +ruff +codespell +isort +twine +hatch +hatch-fancy-pypi-readme +tox +pdoc diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..49f51a5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,63 @@ +import sys + +import mock +import pytest + + +class SPIDevFakeDevice: + def __init__(self): + self.regs = [0 for _ in range(512)] + self.regs[0x00] = 0x49 # Fake part ID + self.regs[0x01] = 0x00 # Fake revision ID + + def xfer2(self, data): + self.ptr = data[0] + return [self.regs[self.ptr - 1 + x] for x in range(len(data))] + + +@pytest.fixture(scope="function", autouse=False) +def PMW3901(): + from pmw3901 import PMW3901 + + yield PMW3901 + del sys.modules["pmw3901"] + + +@pytest.fixture(scope="function", autouse=False) +def PAA5100(): + from pmw3901 import PAA5100 + + yield PAA5100 + del sys.modules["pmw3901"] + + +@pytest.fixture(scope="function", autouse=False) +def spidev(): + """Mock spidev module.""" + fakedev = SPIDevFakeDevice() + spidev = mock.MagicMock() + spidev.SpiDev().xfer2.side_effect = fakedev.xfer2 + spidev._fakedev = fakedev + sys.modules["spidev"] = spidev + yield spidev + del sys.modules["spidev"] + + +@pytest.fixture(scope='function', autouse=False) +def gpiod(): + sys.modules['gpiod'] = mock.Mock() + sys.modules['gpiod.line'] = mock.Mock() + yield sys.modules['gpiod'] + del sys.modules['gpiod.line'] + del sys.modules['gpiod'] + + +@pytest.fixture(scope='function', autouse=False) +def gpiodevice(): + gpiodevice = mock.Mock() + gpiodevice.get_pins_for_platform.return_value = [(mock.Mock(), 0), (mock.Mock(), 0)] + gpiodevice.get_pin.return_value = (mock.Mock(), 0) + + sys.modules['gpiodevice'] = gpiodevice + yield gpiodevice + del sys.modules['gpiodevice'] diff --git a/library/tests/test_features.py b/tests/test_features.py similarity index 85% rename from library/tests/test_features.py rename to tests/test_features.py index b93b70c..c03aab4 100644 --- a/library/tests/test_features.py +++ b/tests/test_features.py @@ -1,15 +1,16 @@ -import pytest import struct +import pytest + -def test_get_motion_timeout(GPIO, spidev, PMW3901): +def test_get_motion_timeout(gpiod, gpiodevice, spidev, PMW3901): pmw3901 = PMW3901() with pytest.raises(RuntimeError): pmw3901.get_motion(timeout=0.1) -def test_get_motion(GPIO, spidev, PMW3901): +def test_get_motion(gpiod, gpiodevice, spidev, PMW3901): pmw3901 = PMW3901() spidev._fakedev.regs[0x15:0x15 + 12] = list( diff --git a/tests/test_paa5100.py b/tests/test_paa5100.py new file mode 100644 index 0000000..3888030 --- /dev/null +++ b/tests/test_paa5100.py @@ -0,0 +1,36 @@ +import pytest + + +def test_setup_cs_gpio(gpiod, gpiodevice, spidev, PAA5100): + paa5100 = PAA5100(spi_cs_gpio=7) + + OUTL = gpiod.LineSettings(direction=gpiod.Direction.OUTPUT, output_value=gpiod.Value.INACTIVE) + + gpiodevice.get_pin.assert_called_with(7, f"{paa5100._device_name}_cs", OUTL) + + +def test_setup_bg_front(gpiod, gpiodevice, spidev, PAA5100): + paa5100 = PAA5100(spi_cs_gpio=7) + + spidev.SpiDev().open.assert_called_with(0, 0) + + OUTL = gpiod.LineSettings(direction=gpiod.Direction.OUTPUT, output_value=gpiod.Value.INACTIVE) + + gpiodevice.get_pin.assert_called_with(7, f"{paa5100._device_name}_cs", OUTL) + + +def test_setup_bg_back(gpiod, gpiodevice, spidev, PAA5100): + paa5100 = PAA5100(spi_cs_gpio=8) + + spidev.SpiDev().open.assert_called_with(0, 0) + + OUTL = gpiod.LineSettings(direction=gpiod.Direction.OUTPUT, output_value=gpiod.Value.INACTIVE) + + gpiodevice.get_pin.assert_called_with(8, f"{paa5100._device_name}_cs", OUTL) + + +def test_setup_not_present(gpiod, gpiodevice, spidev, PAA5100): + spidev._fakedev.regs[0x00] = 0x00 + + with pytest.raises(RuntimeError): + PAA5100() diff --git a/tests/test_pmw3901.py b/tests/test_pmw3901.py new file mode 100644 index 0000000..2673131 --- /dev/null +++ b/tests/test_pmw3901.py @@ -0,0 +1,36 @@ +import pytest + + +def test_setup_cs_gpio(gpiod, gpiodevice, spidev, PMW3901): + pmw3901 = PMW3901(spi_cs_gpio=7) + + OUTL = gpiod.LineSettings(direction=gpiod.Direction.OUTPUT, output_value=gpiod.Value.INACTIVE) + + gpiodevice.get_pin.assert_called_with(7, f"{pmw3901._device_name}_cs", OUTL) + + +def test_setup_bg_front(gpiod, gpiodevice, spidev, PMW3901): + pmw3901 = PMW3901(spi_cs_gpio=7) + + spidev.SpiDev().open.assert_called_with(0, 0) + + OUTL = gpiod.LineSettings(direction=gpiod.Direction.OUTPUT, output_value=gpiod.Value.INACTIVE) + + gpiodevice.get_pin.assert_called_with(7, f"{pmw3901._device_name}_cs", OUTL) + + +def test_setup_bg_back(gpiod, gpiodevice, spidev, PMW3901): + pmw3901 = PMW3901(spi_cs_gpio=8) + + spidev.SpiDev().open.assert_called_with(0, 0) + + OUTL = gpiod.LineSettings(direction=gpiod.Direction.OUTPUT, output_value=gpiod.Value.INACTIVE) + + gpiodevice.get_pin.assert_called_with(8, f"{pmw3901._device_name}_cs", OUTL) + + +def test_setup_not_present(gpiod, gpiodevice, spidev, PMW3901): + spidev._fakedev.regs[0x00] = 0x00 + + with pytest.raises(RuntimeError): + PMW3901() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..44c8654 --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[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 . + codespell . +deps = + check-manifest + ruff + codespell + isort + twine + build + hatch + hatch-fancy-pypi-readme + diff --git a/uninstall.sh b/uninstall.sh index 0709cc0..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,25 +1,72 @@ #!/bin/bash -LIBRARY_VERSION=`cat library/setup.py | grep version | awk -F"'" '{print $2}'` -LIBRARY_NAME=`cat library/setup.py | grep name | awk -F"'" '{print $2}'` +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" -printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" - exit 1 -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" + exit 1 + fi +} -cd library +user_check() { + if [ "$(id -u)" -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" + exit 1 + fi +} -printf "Unnstalling for Python 2..\n" -pip uninstall $LIBRARY_NAME +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 +} -if [ -f "/usr/bin/pip3" ]; then - printf "Uninstalling for Python 3..\n" - pip3 uninstall $LIBRARY_NAME -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)" +} -cd .. +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"