From 47a0c22bec408d3f3e93afaab9dd694bbc1d741b Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 12 Jun 2024 15:26:35 +0100 Subject: [PATCH 1/6] Repackage to hatch/pyproject. Port to gpiod. --- library/.coveragerc => .coveragerc | 2 +- .github/workflows/build.yml | 41 +++ .github/workflows/qa.yml | 39 +++ .github/workflows/test.yml | 24 +- library/CHANGELOG.txt => CHANGELOG.md | 0 Makefile | 81 +++--- README.md | 44 +-- ST7789.py | 5 + check.sh | 84 ++++++ examples/320x240.py | 2 +- examples/framerate.py | 6 +- examples/gif.py | 8 +- examples/image.py | 8 +- examples/round.py | 6 +- examples/scrolling-text.py | 8 +- examples/shapes.py | 8 +- install.sh | 370 +++++++++++++++++++++++++ library/LICENSE.txt | 21 -- library/MANIFEST.in | 5 - library/README.md | 79 ------ library/setup.cfg | 11 - library/setup.py | 23 -- library/tests/conftest.py | 27 -- library/tests/test_dimensions.py | 19 -- library/tests/test_display.py | 18 -- library/tests/test_setup.py | 32 --- library/tox.ini | 26 -- pyproject.toml | 117 ++++++++ requirements-dev.txt | 9 + {library/ST7789 => st7789}/__init__.py | 47 ++-- tests/conftest.py | 47 ++++ tests/test_dimensions.py | 16 ++ tests/test_display.py | 14 + tests/test_setup.py | 27 ++ tox.ini | 36 +++ uninstall.sh | 72 +++++ 36 files changed, 1000 insertions(+), 382 deletions(-) rename library/.coveragerc => .coveragerc (60%) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/qa.yml rename library/CHANGELOG.txt => CHANGELOG.md (100%) create mode 100644 ST7789.py create mode 100755 check.sh create mode 100755 install.sh delete mode 100644 library/LICENSE.txt delete mode 100644 library/MANIFEST.in delete mode 100644 library/README.md delete mode 100644 library/setup.cfg delete mode 100644 library/setup.py delete mode 100644 library/tests/conftest.py delete mode 100644 library/tests/test_dimensions.py delete mode 100644 library/tests/test_display.py delete mode 100644 library/tests/test_setup.py delete mode 100644 library/tox.ini create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt rename {library/ST7789 => st7789}/__init__.py (90%) create mode 100644 tests/conftest.py create mode 100644 tests/test_dimensions.py create mode 100644 tests/test_display.py create mode 100644 tests/test_setup.py create mode 100644 tox.ini create mode 100755 uninstall.sh diff --git a/library/.coveragerc b/.coveragerc similarity index 60% rename from library/.coveragerc rename to .coveragerc index ef5de4e..e216b49 100644 --- a/library/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [run] -source = ST7789 +source = st7789 omit = .tox/* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..07620e3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + env: + RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install Dependencies + run: | + make dev-deps + + - name: Build Packages + run: | + make build + + - name: Upload Packages + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FILE }} + path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..ac672a5 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,39 @@ +name: QA + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: linting & spelling + runs-on: ubuntu-latest + env: + TERM: xterm-256color + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python '3,11' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + make dev-deps + + - name: Run Quality Assurance + run: | + make qa + + - name: Run Code Checks + run: | + make check + + - name: Run Bash Code Checks + run: | + make shellcheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0289da6..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,37 +1,41 @@ -name: Python Tests +name: Tests on: pull_request: push: branches: - - master + - main jobs: test: + name: Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.7, 3.8, 3.9] + python: ['3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - name: Checkout Code + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + - name: Install Dependencies run: | - python -m pip install --upgrade setuptools tox + make dev-deps + - name: Run Tests - working-directory: library run: | - tox -e py + make pytest + - name: Coverage + if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: library run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.9' }} diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md diff --git a/Makefile b/Makefile index d65cf64..56cf0df 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,66 @@ -.PHONY: usage install uninstall +LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) +LIBRARY_VERSION := $(shell hatch version 2> /dev/null) + +.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy usage: +ifdef LIBRARY_NAME + @echo "Library: ${LIBRARY_NAME}" + @echo "Version: ${LIBRARY_VERSION}\n" +else + @echo "WARNING: You should 'make dev-deps'\n" +endif @echo "Usage: make , where target is one of:\n" - @echo "install: install the library locally from source" - @echo "uninstall: uninstall the local library" - @echo "python-readme: generate library/README.md 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 "install: install the library locally from source" + @echo "uninstall: uninstall the local library" + @echo "dev-deps: install Python dev dependencies" + @echo "check: perform basic integrity checks on the codebase" + @echo "qa: run linting and package QA" + @echo "pytest: run Python test fixtures" + @echo "clean: clean Python build and dist directories" + @echo "build: build Python distribution files" + @echo "testdeploy: build and upload to test PyPi" + @echo "deploy: build and upload to PyPi" + @echo "tag: tag the repository with the current version\n" + +version: + @hatch version install: - ./install.sh + ./install.sh --unstable uninstall: ./uninstall.sh -python-readme: library/README.md +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix shellcheck + +check: + @bash check.sh -python-license: library/LICENSE.txt +shellcheck: + shellcheck *.sh -library/README.md: README.md library/CHANGELOG.txt - cp README.md library/README.md - printf "\n# Changelog\n" >> library/README.md - cat library/CHANGELOG.txt >> library/README.md +qa: + tox -e qa -library/LICENSE.txt: LICENSE - cp LICENSE library/LICENSE.txt +pytest: + tox -e py -python-wheels: python-readme python-license - cd library; python3 setup.py bdist_wheel - cd library; python setup.py bdist_wheel +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python setup.py sdist +tag: version + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-clean: - -rm -r library/dist - -rm -r library/build - -rm -r library/*.egg-info +build: check + @hatch build -python-dist: python-clean python-wheels python-sdist - ls library/dist +clean: + -rm -r dist -python-deploy: python-dist - twine upload library/dist/* +testdeploy: build + twine upload --repository testpypi dist/* -python-testdeploy: python-dist - twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* +deploy: nopost build + twine upload dist/* diff --git a/README.md b/README.md index 08af617..2c48500 100644 --- a/README.md +++ b/README.md @@ -6,53 +6,29 @@ [![Python Versions](https://img.shields.io/pypi/pyversions/st7789.svg)](https://pypi.python.org/pypi/st7789) -Python library to control ST7789 TFT LCD displays. +Python library to control an ST7789 TFT LCD display -Designed to work with the following Pimoroni ST7789 based SPI breakouts and Raspberry Pi HATs: +Designed specifically to work with a ST7789 based 240x240 pixel TFT SPI display. (Specifically the [1.3" SPI LCD from Pimoroni](https://shop.pimoroni.com/products/1-3-spi-colour-lcd-240x240-breakout)). -- [1.54" SPI Colour Square LCD (240x240) Breakout](https://shop.pimoroni.com/products/1-54-spi-colour-square-lcd-240x240-breakout) -- [1.3" SPI Colour Square LCD (240x240) Breakout](https://shop.pimoroni.com/products/1-3-spi-colour-lcd-240x240-breakout) -- [1.3" SPI Colour Round LCD (240x240) Breakout](https://shop.pimoroni.com/products/1-3-spi-colour-round-lcd-240x240-breakout) -- [Display HAT Mini](https://shop.pimoroni.com/products/display-hat-mini) (2.0" 320x240 LCD) - -![Photo showing four different Pimoroni ST7789-based products](st7789-combined.jpg) +![Animated GIF showing the ST7789 SPI LCD displaying Deploy/Rainbows in alternating frames](https://raw.githubusercontent.com/pimoroni/st7789-python/master/square-lcd-breakout-1.gif) # Installation -First, make sure you have the following dependencies: +Make sure you have the following dependencies: -````bash -sudo apt update -sudo apt install python3-rpi.gpio python3-spidev python3-pip python3-pil python3-numpy +```` +sudo apt-get update +sudo apt-get install python-rpi.gpio python-spidev python-pip python-pil python-numpy ```` Install this library by running: -````bash -sudo pip3 install st7789 +```` +sudo pip install st7789 ```` -You will also need to make sure I2C and SPI are enabled in raspi-config (`sudo raspi-config`) - you can find them under Interface Options. - -# Examples - -You can find some examples of use in the examples folder. Clone this repo with: - -```bash -git clone https://github.com/pimoroni/st7789-python -``` - -and navigate into the examples folder with: - -```bash -cd ~/st7789-python/examples/ -``` - -You can pass most of them a parameter (`square`, `rect`, `round`, or `dhmini`) to specify the size/shape/rotation of screen, like this: +You might also need to enable I2C and SPI in raspi-config. See example of usage in the examples folder. -```bash -python3 shapes.py dhmini -``` # Licensing & History diff --git a/ST7789.py b/ST7789.py new file mode 100644 index 0000000..2c5ade1 --- /dev/null +++ b/ST7789.py @@ -0,0 +1,5 @@ +from warnings import warn + +from st7789 import * # noqa F403 + +warn("Using \"import ST7789\" is deprecated. Please \"import st7789\" (all lowercase)!", DeprecationWarning, stacklevel=2) \ No newline at end of file diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..38dfc3a --- /dev/null +++ b/check.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# This script handles some basic QA checks on the source + +NOPOST=$1 +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') +TERM=${TERM:="xterm-256color"} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -p|--nopost) + NOPOST=true + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then + warning "Trailing whitespace found!" + exit 1 +else + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then + warning "DOS line-endings found!" + exit 1 +else + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then + warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." + exit 1 +else + success "Changes found for version ${LIBRARY_VERSION}." +fi +printf "\n" + +inform "Checking for git tag ${LIBRARY_VERSION}..." +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then + warning "Missing git tag for version ${LIBRARY_VERSION}" +fi +printf "\n" + +if [[ $NOPOST ]]; then + inform "Checking for .postN on library version..." + if [[ "$POST_VERSION" != "" ]]; then + warning "Found .$POST_VERSION on library version." + inform "Please only use these for testpypi releases." + exit 1 + else + success "OK" + fi +fi diff --git a/examples/320x240.py b/examples/320x240.py index de004d5..80fff1b 100755 --- a/examples/320x240.py +++ b/examples/320x240.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from ST7789 import ST7789, BG_SPI_CS_FRONT +from st7789 import ST7789, BG_SPI_CS_FRONT from PIL import Image, ImageDraw import random diff --git a/examples/framerate.py b/examples/framerate.py index 8c2e513..4cd3963 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -5,7 +5,7 @@ from PIL import Image from PIL import ImageDraw -import ST7789 as ST7789 +import st7789 # Higher SPI bus speed = higher framerate try: @@ -48,12 +48,12 @@ raise RuntimeError("Unsupported display type: {}".format(display_type)) # Create ST7789 LCD display class. -disp = ST7789.ST7789( +disp = st7789.ST7789( width=width, height=height, rotation=rotation, port=0, - cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT + cs=st7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT dc=9, backlight=backlight, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=SPI_SPEED_MHZ * 1000000, diff --git a/examples/gif.py b/examples/gif.py index 8ce7c0a..2f9e70c 100755 --- a/examples/gif.py +++ b/examples/gif.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 from PIL import Image -import ST7789 +import st7789 import time import sys @@ -36,11 +36,11 @@ # Create ST7789 LCD display class. if display_type in ("square", "rect", "round"): - disp = ST7789.ST7789( + disp = st7789.ST7789( height=135 if display_type == "rect" else 240, rotation=0 if display_type == "rect" else 90, port=0, - cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT + cs=st7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT dc=9, backlight=19, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=80 * 1000 * 1000, @@ -49,7 +49,7 @@ ) elif display_type == "dhmini": - disp = ST7789.ST7789( + disp = st7789.ST7789( height=240, width=320, rotation=180, diff --git a/examples/image.py b/examples/image.py index 353502c..9d09ab9 100755 --- a/examples/image.py +++ b/examples/image.py @@ -2,7 +2,7 @@ import sys from PIL import Image -import ST7789 as ST7789 +import st7789 print(""" image.py - Display an image on the LCD. @@ -33,11 +33,11 @@ # Create ST7789 LCD display class. if display_type in ("square", "rect", "round"): - disp = ST7789.ST7789( + disp = st7789.ST7789( height=135 if display_type == "rect" else 240, rotation=0 if display_type == "rect" else 90, port=0, - cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT + cs=st7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT dc=9, backlight=19, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=80 * 1000 * 1000, @@ -46,7 +46,7 @@ ) elif display_type == "dhmini": - disp = ST7789.ST7789( + disp = st7789.ST7789( height=240, width=320, rotation=180, diff --git a/examples/round.py b/examples/round.py index 91df4f1..5be62ed 100755 --- a/examples/round.py +++ b/examples/round.py @@ -8,7 +8,7 @@ from PIL import ImageDraw from PIL import ImageFont -import ST7789 +import st7789 print(""" round.py - Shiny shiny round LCD! @@ -31,9 +31,9 @@ style = "dots" # Create ST7789 LCD display class. -disp = ST7789.ST7789( +disp = st7789.ST7789( port=0, - cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT + cs=st7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT dc=9, backlight=19, # 18 for back BG slot, 19 for front BG slot. rotation=90, diff --git a/examples/scrolling-text.py b/examples/scrolling-text.py index becaded..b3cec15 100755 --- a/examples/scrolling-text.py +++ b/examples/scrolling-text.py @@ -6,7 +6,7 @@ from PIL import ImageDraw from PIL import ImageFont -import ST7789 +import st7789 MESSAGE = "Hello World! How are you today?" @@ -41,11 +41,11 @@ # Create ST7789 LCD display class. if display_type in ("square", "rect", "round"): - disp = ST7789.ST7789( + disp = st7789.ST7789( height=135 if display_type == "rect" else 240, rotation=0 if display_type == "rect" else 90, port=0, - cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT + cs=st7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT dc=9, backlight=19, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=80 * 1000 * 1000, @@ -54,7 +54,7 @@ ) elif display_type == "dhmini": - disp = ST7789.ST7789( + disp = st7789.ST7789( height=240, width=320, rotation=180, diff --git a/examples/shapes.py b/examples/shapes.py index cd751be..6b60741 100755 --- a/examples/shapes.py +++ b/examples/shapes.py @@ -5,7 +5,7 @@ from PIL import ImageDraw from PIL import ImageFont -import ST7789 +import st7789 print(""" shapes.py - Display test shapes on the LCD using PIL. @@ -31,11 +31,11 @@ # Create ST7789 LCD display class. if display_type in ("square", "rect", "round"): - disp = ST7789.ST7789( + disp = st7789.ST7789( height=135 if display_type == "rect" else 240, rotation=0 if display_type == "rect" else 90, port=0, - cs=ST7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT + cs=st7789.BG_SPI_CS_FRONT, # BG_SPI_CS_BACK or BG_SPI_CS_FRONT dc=9, backlight=19, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=80 * 1000 * 1000, @@ -44,7 +44,7 @@ ) elif display_type == "dhmini": - disp = ST7789.ST7789( + disp = st7789.ST7789( height=240, width=320, rotation=180, diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..3db90bc --- /dev/null +++ b/install.sh @@ -0,0 +1,370 @@ +#!/bin/bash +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") +CONFIG_BACKUP=false +APT_HAS_UPDATED=false +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" +USAGE="./install.sh (--unstable)" +POSITIONAL_ARGS=() +FORCE=false +UNSTABLE=false +PYTHON="python" +CMD_ERRORS=false + + +user_check() { + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" + fi +} + +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 +} + +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + +venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" + if [ ! -f "$VENV_BASH_SNIPPET" ]; then + inform "Creating $VENV_BASH_SNIPPET\n" + mkdir -p "$RESOURCES_TOP_DIR" + cat << EOF > "$VENV_BASH_SNIPPET" +# Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +VENV_DIR="$VENV_DIR" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$VENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + PYTHON_BIN=$(which "$PYTHON") + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + if confirm "Would you like us to create and/or use a default one?"; then + printf "\n" + if [ ! -f "$VENV_DIR/bin/activate" ]; then + inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages + venv_bash_snippet + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + else + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + fi + else + printf "\n" + fatal "Please create and/or activate a virtual Python environment and try again!\n" + fi + fi + printf "\n" +} + +check_for_error() { + if [ $? -ne 0 ]; then + CMD_ERRORS=true + warning "^^^ 😬 previous command did not exit cleanly!" + fi +} + +function do_config_backup { + if [ ! $CONFIG_BACKUP == true ]; then + CONFIG_BACKUP=true + FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" + inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" + sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" + mkdir -p "$RESOURCES_TOP_DIR/config-backups/" + cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" + if [ -f "$UNINSTALLER" ]; then + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" + fi + fi +} + +function apt_pkg_install { + PACKAGES_NEEDED=() + PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to + for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do + PACKAGE="${PACKAGES_IN[$i]}" + if [ "$PACKAGE" == "" ]; then continue; fi + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 + if [ "$?" == "1" ]; then + PACKAGES_NEEDED+=("$PACKAGE") + fi + done + PACKAGES="${PACKAGES_NEEDED[*]}" + if ! [ "$PACKAGES" == "" ]; then + printf "\n" + inform "Installing missing packages: $PACKAGES" + if [ ! $APT_HAS_UPDATED ]; then + sudo apt update + APT_HAS_UPDATED=true + fi + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES + check_for_error + if [ -f "$UNINSTALLER" ]; then + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" + fi + fi +} + +function pip_pkg_install { + # A null Keyring prevents pip stalling in the background + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + check_for_error +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -u|--unstable) + UNSTABLE=true + shift + ;; + -f|--force) + FORCE=true + shift + ;; + -p|--python) + PYTHON=$2 + shift + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +printf "Installing %s...\n\n" "$LIBRARY_NAME" + +user_check +venv_check + +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" +fi + +PYTHON_VER=$($PYTHON --version) + +inform "Checking Dependencies. Please wait..." + +# Install toml and try to read pyproject.toml into bash variables + +pip_pkg_install toml + +CONFIG_VARS=$( + $PYTHON - < "$UNINSTALLER" +printf "It's recommended you run these steps manually.\n" +printf "If you want to run the full script, open it in\n" +printf "an editor and remove 'exit 1' from below.\n" +exit 1 +source $VIRTUAL_ENV/bin/activate +EOF + +printf "\n" + +inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages +apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" + +if $UNSTABLE; then + warning "Installing unstable library from source.\n" + pip_pkg_install . +else + inform "Installing stable library from pypi.\n" + pip_pkg_install "$LIBRARY_NAME" +fi + +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag +if [ $? -eq 0 ]; then + success "Done!\n" + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" +fi + +find_config + +printf "\n" + +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + +inform "Running setup commands...\n" +for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do + CMD="${SETUP_CMDS[$i]}" + # Attempt to catch anything that touches config.txt and trigger a backup + if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then + do_config_backup + fi + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi + eval "$CMD" + check_for_error +done + +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + +for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do + CONFIG_LINE="${CONFIG_TXT[$i]}" + if ! [ "$CONFIG_LINE" == "" ]; then + do_config_backup + inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE + if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + fi + fi +done + +printf "\n" + +# Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples + +if [ -d "examples" ]; then + if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then + inform "Copying examples to $RESOURCES_DIR" + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" + success "Done!" + fi +fi + +printf "\n" + +# Use pdoc to generate basic documentation from the installed module + +if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." + pip_pkg_install pdoc + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then + inform "Documentation saved to $RESOURCES_DIR/docs" + success "Done!" + else + warning "Error: Failed to generate documentation." + fi +fi + +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi diff --git a/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index aed751a..0000000 --- a/library/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Pimoroni Ltd. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/library/MANIFEST.in b/library/MANIFEST.in deleted file mode 100644 index 755f8b4..0000000 --- a/library/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include CHANGELOG.txt -include LICENSE.txt -include README.rst -include setup.py -recursive-include ST7789 *.py diff --git a/library/README.md b/library/README.md deleted file mode 100644 index b06dd92..0000000 --- a/library/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# Python ST7789 - -[![Build Status](https://travis-ci.com/pimoroni/st7789-python.svg?branch=master)](https://travis-ci.com/pimoroni/st7789-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/st7789-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/st7789-python?branch=master) -[![PyPi Package](https://img.shields.io/pypi/v/st7789.svg)](https://pypi.python.org/pypi/st7789) -[![Python Versions](https://img.shields.io/pypi/pyversions/st7789.svg)](https://pypi.python.org/pypi/st7789) - - -Python library to control an ST7789 TFT LCD display - -Designed specifically to work with a ST7789 based 240x240 pixel TFT SPI display. (Specifically the [1.3" SPI LCD from Pimoroni](https://shop.pimoroni.com/products/1-3-spi-colour-lcd-240x240-breakout)). - -![Animated GIF showing the ST7789 SPI LCD displaying Deploy/Rainbows in alternating frames](https://raw.githubusercontent.com/pimoroni/st7789-python/master/square-lcd-breakout-1.gif) - -# Installation - -Make sure you have the following dependencies: - -```` -sudo apt-get update -sudo apt-get install python-rpi.gpio python-spidev python-pip python-pil python-numpy -```` - -Install this library by running: - -```` -sudo pip install st7789 -```` - -You might also need to enable I2C and SPI in raspi-config. See example of usage in the examples folder. - - -# Licensing & History - -This library is a modification of a modification of code originally written by Tony DiCola for Adafruit Industries, and modified to work with the ST7735 by Clement Skau. - -To create this ST7789 driver, it has been hard-forked from st7735-python which was originally modified by Pimoroni to include support for their 160x80 SPI LCD breakout. - -## Modifications include: - -* PIL/Pillow has been removed from the underlying display driver to separate concerns- you should create your own PIL image and display it using `display(image)` -* `width`, `height`, `rotation`, `invert`, `offset_left` and `offset_top` parameters can be passed into `__init__` for alternate displays -* `Adafruit_GPIO` has been replaced with `RPi.GPIO` and `spidev` to closely align with our other software (IE: Raspberry Pi only) -* Test fixtures have been added to keep this library stable - -Pimoroni invests time and resources forking and modifying this open source code, please support Pimoroni and open-source software by purchasing products from us, too! - -Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! - -Modified from 'Modified from 'Adafruit Python ILI9341' written by Tony DiCola for Adafruit Industries.' written by Clement Skau. - -MIT license, all text above must be included in any redistribution - -# Changelog - -0.0.4 ------ - -* Add support for 320x240 2.0" LCD (Display HAT Mini) -* Add support for 240x135 1.14" LCD (@slabua) -* Rework numpy RGB888 to RGB565 -* Support displaying numpy arrays (@zecktos) - -0.0.3 ------ - -* Add support for RLCD -* Brought back `offset_left` and `offset_top` parameters - -0.0.2 ------ - -* Fix for image retention -* Drop defunct parameters - -0.0.1 ------ - -* Initial Release diff --git a/library/setup.cfg b/library/setup.cfg deleted file mode 100644 index 5c3c3ea..0000000 --- a/library/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -exclude = - test.py - .tox, - .eggs, - .git, - __pycache__, - build, - dist -ignore = - E501 diff --git a/library/setup.py b/library/setup.py deleted file mode 100644 index 2f14c69..0000000 --- a/library/setup.py +++ /dev/null @@ -1,23 +0,0 @@ -from setuptools import setup, find_packages - - -classifiers = ['Development Status :: 4 - Beta', - 'Operating System :: POSIX :: Linux', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development', - 'Topic :: System :: Hardware'] - -setup(name='ST7789', - version='0.0.4', - description='Library to control ST7789 TFT LCD displays.', - long_description=open('README.md').read(), - long_description_content_type='text/markdown', - license='MIT', - author='Philip Howard', - author_email='phil@pimoroni.com', - classifiers=classifiers, - url='https://github.com/pimoroni/st7789-python/', - packages=find_packages()) diff --git a/library/tests/conftest.py b/library/tests/conftest.py deleted file mode 100644 index 7c1b947..0000000 --- a/library/tests/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys -import mock -import pytest - -@pytest.fixture(scope='function', autouse=False) -def GPIO(): - """Mock RPi.GPIO module.""" - - GPIO = mock.MagicMock() - # Fudge for Python < 37 (possibly earlier) - sys.modules['RPi'] = mock.Mock() - sys.modules['RPi'].GPIO = GPIO - sys.modules['RPi.GPIO'] = GPIO - yield GPIO - del sys.modules['RPi'] - del sys.modules['RPi.GPIO'] - - -@pytest.fixture(scope='function', autouse=False) -def spidev(): - """Mock Spidev module.""" - - spidev = mock.MagicMock() - - sys.modules['spidev'] = spidev - yield spidev - del sys.modules['spidev'] diff --git a/library/tests/test_dimensions.py b/library/tests/test_dimensions.py deleted file mode 100644 index 1139291..0000000 --- a/library/tests/test_dimensions.py +++ /dev/null @@ -1,19 +0,0 @@ -def test_240_240(GPIO, spidev): - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24, width=240, height=240, rotation=0) - assert display.width == 240 - assert display.height == 240 - - -def test_240_135(GPIO, spidev): - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24, width=240, height=135, rotation=0) - assert display.width == 240 - assert display.height == 135 - - -def test_320_240(GPIO, spidev): - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24, width=320, height=240, rotation=0) - assert display.width == 320 - assert display.height == 240 diff --git a/library/tests/test_display.py b/library/tests/test_display.py deleted file mode 100644 index 9162cda..0000000 --- a/library/tests/test_display.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -def test_display_pil_image(GPIO, spidev): - from PIL import Image - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24) - - image = Image.new("RGB", (display.width, display.width)) - display.display(image) - - -def test_display_numpy_array(GPIO, spidev): - import numpy - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24) - - image = numpy.empty((display.width, display.height, 3)) - display.display(image) diff --git a/library/tests/test_setup.py b/library/tests/test_setup.py deleted file mode 100644 index 6b8d282..0000000 --- a/library/tests/test_setup.py +++ /dev/null @@ -1,32 +0,0 @@ -import pytest - -def test_setup(GPIO, spidev): - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24) - del display - - -def test_backlight(GPIO, spidev): - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24, backlight=19) - display.set_backlight(1) - - -def test_reset(GPIO, spidev): - import ST7789 - display = ST7789.ST7789(port=0, cs=0, dc=24, rst=19) - display.reset() - - -def test_unsupported_rotation_320_x_240_90(GPIO, spidev): - import ST7789 - with pytest.raises(ValueError): - display = ST7789.ST7789(port=0, cs=0, dc=24, width=320, height=240, rotation=90) - del display - - -def test_unsupported_rotation_320_x_240_270(GPIO, spidev): - import ST7789 - with pytest.raises(ValueError): - display = ST7789.ST7789(port=0, cs=0, dc=24, width=320, height=240, rotation=270) - del display \ No newline at end of file diff --git a/library/tox.ini b/library/tox.ini deleted file mode 100644 index d6ce04d..0000000 --- a/library/tox.ini +++ /dev/null @@ -1,26 +0,0 @@ -[tox] -envlist = py{27,37,38,39},qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py install - coverage run -m py.test -v -r wsx - coverage report -deps = - numpy - pillow - mock - pytest>=3.1 - pytest-cov - -[testenv:qa] -commands = - check-manifest --ignore tox.ini,tests/*,.coveragerc - python setup.py sdist bdist_wheel - twine check dist/* - flake8 --ignore E501 -deps= - check-manifest - flake8 - twine diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8574a23 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,117 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "st7789" +dynamic = ["version", "readme"] +description = "Driver for ST7789-based TFT LCD displays." +license = {file = "LICENSE"} +requires-python = ">= 3.7" +authors = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +maintainers = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +keywords = [ + "Pi", + "Raspberry", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", +] +dependencies = [ + "gpiod", + "gpiodevice >= 0.0.4" +] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/st7789-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "st7789/__init__.py" + +[tool.hatch.build] +include = [ + "st7789", + "ST7789.py", + "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.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[tool.pimoroni] +apt_packages = [] +configtxt = [] +commands = [] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..525b042 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +check-manifest +ruff +codespell +isort +twine +hatch +hatch-fancy-pypi-readme +tox +pdoc diff --git a/library/ST7789/__init__.py b/st7789/__init__.py similarity index 90% rename from library/ST7789/__init__.py rename to st7789/__init__.py index 1c0ae4f..9214c8f 100644 --- a/library/ST7789/__init__.py +++ b/st7789/__init__.py @@ -20,14 +20,17 @@ # THE SOFTWARE. import numbers import time -import numpy as np +import numpy +import gpiod +import gpiodevice import spidev -import RPi.GPIO as GPIO - +from gpiod.line import Direction, Value __version__ = '0.0.4' +OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE) + BG_SPI_CS_BACK = 0 BG_SPI_CS_FRONT = 1 @@ -117,8 +120,7 @@ def __init__(self, port, cs, dc, backlight=None, rst=None, width=240, if width != height and rotation in [90, 270]: raise ValueError("Invalid rotation {} for {}x{} resolution".format(rotation, width, height)) - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) + gpiodevice.friendly_errors = True self._spi = spidev.SpiDev(port, cs) self._spi.mode = 0 @@ -136,22 +138,25 @@ def __init__(self, port, cs, dc, backlight=None, rst=None, width=240, self._offset_top = offset_top # Set DC as output. - GPIO.setup(dc, GPIO.OUT) + self._dc = gpiodevice.get_pin(dc, "st7789-dc", OUTL) # Setup backlight as output (if provided). - self._backlight = backlight if backlight is not None: - GPIO.setup(backlight, GPIO.OUT) - GPIO.output(backlight, GPIO.LOW) + self._bl = gpiodevice.get_pin(backlight, "st7789-bl", OUTL) + self.set_pin(self._bl, False) time.sleep(0.1) - GPIO.output(backlight, GPIO.HIGH) + self.set_pin(self._bl, True) # Setup reset as output (if provided). if rst is not None: - GPIO.setup(self._rst, GPIO.OUT) - self.reset() + self._rst = gpiodevice.get_pin(rst, "st7789-rst", OUTL) + self._init() + def set_pin(self, pin, state): + lines, offset = pin + lines.set_value(offset, Value.ACTIVE if state else Value.INACTIVE) + def send(self, data, is_data=True, chunk_size=4096): """Write a byte or array of bytes to the display. Is_data parameter controls if byte should be interpreted as display data (True) or command @@ -159,7 +164,7 @@ def send(self, data, is_data=True, chunk_size=4096): single SPI transaction, with a default of 4096. """ # Set DC low for command, high for data. - GPIO.output(self._dc, is_data) + self.set_pin(self._dc, is_data) # Convert scalar argument to list so either can be passed as parameter. if isinstance(data, numbers.Number): data = [data & 0xFF] @@ -170,8 +175,8 @@ def send(self, data, is_data=True, chunk_size=4096): def set_backlight(self, value): """Set the backlight on/off.""" - if self._backlight is not None: - GPIO.output(self._backlight, value) + if self._bl is not None: + self.set_pin(self._bl, value) @property def width(self): @@ -192,11 +197,11 @@ def data(self, data): def reset(self): """Reset the display, if reset pin is connected.""" if self._rst is not None: - GPIO.output(self._rst, 1) + self.set_pin(self._rst, True) time.sleep(0.500) - GPIO.output(self._rst, 0) + self.set_pin(self._rst, False) time.sleep(0.500) - GPIO.output(self._rst, 1) + self.set_pin(self._rst, True) time.sleep(0.500) def _init(self): @@ -342,11 +347,11 @@ def display(self, image): self.data(pixelbytes[i:i + 4096]) def image_to_data(self, image, rotation=0): - if not isinstance(image, np.ndarray): - image = np.array(image.convert('RGB')) + if not isinstance(image, numpy.ndarray): + image = numpy.array(image.convert('RGB')) # Rotate the image - pb = np.rot90(image, rotation // 90).astype('uint16') + pb = numpy.rot90(image, rotation // 90).astype('uint16') # Mask and shift the 888 RGB into 565 RGB red = (pb[..., [0]] & 0xf8) << 8 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..eb8579f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,47 @@ +import sys + +import mock +import pytest + + +@pytest.fixture(scope="function", autouse=False) +def st7789(): + import st7789 + yield st7789 + del sys.modules["st7789"] + + +@pytest.fixture(scope="function", autouse=False) +def gpiod(): + """Mock gpiod module.""" + sys.modules["gpiod"] = mock.MagicMock() + sys.modules["gpiod.line"] = mock.MagicMock() + yield sys.modules["gpiod"] + del sys.modules["gpiod"] + + +@pytest.fixture(scope="function", autouse=False) +def gpiodevice(): + """Mock gpiodevice module.""" + sys.modules["gpiodevice"] = mock.MagicMock() + sys.modules["gpiodevice"].get_pin.return_value = (mock.Mock(), 0) + yield sys.modules["gpiodevice"] + del sys.modules["gpiodevice"] + + +@pytest.fixture(scope="function", autouse=False) +def spidev(): + """Mock spidev module.""" + spidev = mock.MagicMock() + sys.modules["spidev"] = spidev + yield spidev + del sys.modules["spidev"] + + +@pytest.fixture(scope="function", autouse=False) +def numpy(): + """Mock numpy module.""" + numpy = mock.MagicMock() + sys.modules["numpy"] = numpy + yield numpy + del sys.modules["numpy"] diff --git a/tests/test_dimensions.py b/tests/test_dimensions.py new file mode 100644 index 0000000..5de8d62 --- /dev/null +++ b/tests/test_dimensions.py @@ -0,0 +1,16 @@ +def test_240_240(gpiodevice, gpiod, spidev, numpy, st7789): + display = st7789.ST7789(port=0, cs=0, dc=24, width=240, height=240, rotation=0) + assert display.width == 240 + assert display.height == 240 + + +def test_240_135(gpiodevice, gpiod, spidev, numpy, st7789): + display = st7789.ST7789(port=0, cs=0, dc=24, width=240, height=135, rotation=0) + assert display.width == 240 + assert display.height == 135 + + +def test_320_240(gpiodevice, gpiod, spidev, numpy, st7789): + display = st7789.ST7789(port=0, cs=0, dc=24, width=320, height=240, rotation=0) + assert display.width == 320 + assert display.height == 240 diff --git a/tests/test_display.py b/tests/test_display.py new file mode 100644 index 0000000..3b059ce --- /dev/null +++ b/tests/test_display.py @@ -0,0 +1,14 @@ +def test_display_pil_image(gpiodevice, gpiod, spidev, st7789): + from PIL import Image + display = st7789.ST7789(port=0, cs=0, dc=24) + + image = Image.new("RGB", (display.width, display.width)) + display.display(image) + + +def test_display_numpy_array(gpiodevice, gpiod, spidev, st7789): + import numpy + display = st7789.ST7789(port=0, cs=0, dc=24) + + image = numpy.empty((display.width, display.height, 3)) + display.display(image) diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..e635816 --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,27 @@ +import pytest + +def test_setup(gpiodevice, gpiod, spidev, numpy, st7789): + display = st7789.ST7789(port=0, cs=0, dc=24) + del display + + +def test_backlight(gpiodevice, gpiod, spidev, numpy, st7789): + display = st7789.ST7789(port=0, cs=0, dc=24, backlight=19) + display.set_backlight(1) + + +def test_reset(gpiodevice, gpiod, spidev, numpy, st7789): + display = st7789.ST7789(port=0, cs=0, dc=24, rst=19) + display.reset() + + +def test_unsupported_rotation_320_x_240_90(gpiodevice, gpiod, spidev, numpy, st7789): + with pytest.raises(ValueError): + display = st7789.ST7789(port=0, cs=0, dc=24, width=320, height=240, rotation=90) + del display + + +def test_unsupported_rotation_320_x_240_270(gpiodevice, gpiod, spidev, numpy, st7789): + with pytest.raises(ValueError): + display = st7789.ST7789(port=0, cs=0, dc=24, width=320, height=240, rotation=270) + del display \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..d887e83 --- /dev/null +++ b/tox.ini @@ -0,0 +1,36 @@ +[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 + numpy + pillow + +[testenv:qa] +commands = + check-manifest + python -m build --no-isolation + python -m twine check dist/* + isort --check . + ruff check . + codespell . +deps = + check-manifest + ruff + codespell + isort + twine + build + hatch + hatch-fancy-pypi-readme + diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..3314b7f --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +FORCE=false +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME +PYTHON="python" + + +venv_check() { + PYTHON_BIN=$(which $PYTHON) + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + exit 1 + fi +} + +user_check() { + if [ "$(id -u)" -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" + exit 1 + fi +} + +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} + +prompt() { + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" + +user_check +venv_check + +printf "Uninstalling for Python 3...\n" +$PYTHON -m pip uninstall "$LIBRARY_NAME" + +if [ -d "$RESOURCES_DIR" ]; then + if confirm "Would you like to delete $RESOURCES_DIR?"; then + rm -r "$RESOURCES_DIR" + fi +fi + +printf "Done!\n" From 8766ebfcad4ef8b3f8999687b93dd5a937bcf742 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 12 Jun 2024 15:28:26 +0100 Subject: [PATCH 2/6] QA: Fix trailing whitespace. --- .github/ISSUE_TEMPLATE.md | 2 +- examples/image.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 23c9efd..0ad8383 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -19,7 +19,7 @@ common troubleshooting steps below before creating the issue: library the code depends on is not installed. Check the tutorial/guide or README to ensure you have installed the necessary libraries. Usually the missing library can be installed with the `pip` tool, but check the tutorial/guide - for the exact command. + for the exact command. - **Be sure you are supplying adequate power to the board.** Check the specs of your board and power in an external power supply. In many cases just diff --git a/examples/image.py b/examples/image.py index 9d09ab9..bd181e0 100755 --- a/examples/image.py +++ b/examples/image.py @@ -19,7 +19,7 @@ * square - 240x240 1.3" Square LCD * round - 240x240 1.3" Round LCD (applies an offset) * rect - 240x135 1.14" Rectangular LCD (applies an offset) - * dhmini - 320x240 2.0" Display HAT Mini + * dhmini - 320x240 2.0" Display HAT Mini """.format(sys.argv[0])) sys.exit(1) From e94c5db344cbe1a9dc4a108a5734cdcac1088807 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 12 Jun 2024 15:29:48 +0100 Subject: [PATCH 3/6] QA: Apply isort suggestions. --- examples/320x240.py | 7 ++++--- examples/framerate.py | 6 +++--- examples/gif.py | 6 ++++-- examples/image.py | 1 + examples/round.py | 8 +++----- examples/scrolling-text.py | 5 +---- examples/shapes.py | 4 +--- st7789/__init__.py | 2 +- tests/test_setup.py | 1 + 9 files changed, 19 insertions(+), 21 deletions(-) diff --git a/examples/320x240.py b/examples/320x240.py index 80fff1b..3b16254 100755 --- a/examples/320x240.py +++ b/examples/320x240.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -from st7789 import ST7789, BG_SPI_CS_FRONT -from PIL import Image, ImageDraw - import random import time +from PIL import Image, ImageDraw + +from st7789 import BG_SPI_CS_FRONT, ST7789 + # Buttons BUTTON_A = 5 BUTTON_B = 6 diff --git a/examples/framerate.py b/examples/framerate.py index 4cd3963..83fb2f3 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -import time import math import sys +import time + +from PIL import Image, ImageDraw -from PIL import Image -from PIL import ImageDraw import st7789 # Higher SPI bus speed = higher framerate diff --git a/examples/gif.py b/examples/gif.py index 2f9e70c..5629465 100755 --- a/examples/gif.py +++ b/examples/gif.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 +import sys +import time + from PIL import Image + import st7789 -import time -import sys print(""" gif.py - Display a gif on the LCD. diff --git a/examples/image.py b/examples/image.py index bd181e0..13c4794 100755 --- a/examples/image.py +++ b/examples/image.py @@ -2,6 +2,7 @@ import sys from PIL import Image + import st7789 print(""" diff --git a/examples/round.py b/examples/round.py index 5be62ed..d0f46ce 100755 --- a/examples/round.py +++ b/examples/round.py @@ -1,12 +1,10 @@ #!/usr/bin/env python3 -import sys +import colorsys import math +import sys import time -import colorsys -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont +from PIL import Image, ImageDraw, ImageFont import st7789 diff --git a/examples/scrolling-text.py b/examples/scrolling-text.py index b3cec15..98509c1 100755 --- a/examples/scrolling-text.py +++ b/examples/scrolling-text.py @@ -2,13 +2,10 @@ import sys import time -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont +from PIL import Image, ImageDraw, ImageFont import st7789 - MESSAGE = "Hello World! How are you today?" print(""" diff --git a/examples/shapes.py b/examples/shapes.py index 6b60741..0002b87 100755 --- a/examples/shapes.py +++ b/examples/shapes.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 import sys -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont +from PIL import Image, ImageDraw, ImageFont import st7789 diff --git a/st7789/__init__.py b/st7789/__init__.py index 9214c8f..acf7900 100644 --- a/st7789/__init__.py +++ b/st7789/__init__.py @@ -20,10 +20,10 @@ # THE SOFTWARE. import numbers import time -import numpy import gpiod import gpiodevice +import numpy import spidev from gpiod.line import Direction, Value diff --git a/tests/test_setup.py b/tests/test_setup.py index e635816..0964477 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,6 @@ import pytest + def test_setup(gpiodevice, gpiod, spidev, numpy, st7789): display = st7789.ST7789(port=0, cs=0, dc=24) del display From 1f736db29cb6410860bf872b5a661c0b87d4e43a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 12 Jun 2024 15:47:45 +0100 Subject: [PATCH 4/6] QA: Apply ruff suggestions. --- examples/320x240.py | 3 +-- examples/round.py | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/320x240.py b/examples/320x240.py index 3b16254..b78e2c4 100755 --- a/examples/320x240.py +++ b/examples/320x240.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -import random import time from PIL import Image, ImageDraw -from st7789 import BG_SPI_CS_FRONT, ST7789 +from st7789 import ST7789 # Buttons BUTTON_A = 5 diff --git a/examples/round.py b/examples/round.py index d0f46ce..18a56cf 100755 --- a/examples/round.py +++ b/examples/round.py @@ -4,7 +4,7 @@ import sys import time -from PIL import Image, ImageDraw, ImageFont +from PIL import Image, ImageDraw import st7789 @@ -74,12 +74,12 @@ x = RADIUS + int(distance * math.cos(angle)) y = RADIUS + int(distance * math.sin(angle)) - l = ((math.sin(t + angle) + 1) / 2.0) * 10 + line = ((math.sin(t + angle) + 1) / 2.0) * 10 if style == "lines": - draw.line((prev_x + l, prev_y + l, x - l, y - l), fill=(r, g, b)) + draw.line((prev_x + line, prev_y + line, x - line, y - line), fill=(r, g, b)) else: - l += 1 - draw.ellipse((x - l, y - l, x + (l * 2), y + (l * 2)), fill=(r, g, b)) + line += 1 + draw.ellipse((x - line, y - line, x + (line * 2), y + (line * 2)), fill=(r, g, b)) prev_x = x prev_y = y From 027da7558f74c89577df095718b3f5f716d2ca9c Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 12 Jun 2024 15:53:42 +0100 Subject: [PATCH 5/6] QA: Switch to f-strings. --- examples/framerate.py | 15 ++++++--------- examples/gif.py | 8 ++++---- examples/image.py | 6 +++--- examples/round.py | 6 +++--- examples/scrolling-text.py | 6 +++--- examples/shapes.py | 6 +++--- st7789/__init__.py | 4 ++-- 7 files changed, 24 insertions(+), 27 deletions(-) diff --git a/examples/framerate.py b/examples/framerate.py index 83fb2f3..e70f16b 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -20,13 +20,13 @@ except IndexError: display_type = "square" -print(""" +print(f""" framerate.py - Test LCD framerate. If you're using Breakout Garden, plug the 1.3" LCD (SPI) breakout into the front slot. -Usage: {} +Usage: {sys.argv[0]} Where is one of: * square - 240x240 1.3" Square LCD @@ -34,8 +34,8 @@ * rect - 240x135 1.14" Rectangular LCD (applies an offset) * dhmini - 320x240 2.0" Rectangular LCD -Running at: {}MHz on a {} display. -""".format(sys.argv[0], SPI_SPEED_MHZ, display_type)) +Running at: {SPI_SPEED_MHZ}MHz on a {display_type} display. +""") try: width, height, rotation, backlight, offset_left, offset_top = { @@ -45,7 +45,7 @@ "dhmini": (320, 240, 180, 13, 0, 0) }[display_type] except IndexError: - raise RuntimeError("Unsupported display type: {}".format(display_type)) + raise RuntimeError(f"Unsupported display type: {display_type}") # Create ST7789 LCD display class. disp = st7789.ST7789( @@ -89,7 +89,4 @@ count += 1 time_current = time.time() - time_start if count % 120 == 0: - print("Time: {:8.3f}, Frames: {:6d}, FPS: {:8.3f}".format( - time_current, - count, - count / time_current)) + print(f"Time: {time_current:8.3f}, Frames: {count:6d}, FPS: {count / time_current:8.3f}") diff --git a/examples/gif.py b/examples/gif.py index 5629465..7e63fd8 100755 --- a/examples/gif.py +++ b/examples/gif.py @@ -15,17 +15,17 @@ """) if len(sys.argv) < 2: - print("""Usage: {path} + print(f"""Usage: {sys.argv[0]} Where is a .gif file. - Hint: {path} deployrainbows.gif + Hint: {sys.argv[0]} deployrainbows.gif And is one of: * square - 240x240 1.3" Square LCD * round - 240x240 1.3" Round LCD (applies an offset) * rect - 240x135 1.14" Rectangular LCD (applies an offset) * dhmini - 320x240 2.0" Display HAT Mini -""".format(path=sys.argv[0])) +""") sys.exit(1) image_file = sys.argv[1] @@ -74,7 +74,7 @@ height = disp.height # Load an image. -print('Loading gif: {}...'.format(image_file)) +print(f'Loading gif: {image_file}...') image = Image.open(image_file) print('Drawing gif, press Ctrl+C to exit!') diff --git a/examples/image.py b/examples/image.py index 13c4794..c28962a 100755 --- a/examples/image.py +++ b/examples/image.py @@ -14,14 +14,14 @@ """) if len(sys.argv) < 2: - print("""Usage: {} + print(f"""Usage: {sys.argv[0]} Where is one of: * square - 240x240 1.3" Square LCD * round - 240x240 1.3" Round LCD (applies an offset) * rect - 240x135 1.14" Rectangular LCD (applies an offset) * dhmini - 320x240 2.0" Display HAT Mini -""".format(sys.argv[0])) +""") sys.exit(1) image_file = sys.argv[1] @@ -70,7 +70,7 @@ disp.begin() # Load an image. -print('Loading image: {}...'.format(image_file)) +print(f'Loading image: {image_file}...') image = Image.open(image_file) # Resize the image diff --git a/examples/round.py b/examples/round.py index 18a56cf..9389bcf 100755 --- a/examples/round.py +++ b/examples/round.py @@ -8,20 +8,20 @@ import st7789 -print(""" +print(f""" round.py - Shiny shiny round LCD! If you're using Breakout Garden, plug a 1.3" ROUND LCD (SPI) breakout into the front slot. -Usage: {}