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/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/.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..3d3a7cb --- /dev/null +++ b/ST7789.py @@ -0,0 +1,9 @@ +from warnings import warn + +from st7789 import * # noqa F403 + +warn( + 'Using "import ST7789" is deprecated. Please "import st7789" (all lowercase)!', + DeprecationWarning, + stacklevel=2, +) 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..98844af 100755 --- a/examples/320x240.py +++ b/examples/320x240.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -from ST7789 import ST7789, BG_SPI_CS_FRONT +import time + from PIL import Image, ImageDraw -import random -import time +from st7789 import ST7789 # Buttons BUTTON_A = 5 @@ -30,9 +30,9 @@ draw = ImageDraw.Draw(buffer) draw.rectangle((0, 0, 50, 50), (255, 0, 0)) -draw.rectangle((320-50, 0, 320, 50), (0, 255, 0)) -draw.rectangle((0, 240-50, 50, 240), (0, 0, 255)) -draw.rectangle((320-50, 240-50, 320, 240), (255, 255, 0)) +draw.rectangle((320 - 50, 0, 320, 50), (0, 255, 0)) +draw.rectangle((0, 240 - 50, 50, 240), (0, 0, 255)) +draw.rectangle((320 - 50, 240 - 50, 320, 240), (255, 255, 0)) display = ST7789( port=SPI_PORT, @@ -42,9 +42,9 @@ width=WIDTH, height=HEIGHT, rotation=180, - spi_speed_hz=60 * 1000 * 1000 + spi_speed_hz=60 * 1000 * 1000, ) while True: display.display(buffer) - time.sleep(1.0 / 60) \ No newline at end of file + time.sleep(1.0 / 60) diff --git a/examples/framerate.py b/examples/framerate.py index 8c2e513..0589d77 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -import time import math import sys +import time -from PIL import Image -from PIL import ImageDraw -import ST7789 as ST7789 +from PIL import Image, ImageDraw + +import st7789 # Higher SPI bus speed = higher framerate try: @@ -20,13 +20,14 @@ 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,31 +35,32 @@ * 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 = { "square": (240, 240, 90, 19, 0, 0), "round": (240, 240, 90, 19, 40, 0), "rect": (240, 135, 0, 19, 40, 53), - "dhmini": (320, 240, 180, 13, 0, 0) + "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( +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. + backlight=backlight, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=SPI_SPEED_MHZ * 1000000, offset_left=offset_left, - offset_top=offset_top + offset_top=offset_top, ) WIDTH = disp.width @@ -71,9 +73,9 @@ draw = ImageDraw.Draw(image) if step % 2 == 0: - draw.rectangle((WIDTH/2, int(HEIGHT/2), WIDTH, HEIGHT), (0, 128, 0)) + draw.rectangle((WIDTH / 2, int(HEIGHT / 2), WIDTH, HEIGHT), (0, 128, 0)) else: - draw.rectangle((0, 0, WIDTH/2-1, int(HEIGHT/2)-1), (0, 128, 0)) + draw.rectangle((0, 0, WIDTH / 2 - 1, int(HEIGHT / 2) - 1), (0, 128, 0)) f = math.sin((float(step) / STEPS) * math.pi) offset_left = int(f * WIDTH) @@ -89,7 +91,6 @@ 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 8ce7c0a..c4bda8a 100755 --- a/examples/gif.py +++ b/examples/gif.py @@ -1,29 +1,35 @@ #!/usr/bin/env python3 -from PIL import Image -import ST7789 -import time import sys +import time + +from PIL import Image + +import st7789 -print(""" +print( + """ gif.py - Display a gif on the LCD. If you're using Breakout Garden, plug the 1.3" LCD (SPI) breakout into the front slot. -""") +""" +) 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] @@ -36,20 +42,20 @@ # 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. + backlight=19, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=80 * 1000 * 1000, offset_left=0 if display_type == "square" else 40, - offset_top=53 if display_type == "rect" else 0 + offset_top=53 if display_type == "rect" else 0, ) elif display_type == "dhmini": - disp = ST7789.ST7789( + disp = st7789.ST7789( height=240, width=320, rotation=180, @@ -59,11 +65,11 @@ backlight=13, spi_speed_hz=60 * 1000 * 1000, offset_left=0, - offset_top=0 - ) + offset_top=0, + ) else: - print ("Invalid display type!") + print("Invalid display type!") # Initialize display. disp.begin() @@ -72,10 +78,10 @@ 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!') +print("Drawing gif, press Ctrl+C to exit!") frame = 0 diff --git a/examples/image.py b/examples/image.py index 353502c..87af7be 100755 --- a/examples/image.py +++ b/examples/image.py @@ -2,25 +2,30 @@ import sys from PIL import Image -import ST7789 as ST7789 -print(""" +import st7789 + +print( + """ image.py - Display an image on the LCD. If you're using Breakout Garden, plug the 1.3" LCD (SPI) breakout into the front slot. -""") +""" +) 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])) + * dhmini - 320x240 2.0" Display HAT Mini +""" + ) sys.exit(1) image_file = sys.argv[1] @@ -33,20 +38,20 @@ # 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. + backlight=19, # 18 for back BG slot, 19 for front BG slot. spi_speed_hz=80 * 1000 * 1000, offset_left=0 if display_type == "square" else 40, - offset_top=53 if display_type == "rect" else 0 + offset_top=53 if display_type == "rect" else 0, ) elif display_type == "dhmini": - disp = ST7789.ST7789( + disp = st7789.ST7789( height=240, width=320, rotation=180, @@ -56,11 +61,11 @@ backlight=13, spi_speed_hz=60 * 1000 * 1000, offset_left=0, - offset_top=0 - ) + offset_top=0, + ) else: - print ("Invalid display type!") + print("Invalid display type!") WIDTH = disp.width HEIGHT = disp.height @@ -69,13 +74,13 @@ 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 image = image.resize((WIDTH, HEIGHT)) # Draw the image on the display hardware. -print('Drawing image') +print("Drawing image") disp.display(image) diff --git a/examples/round.py b/examples/round.py index 91df4f1..b37b0d7 100755 --- a/examples/round.py +++ b/examples/round.py @@ -1,29 +1,29 @@ #!/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 -import ST7789 +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: {}