From 11fb4fe5cb8fbfdc309bb637e28574b15dc85569 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 13:22:13 +0000 Subject: [PATCH 01/27] Repackage to hatch/pyproject.toml. --- library/.coveragerc => .coveragerc | 2 +- .github/workflows/build.yml | 41 ++ .github/workflows/qa.yml | 36 ++ .github/workflows/test.yml | 25 +- .stickler.yml | 5 - library/CHANGELOG.txt => CHANGELOG.md | 0 library/MANIFEST.in => MANIFEST.in | 0 Makefile | 85 ++-- README.md | 14 +- check.sh | 88 +++++ {library/inky => inky}/__init__.py | 0 {library/inky => inky}/auto.py | 0 {library/inky => inky}/eeprom.py | 0 {library/inky => inky}/inky.py | 2 + {library/inky => inky}/inky_ac073tc1a.py | 0 {library/inky => inky}/inky_ssd1608.py | 0 {library/inky => inky}/inky_ssd1683.py | 0 {library/inky => inky}/inky_uc8159.py | 0 {library/inky => inky}/mock.py | 0 {library/inky => inky}/phat.py | 0 {library/inky => inky}/ssd1608.py | 0 {library/inky => inky}/ssd1683.py | 0 {library/inky => inky}/what.py | 0 install.sh | 316 ++++++++++++++- library/LICENSE.txt | 21 - library/README.md | 117 ------ library/setup.cfg | 11 - library/setup.py | 61 --- library/tox.ini | 26 -- pyproject.toml | 133 +++++++ requirements-dev.txt | 9 + sphinx/_static/custom.css | 53 --- sphinx/_templates/breadcrumbs.html | 0 sphinx/_templates/layout.html | 43 --- sphinx/conf.py | 365 ------------------ sphinx/favicon.png | Bin 26049 -> 0 bytes sphinx/index.rst | 25 -- sphinx/inky.rst | 5 - sphinx/phat.rst | 8 - sphinx/requirements.txt | 30 -- sphinx/shop-logo.png | Bin 19652 -> 0 bytes sphinx/what.rst | 8 - {library/tests => tests}/conftest.py | 0 {library/tests => tests}/test_auto.py | 0 {library/tests => tests}/test_eeprom.py | 0 {library/tests => tests}/test_init.py | 0 .../tests => tests}/test_install_helpers.py | 0 {library/tests => tests}/test_simulator.py | 0 {library/tests => tests}/tools.py | 0 tox.ini | 34 ++ uninstall.sh | 76 +++- 51 files changed, 769 insertions(+), 870 deletions(-) rename library/.coveragerc => .coveragerc (70%) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/qa.yml delete mode 100644 .stickler.yml rename library/CHANGELOG.txt => CHANGELOG.md (100%) rename library/MANIFEST.in => MANIFEST.in (100%) create mode 100755 check.sh rename {library/inky => inky}/__init__.py (100%) rename {library/inky => inky}/auto.py (100%) rename {library/inky => inky}/eeprom.py (100%) rename {library/inky => inky}/inky.py (99%) rename {library/inky => inky}/inky_ac073tc1a.py (100%) rename {library/inky => inky}/inky_ssd1608.py (100%) rename {library/inky => inky}/inky_ssd1683.py (100%) rename {library/inky => inky}/inky_uc8159.py (100%) rename {library/inky => inky}/mock.py (100%) rename {library/inky => inky}/phat.py (100%) rename {library/inky => inky}/ssd1608.py (100%) rename {library/inky => inky}/ssd1683.py (100%) rename {library/inky => inky}/what.py (100%) delete mode 100644 library/LICENSE.txt delete mode 100644 library/README.md delete mode 100644 library/setup.cfg delete mode 100755 library/setup.py delete mode 100644 library/tox.ini create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt delete mode 100644 sphinx/_static/custom.css delete mode 100644 sphinx/_templates/breadcrumbs.html delete mode 100644 sphinx/_templates/layout.html delete mode 100644 sphinx/conf.py delete mode 100644 sphinx/favicon.png delete mode 100644 sphinx/index.rst delete mode 100644 sphinx/inky.rst delete mode 100644 sphinx/phat.rst delete mode 100644 sphinx/requirements.txt delete mode 100644 sphinx/shop-logo.png delete mode 100644 sphinx/what.rst rename {library/tests => tests}/conftest.py (100%) rename {library/tests => tests}/test_auto.py (100%) rename {library/tests => tests}/test_eeprom.py (100%) rename {library/tests => tests}/test_init.py (100%) rename {library/tests => tests}/test_install_helpers.py (100%) rename {library/tests => tests}/test_simulator.py (100%) rename {library/tests => tests}/tools.py (100%) create mode 100644 tox.ini diff --git a/library/.coveragerc b/.coveragerc similarity index 70% rename from library/.coveragerc rename to .coveragerc index bef4d942..ca9710de 100644 --- a/library/.coveragerc +++ b/.coveragerc @@ -3,4 +3,4 @@ source = inky omit = .tox/* relative_files = True -data_file = ../.coverage +data_file = .coverage diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..87200efb --- /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@v3 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v3 + 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@v3 + with: + name: ${{ env.RELEASE_FILE }} + path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 00000000..4f858832 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,36 @@ +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@v2 + + - name: Set up Python '3,11' + uses: actions/setup-python@v3 + 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 786b7600..016a6780 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,38 +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: [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@v3 with: python-version: ${{ matrix.python }} + - name: Install Dependencies run: | - python -m pip install 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/.stickler.yml b/.stickler.yml deleted file mode 100644 index 2466815b..00000000 --- a/.stickler.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -linters: - flake8: - python: 3 - max-line-length: 160 diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md 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 8f176715..9e0c15c5 100644 --- a/Makefile +++ b/Makefile @@ -1,61 +1,60 @@ -LIBRARY_VERSION=`cat library/setup.py | grep version | awk -F"'" '{print $$2}'` -LIBRARY_NAME=`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.rst from README.md" - @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 "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" install: - ./install.sh + ./install.sh --unstable uninstall: ./uninstall.sh -check: - @echo "Checking for trailing whitespace" - @! grep -IlUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO - @echo "Checking for DOS line-endings" - @! grep -IlUrn --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}'" - -python-readme: library/README.md +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix -python-license: library/LICENSE.txt +check: + @bash check.sh -library/README.md: README.md - cp README.md 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 +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python3 setup.py sdist +tag: + 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 2cf1f74b..406a8e85 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # Inky -[![Build Status](https://travis-ci.com/pimoroni/inky.svg?branch=master)](https://travis-ci.com/pimoroni/inky) +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/inky/test.yml?branch=main)](https://github.com/pimoroni/inky/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/inky/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/inky?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/inky.svg)](https://pypi.python.org/pypi/inky) [![Python Versions](https://img.shields.io/pypi/pyversions/inky.svg)](https://pypi.python.org/pypi/inky) + Python library for [Inky pHAT](https://shop.pimoroni.com/products/inky-phat), [Inky wHAT](https://shop.pimoroni.com/products/inky-what) and [Inky Impression](https://shop.pimoroni.com/?q=inky+impression) e-paper displays for Raspberry Pi. ## Inky pHAT [Inky pHAT](https://shop.pimoroni.com/products/inky-phat) is a 250x122 pixel e-paper display, available in red/black/white, yellow/black/white and black/white. It's great for nametags and displaying very low frequency information such as a daily calendar or weather overview. - ## Inky wHAT [Inky wHAT](https://shop.pimoroni.com/products/inky-what) is a 400x300 pixel e-paper display available in red/black/white, yellow/black/white and black/white. It's got tons of resolution for detailed daily to-do lists, multi-day weather forecasts, bus timetables and more. @@ -27,19 +27,11 @@ First, make sure you have I2C and SPI enabled in `sudo raspi-config`. The Python pip package is named inky, on the Raspberry Pi install with: ``` -pip3 install inky[rpi,example-depends] +python3 -m pip install inky ``` This will install Inky along with dependencies for the Raspberry Pi, plus fonts used by the examples. -If you want to simulate Inky on your desktop, use: - -``` -pip3 install inky -``` - -You may need to use `sudo pip3` or `sudo pip` depending on your environment and Python version. - # Usage The library should be run with Python 3. diff --git a/check.sh b/check.sh new file mode 100755 index 00000000..4395d89c --- /dev/null +++ b/check.sh @@ -0,0 +1,88 @@ +#!/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: $1\n"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO +if [[ $? -eq 0 ]]; then + warning "Trailing whitespace found!" + exit 1 +else + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile +if [[ $? -eq 0 ]]; then + warning "DOS line-endings found!" + exit 1 +else + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 +if [[ $? -eq 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}..." +git tag -l | grep -E "${LIBRARY_VERSION}$" +if [[ $? -eq 1 ]]; 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/library/inky/__init__.py b/inky/__init__.py similarity index 100% rename from library/inky/__init__.py rename to inky/__init__.py diff --git a/library/inky/auto.py b/inky/auto.py similarity index 100% rename from library/inky/auto.py rename to inky/auto.py diff --git a/library/inky/eeprom.py b/inky/eeprom.py similarity index 100% rename from library/inky/eeprom.py rename to inky/eeprom.py diff --git a/library/inky/inky.py b/inky/inky.py similarity index 99% rename from library/inky/inky.py rename to inky/inky.py index 53b87ba1..4d16ee10 100644 --- a/library/inky/inky.py +++ b/inky/inky.py @@ -9,6 +9,8 @@ except ImportError: raise ImportError('This library requires the numpy module\nInstall with: sudo apt install python-numpy') +__version__ = "1.5.0" + # Display colour codes WHITE = 0 BLACK = 1 diff --git a/library/inky/inky_ac073tc1a.py b/inky/inky_ac073tc1a.py similarity index 100% rename from library/inky/inky_ac073tc1a.py rename to inky/inky_ac073tc1a.py diff --git a/library/inky/inky_ssd1608.py b/inky/inky_ssd1608.py similarity index 100% rename from library/inky/inky_ssd1608.py rename to inky/inky_ssd1608.py diff --git a/library/inky/inky_ssd1683.py b/inky/inky_ssd1683.py similarity index 100% rename from library/inky/inky_ssd1683.py rename to inky/inky_ssd1683.py diff --git a/library/inky/inky_uc8159.py b/inky/inky_uc8159.py similarity index 100% rename from library/inky/inky_uc8159.py rename to inky/inky_uc8159.py diff --git a/library/inky/mock.py b/inky/mock.py similarity index 100% rename from library/inky/mock.py rename to inky/mock.py diff --git a/library/inky/phat.py b/inky/phat.py similarity index 100% rename from library/inky/phat.py rename to inky/phat.py diff --git a/library/inky/ssd1608.py b/inky/ssd1608.py similarity index 100% rename from library/inky/ssd1608.py rename to inky/ssd1608.py diff --git a/library/inky/ssd1683.py b/inky/ssd1683.py similarity index 100% rename from library/inky/ssd1683.py rename to inky/ssd1683.py diff --git a/library/inky/what.py b/inky/what.py similarity index 100% rename from library/inky/what.py rename to inky/what.py diff --git a/install.sh b/install.sh index cbd9bf3a..38f19e9a 100755 --- a/install.sh +++ b/install.sh @@ -1,21 +1,313 @@ #!/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_DIR/auto_venv.sh +VENV_DIR=$HOME/.virtualenvs/pimoroni +WD=`pwd` +USAGE="./install.sh (--unstable)" +POSITIONAL_ARGS=() +FORCE=false +UNSTABLE=false +PYTHON="python" -printf "Inky Python Library: Installer\n\n" -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./install.sh'\n" - exit 1 +user_check() { + if [ $(id -u) -eq 0 ]; then + printf "Script should not be run as root. Try './install.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)" +} + +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE"]; then + warning "Could not find $CONFIG_FILE!" + exit 1 + fi + else + if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then + warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" + warning "You might want to fix this!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + +venv_bash_snippet() { + if [ ! -f $VENV_BASH_SNIPPET ]; then + cat << EOF > $VENV_BASH_SNIPPET +# Add `source $RESOURCES_DIR/auto_venv.sh` 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 one for you?"; then + if [ ! -f $VENV_DIR/bin/activate ]; then + inform "Creating 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 + else + inform "Found existing virtual Python environment in $VENV_DIR\n" + fi + inform "Activating virtual Python environment in $VENV_DIR..." + inform "source $VENV_DIR/bin/activate\n" + source $VENV_DIR/bin/activate -function py_install() { - if [ -f "$1" ]; then - VERSION=`$1 --version 2>&1` - printf "Installing for $VERSION..\n" - $1 -m pip install --no-binary .[example-depends] ./library/ + else + exit 1 + fi fi } -py_install /usr/bin/python -py_install /usr/bin/python3 +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=() + PACKAGES_IN=("$@") + for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do + PACKAGE="${PACKAGES_IN[$i]}" + if [ "$PACKAGE" == "" ]; then continue; fi + printf "Checking for $PACKAGE\n" + dpkg -L $PACKAGE > /dev/null 2>&1 + if [ "$?" == "1" ]; then + PACKAGES+=("$PACKAGE") + fi + done + PACKAGES="${PACKAGES[@]}" + if ! [ "$PACKAGES" == "" ]; then + echo "Installing missing packages: $PACKAGES" + if [ ! $APT_HAS_UPDATED ]; then + sudo apt update + APT_HAS_UPDATED=true + fi + sudo apt install -y $PACKAGES + if [ -f "$UNINSTALLER" ]; then + echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + fi + fi +} + +function pip_pkg_install { + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" +} + +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: $1\n"; + printf "Usage: $USAGE\n"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +user_check +venv_check + +if [ ! -f `which $PYTHON` ]; then + printf "Python path $PYTHON not found!\n" + exit 1 +fi + +PYTHON_VER=`$PYTHON --version` + +printf "$LIBRARY_NAME Python Library: Installer\n\n" + +inform "Checking Dependencies. Please wait..." + +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 + +if $UNSTABLE; then + warning "Installing unstable library from source.\n\n" +else + printf "Installing stable library from pypi.\n\n" +fi + +inform "Installing for $PYTHON_VER...\n" +apt_pkg_install "${APT_PACKAGES[@]}" +if $UNSTABLE; then + pip_pkg_install . +else + pip_pkg_install $LIBRARY_NAME +fi +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER +fi + +cd $WD + +find_config + +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 + eval $CMD +done + +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\n" + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE + if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then + printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + fi + fi +done + +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 confirm "Would you like to generate documentation?"; then + pip_pkg_install pdoc + printf "Generating documentation.\n" + $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null + if [ $? -eq 0 ]; then + inform "Documentation saved to $RESOURCES_DIR/docs" + success "Done!" + else + warning "Error: Failed to generate documentation." + fi +fi -printf "Done!\n" +success "\nAll done!" +inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" +inform "Find uninstall steps in $UNINSTALLER\n" diff --git a/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index aed751a0..00000000 --- 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 2cf1f74b..00000000 --- a/library/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# Inky - -[![Build Status](https://travis-ci.com/pimoroni/inky.svg?branch=master)](https://travis-ci.com/pimoroni/inky) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/inky/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/inky?branch=master) -[![PyPi Package](https://img.shields.io/pypi/v/inky.svg)](https://pypi.python.org/pypi/inky) -[![Python Versions](https://img.shields.io/pypi/pyversions/inky.svg)](https://pypi.python.org/pypi/inky) - -Python library for [Inky pHAT](https://shop.pimoroni.com/products/inky-phat), [Inky wHAT](https://shop.pimoroni.com/products/inky-what) and [Inky Impression](https://shop.pimoroni.com/?q=inky+impression) e-paper displays for Raspberry Pi. - -## Inky pHAT - -[Inky pHAT](https://shop.pimoroni.com/products/inky-phat) is a 250x122 pixel e-paper display, available in red/black/white, yellow/black/white and black/white. It's great for nametags and displaying very low frequency information such as a daily calendar or weather overview. - - -## Inky wHAT - -[Inky wHAT](https://shop.pimoroni.com/products/inky-what) is a 400x300 pixel e-paper display available in red/black/white, yellow/black/white and black/white. It's got tons of resolution for detailed daily to-do lists, multi-day weather forecasts, bus timetables and more. - -## Inky Impression - -[Inky Impression](https://shop.pimoroni.com/?q=inky+impression) is our line of glorious 7 colour eInk displays, available in [4"](https://shop.pimoroni.com/products/inky-impression-4) (640 x 400 pixel) [5.7"](https://shop.pimoroni.com/products/inky-impression-5-7) (600 x 448 pixel) and [7.3"](https://shop.pimoroni.com/products/inky-impression-7-3) (800 x 480 pixel) flavours. They're packed with strong colours and perfect for displaying striking graphics or lots of data. - -# Installation - -First, make sure you have I2C and SPI enabled in `sudo raspi-config`. - -The Python pip package is named inky, on the Raspberry Pi install with: - -``` -pip3 install inky[rpi,example-depends] -``` - -This will install Inky along with dependencies for the Raspberry Pi, plus fonts used by the examples. - -If you want to simulate Inky on your desktop, use: - -``` -pip3 install inky -``` - -You may need to use `sudo pip3` or `sudo pip` depending on your environment and Python version. - -# Usage - -The library should be run with Python 3. - -## Auto Setup - -Inky can try to automatically identify your board (from the information stored on its EEPROM) and set up accordingly. This is the easiest way to work with recent Inky displays. - -```python -from inky.auto import auto -display = auto() -``` - -You can then get the colour and resolution from the board: - -```python -display.colour -display.resolution -``` - -## Manual Setup - -If you have an older Inky without an EEPROM, you can specify the type manually. The Inky library contains modules for both the pHAT and wHAT, load the Inky pHAT one as follows: - -```python -from inky import InkyPHAT -``` - -You'll then need to pick your colour, one of 'red', 'yellow' or 'black' and instantiate the class: - -```python -display = InkyPHAT('red') -``` - -If you're using the wHAT you'll need to load the InkyWHAT class from the Inky library like so: - -```python -from inky import InkyWHAT -display = InkyWHAT('red') -``` - -Once you've initialised Inky, there are only three methods you need to be concerned with: - -## Set Image - -Set a PIL image, numpy array or list to Inky's internal buffer. The image dimensions should match the dimensions of the pHAT or wHAT you're using. - -```python -display.set_image(image) -``` - -You should use `PIL` to create an image. `PIL` provides an `ImageDraw` module which allow you to draw text, lines and shapes over your image. See: https://pillow.readthedocs.io/en/stable/reference/ImageDraw.html - -## Set Border - -Set the border colour of you pHAT or wHAT. - -```python -display.set_border(colour) -``` - -`colour` should be one of `inky.RED`, `inky.YELLOW`, `inky.WHITE` or `inky.BLACK` with available colours depending on your display type. - -## Update The Display - -Once you've prepared and set your image, and chosen a border colour, you can update your e-ink display with: - -```python -display.show() -``` - - -# Migrating - -If you're migrating code from the old `inkyphat` library you'll find that much of the drawing and image manipulation functions have been removed from Inky. These functions were always supplied by PIL, and the recommended approach is to use PIL to create and prepare your image before setting it to Inky with `set_image()`. diff --git a/library/setup.cfg b/library/setup.cfg deleted file mode 100644 index 5c3c3ea2..00000000 --- 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 6c442cce..00000000 --- a/library/setup.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 - -""" -Copyright (c) 2017 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. -""" - -from setuptools 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='inky', - version='1.5.0', - author='Philip Howard', - author_email='phil@pimoroni.com', - description='Inky pHAT Driver', - long_description=open('README.md').read() + '\n' + open('CHANGELOG.txt').read(), - long_description_content_type="text/markdown", - license='MIT', - keywords='Raspberry Pi e-paper display driver', - url='http://www.pimoroni.com', - project_urls={'GitHub': 'https://www.github.com/pimoroni/inky'}, - classifiers=classifiers, - py_modules=[], - packages=['inky'], - include_package_data=True, - install_requires=['numpy', 'smbus2', 'spidev'], - extras_require={ - 'rpi-gpio-output': ['RPi.GPIO'], - 'rpi': ['RPi.GPIO'], - 'example-depends': ['requests', 'geocoder', 'beautifulsoup4', 'font-fredoka-one', 'font-source-serif-pro', 'font-hanken-grotesk', 'font-intuitive'] - } -) diff --git a/library/tox.ini b/library/tox.ini deleted file mode 100644 index 4e838450..00000000 --- a/library/tox.ini +++ /dev/null @@ -1,26 +0,0 @@ -[tox] -envlist = py{39},qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py develop --no-deps - coverage run -m pytest -v -r wsx - coverage report -m -deps = - mock - pytest>=3.1 - pytest-cov - -[testenv:qa] -commands = - check-manifest --ignore tox.ini,tests/*,.coveragerc - flake8 --ignore E501,E122,E241,F401,W504,Q000 - python setup.py sdist bdist_wheel - twine check dist/* -deps = - check-manifest - flake8 - flake8-docstrings - flake8-quotes - twine diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..65488cc8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,133 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "inky" +dynamic = ["version", "readme"] +description = "Inky pHAT Driver" +license = {file = "LICENSE"} +requires-python = ">= 3.7" +authors = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +maintainers = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +keywords = [ + "Raspberry Pi", + "e-paper", +] +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 = [ + "numpy", + "smbus2", + "spidev" +] + +[project.optional-dependencies] +example-depends = [ + "requests", + "geocoder", + "beautifulsoup4", + "font-fredoka-one", + "font-source-serif-pro", + "font-hanken-grotesk", + "font-intuitive" +] +rpi-gpio-output = ['RPi.GPIO'] +rpi = ['RPi.GPIO'] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/inky" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "inky/__init__.py" + +[tool.hatch.build] +include = [ + "inky", + "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 00000000..525b0427 --- /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/sphinx/_static/custom.css b/sphinx/_static/custom.css deleted file mode 100644 index 141c20cd..00000000 --- a/sphinx/_static/custom.css +++ /dev/null @@ -1,53 +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; -} -.wy-menu-vertical a { - color:#cccccc -} -.wy-menu-vertical p.caption { - background: #333333; - color: #6d6d6d; -} -.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 blockquote { - margin-left:0; -} -.rst-content #at-a-glance dl:not(.docutils) dt { - border:none; - background:#f0f0f0; -} -.rst-content #at-a-glance dl:not(.docutils) dd, -.rst-content #at-a-glance dl:not(.docutils) dd dl:not(.docutils) dd { - display:none; -} -.rst-content #at-a-glance dl:not(.docutils) { - margin-bottom:0; -} diff --git a/sphinx/_templates/breadcrumbs.html b/sphinx/_templates/breadcrumbs.html deleted file mode 100644 index e69de29b..00000000 diff --git a/sphinx/_templates/layout.html b/sphinx/_templates/layout.html deleted file mode 100644 index a2bd1c5c..00000000 --- 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 a4877486..00000000 --- a/sphinx/conf.py +++ /dev/null @@ -1,365 +0,0 @@ -#-*- coding: utf-8 -*- - -import sys -import site - -from unittest import mock -PACKAGE_NAME = u"Inky" -PACKAGE_HANDLE = "Inky" -PACKAGE_MODULE = "inky" - -# Prompte /usr/local/lib to the front of sys.path -#sys.path.insert(0,site.getsitepackages()[0]) - -import sphinx_rtd_theme - -MOCK_MODULES = ['RPi', 'RPi.GPIO', 'smbus2', 'smbus', 'numpy', 'spidev', 'PIL'] -for module_name in MOCK_MODULES: - sys.modules[module_name] = mock.MagicMock() - -sys.path.insert(0, '../library/') - - -import inky - -from sphinx.ext import autodoc - - -# -- 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.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx.ext.autosummary' -] - -autoclass_content = 'both' - -# Intersphinx configuration -intersphinx_mapping = { - 'numpy': ('https://docs.scipy.org/doc/numpy/', None), - 'PIL': ('https://pillow.readthedocs.io/en/stable/', None), - 'python': ('https://docs.python.org/3', None), - 'smbus2': ('https://smbus2.readthedocs.io/en/latest/', None), - } - -# 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'2019, 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(inky.__version__) -# The full version, including alpha/beta/rc tags. -release = u'{}'.format(inky.__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', 'sphinx.virtualenv'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -# keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' -#html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -html_theme_options = { - 'collapse_navigation': False, - 'display_version': True -} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [ - '_themes', - sphinx_rtd_theme.get_html_theme_path() -] - -# The name for this set of Sphinx documents. -# " v documentation" by default. -# -# html_title = PACKAGE_NAME + u' v0.1.2' - -# A shorter title for the navigation bar. Default is the same as html_title. -# -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# -html_logo = 'shop-logo.png' - -# The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# -html_favicon = 'favicon.png' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -# -# html_extra_path = [] - -# If not None, a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# The empty string is equivalent to '%b %d, %Y'. -# -# html_last_updated_fmt = None - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# -# html_additional_pages = {} - -# If false, no module index is generated. -# -# html_domain_indices = True - -# If false, no index is generated. -# -html_use_index = False - -# If true, the index is split into individual pages for each letter. -# -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# -html_show_sourcelink = False - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# -html_show_sphinx = False - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' -# -# html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# 'ja' uses this config value. -# 'zh' user can custom change `jieba` dictionary path. -# -# html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -# -# html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = PACKAGE_HANDLE + 'doc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, PACKAGE_HANDLE + '.tex', PACKAGE_NAME + u' Documentation', - u'Phil Howard', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# -# latex_use_parts = False - -# If true, show page references after internal links. -# -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# -# latex_appendices = [] - -# It false, will not define \strong, \code, itleref, \crossref ... but only -# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added -# packages. -# -# latex_keep_old_macro_names = True - -# If false, no module index is generated. -# -# latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, PACKAGE_MODULE, PACKAGE_NAME + u' Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -# -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, PACKAGE_HANDLE, PACKAGE_NAME + u' Documentation', - author, PACKAGE_HANDLE, 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# -# texinfo_appendices = [] - -# If false, no module index is generated. -# -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# -# texinfo_no_detailmenu = False diff --git a/sphinx/favicon.png b/sphinx/favicon.png deleted file mode 100644 index 5ed0316c76a3c90cc6c8844cdd3c1385ad5cf09d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26049 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 diff --git a/sphinx/index.rst b/sphinx/index.rst deleted file mode 100644 index 7e0c786a..00000000 --- a/sphinx/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. role:: python(code) - :language: python - -Welcome -------- - -This documentation will guide you through the methods available in the Inky python library. - -The Pimoroni website has tutorials demonstrating how to set up and use the `Inky pHAT`_ and `Inky wHAT`_. - -Further examples can be found on the `Inky Git repository`_. - -.. _`Inky pHAT`: https://learn.pimoroni.com/tutorial/sandyj/getting-started-with-inky-phat -.. _`Inky wHAT`: https://learn.pimoroni.com/tutorial/sandyj/getting-started-with-inky-what -.. _`Inky Git repository`: https://github.com/pimoroni/inky/tree/master/examples - -.. currentmodule:: inky - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - phat - what - inky diff --git a/sphinx/inky.rst b/sphinx/inky.rst deleted file mode 100644 index b07f07af..00000000 --- a/sphinx/inky.rst +++ /dev/null @@ -1,5 +0,0 @@ -Inky Base Class ---------------- - -.. autoclass:: inky.inky.Inky - :members: diff --git a/sphinx/phat.rst b/sphinx/phat.rst deleted file mode 100644 index 22a31f95..00000000 --- a/sphinx/phat.rst +++ /dev/null @@ -1,8 +0,0 @@ -Inky pHAT ---------- - -.. automodule:: inky.phat - -.. autoclass:: inky.InkyPHAT - :members: - :inherited-members: diff --git a/sphinx/requirements.txt b/sphinx/requirements.txt deleted file mode 100644 index fb969a5a..00000000 --- a/sphinx/requirements.txt +++ /dev/null @@ -1,30 +0,0 @@ -alabaster==0.7.12 -Babel==2.9.1 -certifi==2021.5.30 -chardet==4.0.0 -charset-normalizer==2.0.3 -docutils==0.16 -funcsigs==1.0.2 -idna==3.2 -imagesize==1.2.0 -Jinja2==3.0.1 -MarkupSafe==2.0.1 -packaging==21.0 -Pillow==8.3.1 -Pygments==2.9.0 -pyparsing==2.4.7 -pytz==2021.1 -requests==2.26.0 -six==1.16.0 -snowballstemmer==2.1.0 -Sphinx==4.1.2 -sphinx-rtd-theme==0.5.2 -sphinxcontrib-applehelp==1.0.2 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==2.0.0 -sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.5 -sphinxcontrib-websupport==1.2.4 -typing==3.7.4.3 -urllib3==1.26.6 diff --git a/sphinx/shop-logo.png b/sphinx/shop-logo.png deleted file mode 100644 index 8fd0cda225cb6add7db0f7137919d92322c89f06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19652 zcmeI4c|6qJ+sD76O(@y2Z%ML@nX%1e-x~WG#y-Yivdl1pAt6b!Wi7flqJ$LwBB=f&vQSo-}Ahle`a2o?{%*8xvuklpL3n>`RD7Tx#B5#7%!FXX2p#d1H zP*ZDks5ctsE~cT*tV)7Y0{CKxC=rscj~@X}QWIN`3#VMKHG{-N*1HhB)x@;c5{lTF zn2YG(0x%+qa!RsjFc>VN43h&Zsz4!%G9nOpup&qv1_H~=%7fuxc{oH~ljcD%m6 zzfDab>IG2{d_($MCBZrbj{#X?2)MuiG)6Coax3D$yL%$mlRZ0~^E&4g* zKjUF^QACV7Wko8RLAMlFeiP1Z39zBuhjs?T^00~)}O=rq5LMp z{$>LI73Ht-f06%lj$kj0-#=8)*C^{9{#j4Jc@Tq90T?Y$%5wO3mHjpDUxm~OR6%RI zy$|;PU=|3dp#N|V{#6nEXLIm<8~Xp8gP-dc?S=C5#JH=2zAxSH4gYT1zE0*}YwG(} z@b?;2U3>PxDfLTPP^k5~0jq-k*7h^=*Zy{Eb?6hg-d`CaPx(+zD3s4P>91~TDU!DI zS2wlvr}Cg5mZ(nI;MNC9)%=TU?E(39E4QMYii%K0RSN)nRU&^XmA_|O7yjb-Cj2^AzZ|>PEAVS88?IxgCLsJwJsR6uB(bX>IgP9}a~q4LskQ30WC(s9w|L*=F8q5?wOq~oH^hssOGMFoVmNykN-50#gWiwX#Bla7lv zA1W^$7ZniNCLI@TK2%;hE-E0jO*$^xe5ky1TvR}4n{-^X`A~W3xTt{8HtD!%^P%$6 zaZv%GZPIbk=0oMB7&4A0wX&^Bq}V*dGxCdQBQUS=@m z1oYDggkI#{gjI3+4MY00hbdz@#exz%v1W2bb#7tPcP?0}OPv ztVsiJooX%lO?Kk;j!VASmMv2o>G3@#yWA}Ir+f{@Gmd!d$A<7A-TU5=KmgyG*$2qiEzez$Qm&g-N zLP_UaNKaBg2G|d;c;ty)71m{gXaR54Y98 c29iqXq3)xp_>0_3<6H>b_N?3w_6T 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 diff --git a/sphinx/what.rst b/sphinx/what.rst deleted file mode 100644 index 69645c7c..00000000 --- a/sphinx/what.rst +++ /dev/null @@ -1,8 +0,0 @@ -Inky wHAT ---------- - -.. automodule:: inky.what - -.. autoclass:: inky.InkyWHAT - :members: - :inherited-members: diff --git a/library/tests/conftest.py b/tests/conftest.py similarity index 100% rename from library/tests/conftest.py rename to tests/conftest.py diff --git a/library/tests/test_auto.py b/tests/test_auto.py similarity index 100% rename from library/tests/test_auto.py rename to tests/test_auto.py diff --git a/library/tests/test_eeprom.py b/tests/test_eeprom.py similarity index 100% rename from library/tests/test_eeprom.py rename to tests/test_eeprom.py diff --git a/library/tests/test_init.py b/tests/test_init.py similarity index 100% rename from library/tests/test_init.py rename to tests/test_init.py diff --git a/library/tests/test_install_helpers.py b/tests/test_install_helpers.py similarity index 100% rename from library/tests/test_install_helpers.py rename to tests/test_install_helpers.py diff --git a/library/tests/test_simulator.py b/tests/test_simulator.py similarity index 100% rename from library/tests/test_simulator.py rename to tests/test_simulator.py diff --git a/library/tests/tools.py b/tests/tools.py similarity index 100% rename from library/tests/tools.py rename to tests/tools.py diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..44c86546 --- /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 0928b014..f213fc52 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,24 +1,72 @@ #!/bin/bash -PACKAGE="inky" +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 "Inky 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 $PACKAGE +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 $PACKAGE -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 "$LIBRARY_NAME Python Library: Uninstaller\n\n" + +user_check +venv_check + +printf "Uninstalling for Python 3...\n" +$PYTHON -m pip uninstall $LIBRARY_NAME + +if [ -d $RESOURCES_DIR ]; then + if confirm "Would you like to delete $RESOURCES_DIR?"; then + rm -r $RESOURCES_DIR + fi +fi printf "Done!\n" From de42fb4cf970c8039b7e793fbc63e5189fbf2383 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 13:26:36 +0000 Subject: [PATCH 02/27] QA: Apply isort suggestions. --- examples/7color/advanced/dither.py | 3 ++- examples/7color/buttons.py | 1 + examples/7color/colour-palette.py | 3 ++- examples/7color/graph.py | 7 ++++--- examples/clean.py | 3 ++- examples/logo.py | 3 ++- examples/name-badge.py | 11 ++++++----- examples/phat/calendar-phat.py | 3 ++- examples/phat/weather-phat.py | 5 +++-- examples/tests/border.py | 4 +++- examples/what/dither-image-what.py | 2 ++ examples/what/quotes-what.py | 8 ++++---- inky/__init__.py | 16 ++++++++-------- inky/auto.py | 12 ++++++------ inky/eeprom.py | 1 - inky/inky.py | 2 +- inky/inky_ssd1608.py | 1 + inky/inky_ssd1683.py | 1 + inky/inky_uc8159.py | 2 +- inky/mock.py | 6 ++---- tests/conftest.py | 1 + tests/test_auto.py | 9 +++------ tests/test_eeprom.py | 4 ++-- tests/test_init.py | 1 + 24 files changed, 60 insertions(+), 49 deletions(-) diff --git a/examples/7color/advanced/dither.py b/examples/7color/advanced/dither.py index a4639707..d087133a 100755 --- a/examples/7color/advanced/dither.py +++ b/examples/7color/advanced/dither.py @@ -3,9 +3,10 @@ import sys import hitherdither -from inky import auto from PIL import Image +from inky import auto + print("""dither.py Advanced dithering example using Hitherdither by Henrik Blidh: diff --git a/examples/7color/buttons.py b/examples/7color/buttons.py index 3f3597a1..016b2d55 100755 --- a/examples/7color/buttons.py +++ b/examples/7color/buttons.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import signal + import RPi.GPIO as GPIO print("""buttons.py - Detect which button has been pressed diff --git a/examples/7color/colour-palette.py b/examples/7color/colour-palette.py index 5e8d8104..aa6b7491 100755 --- a/examples/7color/colour-palette.py +++ b/examples/7color/colour-palette.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -from inky.inky_uc8159 import Inky import argparse import pathlib import struct import sys +from inky.inky_uc8159 import Inky + parser = argparse.ArgumentParser() parser.add_argument('--type', '-t', choices=['css', 'act', 'raw', 'pal', 'gpl'], help='Type of palette to output') diff --git a/examples/7color/graph.py b/examples/7color/graph.py index 9fdf0a93..b97fe515 100755 --- a/examples/7color/graph.py +++ b/examples/7color/graph.py @@ -1,12 +1,13 @@ #!/usr/bin/env python3 +import argparse import io +import seaborn +from matplotlib import pyplot from PIL import Image + from inky.auto import auto -from matplotlib import pyplot -import seaborn -import argparse print(""" diff --git a/examples/clean.py b/examples/clean.py index fe72680a..f59f62be 100755 --- a/examples/clean.py +++ b/examples/clean.py @@ -4,9 +4,10 @@ import argparse import time -from inky.auto import auto from PIL import Image +from inky.auto import auto + print("""Inky pHAT: Clean Displays solid blocks of red, black, and white to clean the Inky pHAT diff --git a/examples/logo.py b/examples/logo.py index 0a2cc14e..9b1d6a2e 100755 --- a/examples/logo.py +++ b/examples/logo.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 import os + from PIL import Image -from inky.auto import auto +from inky.auto import auto print("""Inky pHAT/wHAT: Logo diff --git a/examples/name-badge.py b/examples/name-badge.py index 3fbd2820..b71098e8 100755 --- a/examples/name-badge.py +++ b/examples/name-badge.py @@ -2,14 +2,11 @@ import argparse -from PIL import Image, ImageFont, ImageDraw from font_hanken_grotesk import HankenGroteskBold, HankenGroteskMedium from font_intuitive import Intuitive -from inky.auto import auto +from PIL import Image, ImageDraw, ImageFont -def getsize(font, text): - _, _, right, bottom = font.getbbox(text) - return (right, bottom) +from inky.auto import auto print("""Inky pHAT/wHAT: Hello... my name is: @@ -17,6 +14,10 @@ def getsize(font, text): """) +def getsize(font, text): + _, _, right, bottom = font.getbbox(text) + return (right, bottom) + try: inky_display = auto(ask_user=True, verbose=True) except TypeError: diff --git a/examples/phat/calendar-phat.py b/examples/phat/calendar-phat.py index 99a2e37f..bf8a489b 100755 --- a/examples/phat/calendar-phat.py +++ b/examples/phat/calendar-phat.py @@ -5,9 +5,10 @@ import datetime import os -from inky.auto import auto from PIL import Image, ImageDraw +from inky.auto import auto + print("""Inky pHAT: Calendar Draws a calendar for the current month to your Inky pHAT. diff --git a/examples/phat/weather-phat.py b/examples/phat/weather-phat.py index 751527ee..f50c23c4 100755 --- a/examples/phat/weather-phat.py +++ b/examples/phat/weather-phat.py @@ -2,15 +2,16 @@ # -*- coding: utf-8 -*- import glob +import json import os import time -import json from sys import exit from font_fredoka_one import FredokaOne -from inky.auto import auto from PIL import Image, ImageDraw, ImageFont +from inky.auto import auto + """ To run this example on Python 2.x you should: sudo apt install python-lxml diff --git a/examples/tests/border.py b/examples/tests/border.py index 2f57441d..dfeb5679 100755 --- a/examples/tests/border.py +++ b/examples/tests/border.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 -import time import sys +import time + from PIL import Image + from inky import InkyPHAT INKY_COLOUR = None diff --git a/examples/what/dither-image-what.py b/examples/what/dither-image-what.py index d183ada8..96bd064b 100755 --- a/examples/what/dither-image-what.py +++ b/examples/what/dither-image-what.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import argparse + from PIL import Image + from inky.auto import auto print("""Inky wHAT: Dither image diff --git a/examples/what/quotes-what.py b/examples/what/quotes-what.py index 6670d7d2..3cfdf31f 100755 --- a/examples/what/quotes-what.py +++ b/examples/what/quotes-what.py @@ -4,11 +4,11 @@ import random import sys -from inky.auto import auto - -from PIL import Image, ImageFont, ImageDraw -from font_source_serif_pro import SourceSerifProSemibold from font_source_sans_pro import SourceSansProSemibold +from font_source_serif_pro import SourceSerifProSemibold +from PIL import Image, ImageDraw, ImageFont + +from inky.auto import auto print("""Inky wHAT: Quotes diff --git a/inky/__init__.py b/inky/__init__.py index 240a28f5..0eee3197 100644 --- a/inky/__init__.py +++ b/inky/__init__.py @@ -1,14 +1,14 @@ """Inky e-Ink Display Drivers.""" -from . import inky # noqa: F401 -from .inky import BLACK, WHITE, RED, YELLOW # noqa: F401 -from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 -from .what import InkyWHAT # noqa: F401 -from .mock import InkyMockPHAT, InkyMockWHAT # noqa: F401 -from .inky_uc8159 import Inky as Inky7Colour # noqa: F401 -from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 +from . import inky # noqa: F401 +from .auto import auto # noqa: F401 +from .inky import BLACK, RED, WHITE, YELLOW # noqa: F401 from .inky_ac073tc1a import Inky as Inky_Impressions_7 # noqa: F401 -from .auto import auto # noqa: F401 +from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 +from .inky_uc8159 import Inky as Inky7Colour # noqa: F401 +from .mock import InkyMockPHAT, InkyMockWHAT # noqa: F401 +from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 +from .what import InkyWHAT # noqa: F401 __version__ = '1.5.0' diff --git a/inky/auto.py b/inky/auto.py index 3b05f9a3..9910c873 100644 --- a/inky/auto.py +++ b/inky/auto.py @@ -1,12 +1,12 @@ """Automatic Inky setup from i2c EEPROM.""" -from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 -from .what import InkyWHAT # noqa: F401 -from .inky_uc8159 import Inky as InkyUC8159 # noqa: F401 -from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 -from .inky_ac073tc1a import Inky as InkyAC073TC1A # noqa: F401 -from . import eeprom import argparse +from . import eeprom +from .inky_ac073tc1a import Inky as InkyAC073TC1A # noqa: F401 +from .inky_ssd1683 import Inky as InkyWHAT_SSD1683 # noqa: F401 +from .inky_uc8159 import Inky as InkyUC8159 # noqa: F401 +from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 +from .what import InkyWHAT # noqa: F401 DISPLAY_TYPES = ["what", "phat", "phatssd1608", "impressions", "7colour", "whatssd1683", "impressions73"] DISPLAY_COLORS = ["red", "black", "yellow"] diff --git a/inky/eeprom.py b/inky/eeprom.py index ddb2984a..4c07cdc9 100644 --- a/inky/eeprom.py +++ b/inky/eeprom.py @@ -5,7 +5,6 @@ import datetime import struct - EEP_ADDRESS = 0x50 EEP_WP = 12 diff --git a/inky/inky.py b/inky/inky.py index 4d16ee10..fc641182 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -1,6 +1,6 @@ """Inky e-Ink Display Driver.""" -import time import struct +import time from . import eeprom diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index db553541..717bd20f 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -2,6 +2,7 @@ import time from PIL import Image + from . import eeprom, ssd1608 try: diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index fcc15408..81d97def 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -2,6 +2,7 @@ import time from PIL import Image + from . import eeprom, ssd1683 try: diff --git a/inky/inky_uc8159.py b/inky/inky_uc8159.py index 31534c8c..0decc8be 100644 --- a/inky/inky_uc8159.py +++ b/inky/inky_uc8159.py @@ -1,6 +1,6 @@ """Inky e-Ink Display Driver.""" -import time import struct +import time import warnings try: diff --git a/inky/mock.py b/inky/mock.py index 417ecd0c..04bbd441 100644 --- a/inky/mock.py +++ b/inky/mock.py @@ -1,9 +1,7 @@ """PIL/Tkinter based simulator for InkyWHAT and InkyWHAT.""" import numpy - -from . import inky -from . import inky_uc8159 +from . import inky, inky_uc8159 class InkyMock(inky.Inky): @@ -23,7 +21,7 @@ def __init__(self, colour, h_flip=False, v_flip=False): raise ImportError('Simulation requires tkinter') try: - from PIL import ImageTk, Image + from PIL import Image, ImageTk except ImportError: raise ImportError('Simulation requires PIL ImageTk and Image') diff --git a/tests/conftest.py b/tests/conftest.py index 8357ee90..952b4ee4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ """ import sys from unittest import mock + import pytest from tools import MockSMBus diff --git a/tests/test_auto.py b/tests/test_auto.py index 8ee53364..4357adbc 100644 --- a/tests/test_auto.py +++ b/tests/test_auto.py @@ -1,7 +1,7 @@ """Auto-detect tests for Inky.""" -import pytest import sys +import pytest DISPLAY_VARIANT = [ None, @@ -32,8 +32,7 @@ @pytest.mark.parametrize('inky_type', ['phat', 'what', 'phatssd1608', 'impressions', '7colour', 'whatssd1683']) def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour, verbose): """Test auto init of 'phat', 'black'.""" - from inky import InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, Inky7Colour, InkyWHAT_SSD1683 - from inky import auto + from inky import Inky7Colour, InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, InkyWHAT_SSD1683, auto if inky_type in ['impressions', '7colour']: if inky_colour is not None: @@ -64,9 +63,7 @@ def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour, verbose): @pytest.mark.parametrize('inky_display', enumerate(DISPLAY_VARIANT)) def test_auto(spidev, smbus2_eeprom, PIL, inky_display): """Test auto init of 'phat', 'black'.""" - from inky import InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, Inky7Colour, InkyWHAT_SSD1683 - from inky import auto - from inky import eeprom + from inky import Inky7Colour, InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, InkyWHAT_SSD1683, auto, eeprom display_id, display_name = inky_display diff --git a/tests/test_eeprom.py b/tests/test_eeprom.py index c310848f..9ea24962 100644 --- a/tests/test_eeprom.py +++ b/tests/test_eeprom.py @@ -3,8 +3,8 @@ def test_eeprom_7color_5_7_inch(spidev, smbus2_eeprom, PIL): """Test EEPROM for 7color 5.7" Inky.""" - from inky.inky_uc8159 import Inky from inky.eeprom import EPDType + from inky.inky_uc8159 import Inky eeprom_data = EPDType(600, 448, 0, 0, 14).encode() @@ -17,8 +17,8 @@ def test_eeprom_7color_5_7_inch(spidev, smbus2_eeprom, PIL): def test_eeprom_7color_4_inch(spidev, smbus2_eeprom, PIL): """Test EEPROM for 7color 4" Inky.""" - from inky.inky_uc8159 import Inky from inky.eeprom import EPDType + from inky.inky_uc8159 import Inky eeprom_data = EPDType(640, 400, 0, 0, 16).encode() diff --git a/tests/test_init.py b/tests/test_init.py index b4ff8bf4..fe6e0866 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,6 +1,7 @@ """Initialization tests for Inky.""" from unittest import mock + import pytest From faa14c80581a866f0ef38e80628605875ef983b9 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 13:29:00 +0000 Subject: [PATCH 03/27] QA: Apply ruff suggestions. --- inky/eeprom.py | 4 ++-- inky/inky_ac073tc1a.py | 2 +- inky/inky_uc8159.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/inky/eeprom.py b/inky/eeprom.py index 4c07cdc9..ba926783 100644 --- a/inky/eeprom.py +++ b/inky/eeprom.py @@ -44,7 +44,7 @@ def __init__(self, width, height, color, pcb_variant, display_variant, write_tim self.width = width self.height = height self.color = color - if type(color) == str: + if isinstance(color, str): self.set_color(color) self.pcb_variant = pcb_variant self.display_variant = display_variant @@ -87,7 +87,7 @@ def encode(self): def to_list(self): """Return a list of bytes representing the EEPROM data structure.""" result = self.encode() - if type(result) is bytes: + if isinstance(result, bytes): return result return [ord(c) for c in self.encode()] diff --git a/inky/inky_ac073tc1a.py b/inky/inky_ac073tc1a.py index c0eade3c..03528a7f 100644 --- a/inky/inky_ac073tc1a.py +++ b/inky/inky_ac073tc1a.py @@ -388,7 +388,7 @@ def _spi_write(self, dc, values): self._gpio.output(self.cs_pin, 0) self._gpio.output(self.dc_pin, dc) - if type(values) is str: + if isinstance(values, str): values = [ord(c) for c in values] for byte_value in values: diff --git a/inky/inky_uc8159.py b/inky/inky_uc8159.py index 0decc8be..eeec8376 100644 --- a/inky/inky_uc8159.py +++ b/inky/inky_uc8159.py @@ -425,7 +425,7 @@ def _spi_write(self, dc, values): self._gpio.output(self.cs_pin, 0) self._gpio.output(self.dc_pin, dc) - if type(values) is str: + if isinstance(values, str): values = [ord(c) for c in values] try: From 624ca6e8068b9f7e7ad9be0bec53c46fbfc377b7 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 13:30:49 +0000 Subject: [PATCH 04/27] QA: Apply codespell suggestions. --- inky/inky_ssd1608.py | 2 +- inky/inky_ssd1683.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 717bd20f..201a1abf 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -179,7 +179,7 @@ def _update(self, buf_a, buf_b, busy_wait=True): self._send_command(ssd1608.WRITE_DUMMY, [0x1B]) # Set Line Width self._send_command(ssd1608.WRITE_GATELINE, [0x0B]) - # Data entry squence (scan direction leftward and downward) + # Data entry sequence (scan direction leftward and downward) self._send_command(ssd1608.DATA_MODE, [0x03]) # Set ram X start and end position xposBuf = [0x00, self.cols // 8 - 1] diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index 81d97def..0d7a02d5 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -167,7 +167,7 @@ def _update(self, buf_a, buf_b, busy_wait=True): self._send_command(ssd1683.WRITE_DUMMY, [0x1B]) # Set Line Width self._send_command(ssd1683.WRITE_GATELINE, [0x0B]) - # Data entry squence (scan direction leftward and downward) + # Data entry sequence (scan direction leftward and downward) self._send_command(ssd1683.DATA_MODE, [0x03]) # Set ram X start and end position xposBuf = [0x00, self.cols // 8 - 1] From 08652471172e926e06ae88dd5f763381de19cedf Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 14:01:27 +0000 Subject: [PATCH 05/27] Packaging: Add pillow as an example dependency. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 65488cc8..4ccc0a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ [project.optional-dependencies] example-depends = [ + "pillow", "requests", "geocoder", "beautifulsoup4", From fe70476f43d0867ee3c53190d3d2ed39c4ac12b7 Mon Sep 17 00:00:00 2001 From: Corran Webster Date: Mon, 20 Nov 2023 14:03:44 +0000 Subject: [PATCH 06/27] Add resolution parameter to InkyMockImpression. Copied from https://github.com/pimoroni/inky/pull/169 Co-authored-by: Phil Howard --- inky/mock.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/inky/mock.py b/inky/mock.py index 04bbd441..d7cfa4c0 100644 --- a/inky/mock.py +++ b/inky/mock.py @@ -7,10 +7,11 @@ class InkyMock(inky.Inky): """Base simulator class for Inky.""" - def __init__(self, colour, h_flip=False, v_flip=False): + def __init__(self, colour, h_flip=False, v_flip=False, resolution=None): """Initialise an Inky pHAT Display. :param colour: one of red, black or yellow, default: black + :param resolution: (width, height) in pixels """ global tkinter, ImageTk, Image @@ -25,7 +26,8 @@ def __init__(self, colour, h_flip=False, v_flip=False): except ImportError: raise ImportError('Simulation requires PIL ImageTk and Image') - resolution = (self.WIDTH, self.HEIGHT) + if resolution is None: + resolution = (self.WIDTH, self.HEIGHT) if resolution not in inky._RESOLUTION.keys(): raise ValueError('Resolution {}x{} not supported!'.format(*resolution)) @@ -74,12 +76,12 @@ def __init__(self, colour, h_flip=False, v_flip=False): self._tk_done = False self.tk_root = tkinter.Tk() self.tk_root.title('Inky Preview') - self.tk_root.geometry('{}x{}'.format(self.WIDTH, self.HEIGHT)) - self.tk_root.aspect(self.WIDTH, self.HEIGHT, self.WIDTH, self.HEIGHT) + self.tk_root.geometry('{}x{}'.format(self.width, self.height)) + self.tk_root.aspect(self.width, self.height, self.width, self.height) self.tk_root.protocol('WM_DELETE_WINDOW', self._close_window) self.cv = None - self.cvh = self.HEIGHT - self.cvw = self.WIDTH + self.cvh = self.height + self.cvw = self.width def wait_for_window_close(self): """Wait until the Tkinter window has closed.""" @@ -118,7 +120,7 @@ def _display(self, region): image = self.disp_img_copy.resize([self.cvw, self.cvh]) self.photo = ImageTk.PhotoImage(image) if self.cv is None: - self.cv = tkinter.Canvas(self.tk_root, width=self.WIDTH, height=self.HEIGHT) + self.cv = tkinter.Canvas(self.tk_root, width=self.width, height=self.height) self.cv.pack(side='top', fill='both', expand='yes') self.cvhandle = self.cv.create_image(0, 0, image=self.photo, anchor='nw') self.cv.bind('', self.resize) @@ -130,7 +132,7 @@ def show(self, busy_wait=True): :param busy_wait: Ignored. Updates are simulated and instant. """ - print('>> Simulating {} {}x{}...'.format(self.colour, self.WIDTH, self.HEIGHT)) + print('>> Simulating {} {}x{}...'.format(self.colour, self.width, self.height)) region = self.buf @@ -234,9 +236,13 @@ class InkyMockImpression(InkyMock): [177, 106, 73], [255, 255, 255]] - def __init__(self): - """Initialize a new mock Inky Impression.""" - InkyMock.__init__(self, 'multi') + def __init__(self, resolution=None): + """Initialize a new mock Inky Impression. + + :param resolution: (width, height) in pixels, default: (600, 448) + + """ + InkyMock.__init__(self, 'multi', resolution=resolution) def _simulate(self, region): self._display(region) From b8635c03f4f9709b7ab8cbfa049efca88ea7b247 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 14:27:29 +0000 Subject: [PATCH 07/27] QA: Apply cherry-picked Black suggestions. --- inky/__init__.py | 4 ++- inky/auto.py | 6 ++-- inky/eeprom.py | 56 +++++++++++++++++++------------------- inky/inky.py | 62 +++++++++++++++++++++--------------------- inky/inky_ac073tc1a.py | 34 +++++++++++------------ inky/inky_ssd1608.py | 30 ++++++++++---------- inky/inky_ssd1683.py | 28 ++++++++++--------- inky/inky_uc8159.py | 28 +++++++++---------- inky/mock.py | 40 +++++++++++++-------------- 9 files changed, 147 insertions(+), 141 deletions(-) diff --git a/inky/__init__.py b/inky/__init__.py index 0eee3197..db6b0f86 100644 --- a/inky/__init__.py +++ b/inky/__init__.py @@ -10,11 +10,13 @@ from .phat import InkyPHAT, InkyPHAT_SSD1608 # noqa: F401 from .what import InkyWHAT # noqa: F401 -__version__ = '1.5.0' +__version__ = "1.5.0" try: from pkg_resources import declare_namespace + declare_namespace(__name__) except ImportError: from pkgutil import extend_path + __path__ = extend_path(__path__, __name__) diff --git a/inky/auto.py b/inky/auto.py index 9910c873..f37ee459 100644 --- a/inky/auto.py +++ b/inky/auto.py @@ -39,9 +39,9 @@ def auto(i2c_bus=None, ask_user=False, verbose=False): if verbose: print("Failed to detect an Inky board. Trying --type/--colour arguments instead...\n") parser = argparse.ArgumentParser() - parser.add_argument('--simulate', '-s', action='store_true', default=False, help="Simulate Inky display") - parser.add_argument('--type', '-t', type=str, required=True, choices=DISPLAY_TYPES, help="Type of display") - parser.add_argument('--colour', '-c', type=str, required=False, choices=DISPLAY_COLORS, help="Display colour") + parser.add_argument("--simulate", "-s", action="store_true", default=False, help="Simulate Inky display") + parser.add_argument("--type", "-t", type=str, required=True, choices=DISPLAY_TYPES, help="Type of display") + parser.add_argument("--colour", "-c", type=str, required=False, choices=DISPLAY_COLORS, help="Display colour") args, _ = parser.parse_known_args() if args.simulate: cls = None diff --git a/inky/eeprom.py b/inky/eeprom.py index ba926783..65a11446 100644 --- a/inky/eeprom.py +++ b/inky/eeprom.py @@ -11,33 +11,33 @@ DISPLAY_VARIANT = [ None, - 'Red pHAT (High-Temp)', - 'Yellow wHAT', - 'Black wHAT', - 'Black pHAT', - 'Yellow pHAT', - 'Red wHAT', - 'Red wHAT (High-Temp)', - 'Red wHAT', + "Red pHAT (High-Temp)", + "Yellow wHAT", + "Black wHAT", + "Black pHAT", + "Yellow pHAT", + "Red wHAT", + "Red wHAT (High-Temp)", + "Red wHAT", None, - 'Black pHAT (SSD1608)', - 'Red pHAT (SSD1608)', - 'Yellow pHAT (SSD1608)', + "Black pHAT (SSD1608)", + "Red pHAT (SSD1608)", + "Yellow pHAT (SSD1608)", None, - '7-Colour (UC8159)', - '7-Colour 640x400 (UC8159)', - '7-Colour 640x400 (UC8159)', - 'Black wHAT (SSD1683)', - 'Red wHAT (SSD1683)', - 'Yellow wHAT (SSD1683)', - '7-Colour 800x480 (AC073TC1A)' + "7-Colour (UC8159)", + "7-Colour 640x400 (UC8159)", + "7-Colour 640x400 (UC8159)", + "Black wHAT (SSD1683)", + "Red wHAT (SSD1683)", + "Yellow wHAT (SSD1683)", + "7-Colour 800x480 (AC073TC1A)", ] class EPDType: """Class to represent EPD EEPROM structure.""" - valid_colors = [None, 'black', 'red', 'yellow', None, '7colour'] + valid_colors = [None, "black", "red", "yellow", None, "7colour"] def __init__(self, width, height, color, pcb_variant, display_variant, write_time=None): """Initialise new EEPROM data structure.""" @@ -67,7 +67,7 @@ def __repr__(self): def from_bytes(class_object, data): """Initialise new EEPROM data structure from a bytes-like object or list.""" data = bytearray(data) - data = struct.unpack('', self.resize) + self.cv.pack(side="top", fill="both", expand="yes") + self.cvhandle = self.cv.create_image(0, 0, image=self.photo, anchor="nw") + self.cv.bind("", self.resize) self.tk_root.update() def show(self, busy_wait=True): @@ -132,7 +132,7 @@ def show(self, busy_wait=True): :param busy_wait: Ignored. Updates are simulated and instant. """ - print('>> Simulating {} {}x{}...'.format(self.colour, self.width, self.height)) + print(">> Simulating {} {}x{}...".format(self.colour, self.width, self.height)) region = self.buf @@ -242,14 +242,14 @@ def __init__(self, resolution=None): :param resolution: (width, height) in pixels, default: (600, 448) """ - InkyMock.__init__(self, 'multi', resolution=resolution) + InkyMock.__init__(self, "multi", resolution=resolution) def _simulate(self, region): self._display(region) def set_pixel(self, x, y, v): """Set a single pixel on the display.""" - self.buf[y][x] = v & 0xf + self.buf[y][x] = v & 0xF def set_image(self, image, saturation=0.5): """Copy an image to the display. From 532e3066909ed9567e4b1ad4ceed8f8e8a4cd7dc Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Mon, 20 Nov 2023 14:39:38 +0000 Subject: [PATCH 08/27] QA: Examples: Apply cherry-picked Black suggestions. --- examples/7color/advanced/dither.py | 2 +- examples/7color/buttons.py | 2 +- examples/7color/colour-palette.py | 36 +++++++++++++++--------------- examples/7color/cycle.py | 2 +- examples/clean.py | 2 +- examples/logo.py | 10 ++++----- examples/name-badge.py | 2 +- examples/phat/calendar-phat.py | 2 +- examples/tests/border.py | 14 ++++++------ examples/what/dither-image-what.py | 2 +- examples/what/quotes-what.py | 2 +- 11 files changed, 38 insertions(+), 38 deletions(-) diff --git a/examples/7color/advanced/dither.py b/examples/7color/advanced/dither.py index d087133a..6c554420 100755 --- a/examples/7color/advanced/dither.py +++ b/examples/7color/advanced/dither.py @@ -43,7 +43,7 @@ if len(sys.argv) > 2: saturation = float(sys.argv[2]) -palette = hitherdither.palette.Palette(inky._palette_blend(saturation, dtype='uint24')) +palette = hitherdither.palette.Palette(inky._palette_blend(saturation, dtype="uint24")) image = Image.open(sys.argv[1]).convert("RGB") image_resized = image.resize(inky.resolution) diff --git a/examples/7color/buttons.py b/examples/7color/buttons.py index 016b2d55..9559d38c 100755 --- a/examples/7color/buttons.py +++ b/examples/7color/buttons.py @@ -18,7 +18,7 @@ BUTTONS = [5, 6, 16, 24] # These correspond to buttons A, B, C and D respectively -LABELS = ['A', 'B', 'C', 'D'] +LABELS = ["A", "B", "C", "D"] # Set up RPi.GPIO with the "BCM" numbering scheme GPIO.setmode(GPIO.BCM) diff --git a/examples/7color/colour-palette.py b/examples/7color/colour-palette.py index aa6b7491..6bac6998 100755 --- a/examples/7color/colour-palette.py +++ b/examples/7color/colour-palette.py @@ -9,15 +9,15 @@ parser = argparse.ArgumentParser() -parser.add_argument('--type', '-t', choices=['css', 'act', 'raw', 'pal', 'gpl'], help='Type of palette to output') -parser.add_argument('--saturation', '-s', type=float, default=0.5, help='Colour palette saturation') -parser.add_argument('--file', '-f', type=pathlib.Path, help='Output file') +parser.add_argument("--type", "-t", choices=["css", "act", "raw", "pal", "gpl"], help="Type of palette to output") +parser.add_argument("--saturation", "-s", type=float, default=0.5, help="Colour palette saturation") +parser.add_argument("--file", "-f", type=pathlib.Path, help="Output file") args = parser.parse_args() inky = Inky() -names = ['black', 'white', 'green', 'blue', 'red', 'yellow', 'orange'] +names = ["black", "white", "green", "blue", "red", "yellow", "orange"] if args.file is None: print("You must specify an output palette file.") @@ -26,13 +26,13 @@ def raw_palette(): palette = bytearray(768) - palette[0:8 * 3] = inky._palette_blend(args.saturation, dtype='uint8') + palette[0 : 8 * 3] = inky._palette_blend(args.saturation, dtype="uint8") return palette -if args.type == 'css': - palette = inky._palette_blend(args.saturation, dtype='uint24') - with open(args.file, 'w+') as f: +if args.type == "css": + palette = inky._palette_blend(args.saturation, dtype="uint24") + with open(args.file, "w+") as f: for i in range(7): name = names[i] colour = palette[i] @@ -40,27 +40,27 @@ def raw_palette(): .{name}_bg {{background-color:#{colour:06x}}} """.format(name=name, colour=colour)) -if args.type == 'gpl': - palette = inky._palette_blend(args.saturation, dtype='uint24') - with open(args.file, 'w+') as f: +if args.type == "gpl": + palette = inky._palette_blend(args.saturation, dtype="uint24") + with open(args.file, "w+") as f: f.write("GIMP Palette\n") f.write("Name: InkyImpressions\n") f.write("Columns: 7\n") for i in range(7): name = names[i] colour = palette[i] - r = (colour & 0xff0000) >> 16 - g = (colour & 0x00ff00) >> 8 - b = (colour & 0x0000ff) + r = (colour & 0xFF0000) >> 16 + g = (colour & 0x00FF00) >> 8 + b = (colour & 0x0000FF) f.write("{r} {g} {b} Index {i} # {name}\n".format(r=r, g=g, b=b, i=i, name=name)) -if args.type in ('pal', 'raw'): +if args.type in ("pal", "raw"): palette = raw_palette() - with open(args.file, 'wb+') as f: + with open(args.file, "wb+") as f: f.write(palette) -if args.type == 'act': +if args.type == "act": palette = raw_palette() palette += struct.pack(">HH", 7, 0xFFFF) - with open(args.file, 'wb+') as f: + with open(args.file, "wb+") as f: f.write(palette) diff --git a/examples/7color/cycle.py b/examples/7color/cycle.py index 318ee01a..02d7cfb0 100755 --- a/examples/7color/cycle.py +++ b/examples/7color/cycle.py @@ -5,7 +5,7 @@ inky = auto(ask_user=True, verbose=True) -colors = ['Black', 'White', 'Green', 'Blue', 'Red', 'Yellow', 'Orange'] +colors = ["Black", "White", "Green", "Blue", "Red", "Yellow", "Orange"] for color in range(7): print("Color: {}".format(colors[color])) diff --git a/examples/clean.py b/examples/clean.py index f59f62be..9e46a7e5 100755 --- a/examples/clean.py +++ b/examples/clean.py @@ -19,7 +19,7 @@ # Command line arguments to determine number of cycles to run parser = argparse.ArgumentParser() -parser.add_argument('--number', '-n', type=int, required=False, help="number of cycles") +parser.add_argument("--number", "-n", type=int, required=False, help="number of cycles") args, _ = parser.parse_known_args() # The number of red / black / white refreshes to run diff --git a/examples/logo.py b/examples/logo.py index 9b1d6a2e..530ca721 100755 --- a/examples/logo.py +++ b/examples/logo.py @@ -30,24 +30,24 @@ if inky_display.resolution in ((212, 104), (250, 122)): if inky_display.resolution == (250, 122): - if inky_display.colour == 'black': + if inky_display.colour == "black": img = Image.open(os.path.join(PATH, "phat/resources/InkypHAT-250x122-bw.png")) else: img = Image.open(os.path.join(PATH, "phat/resources/InkypHAT-250x122.png")) else: - if inky_display.colour == 'black': + if inky_display.colour == "black": img = Image.open(os.path.join(PATH, "phat/resources/InkypHAT-212x104-bw.png")) else: img = Image.open(os.path.join(PATH, "phat/resources/InkypHAT-212x104.png")) -elif inky_display.resolution in ((400, 300), ): - if inky_display.colour == 'black': +elif inky_display.resolution in ((400, 300),): + if inky_display.colour == "black": img = Image.open(os.path.join(PATH, "what/resources/InkywHAT-400x300-bw.png")) else: img = Image.open(os.path.join(PATH, "what/resources/InkywHAT-400x300.png")) -elif inky_display.resolution in ((600, 448), ): +elif inky_display.resolution in ((600, 448),): img = Image.open(os.path.join(PATH, "what/resources/InkywHAT-400x300.png")) img = img.resize(inky_display.resolution) diff --git a/examples/name-badge.py b/examples/name-badge.py index b71098e8..ade53549 100755 --- a/examples/name-badge.py +++ b/examples/name-badge.py @@ -24,7 +24,7 @@ def getsize(font, text): raise TypeError("You need to update the Inky library to >= v1.1.0") parser = argparse.ArgumentParser() -parser.add_argument('--name', '-n', type=str, required=True, help="Your name") +parser.add_argument("--name", "-n", type=str, required=True, help="Your name") args, _ = parser.parse_known_args() # inky_display.set_rotation(180) diff --git a/examples/phat/calendar-phat.py b/examples/phat/calendar-phat.py index bf8a489b..4ca9fefa 100755 --- a/examples/phat/calendar-phat.py +++ b/examples/phat/calendar-phat.py @@ -160,7 +160,7 @@ def print_number(position, number, colour): crop_x = 2 + (16 * x) # Crop the relevant day name from our text image - crop_region = ((crop_x, 0, crop_x + 16, 9)) + crop_region = (crop_x, 0, crop_x + 16, 9) day_mask = text_mask.crop(crop_region) img.paste(inky_display.WHITE, (o_x + 4, cal_y + 2), day_mask) diff --git a/examples/tests/border.py b/examples/tests/border.py index dfeb5679..c9889cb3 100755 --- a/examples/tests/border.py +++ b/examples/tests/border.py @@ -11,14 +11,14 @@ if len(sys.argv) > 1: INKY_COLOUR = sys.argv[1] -if INKY_COLOUR not in ['red', 'yellow', 'black']: +if INKY_COLOUR not in ["red", "yellow", "black"]: print("Usage: {} ".format(sys.argv[0])) sys.exit(1) phat = InkyPHAT(INKY_COLOUR) -white = Image.new('P', (212, 104), phat.WHITE) -black = Image.new('P', (212, 104), phat.BLACK) +white = Image.new("P", (212, 104), phat.WHITE) +black = Image.new("P", (212, 104), phat.BLACK) while True: print("White") @@ -27,14 +27,14 @@ phat.show() time.sleep(1) - if INKY_COLOUR == 'red': + if INKY_COLOUR == "red": print("Red") phat.set_border(phat.RED) phat.set_image(white) phat.show() time.sleep(1) - if INKY_COLOUR == 'yellow': + if INKY_COLOUR == "yellow": print("Yellow") phat.set_border(phat.YELLOW) phat.set_image(white) @@ -47,14 +47,14 @@ phat.show() time.sleep(1) - if INKY_COLOUR == 'red': + if INKY_COLOUR == "red": print("Red") phat.set_border(phat.RED) phat.set_image(white) phat.show() time.sleep(1) - if INKY_COLOUR == 'yellow': + if INKY_COLOUR == "yellow": print("Yellow") phat.set_border(phat.YELLOW) phat.set_image(white) diff --git a/examples/what/dither-image-what.py b/examples/what/dither-image-what.py index 96bd064b..7cc4f6d7 100755 --- a/examples/what/dither-image-what.py +++ b/examples/what/dither-image-what.py @@ -19,7 +19,7 @@ # Grab the image argument from the command line parser = argparse.ArgumentParser() -parser.add_argument('--image', '-i', type=str, required=True, help="Input image to be converted/displayed") +parser.add_argument("--image", "-i", type=str, required=True, help="Input image to be converted/displayed") args, _ = parser.parse_known_args() img_file = args.image diff --git a/examples/what/quotes-what.py b/examples/what/quotes-what.py index 3cfdf31f..8aa0a7e9 100755 --- a/examples/what/quotes-what.py +++ b/examples/what/quotes-what.py @@ -96,7 +96,7 @@ def reflow_quote(quote, width, font): "Niels Bohr", "Nikola Tesla", "Rosalind Franklin", - "Stephen Hawking" + "Stephen Hawking", ] # The amount of padding around the quote. Note that From 88b8ea85d7719df152f4a159db6f6404156187ee Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 21 Nov 2023 13:46:52 +0000 Subject: [PATCH 09/27] Begin port to gpiod. --- examples/7color/buttons.py | 55 ++++++++++++---------- inky/inky.py | 59 +++++++++++++----------- inky/inky_ssd1608.py | 57 ++++++++++++----------- inky/inky_ssd1683.py | 51 +++++++++++---------- inky/inky_uc8159.py | 94 ++++++++++++++++++++------------------ 5 files changed, 170 insertions(+), 146 deletions(-) diff --git a/examples/7color/buttons.py b/examples/7color/buttons.py index 9559d38c..c699b4d9 100755 --- a/examples/7color/buttons.py +++ b/examples/7color/buttons.py @@ -1,46 +1,53 @@ #!/usr/bin/env python3 -import signal - -import RPi.GPIO as GPIO +import gpiod +import gpiodevice +from gpiod.line import Bias, Direction, Edge print("""buttons.py - Detect which button has been pressed This example should demonstrate how to: - 1. set up RPi.GPIO to read buttons, + 1. set up gpiod to read buttons, 2. determine which button has been pressed Press Ctrl+C to exit! """) -# Gpio pins for each button (from top to bottom) -BUTTONS = [5, 6, 16, 24] +# GPIO pins for each button (from top to bottom) +# These will vary depending on platform and the ones +# below should be correct for Raspberry Pi 5. +# Run "gpioinfo" to find out what yours might be +BUTTONS = ["PIN29", "PIN31", "PIN36", "PIN18"] # These correspond to buttons A, B, C and D respectively LABELS = ["A", "B", "C", "D"] -# Set up RPi.GPIO with the "BCM" numbering scheme -GPIO.setmode(GPIO.BCM) +# Create settings for all the input pins, we want them to be inputs +# with a pull-up and a falling edge detection. +INPUT = gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_UP, edge_detection=Edge.FALLING) -# Buttons connect to ground when pressed, so we should set them up -# with a "PULL UP", which weakly pulls the input signal to 3.3V. -GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP) +# Find the gpiochip device we need, we'll use +# gpiodevice for this, since it knows the right device +# for its supported platforms. +chip = gpiodevice.find_chip_by_platform() +# Build our config for each pin/line we want to use +OFFSETS = [chip.line_offset_from_id(id) for id in BUTTONS] +line_config = dict.fromkeys(OFFSETS, INPUT) -# "handle_button" will be called every time a button is pressed -# It receives one argument: the associated input pin. -def handle_button(pin): - label = LABELS[BUTTONS.index(pin)] - print("Button press detected on pin: {} label: {}".format(pin, label)) +# Request the lines, *whew* +request = chip.request_lines(consumer="inky7-buttons", config=line_config) +# "handle_button" will be called every time a button is pressed +# It receives one argument: the associated gpiod event object. +def handle_button(event): + index = OFFSETS.index(event.line_offset) + pin = BUTTONS[index] + label = LABELS[index] + print(f"Button press detected on pin: {pin} label: {label}") -# Loop through out buttons and attach the "handle_button" function to each -# We're watching the "FALLING" edge (transition from 3.3V to Ground) and -# picking a generous bouncetime of 250ms to smooth out button presses. -for pin in BUTTONS: - GPIO.add_event_detect(pin, GPIO.FALLING, handle_button, bouncetime=250) -# Finally, since button handlers don't require a "while True" loop, -# we pause the script to prevent it exiting immediately. -signal.pause() +while True: + for event in request.read_edge_events(): + handle_button(event) diff --git a/inky/inky.py b/inky/inky.py index 5ade68b7..24b01ba8 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -1,13 +1,14 @@ """Inky e-Ink Display Driver.""" import struct import time +from datetime import timedelta -from . import eeprom +import gpiod +import gpiodevice +import numpy +from gpiod.line import Direction, Edge, Value -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") +from . import eeprom __version__ = "1.5.0" @@ -17,9 +18,9 @@ RED = YELLOW = 2 # GPIO pins required by BCM number -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 # In addition the following pins are used for SPI # CS_PIN = 8 @@ -71,8 +72,7 @@ def __init__(self, resolution=(400, 300), colour="black", cs_channel=CS0, dc_pin :type spi_bus: :class:`spidev.SpiDev` :param i2c_bus: SMB object. If `None` then :class:`smbus2.SMBus(1)` is used. :type i2c_bus: :class:`smbus2.SMBus` - :param gpio: GPIO module. If `None` then `RPi.GPIO` is imported. Default: `None`. - :type gpio: :class:`RPi.GPIO` + :param gpio: deprecated """ self._spi_bus = spi_bus self._i2c_bus = i2c_bus @@ -222,16 +222,22 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + + if gpiodevice.check_pins_available(gpiochip, { + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING) + }) if self._spi_bus is None: import spidev @@ -242,18 +248,19 @@ def setup(self): self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.1) self._send_command(0x12) # Soft Reset self._busy_wait() - def _busy_wait(self): + def _busy_wait(self, timeout=30.0): """Wait for busy/wait pin.""" - while self._gpio.input(self.busy_pin) != self._gpio.LOW: - time.sleep(0.01) + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -376,7 +383,7 @@ def _spi_write(self, dc, values): :param dc: whether to write as data or command :param values: list of values to write """ - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) except AttributeError: diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 80d21f8f..0e272d0d 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -1,22 +1,22 @@ """Inky e-Ink Display Driver.""" import time +from datetime import timedelta +import gpiod +import gpiodevice +import numpy +from gpiod.line import Direction, Edge, Value from PIL import Image from . import eeprom, ssd1608 -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") - WHITE = 0 BLACK = 1 RED = YELLOW = 2 -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 MOSI_PIN = 10 SCLK_PIN = 11 @@ -126,17 +126,22 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev @@ -148,9 +153,9 @@ def setup(self): self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.5) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.5) self._send_command(0x12) # Soft Reset @@ -159,11 +164,9 @@ def setup(self): def _busy_wait(self, timeout=5.0): """Wait for busy/wait pin.""" - t_start = time.time() - while self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - raise RuntimeError("Timeout waiting for busy signal to clear.") + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -269,7 +272,7 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) except AttributeError: diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index f299be1e..1d73cea4 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -1,15 +1,15 @@ """Inky e-Ink Display Driver.""" import time +from datetime import timedelta +import gpiod +import gpiodevice +import numpy +from gpiod.line import Direction, Edge, Value from PIL import Image from . import eeprom, ssd1683 -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") - WHITE = 0 BLACK = 1 RED = YELLOW = 2 @@ -114,17 +114,22 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev @@ -136,9 +141,9 @@ def setup(self): self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.5) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.5) self._send_command(0x12) # Soft Reset @@ -147,11 +152,9 @@ def setup(self): def _busy_wait(self, timeout=5.0): """Wait for busy/wait pin.""" - t_start = time.time() - while self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - raise RuntimeError("Timeout waiting for busy signal to clear.") + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -257,7 +260,7 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) except AttributeError: diff --git a/inky/inky_uc8159.py b/inky/inky_uc8159.py index bd453cc4..004b7027 100644 --- a/inky/inky_uc8159.py +++ b/inky/inky_uc8159.py @@ -2,19 +2,16 @@ import struct import time import warnings +from datetime import timedelta -try: - from PIL import Image -except ImportError: - Image = None +import gpiod +import gpiodevice +import numpy +from gpiod.line import Direction, Edge, Value +from PIL import Image from . import eeprom -try: - import numpy -except ImportError: - raise ImportError("This library requires the numpy module\nInstall with: sudo apt install python-numpy") - BLACK = 0 WHITE = 1 GREEN = 2 @@ -46,9 +43,9 @@ [255, 255, 255] ] -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 MOSI_PIN = 10 SCLK_PIN = 11 @@ -157,7 +154,7 @@ def __init__(self, resolution=None, colour="multi", cs_pin=CS0_PIN, dc_pin=DC_PI resolution = _RESOLUTION_5_7_INCH if resolution not in _RESOLUTION.keys(): - raise ValueError("Resolution {}x{} not supported!".format(*resolution)) + raise ValueError(f"Resolution {resolution[0]}x{resolution[1]} not supported!") self.resolution = resolution self.width, self.height = resolution @@ -166,7 +163,7 @@ def __init__(self, resolution=None, colour="multi", cs_pin=CS0_PIN, dc_pin=DC_PI self.cols, self.rows, self.rotation, self.offset_x, self.offset_y, self.resolution_setting = _RESOLUTION[resolution] if colour not in ("multi"): - raise ValueError("Colour {} is not supported!".format(colour)) + raise ValueError(f"Colour {colour} is not supported!") self.colour = colour self.lut = colour @@ -209,31 +206,42 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.cs_pin, self._gpio.OUT, initial=self._gpio.HIGH) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.RISING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev self._spi_bus = spidev.SpiDev() self._spi_bus.open(0, self.cs_channel) - self._spi_bus.no_cs = True + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") self._spi_bus.max_speed_hz = 3000000 self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) self._busy_wait(1.0) @@ -319,19 +327,18 @@ def _busy_wait(self, timeout=40.0): # If the busy_pin is *high* (pulled up by host) # then assume we're not getting a signal from inky # and wait the timeout period to be safe. - if self._gpio.input(self.busy_pin): - warnings.warn("Busy Wait: Held high. Waiting for {:0.2f}s".format(timeout)) + if self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + warnings.warn(f"Busy Wait: Held high. Waiting for {timeout:0.2f}s") time.sleep(timeout) return - # If the busy_pin is *low* (pulled down by inky) - # then wait for it to high. - t_start = time.time() - while not self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - warnings.warn("Busy Wait: Timed out after {:0.2f}s".format(time.time() - t_start)) - return + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + warnings.warn(f"Busy Wait: Timed out after {timeout:0.2f}s") + return + + for event in self._gpio.read_edge_events(): + print(timeout, event) def _update(self, buf): """Update display. @@ -343,7 +350,6 @@ def _update(self, buf): """ self.setup() - self._send_command(UC8159_DTM1, buf) self._send_command(UC8159_PON) @@ -401,10 +407,8 @@ def set_image(self, image, saturation=0.5): """ if not image.size == (self.width, self.height): - raise ValueError("Image must be ({}x{}) pixels!".format(self.width, self.height)) + raise ValueError(f"Image must be ({self.width}x{self.height}) pixels!") if not image.mode == "P": - if Image is None: - raise RuntimeError("PIL is required for converting images: sudo apt install python-pil python3-pil") palette = self._palette_blend(saturation) # Image size doesn't matter since it's just the palette we're using palette_image = Image.new("P", (1, 1)) @@ -422,8 +426,8 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.cs_pin, 0) - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.cs_pin, Value.INACTIVE) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) if isinstance(values, str): values = [ord(c) for c in values] @@ -434,7 +438,7 @@ def _spi_write(self, dc, values): for x in range(((len(values) - 1) // _SPI_CHUNK_SIZE) + 1): offset = x * _SPI_CHUNK_SIZE self._spi_bus.xfer(values[offset : offset + _SPI_CHUNK_SIZE]) - self._gpio.output(self.cs_pin, 1) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) def _send_command(self, command, data=None): """Send command over SPI. From 4feb1e4243993d4b5d1ec25b829ac22ca8f5a185 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 21 Nov 2023 13:47:26 +0000 Subject: [PATCH 10/27] Add auto dither to what. --- examples/7color/image.py | 28 +++++++++++++++++++--------- inky/inky_ssd1608.py | 18 +++++++++++++++--- inky/inky_ssd1683.py | 14 +++++++++++++- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/examples/7color/image.py b/examples/7color/image.py index 62fcc956..3d2ab1cc 100755 --- a/examples/7color/image.py +++ b/examples/7color/image.py @@ -1,25 +1,35 @@ #!/usr/bin/env python3 +import argparse +import pathlib import sys from PIL import Image from inky.auto import auto +parser = argparse.ArgumentParser() + +parser.add_argument("--saturation", "-s", type=float, default=0.5, help="Colour palette saturation") +parser.add_argument("--file", "-f", type=pathlib.Path, help="Image file") + inky = auto(ask_user=True, verbose=True) -saturation = 0.5 -if len(sys.argv) == 1: - print(""" -Usage: {file} image-file -""".format(file=sys.argv[0])) +args, _ = parser.parse_known_args() + +saturation = args.saturation + +if not args.file: + print(f"""Usage: + {sys.argv[0]} --file image.png (--saturation 0.5)""") sys.exit(1) -image = Image.open(sys.argv[1]) +image = Image.open(args.file) resizedimage = image.resize(inky.resolution) -if len(sys.argv) > 2: - saturation = float(sys.argv[2]) +try: + inky.set_image(resizedimage, saturation=saturation) +except TypeError: + inky.set_image(resizedimage) -inky.set_image(resizedimage, saturation=saturation) inky.show() diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 0e272d0d..6abae4e3 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -261,9 +261,21 @@ def set_border(self, colour): def set_image(self, image): """Copy an image to the display.""" - canvas = Image.new("P", (self.rows, self.cols)) - canvas.paste(image, (self.offset_x, self.offset_y)) - self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.cols, self.rows)) + if not image.mode == "P": + palette_image = Image.new("P", (1, 1)) + r, g, b = 0, 0, 0 + if self.colour == "red": + r = 255 + if self.colour == "yellow": + g = 255 + palette_image.putpalette([255, 255, 255, 0, 0, 0, r, g, b] + [0, 0, 0] * 252) + image.load() + image = image.im.convert("P", True, palette_image.im) + + canvas = Image.new("P", (self.cols, self.rows)) + width, height = image.size + canvas.paste(image, (self.offset_x, self.offset_y, width, height)) + self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.rows, self.cols)) def _spi_write(self, dc, values): """Write values over SPI. diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index 1d73cea4..07271431 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -249,8 +249,20 @@ def set_border(self, colour): def set_image(self, image): """Copy an image to the display.""" + if not image.mode == "P": + palette_image = Image.new("P", (1, 1)) + r, g, b = 0, 0, 0 + if self.colour == "red": + r = 255 + if self.colour == "yellow": + g = 255 + palette_image.putpalette([255, 255, 255, 0, 0, 0, r, g, b] + [0, 0, 0] * 252) + image.load() + image = image.im.convert("P", True, palette_image.im) + canvas = Image.new("P", (self.cols, self.rows)) - canvas.paste(image, (self.offset_x, self.offset_y)) + width, height = image.size + canvas.paste(image, (self.offset_x, self.offset_y, width, height)) self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.rows, self.cols)) def _spi_write(self, dc, values): From e5ab9ee3e3e6057b3272ef030201e12a4ac71b1c Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 11 Jan 2024 11:05:16 +0000 Subject: [PATCH 11/27] Sync with boilerplate. * Fix VENV_DIR path. * Fix auto_venv.sh creation. * install.sh: normalise whitespace. * install.sh: rework for better error reporting and fix some bugs. * README.md: Correct coveralls badge branch. * CI: Update GitHub Actions versions. * QA: Add shellcheck and fix/ignore all issues. * install.sh: slightly better feedback for setup commands. * install.sh: fix quoting bug in do_config_backup. * install.sh: don't output printf commands. --- .github/workflows/build.yml | 6 +- .github/workflows/qa.yml | 9 +- .github/workflows/test.yml | 2 +- Makefile | 5 +- README.md | 2 +- check.sh | 20 ++- install.sh | 245 ++++++++++++++++++++++-------------- uninstall.sh | 14 +-- 8 files changed, 183 insertions(+), 120 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87200efb..07620e34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -35,7 +35,7 @@ jobs: make build - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.RELEASE_FILE }} path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4f858832..ac672a52 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -10,16 +10,15 @@ jobs: test: name: linting & spelling runs-on: ubuntu-latest - env: TERM: xterm-256color steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python '3,11' - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -34,3 +33,7 @@ jobs: - 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 016a6780..6f8cff73 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/Makefile b/Makefile index 9e0c15c5..34f4a7dd 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,14 @@ uninstall: dev-deps: python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix + sudo apt install dos2unix shellcheck check: @bash check.sh +shellcheck: + shellcheck *.sh + qa: tox -e qa diff --git a/README.md b/README.md index 406a8e85..27c0f4b8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Inky [![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/inky/test.yml?branch=main)](https://github.com/pimoroni/inky/actions/workflows/test.yml) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/inky/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/inky?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/pimoroni/inky/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/inky?branch=main) [![PyPi Package](https://img.shields.io/pypi/v/inky.svg)](https://pypi.python.org/pypi/inky) [![Python Versions](https://img.shields.io/pypi/pyversions/inky.svg)](https://pypi.python.org/pypi/inky) diff --git a/check.sh b/check.sh index 4395d89c..38dfc3a1 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,9 @@ # 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))}'` +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() { @@ -29,7 +29,7 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; + printf "Unrecognised option: %s\n" "$1"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -40,8 +40,7 @@ done inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" inform "Checking for trailing whitespace..." -grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO -if [[ $? -eq 0 ]]; then +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 @@ -50,8 +49,7 @@ fi printf "\n" inform "Checking for DOS line-endings..." -grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile -if [[ $? -eq 0 ]]; then +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 @@ -60,8 +58,7 @@ fi printf "\n" inform "Checking CHANGELOG.md..." -cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 -if [[ $? -eq 1 ]]; then +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 @@ -70,8 +67,7 @@ fi printf "\n" inform "Checking for git tag ${LIBRARY_VERSION}..." -git tag -l | grep -E "${LIBRARY_VERSION}$" -if [[ $? -eq 1 ]]; then +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then warning "Missing git tag for version ${LIBRARY_VERSION}" fi printf "\n" diff --git a/install.sh b/install.sh index 38f19e9a..bb671f29 100755 --- a/install.sh +++ b/install.sh @@ -1,25 +1,24 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +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"` +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/Pimoroni -VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh -VENV_DIR=$HOME/.virtualenvs/pimoroni -WD=`pwd` +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 - printf "Script should not be run as root. Try './install.sh'\n" - exit 1 + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -36,15 +35,6 @@ confirm() { 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)" } @@ -54,29 +44,36 @@ inform() { } warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" + 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 - warning "Could not find $CONFIG_FILE!" - exit 1 + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" + fi + else + if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then + warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" + warning "You might want to fix this!" fi - else - if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then - warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" - warning "You might want to fix this!" - fi fi - inform "Using $CONFIG_FILE in $CONFIG_DIR" + inform "Using $CONFIG_FILE in $CONFIG_DIR" } venv_bash_snippet() { - if [ ! -f $VENV_BASH_SNIPPET ]; then - cat << EOF > $VENV_BASH_SNIPPET -# Add `source $RESOURCES_DIR/auto_venv.sh` to your ~/.bashrc to activate + 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 @@ -91,26 +88,37 @@ EOF } venv_check() { - PYTHON_BIN=`which $PYTHON` + 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 one for you?"; then - if [ ! -f $VENV_DIR/bin/activate ]; then - inform "Creating virtual Python environment in $VENV_DIR, please wait...\n" - mkdir -p $VENV_DIR - /usr/bin/python3 -m venv $VENV_DIR --system-site-packages + 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 "Found existing virtual Python environment in $VENV_DIR\n" + 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 - inform "Activating virtual Python environment in $VENV_DIR..." - inform "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate - else - exit 1 + 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 { @@ -118,43 +126,48 @@ function do_config_backup { 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 + 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 + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" fi fi } function apt_pkg_install { - PACKAGES=() + 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 $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") + PACKAGES_NEEDED+=("$PACKAGE") fi done - PACKAGES="${PACKAGES[@]}" + PACKAGES="${PACKAGES_NEEDED[*]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y $PACKAGES + sudo apt install -y "$PACKAGES" + check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + 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 @@ -175,8 +188,8 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -184,57 +197,67 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing %s...\n\n" "$LIBRARY_NAME" + user_check venv_check -if [ ! -f `which $PYTHON` ]; then - printf "Python path $PYTHON not found!\n" - exit 1 +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` - -printf "$LIBRARY_NAME Python Library: Installer\n\n" +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 +# Create a stub uninstaller file, we'll try to add the inverse of every +# install command run to here, though it's not complete. +cat << EOF > "$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" @@ -242,65 +265,87 @@ exit 1 source $VIRTUAL_ENV/bin/activate EOF -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi +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 - pip_pkg_install $LIBRARY_NAME + 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 + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" fi -cd $WD - 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 - eval $CMD + 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\n" + 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 "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + 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 + 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 - printf "Generating documentation.\n" - $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + 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 @@ -308,6 +353,22 @@ if confirm "Would you like to generate documentation?"; then fi fi -success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" -inform "Find uninstall steps in $UNINSTALLER\n" +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/uninstall.sh b/uninstall.sh index f213fc52..3314b7fc 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,13 +1,13 @@ #!/bin/bash FORCE=false -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +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` + 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 @@ -15,7 +15,7 @@ venv_check() { } user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi @@ -55,17 +55,17 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } -printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" +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 +$PYTHON -m pip uninstall "$LIBRARY_NAME" -if [ -d $RESOURCES_DIR ]; then +if [ -d "$RESOURCES_DIR" ]; then if confirm "Would you like to delete $RESOURCES_DIR?"; then - rm -r $RESOURCES_DIR + rm -r "$RESOURCES_DIR" fi fi From 0fb414ad76035a203ea3b44d3cc71ee08428eb35 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 6 Feb 2024 10:47:33 +0000 Subject: [PATCH 12/27] Inky 7.3: port to gpiod. --- inky/inky_ac073tc1a.py | 83 +++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/inky/inky_ac073tc1a.py b/inky/inky_ac073tc1a.py index 40cd5615..88a16f3c 100644 --- a/inky/inky_ac073tc1a.py +++ b/inky/inky_ac073tc1a.py @@ -1,11 +1,12 @@ """Inky e-Ink Display Driver.""" import time import warnings +from datetime import timedelta -try: - from PIL import Image -except ImportError: - Image = None +import gpiod +import gpiodevice +from gpiod.line import Direction, Edge, Value +from PIL import Image from . import eeprom @@ -23,9 +24,9 @@ ORANGE = 6 CLEAN = 7 -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +RESET_PIN = "PIN13" # GPIO 27 +BUSY_PIN = "PIN11" # GPIO 17 +DC_PIN = "PIN15" # GPIO 22 MOSI_PIN = 10 SCLK_PIN = 11 @@ -192,36 +193,47 @@ def setup(self): """Set up Inky GPIO and reset display.""" if not self._gpio_setup: if self._gpio is None: - try: - import RPi.GPIO as GPIO - self._gpio = GPIO - except ImportError: - raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo apt install python-rpi.gpio") - self._gpio.setmode(self._gpio.BCM) - self._gpio.setwarnings(False) - self._gpio.setup(self.cs_pin, self._gpio.OUT) - self._gpio.setup(self.dc_pin, self._gpio.OUT, initial=self._gpio.LOW, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.reset_pin, self._gpio.OUT, initial=self._gpio.HIGH, pull_up_down=self._gpio.PUD_OFF) - self._gpio.setup(self.busy_pin, self._gpio.IN, pull_up_down=self._gpio.PUD_OFF) + gpiochip = gpiodevice.find_chip_by_platform() + gpiodevice.friendly_errors = True + if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, + "Data/Command": self.dc_pin, + "Reset": self.reset_pin, + "Busy": self.busy_pin + }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) + self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) + self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) + self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) + + self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.RISING, debounce_period=timedelta(milliseconds=10)) + }) if self._spi_bus is None: import spidev self._spi_bus = spidev.SpiDev() self._spi_bus.open(0, self.cs_channel) - self._spi_bus.no_cs = True + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") self._spi_bus.max_speed_hz = 5000000 self._gpio_setup = True - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.LOW) + self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) - self._gpio.output(self.reset_pin, self._gpio.HIGH) + self._gpio.set_value(self.reset_pin, Value.ACTIVE) self._busy_wait(1.0) @@ -269,21 +281,18 @@ def _busy_wait(self, timeout=40.0): # If the busy_pin is *high* (pulled up by host) # then assume we're not getting a signal from inky # and wait the timeout period to be safe. - if self._gpio.input(self.busy_pin): + if self._gpio.get_value(self.busy_pin) == Value.ACTIVE: warnings.warn("Busy Wait: Held high. Waiting for {:0.2f}s".format(timeout)) time.sleep(timeout) return - # If the busy_pin is *low* (pulled down by inky) - # then wait for it to high. - t_start = time.time() - while not self._gpio.input(self.busy_pin): - time.sleep(0.01) - if time.time() - t_start >= timeout: - warnings.warn("Busy Wait: Timed out after {:0.2f}s".format(time.time() - t_start)) - return + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + warnings.warn(f"Busy Wait: Timed out after {timeout:0.2f}s") + return - # print("Busy_waited", time.time()-t_start, "out of", timeout, "seconds") + for event in self._gpio.read_edge_events(): + print(timeout, event) def _update(self, buf): """Update display. @@ -366,8 +375,6 @@ def set_image(self, image, saturation=0.5): if not image.size == (self.width, self.height): raise ValueError("Image must be ({}x{}) pixels!".format(self.width, self.height)) if not image.mode == "P": - if Image is None: - raise RuntimeError("PIL is required for converting images: sudo apt install python-pil python3-pil") palette = self._palette_blend(saturation) # Image size doesn't matter since it's just the palette we're using palette_image = Image.new("P", (1, 1)) @@ -385,8 +392,8 @@ def _spi_write(self, dc, values): :param values: list of values to write """ - self._gpio.output(self.cs_pin, 0) - self._gpio.output(self.dc_pin, dc) + self._gpio.set_value(self.cs_pin, Value.INACTIVE) + self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) if isinstance(values, str): values = [ord(c) for c in values] @@ -394,7 +401,7 @@ def _spi_write(self, dc, values): for byte_value in values: self._spi_bus.xfer([byte_value]) - self._gpio.output(self.cs_pin, 1) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) def _send_command(self, command, data=None): """Send command over SPI. From b14439a76791e9e5dac5d4207664008228398ffd Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 6 Feb 2024 11:01:13 +0000 Subject: [PATCH 13/27] Packaging: Add dependencies and setup commands. --- pyproject.toml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ccc0a1d..be23fda7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,8 @@ classifiers = [ dependencies = [ "numpy", "smbus2", - "spidev" + "spidev", + "gpiodevice>=0.0.3" ] [project.optional-dependencies] @@ -130,5 +131,11 @@ ignore = [ [tool.pimoroni] apt_packages = [] -configtxt = [] -commands = [] +configtxt = [ + "dtoverlay=i2c1", + "dtoverlay=i2c1-pi5", + "dtoverlay=spi0-0cs" +] +commands = [ + "sudo raspi-config nonint do_spi 0" +] From f1eebdef659a532b6ba14ccdc615f811a14d95fd Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 6 Feb 2024 11:12:20 +0000 Subject: [PATCH 14/27] QA: Fix tests. GPIO setup tests have been pruned and could use rewriting. PIL is now always assumed to be installed. --- tests/conftest.py | 34 +++++++++++---- tests/test_init.py | 82 ++++++----------------------------- tests/test_install_helpers.py | 19 +------- 3 files changed, 40 insertions(+), 95 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 952b4ee4..964421c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,17 +12,33 @@ from tools import MockSMBus +@pytest.fixture(scope='function', autouse=True) +def cleanup(): + for module in list(sys.modules.keys()): + if module.startswith('inky'): + del sys.modules[module] + + +@pytest.fixture(scope='function', autouse=False) +def nopath(): + path = sys.path + sys.path = [path for path in sys.path if not path.startswith("/usr/lib") and not path.startswith("/opt/hostedtoolcache")] + yield + sys.path = path + + @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.MagicMock() - sys.modules['RPi'].GPIO = GPIO - sys.modules['RPi.GPIO'] = GPIO - yield GPIO - del sys.modules['RPi'] - del sys.modules['RPi.GPIO'] + """Mock gpiod and gpiodevice modules.""" + gpiod = mock.MagicMock() + gpiodevice = mock.MagicMock() + sys.modules['gpiod'] = gpiod + sys.modules['gpiodevice'] = gpiodevice + sys.modules['gpiodevice.platform'] = mock.MagicMock() + yield gpiod, gpiodevice + del sys.modules['gpiod'] + del sys.modules['gpiodevice'] + del sys.modules['gpiodevice.platform'] @pytest.fixture(scope='function', autouse=False) diff --git a/tests/test_init.py b/tests/test_init.py index fe6e0866..ea46c7da 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -1,7 +1,5 @@ """Initialization tests for Inky.""" -from unittest import mock - import pytest @@ -19,49 +17,49 @@ def test_init_mock_what_black(tkinter, PIL): InkyMockWHAT('black') -def test_init_phat_black(spidev, smbus2): +def test_init_phat_black(spidev, smbus2, PIL): """Test initialisation of InkyPHAT with 'black' colour choice.""" from inky import InkyPHAT InkyPHAT('black') -def test_init_phat_red(spidev, smbus2): +def test_init_phat_red(spidev, smbus2, PIL): """Test initialisation of InkyPHAT with 'red' colour choice.""" from inky import InkyPHAT InkyPHAT('red') -def test_init_phat_yellow(spidev, smbus2): +def test_init_phat_yellow(spidev, smbus2, PIL): """Test initialisation of InkyPHAT with 'yellow' colour choice.""" from inky import InkyPHAT InkyPHAT('red') -def test_init_what_black(spidev, smbus2): +def test_init_what_black(spidev, smbus2, PIL): """Test initialisation of InkyWHAT with 'black' colour choice.""" from inky import InkyWHAT InkyWHAT('black') -def test_init_what_red(spidev, smbus2): +def test_init_what_red(spidev, smbus2, PIL): """Test initialisation of InkyWHAT with 'red' colour choice.""" from inky import InkyWHAT InkyWHAT('red') -def test_init_what_yellow(spidev, smbus2): +def test_init_what_yellow(spidev, smbus2, PIL): """Test initialisation of InkyWHAT with 'yellow' colour choice.""" from inky import InkyWHAT InkyWHAT('yellow') -def test_init_invalid_colour(spidev, smbus2): +def test_init_invalid_colour(spidev, smbus2, PIL): """Test initialisation of InkyWHAT with an invalid colour choice.""" from inky import InkyWHAT @@ -69,17 +67,7 @@ def test_init_invalid_colour(spidev, smbus2): InkyWHAT('octarine') -def test_init_what_setup_no_gpio(spidev, smbus2): - """Test Inky init with a missing RPi.GPIO library.""" - from inky import InkyWHAT - - inky = InkyWHAT('red') - - with pytest.raises(ImportError): - inky.setup() - - -def test_init_what_setup(spidev, smbus2, GPIO): +def test_init_what_setup(spidev, smbus2, GPIO, PIL): """Test initialisation and setup of InkyWHAT. Verify our expectations for GPIO setup in order to catch regressions. @@ -87,46 +75,17 @@ def test_init_what_setup(spidev, smbus2, GPIO): """ from inky import InkyWHAT - # TODO: _busy_wait should timeout after N seconds - GPIO.input.return_value = GPIO.LOW + # _busy_wait will timeout after N seconds + # GPIO.input.return_value = GPIO.LOW inky = InkyWHAT('red') inky.setup() - # Check GPIO setup - GPIO.setwarnings.assert_called_with(False) - GPIO.setmode.assert_called_with(GPIO.BCM) - GPIO.setup.assert_has_calls([ - mock.call(inky.dc_pin, GPIO.OUT, initial=GPIO.LOW, pull_up_down=GPIO.PUD_OFF), - mock.call(inky.reset_pin, GPIO.OUT, initial=GPIO.HIGH, pull_up_down=GPIO.PUD_OFF), - mock.call(inky.busy_pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF) - ]) - - # Check device will been reset - GPIO.output.assert_has_calls([ - mock.call(inky.reset_pin, GPIO.LOW), - mock.call(inky.reset_pin, GPIO.HIGH) - ]) - # Check API will been opened spidev.SpiDev().open.assert_called_with(0, inky.cs_channel) -def test_init_7colour_setup_no_gpio(spidev, smbus2): - """Test initialisation and setup of 7-colour Inky. - - Verify an error is raised when RPi.GPIO is not present. - - """ - from inky.inky_uc8159 import Inky - - inky = Inky() - - with pytest.raises(ImportError): - inky.setup() - - -def test_init_7colour_setup(spidev, smbus2, GPIO): +def test_init_7colour_setup(spidev, smbus2, GPIO, PIL): """Test initialisation and setup of 7-colour Inky. Verify our expectations for GPIO setup in order to catch regressions. @@ -134,26 +93,11 @@ def test_init_7colour_setup(spidev, smbus2, GPIO): """ from inky.inky_uc8159 import Inky - # TODO: _busy_wait should timeout after N seconds - GPIO.input.return_value = GPIO.LOW + # _busy_wait will timeout after N seconds + # GPIO.input.return_value = GPIO.LOW inky = Inky() inky.setup() - # Check GPIO setup - GPIO.setwarnings.assert_called_with(False) - GPIO.setmode.assert_called_with(GPIO.BCM) - GPIO.setup.assert_has_calls([ - mock.call(inky.dc_pin, GPIO.OUT, initial=GPIO.LOW, pull_up_down=GPIO.PUD_OFF), - mock.call(inky.reset_pin, GPIO.OUT, initial=GPIO.HIGH, pull_up_down=GPIO.PUD_OFF), - mock.call(inky.busy_pin, GPIO.IN, pull_up_down=GPIO.PUD_OFF) - ]) - - # Check device will been reset - GPIO.output.assert_has_calls([ - mock.call(inky.reset_pin, GPIO.LOW), - mock.call(inky.reset_pin, GPIO.HIGH) - ]) - # Check API will been opened spidev.SpiDev().open.assert_called_with(0, inky.cs_channel) diff --git a/tests/test_install_helpers.py b/tests/test_install_helpers.py index 55c20395..e90e0cbe 100644 --- a/tests/test_install_helpers.py +++ b/tests/test_install_helpers.py @@ -8,7 +8,7 @@ import pytest -def test_mock_phat_no_tkinter(): +def test_mock_phat_no_tkinter(PIL, nopath): """Test initialisation of InkyMockPHAT without tkinter.""" from inky import InkyMockPHAT @@ -16,25 +16,10 @@ def test_mock_phat_no_tkinter(): InkyMockPHAT('black') -def test_mock_what_no_tkinter(): +def test_mock_what_no_tkinter(PIL, nopath): """Test initialisation of InkyMockWHAT without tkinter.""" from inky import InkyMockWHAT with pytest.raises(ImportError): InkyMockWHAT('black') - -def test_mock_phat_no_pil(tkinter): - """Test initialisation of InkyMockPHAT without PIL.""" - from inky import InkyMockPHAT - - with pytest.raises(ImportError): - InkyMockPHAT('black') - - -def test_mock_what_no_pil(tkinter): - """Test initialisation of InkyMockWHAT without PIL.""" - from inky import InkyMockWHAT - - with pytest.raises(ImportError): - InkyMockWHAT('black') From 70d8d228e39812c36c91ce3c140587bf23774512 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 6 Feb 2024 16:07:37 +0000 Subject: [PATCH 15/27] All: Always use software CS. --- inky/inky.py | 54 ++++++++++++++++++++++++++---------------- inky/inky_ac073tc1a.py | 12 +++++++--- inky/inky_ssd1608.py | 12 +++++++--- inky/inky_ssd1683.py | 48 ++++++++++++++++++++++++++----------- inky/inky_uc8159.py | 31 ++++++++++++++---------- 5 files changed, 104 insertions(+), 53 deletions(-) diff --git a/inky/inky.py b/inky/inky.py index 24b01ba8..dbf5f650 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -6,7 +6,8 @@ import gpiod import gpiodevice import numpy -from gpiod.line import Direction, Edge, Value +from gpiod.line import Bias, Direction, Edge, Value +from gpiodevice import platform from . import eeprom @@ -17,19 +18,19 @@ BLACK = 1 RED = YELLOW = 2 -# GPIO pins required by BCM number -RESET_PIN = "PIN13" # GPIO 27 -BUSY_PIN = "PIN11" # GPIO 17 -DC_PIN = "PIN15" # GPIO 22 +if platform.get_name().startswith("Raspberry Pi 5"): + RESET_PIN = "PIN13" # GPIO 27 + BUSY_PIN = "PIN11" # GPIO 17 + DC_PIN = "PIN15" # GPIO 22 +else: + RESET_PIN = "GPIO27" + BUSY_PIN = "GPIO17" + DC_PIN = "GPIO22" # In addition the following pins are used for SPI -# CS_PIN = 8 -# MOSI_PIN = 10 -# SCLK_PIN = 11 -# SCLK_PIN = 11 - -# SPI channel for device 0 -CS0 = 0 +MOSI_PIN = 10 +SCLK_PIN = 11 +CS0_PIN = 8 _SPI_CHUNK_SIZE = 4096 _SPI_COMMAND = 0 @@ -55,7 +56,7 @@ class Inky: RED = 2 YELLOW = 2 - def __init__(self, resolution=(400, 300), colour="black", cs_channel=CS0, dc_pin=DC_PIN, reset_pin=RESET_PIN, busy_pin=BUSY_PIN, h_flip=False, v_flip=False, + def __init__(self, resolution=(400, 300), colour="black", cs_pin=CS0_PIN, dc_pin=DC_PIN, reset_pin=RESET_PIN, busy_pin=BUSY_PIN, h_flip=False, v_flip=False, spi_bus=None, i2c_bus=None, gpio=None): """Initialise an Inky Display. @@ -103,7 +104,11 @@ def __init__(self, resolution=(400, 300), colour="black", cs_channel=CS0, dc_pin self.dc_pin = dc_pin self.reset_pin = reset_pin self.busy_pin = busy_pin - self.cs_channel = cs_channel + self.cs_pin = cs_pin + try: + self.cs_channel = [8, 7].index(cs_pin) + except ValueError: + self.cs_channel = 0 self.h_flip = h_flip self.v_flip = v_flip @@ -225,18 +230,21 @@ def setup(self): gpiochip = gpiodevice.find_chip_by_platform() if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, "Data/Command": self.dc_pin, "Reset": self.reset_pin, "Busy": self.busy_pin }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) self._gpio = gpiochip.request_lines(consumer="inky", config={ - self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT), - self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT), - self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING) + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE, bias=Bias.DISABLED), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, bias=Bias.DISABLED) }) if self._spi_bus is None: @@ -258,9 +266,12 @@ def setup(self): def _busy_wait(self, timeout=30.0): """Wait for busy/wait pin.""" - event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) - if not event: - raise RuntimeError("Timeout waiting for busy signal to clear.") + if self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") + for event in self._gpio.read_edge_events(): + pass def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -383,6 +394,7 @@ def _spi_write(self, dc, values): :param dc: whether to write as data or command :param values: list of values to write """ + self._gpio.set_value(self.cs_pin, Value.INACTIVE) self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) @@ -391,6 +403,8 @@ def _spi_write(self, dc, values): offset = x * _SPI_CHUNK_SIZE self._spi_bus.xfer(values[offset : offset + _SPI_CHUNK_SIZE]) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) + def _send_command(self, command, data=None): """Send command over SPI. diff --git a/inky/inky_ac073tc1a.py b/inky/inky_ac073tc1a.py index 88a16f3c..c29054f7 100644 --- a/inky/inky_ac073tc1a.py +++ b/inky/inky_ac073tc1a.py @@ -6,6 +6,7 @@ import gpiod import gpiodevice from gpiod.line import Direction, Edge, Value +from gpiodevice import platform from PIL import Image from . import eeprom @@ -24,9 +25,14 @@ ORANGE = 6 CLEAN = 7 -RESET_PIN = "PIN13" # GPIO 27 -BUSY_PIN = "PIN11" # GPIO 17 -DC_PIN = "PIN15" # GPIO 22 +if platform.get_name().startswith("Raspberry Pi 5"): + RESET_PIN = "PIN13" # GPIO 27 + BUSY_PIN = "PIN11" # GPIO 17 + DC_PIN = "PIN15" # GPIO 22 +else: + RESET_PIN = "GPIO27" + BUSY_PIN = "GPIO17" + DC_PIN = "GPIO22" MOSI_PIN = 10 SCLK_PIN = 11 diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 6abae4e3..0e565ba8 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -6,6 +6,7 @@ import gpiodevice import numpy from gpiod.line import Direction, Edge, Value +from gpiodevice import platform from PIL import Image from . import eeprom, ssd1608 @@ -14,9 +15,14 @@ BLACK = 1 RED = YELLOW = 2 -RESET_PIN = "PIN13" # GPIO 27 -BUSY_PIN = "PIN11" # GPIO 17 -DC_PIN = "PIN15" # GPIO 22 +if platform.get_name().startswith("Raspberry Pi 5"): + RESET_PIN = "PIN13" # GPIO 27 + BUSY_PIN = "PIN11" # GPIO 17 + DC_PIN = "PIN15" # GPIO 22 +else: + RESET_PIN = "GPIO27" + BUSY_PIN = "GPIO17" + DC_PIN = "GPIO22" MOSI_PIN = 10 SCLK_PIN = 11 diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index 07271431..2da3f71b 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -5,7 +5,8 @@ import gpiod import gpiodevice import numpy -from gpiod.line import Direction, Edge, Value +from gpiod.line import Bias, Direction, Edge, Value +from gpiodevice import platform from PIL import Image from . import eeprom, ssd1683 @@ -14,13 +15,18 @@ BLACK = 1 RED = YELLOW = 2 -RESET_PIN = 27 -BUSY_PIN = 17 -DC_PIN = 22 +if platform.get_name().startswith("Raspberry Pi 5"): + RESET_PIN = "PIN13" # GPIO 27 + BUSY_PIN = "PIN11" # GPIO 17 + DC_PIN = "PIN15" # GPIO 22 +else: + RESET_PIN = "GPIO27" + BUSY_PIN = "GPIO17" + DC_PIN = "GPIO22" MOSI_PIN = 10 SCLK_PIN = 11 -CS0_PIN = 0 +CS0_PIN = 8 SUPPORTED_DISPLAYS = 17, 18, 19 @@ -86,6 +92,10 @@ def __init__(self, resolution=(400, 300), colour="black", cs_pin=CS0_PIN, dc_pin self.reset_pin = reset_pin self.busy_pin = busy_pin self.cs_pin = cs_pin + try: + self.cs_channel = [8, 7].index(cs_pin) + except ValueError: + self.cs_channel = 0 self.h_flip = h_flip self.v_flip = v_flip @@ -117,18 +127,21 @@ def setup(self): gpiochip = gpiodevice.find_chip_by_platform() gpiodevice.friendly_errors = True if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, "Data/Command": self.dc_pin, "Reset": self.reset_pin, "Busy": self.busy_pin }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) self._gpio = gpiochip.request_lines(consumer="inky", config={ - self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), - self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), - self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, debounce_period=timedelta(milliseconds=10)) + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE, bias=Bias.DISABLED), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE, bias=Bias.DISABLED), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, bias=Bias.DISABLED) }) if self._spi_bus is None: @@ -136,7 +149,7 @@ def setup(self): self._spi_bus = spidev.SpiDev() - self._spi_bus.open(0, self.cs_pin) + self._spi_bus.open(0, self.cs_channel) self._spi_bus.max_speed_hz = 10000000 # Should be good for 20MHz according to datasheet self._gpio_setup = True @@ -147,14 +160,17 @@ def setup(self): time.sleep(0.5) self._send_command(0x12) # Soft Reset - time.sleep(1.0) + time.sleep(1.0) # Required, or we'll miss buf_a (black) self._busy_wait() - def _busy_wait(self, timeout=5.0): + def _busy_wait(self, timeout=30.0): """Wait for busy/wait pin.""" - event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) - if not event: - raise RuntimeError("Timeout waiting for busy signal to clear.") + if self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") + for event in self._gpio.read_edge_events(): + pass def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -272,7 +288,9 @@ def _spi_write(self, dc, values): :param values: list of values to write """ + self._gpio.set_value(self.cs_pin, Value.INACTIVE) self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) + try: self._spi_bus.xfer3(values) except AttributeError: @@ -280,6 +298,8 @@ def _spi_write(self, dc, values): offset = x * _SPI_CHUNK_SIZE self._spi_bus.xfer(values[offset:offset + _SPI_CHUNK_SIZE]) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) + def _send_command(self, command, data=None): """Send command over SPI. diff --git a/inky/inky_uc8159.py b/inky/inky_uc8159.py index 004b7027..aac60084 100644 --- a/inky/inky_uc8159.py +++ b/inky/inky_uc8159.py @@ -7,7 +7,8 @@ import gpiod import gpiodevice import numpy -from gpiod.line import Direction, Edge, Value +from gpiod.line import Bias, Direction, Edge, Value +from gpiodevice import platform from PIL import Image from . import eeprom @@ -43,9 +44,14 @@ [255, 255, 255] ] -RESET_PIN = "PIN13" # GPIO 27 -BUSY_PIN = "PIN11" # GPIO 17 -DC_PIN = "PIN15" # GPIO 22 +if platform.get_name().startswith("Raspberry Pi 5"): + RESET_PIN = "PIN13" # GPIO 27 + BUSY_PIN = "PIN11" # GPIO 17 + DC_PIN = "PIN15" # GPIO 22 +else: + RESET_PIN = "GPIO27" + BUSY_PIN = "GPIO17" + DC_PIN = "GPIO22" MOSI_PIN = 10 SCLK_PIN = 11 @@ -218,12 +224,11 @@ def setup(self): self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) - self._gpio = gpiochip.request_lines(consumer="inky", config={ - self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), - self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), - self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), - self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.RISING, debounce_period=timedelta(milliseconds=10)) + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE, bias=Bias.DISABLED), + self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), + self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.RISING, debounce_period=timedelta(milliseconds=10), bias=Bias.DISABLED) }) if self._spi_bus is None: @@ -242,6 +247,7 @@ def setup(self): self._gpio.set_value(self.reset_pin, Value.INACTIVE) time.sleep(0.1) self._gpio.set_value(self.reset_pin, Value.ACTIVE) + time.sleep(0.1) self._busy_wait(1.0) @@ -338,16 +344,14 @@ def _busy_wait(self, timeout=40.0): return for event in self._gpio.read_edge_events(): - print(timeout, event) + if event.Type == Edge.RISING: + return def _update(self, buf): """Update display. Dispatches display update to correct driver. - :param buf_a: Black/White pixels - :param buf_b: Yellow/Red pixels - """ self.setup() self._send_command(UC8159_DTM1, buf) @@ -438,6 +442,7 @@ def _spi_write(self, dc, values): for x in range(((len(values) - 1) // _SPI_CHUNK_SIZE) + 1): offset = x * _SPI_CHUNK_SIZE self._spi_bus.xfer(values[offset : offset + _SPI_CHUNK_SIZE]) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) def _send_command(self, command, data=None): From cd66f78bd5b46d7d8c859a8658d69a9b01ac7446 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 7 Feb 2024 12:21:38 +0000 Subject: [PATCH 16/27] Inky: Fix SPI CS, normalise image conversion. --- inky/inky.py | 34 ++++++++++++++++++++++++---------- inky/inky_ssd1608.py | 40 ++++++++++++++++++++++++++++++---------- inky/inky_ssd1683.py | 2 +- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/inky/inky.py b/inky/inky.py index dbf5f650..b333dcf0 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -1,6 +1,7 @@ """Inky e-Ink Display Driver.""" import struct import time +import warnings from datetime import timedelta import gpiod @@ -8,6 +9,7 @@ import numpy from gpiod.line import Bias, Direction, Edge, Value from gpiodevice import platform +from PIL import Image from . import eeprom @@ -252,6 +254,10 @@ def setup(self): self._spi_bus = spidev.SpiDev() self._spi_bus.open(0, self.cs_channel) + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") self._spi_bus.max_speed_hz = 488000 self._gpio_setup = True @@ -262,7 +268,7 @@ def setup(self): time.sleep(0.1) self._send_command(0x12) # Soft Reset - self._busy_wait() + self._busy_wait(1.0) def _busy_wait(self, timeout=30.0): """Wait for busy/wait pin.""" @@ -377,16 +383,24 @@ def set_border(self, colour): def set_image(self, image): """Copy an image to the buffer. - - The dimensions of `image` should match the dimensions of the display being used. - - :param image: Image to copy. - :type image: :class:`PIL.Image.Image` or :class:`numpy.ndarray` or list """ - if self.rotation % 180 == 0: - self.buf = numpy.array(image, dtype=numpy.uint8).reshape((self.width, self.height)) - else: - self.buf = numpy.array(image, dtype=numpy.uint8).reshape((self.height, self.width)) + image = image.resize((self.width, self.height)) + + if not image.mode == "P": + palette_image = Image.new("P", (1, 1)) + r, g, b = 0, 0, 0 + if self.colour == "red": + r = 255 + if self.colour == "yellow": + r = g = 255 + palette_image.putpalette([255, 255, 255, 0, 0, 0, r, g, b] + [0, 0, 0] * 252) + image.load() + image = image.im.convert("P", True, palette_image.im) + + canvas = Image.new("P", (self.rows, self.cols)) + width, height = image.size + canvas.paste(image, (0, 0, width, height)) + self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.cols, self.rows)) def _spi_write(self, dc, values): """Write values over SPI. diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 0e565ba8..1698313d 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -1,11 +1,12 @@ """Inky e-Ink Display Driver.""" import time +import warnings from datetime import timedelta import gpiod import gpiodevice import numpy -from gpiod.line import Direction, Edge, Value +from gpiod.line import Bias, Direction, Edge, Value from gpiodevice import platform from PIL import Image @@ -26,7 +27,7 @@ MOSI_PIN = 10 SCLK_PIN = 11 -CS0_PIN = 0 +CS0_PIN = 8 _SPI_CHUNK_SIZE = 4096 _SPI_COMMAND = 0 @@ -104,6 +105,10 @@ def __init__(self, resolution=(250, 122), colour="black", cs_pin=CS0_PIN, dc_pin self.reset_pin = reset_pin self.busy_pin = busy_pin self.cs_pin = cs_pin + try: + self.cs_channel = [8, 7].index(cs_pin) + except ValueError: + self.cs_channel = 0 self.h_flip = h_flip self.v_flip = v_flip @@ -135,15 +140,18 @@ def setup(self): gpiochip = gpiodevice.find_chip_by_platform() gpiodevice.friendly_errors = True if gpiodevice.check_pins_available(gpiochip, { + "Chip Select": self.cs_pin, "Data/Command": self.dc_pin, "Reset": self.reset_pin, "Busy": self.busy_pin }): + self.cs_pin = gpiochip.line_offset_from_id(self.cs_pin) self.dc_pin = gpiochip.line_offset_from_id(self.dc_pin) self.reset_pin = gpiochip.line_offset_from_id(self.reset_pin) self.busy_pin = gpiochip.line_offset_from_id(self.busy_pin) self._gpio = gpiochip.request_lines(consumer="inky", config={ + self.cs_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE, bias=Bias.DISABLED), self.dc_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE), self.reset_pin: gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE), self.busy_pin: gpiod.LineSettings(direction=Direction.INPUT, edge_detection=Edge.FALLING, debounce_period=timedelta(milliseconds=10)) @@ -154,7 +162,11 @@ def setup(self): self._spi_bus = spidev.SpiDev() - self._spi_bus.open(0, self.cs_pin) + self._spi_bus.open(0, self.cs_channel) + try: + self._spi_bus.no_cs = True + except OSError: + warnings.warn("SPI: Cannot disable chip-select!") self._spi_bus.max_speed_hz = 488000 self._gpio_setup = True @@ -170,9 +182,12 @@ def setup(self): def _busy_wait(self, timeout=5.0): """Wait for busy/wait pin.""" - event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) - if not event: - raise RuntimeError("Timeout waiting for busy signal to clear.") + if self._gpio.get_value(self.busy_pin) == Value.ACTIVE: + event = self._gpio.wait_edge_events(timedelta(seconds=timeout)) + if not event: + raise RuntimeError("Timeout waiting for busy signal to clear.") + for event in self._gpio.read_edge_events(): + pass def _update(self, buf_a, buf_b, busy_wait=True): """Update display. @@ -267,21 +282,23 @@ def set_border(self, colour): def set_image(self, image): """Copy an image to the display.""" + image = image.resize((self.width, self.height)) + if not image.mode == "P": palette_image = Image.new("P", (1, 1)) r, g, b = 0, 0, 0 if self.colour == "red": r = 255 if self.colour == "yellow": - g = 255 + r = g = 255 palette_image.putpalette([255, 255, 255, 0, 0, 0, r, g, b] + [0, 0, 0] * 252) image.load() image = image.im.convert("P", True, palette_image.im) - canvas = Image.new("P", (self.cols, self.rows)) + canvas = Image.new("P", (self.rows, self.cols)) width, height = image.size - canvas.paste(image, (self.offset_x, self.offset_y, width, height)) - self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.rows, self.cols)) + canvas.paste(image, (self.offset_x, self.offset_y, width + self.offset_x, height + self.offset_y)) + self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.cols, self.rows)) def _spi_write(self, dc, values): """Write values over SPI. @@ -290,6 +307,7 @@ def _spi_write(self, dc, values): :param values: list of values to write """ + self._gpio.set_value(self.cs_pin, Value.INACTIVE) self._gpio.set_value(self.dc_pin, Value.ACTIVE if dc else Value.INACTIVE) try: self._spi_bus.xfer3(values) @@ -298,6 +316,8 @@ def _spi_write(self, dc, values): offset = x * _SPI_CHUNK_SIZE self._spi_bus.xfer(values[offset : offset + _SPI_CHUNK_SIZE]) + self._gpio.set_value(self.cs_pin, Value.ACTIVE) + def _send_command(self, command, data=None): """Send command over SPI. diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index 2da3f71b..05321d3b 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -271,7 +271,7 @@ def set_image(self, image): if self.colour == "red": r = 255 if self.colour == "yellow": - g = 255 + r = g = 255 palette_image.putpalette([255, 255, 255, 0, 0, 0, r, g, b] + [0, 0, 0] * 252) image.load() image = image.im.convert("P", True, palette_image.im) From 841bffa0131099ad265ff5f378a7d12071868696 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 7 Feb 2024 12:23:44 +0000 Subject: [PATCH 17/27] Packaging: Add pillow as a dependency. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index be23fda7..db474c33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ ] dependencies = [ "numpy", + "pillow", "smbus2", "spidev", "gpiodevice>=0.0.3" From a6f270aa504c7a57e8ae19352a2c1c9537d024de Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 7 Feb 2024 12:44:20 +0000 Subject: [PATCH 18/27] QA: Include gpiod/gpiodevice mocks in auto tests. --- tests/conftest.py | 6 ++++-- tests/test_auto.py | 4 ++-- tests/test_eeprom.py | 4 ++-- tests/test_init.py | 22 +++++++++++----------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 964421c5..607de452 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,10 +21,10 @@ def cleanup(): @pytest.fixture(scope='function', autouse=False) def nopath(): - path = sys.path + old_path = sys.path sys.path = [path for path in sys.path if not path.startswith("/usr/lib") and not path.startswith("/opt/hostedtoolcache")] yield - sys.path = path + sys.path = old_path @pytest.fixture(scope='function', autouse=False) @@ -33,10 +33,12 @@ def GPIO(): gpiod = mock.MagicMock() gpiodevice = mock.MagicMock() sys.modules['gpiod'] = gpiod + sys.modules['gpiod.line'] = gpiod sys.modules['gpiodevice'] = gpiodevice sys.modules['gpiodevice.platform'] = mock.MagicMock() yield gpiod, gpiodevice del sys.modules['gpiod'] + del sys.modules['gpiod.line'] del sys.modules['gpiodevice'] del sys.modules['gpiodevice.platform'] diff --git a/tests/test_auto.py b/tests/test_auto.py index 4357adbc..ae23289a 100644 --- a/tests/test_auto.py +++ b/tests/test_auto.py @@ -30,7 +30,7 @@ @pytest.mark.parametrize('verbose', [True, False]) @pytest.mark.parametrize('inky_colour', ['black', 'red', 'yellow', None]) @pytest.mark.parametrize('inky_type', ['phat', 'what', 'phatssd1608', 'impressions', '7colour', 'whatssd1683']) -def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour, verbose): +def test_auto_fallback(GPIO, spidev, smbus2, PIL, inky_type, inky_colour, verbose): """Test auto init of 'phat', 'black'.""" from inky import Inky7Colour, InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, InkyWHAT_SSD1683, auto @@ -61,7 +61,7 @@ def test_auto_fallback(spidev, smbus2, PIL, inky_type, inky_colour, verbose): @pytest.mark.parametrize('inky_display', enumerate(DISPLAY_VARIANT)) -def test_auto(spidev, smbus2_eeprom, PIL, inky_display): +def test_auto(GPIO, spidev, smbus2_eeprom, PIL, inky_display): """Test auto init of 'phat', 'black'.""" from inky import Inky7Colour, InkyPHAT, InkyPHAT_SSD1608, InkyWHAT, InkyWHAT_SSD1683, auto, eeprom diff --git a/tests/test_eeprom.py b/tests/test_eeprom.py index 9ea24962..047c56a6 100644 --- a/tests/test_eeprom.py +++ b/tests/test_eeprom.py @@ -1,7 +1,7 @@ """EEPROM tests for Inky.""" -def test_eeprom_7color_5_7_inch(spidev, smbus2_eeprom, PIL): +def test_eeprom_7color_5_7_inch(GPIO, spidev, smbus2_eeprom, PIL): """Test EEPROM for 7color 5.7" Inky.""" from inky.eeprom import EPDType from inky.inky_uc8159 import Inky @@ -15,7 +15,7 @@ def test_eeprom_7color_5_7_inch(spidev, smbus2_eeprom, PIL): assert inky.resolution == (600, 448) -def test_eeprom_7color_4_inch(spidev, smbus2_eeprom, PIL): +def test_eeprom_7color_4_inch(GPIO, spidev, smbus2_eeprom, PIL): """Test EEPROM for 7color 4" Inky.""" from inky.eeprom import EPDType from inky.inky_uc8159 import Inky diff --git a/tests/test_init.py b/tests/test_init.py index ea46c7da..6e2b6557 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -3,63 +3,63 @@ import pytest -def test_init_mock_phat_black(tkinter, PIL): +def test_init_mock_phat_black(GPIO, tkinter, PIL): """Test initialisation of InkyMockPHAT with 'black' colour choice.""" from inky import InkyMockPHAT InkyMockPHAT('black') -def test_init_mock_what_black(tkinter, PIL): +def test_init_mock_what_black(GPIO, tkinter, PIL): """Test initialisation of InkyMockWHAT with 'black' colour choice.""" from inky import InkyMockWHAT InkyMockWHAT('black') -def test_init_phat_black(spidev, smbus2, PIL): +def test_init_phat_black(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyPHAT with 'black' colour choice.""" from inky import InkyPHAT InkyPHAT('black') -def test_init_phat_red(spidev, smbus2, PIL): +def test_init_phat_red(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyPHAT with 'red' colour choice.""" from inky import InkyPHAT InkyPHAT('red') -def test_init_phat_yellow(spidev, smbus2, PIL): +def test_init_phat_yellow(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyPHAT with 'yellow' colour choice.""" from inky import InkyPHAT InkyPHAT('red') -def test_init_what_black(spidev, smbus2, PIL): +def test_init_what_black(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyWHAT with 'black' colour choice.""" from inky import InkyWHAT InkyWHAT('black') -def test_init_what_red(spidev, smbus2, PIL): +def test_init_what_red(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyWHAT with 'red' colour choice.""" from inky import InkyWHAT InkyWHAT('red') -def test_init_what_yellow(spidev, smbus2, PIL): +def test_init_what_yellow(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyWHAT with 'yellow' colour choice.""" from inky import InkyWHAT InkyWHAT('yellow') -def test_init_invalid_colour(spidev, smbus2, PIL): +def test_init_invalid_colour(GPIO, spidev, smbus2, PIL): """Test initialisation of InkyWHAT with an invalid colour choice.""" from inky import InkyWHAT @@ -67,7 +67,7 @@ def test_init_invalid_colour(spidev, smbus2, PIL): InkyWHAT('octarine') -def test_init_what_setup(spidev, smbus2, GPIO, PIL): +def test_init_what_setup(GPIO, spidev, smbus2, PIL): """Test initialisation and setup of InkyWHAT. Verify our expectations for GPIO setup in order to catch regressions. @@ -85,7 +85,7 @@ def test_init_what_setup(spidev, smbus2, GPIO, PIL): spidev.SpiDev().open.assert_called_with(0, inky.cs_channel) -def test_init_7colour_setup(spidev, smbus2, GPIO, PIL): +def test_init_7colour_setup(GPIO, spidev, smbus2, PIL): """Test initialisation and setup of 7-colour Inky. Verify our expectations for GPIO setup in order to catch regressions. From ffcc67a3b704271192851ec27517053b41a6aabd Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 7 Feb 2024 12:54:58 +0000 Subject: [PATCH 19/27] All: Revert to using line numbers. Avoid calling gpiodevice's platform detect on import time. --- inky/inky.py | 12 +++--------- inky/inky_ac073tc1a.py | 12 +++--------- inky/inky_ssd1608.py | 12 +++--------- inky/inky_ssd1683.py | 12 +++--------- inky/inky_uc8159.py | 12 +++--------- 5 files changed, 15 insertions(+), 45 deletions(-) diff --git a/inky/inky.py b/inky/inky.py index b333dcf0..4f2575a4 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -8,7 +8,6 @@ import gpiodevice import numpy from gpiod.line import Bias, Direction, Edge, Value -from gpiodevice import platform from PIL import Image from . import eeprom @@ -20,14 +19,9 @@ BLACK = 1 RED = YELLOW = 2 -if platform.get_name().startswith("Raspberry Pi 5"): - RESET_PIN = "PIN13" # GPIO 27 - BUSY_PIN = "PIN11" # GPIO 17 - DC_PIN = "PIN15" # GPIO 22 -else: - RESET_PIN = "GPIO27" - BUSY_PIN = "GPIO17" - DC_PIN = "GPIO22" +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 # In addition the following pins are used for SPI MOSI_PIN = 10 diff --git a/inky/inky_ac073tc1a.py b/inky/inky_ac073tc1a.py index c29054f7..c6625f19 100644 --- a/inky/inky_ac073tc1a.py +++ b/inky/inky_ac073tc1a.py @@ -6,7 +6,6 @@ import gpiod import gpiodevice from gpiod.line import Direction, Edge, Value -from gpiodevice import platform from PIL import Image from . import eeprom @@ -25,14 +24,9 @@ ORANGE = 6 CLEAN = 7 -if platform.get_name().startswith("Raspberry Pi 5"): - RESET_PIN = "PIN13" # GPIO 27 - BUSY_PIN = "PIN11" # GPIO 17 - DC_PIN = "PIN15" # GPIO 22 -else: - RESET_PIN = "GPIO27" - BUSY_PIN = "GPIO17" - DC_PIN = "GPIO22" +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 MOSI_PIN = 10 SCLK_PIN = 11 diff --git a/inky/inky_ssd1608.py b/inky/inky_ssd1608.py index 1698313d..a5474c01 100644 --- a/inky/inky_ssd1608.py +++ b/inky/inky_ssd1608.py @@ -7,7 +7,6 @@ import gpiodevice import numpy from gpiod.line import Bias, Direction, Edge, Value -from gpiodevice import platform from PIL import Image from . import eeprom, ssd1608 @@ -16,14 +15,9 @@ BLACK = 1 RED = YELLOW = 2 -if platform.get_name().startswith("Raspberry Pi 5"): - RESET_PIN = "PIN13" # GPIO 27 - BUSY_PIN = "PIN11" # GPIO 17 - DC_PIN = "PIN15" # GPIO 22 -else: - RESET_PIN = "GPIO27" - BUSY_PIN = "GPIO17" - DC_PIN = "GPIO22" +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 MOSI_PIN = 10 SCLK_PIN = 11 diff --git a/inky/inky_ssd1683.py b/inky/inky_ssd1683.py index 05321d3b..66e39f33 100644 --- a/inky/inky_ssd1683.py +++ b/inky/inky_ssd1683.py @@ -6,7 +6,6 @@ import gpiodevice import numpy from gpiod.line import Bias, Direction, Edge, Value -from gpiodevice import platform from PIL import Image from . import eeprom, ssd1683 @@ -15,14 +14,9 @@ BLACK = 1 RED = YELLOW = 2 -if platform.get_name().startswith("Raspberry Pi 5"): - RESET_PIN = "PIN13" # GPIO 27 - BUSY_PIN = "PIN11" # GPIO 17 - DC_PIN = "PIN15" # GPIO 22 -else: - RESET_PIN = "GPIO27" - BUSY_PIN = "GPIO17" - DC_PIN = "GPIO22" +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 MOSI_PIN = 10 SCLK_PIN = 11 diff --git a/inky/inky_uc8159.py b/inky/inky_uc8159.py index aac60084..1b0da806 100644 --- a/inky/inky_uc8159.py +++ b/inky/inky_uc8159.py @@ -8,7 +8,6 @@ import gpiodevice import numpy from gpiod.line import Bias, Direction, Edge, Value -from gpiodevice import platform from PIL import Image from . import eeprom @@ -44,14 +43,9 @@ [255, 255, 255] ] -if platform.get_name().startswith("Raspberry Pi 5"): - RESET_PIN = "PIN13" # GPIO 27 - BUSY_PIN = "PIN11" # GPIO 17 - DC_PIN = "PIN15" # GPIO 22 -else: - RESET_PIN = "GPIO27" - BUSY_PIN = "GPIO17" - DC_PIN = "GPIO22" +RESET_PIN = 27 # PIN13 +BUSY_PIN = 17 # PIN11 +DC_PIN = 22 # PIN15 MOSI_PIN = 10 SCLK_PIN = 11 From caac90e1e589c0d04a8dc9a62a07aa8c66619b61 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 27 Jun 2024 09:55:52 +0100 Subject: [PATCH 20/27] Sync with boilerplate. --- Makefile | 5 ++++- install.sh | 8 ++------ tox.ini | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 34f4a7dd..56cf0dfe 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ endif @echo "deploy: build and upload to PyPi" @echo "tag: tag the repository with the current version\n" +version: + @hatch version + install: ./install.sh --unstable @@ -47,7 +50,7 @@ pytest: nopost: @bash check.sh --nopost -tag: +tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check diff --git a/install.sh b/install.sh index bb671f29..3db90bc0 100755 --- a/install.sh +++ b/install.sh @@ -58,11 +58,6 @@ find_config() { if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then fatal "Could not find $CONFIG_FILE!" fi - else - if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then - warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" - warning "You might want to fix this!" - fi fi inform "Using $CONFIG_FILE in $CONFIG_DIR" } @@ -156,7 +151,8 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y "$PACKAGES" + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" diff --git a/tox.ini b/tox.ini index 44c86546..4726cef0 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ commands = python -m build --no-isolation python -m twine check dist/* isort --check . - ruff . + ruff check . codespell . deps = check-manifest From 99ac046f58829eb2e2e3aca24741289ea81acea6 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 27 Jun 2024 10:15:25 +0100 Subject: [PATCH 21/27] Packaging: add example requirements file. --- pyproject.toml | 22 ++++++---------------- requirements-dev.txt | 1 + requirements-examples.txt | 12 ++++++++++++ tox.ini | 9 +-------- 4 files changed, 20 insertions(+), 24 deletions(-) create mode 100644 requirements-examples.txt diff --git a/pyproject.toml b/pyproject.toml index db474c33..5e92f538 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["hatchling", "hatch-fancy-pypi-readme"] +requires = ["hatchling", "hatch-fancy-pypi-readme", "hatch-requirements-txt"] build-backend = "hatchling.build" [project] name = "inky" -dynamic = ["version", "readme"] +dynamic = ["version", "readme", "optional-dependencies"] description = "Inky pHAT Driver" license = {file = "LICENSE"} requires-python = ">= 3.7" @@ -42,19 +42,8 @@ dependencies = [ "gpiodevice>=0.0.3" ] -[project.optional-dependencies] -example-depends = [ - "pillow", - "requests", - "geocoder", - "beautifulsoup4", - "font-fredoka-one", - "font-source-serif-pro", - "font-hanken-grotesk", - "font-intuitive" -] -rpi-gpio-output = ['RPi.GPIO'] -rpi = ['RPi.GPIO'] +[tool.hatch.metadata.hooks.requirements_txt.optional-dependencies] +example-depends = ["requirements-examples.txt"] [project.urls] GitHub = "https://www.github.com/pimoroni/inky" @@ -68,7 +57,8 @@ include = [ "inky", "README.md", "CHANGELOG.md", - "LICENSE" + "LICENSE", + "requirements-examples.txt" ] [tool.hatch.build.targets.sdist] diff --git a/requirements-dev.txt b/requirements-dev.txt index 525b0427..d392e8fe 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,5 +5,6 @@ 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 00000000..f711a6f1 --- /dev/null +++ b/requirements-examples.txt @@ -0,0 +1,12 @@ +pillow +requests +beautifulsoup4 +fonts +font-source-sans-pro +font-source-serif-pro +font-fredoka-one +font-hanken-grotesk +font-intuitive +geocoder +seaborn +wikiquotes \ No newline at end of file diff --git a/tox.ini b/tox.ini index 4726cef0..2b6d87b8 100644 --- a/tox.ini +++ b/tox.ini @@ -23,12 +23,5 @@ commands = ruff check . codespell . deps = - check-manifest - ruff - codespell - isort - twine - build - hatch - hatch-fancy-pypi-readme + -r{toxinidir}/requirements-dev.txt From 47d6aaf9a6d0286c6bddb432125e32464c1729c4 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 27 Jun 2024 11:02:35 +0100 Subject: [PATCH 22/27] CI: Test install. --- .github/workflows/build.yml | 2 ++ .github/workflows/install.yml | 40 +++++++++++++++++++++++++++++++++++ .github/workflows/test.yml | 2 ++ 3 files changed, 44 insertions(+) create mode 100644 .github/workflows/install.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 07620e34..976fee29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,8 @@ jobs: test: name: Python ${{ matrix.python }} runs-on: ubuntu-latest + env: + TERM: xterm-256color strategy: matrix: python: ['3.9', '3.10', '3.11'] diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml new file mode 100644 index 00000000..596dd758 --- /dev/null +++ b/.github/workflows/install.yml @@ -0,0 +1,40 @@ +name: Install Test + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: 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 Tests + run: | + ./install.sh --unstable --force diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f8cff73..170a4a7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,6 +10,8 @@ jobs: test: name: Python ${{ matrix.python }} runs-on: ubuntu-latest + env: + TERM: xterm-256color strategy: matrix: python: ['3.9', '3.10', '3.11'] From bd6bf4c98678f9a7e6c6becbfa5a21187debfb03 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 27 Jun 2024 11:12:31 +0100 Subject: [PATCH 23/27] CI: Sync with boilerplate for cleaned up names. --- .github/workflows/build.yml | 2 +- .github/workflows/install.yml | 4 ++-- .github/workflows/qa.yml | 2 +- .github/workflows/test.yml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 976fee29..99fdeab6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ on: jobs: test: - name: Python ${{ matrix.python }} + name: Build (Python ${{ matrix.python }}) runs-on: ubuntu-latest env: TERM: xterm-256color diff --git a/.github/workflows/install.yml b/.github/workflows/install.yml index 596dd758..f3c1a2d6 100644 --- a/.github/workflows/install.yml +++ b/.github/workflows/install.yml @@ -8,7 +8,7 @@ on: jobs: test: - name: Python ${{ matrix.python }} + name: Install (Python ${{ matrix.python }}) runs-on: ubuntu-latest env: TERM: xterm-256color @@ -35,6 +35,6 @@ jobs: chmod +x raspi-config echo `pwd` >> $GITHUB_PATH - - name: Run Tests + - name: Run install.sh run: | ./install.sh --unstable --force diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index ac672a52..2e166c00 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -8,7 +8,7 @@ on: jobs: test: - name: linting & spelling + name: Linting & Spelling runs-on: ubuntu-latest env: TERM: xterm-256color diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 170a4a7f..9e29cb90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - name: Python ${{ matrix.python }} + name: Test (Python ${{ matrix.python }}) runs-on: ubuntu-latest env: TERM: xterm-256color @@ -18,7 +18,7 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} uses: actions/setup-python@v5 From 113285ef7732dadc2bef246aa33ff111bcd2ffa5 Mon Sep 17 00:00:00 2001 From: Hel Gibbons Date: Thu, 27 Jun 2024 12:01:54 +0100 Subject: [PATCH 24/27] Update README.md --- README.md | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 27c0f4b8..1652b9e0 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ [![PyPi Package](https://img.shields.io/pypi/v/inky.svg)](https://pypi.python.org/pypi/inky) [![Python Versions](https://img.shields.io/pypi/pyversions/inky.svg)](https://pypi.python.org/pypi/inky) - Python library for [Inky pHAT](https://shop.pimoroni.com/products/inky-phat), [Inky wHAT](https://shop.pimoroni.com/products/inky-what) and [Inky Impression](https://shop.pimoroni.com/?q=inky+impression) e-paper displays for Raspberry Pi. ## Inky pHAT [Inky pHAT](https://shop.pimoroni.com/products/inky-phat) is a 250x122 pixel e-paper display, available in red/black/white, yellow/black/white and black/white. It's great for nametags and displaying very low frequency information such as a daily calendar or weather overview. + ## Inky wHAT [Inky wHAT](https://shop.pimoroni.com/products/inky-what) is a 400x300 pixel e-paper display available in red/black/white, yellow/black/white and black/white. It's got tons of resolution for detailed daily to-do lists, multi-day weather forecasts, bus timetables and more. @@ -22,15 +22,55 @@ Python library for [Inky pHAT](https://shop.pimoroni.com/products/inky-phat), [I # Installation -First, make sure you have I2C and SPI enabled in `sudo raspi-config`. +# Installing + +We'd recommend using this library with Raspberry Pi OS Bookworm or later. It requires Python ≥3.7. + +## Full install (recommended): + +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 commands exactly as it appears below (check for typos) and follow the on-screen instructions: + +```bash +git clone https://github.com/pimoroni/inky +cd inky +./install.sh +``` -The Python pip package is named inky, on the Raspberry Pi install with: +**Note** Libraries will be installed in the "pimoroni" virtual environment, you will need to activate it to run examples: ``` -python3 -m pip install inky +source ~/.virtualenvs/pimoroni/bin/activate ``` -This will install Inky along with dependencies for the Raspberry Pi, plus fonts used by the examples. +## 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 +git clone https://github.com/pimoroni/inky +cd inky +./install.sh --unstable +``` + +## 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 inky` + +In some cases you may need to us `sudo` or install pip with: `sudo apt install python3-pip`. + +This will not make any configuration changes, so you may also need to enable: + +* i2c: `sudo raspi-config nonint do_i2c 0` +* spi: `sudo raspi-config nonint do_spi 0` + +You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. # Usage From 87b48d0f876c822e90e2965c7bb6d7244d853783 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Apr 2024 12:42:36 +0100 Subject: [PATCH 25/27] Add tests for set_image. --- tests/test_set_image.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/test_set_image.py diff --git a/tests/test_set_image.py b/tests/test_set_image.py new file mode 100644 index 00000000..61bbf657 --- /dev/null +++ b/tests/test_set_image.py @@ -0,0 +1,28 @@ +"""Set image tests for Inky.""" +import pytest + + +@pytest.mark.parametrize('resolution', [(800, 480), (600, 448), (400, 300), (212, 104), (250, 122)]) +def test_inky_set_image(GPIO, spidev, smbus2, resolution): + from PIL import Image + + from inky.inky import Inky + + phat = Inky(resolution) + + width, height = phat.resolution + + image = Image.new("P", (width, height)) + + for x in range(width): + image.putpixel((x, 0), x % 3) + + assert image.size == (width, height) + + phat.set_image(image) + phat.set_pixel(0, 0, 2) + + data = [x % 3 for x in range(width)] + data[0] = 2 + + assert phat.buf.flatten().tolist()[0:width] == data From 0135ce3e94a76b36c848de0c9d13430010d27057 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Apr 2024 14:38:44 +0100 Subject: [PATCH 26/27] Inky: remove unecessary canvas paste. The image is resized to the display width/height, what was this even for? --- inky/inky.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/inky/inky.py b/inky/inky.py index 4f2575a4..4d9c122a 100644 --- a/inky/inky.py +++ b/inky/inky.py @@ -391,10 +391,7 @@ def set_image(self, image): image.load() image = image.im.convert("P", True, palette_image.im) - canvas = Image.new("P", (self.rows, self.cols)) - width, height = image.size - canvas.paste(image, (0, 0, width, height)) - self.buf = numpy.array(canvas, dtype=numpy.uint8).reshape((self.cols, self.rows)) + self.buf = numpy.array(image, dtype=numpy.uint8).reshape((self.cols, self.rows)) def _spi_write(self, dc, values): """Write values over SPI. From 85e868b96427f1684aa25bddc5038ed7eb5777da Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 6 Sep 2024 15:08:45 +0100 Subject: [PATCH 27/27] CI: Fix duplicate ENV in build.yml. --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 99fdeab6..3a38301b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,14 +10,13 @@ jobs: test: name: Build (Python ${{ matrix.python }}) runs-on: ubuntu-latest - env: - TERM: xterm-256color 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 }} + TERM: xterm-256color steps: - name: Checkout Code