Merge pull request #33 from pimoroni/repackage

Repackage to pyproject/hatch and port to gpiod
This commit is contained in:
Philip Howard
2023-11-08 13:38:45 +00:00
committed by GitHub
35 changed files with 725 additions and 501 deletions

41
.github/workflows/build.yml vendored Normal file
View File

@@ -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/

36
.github/workflows/qa.yml vendored Normal file
View File

@@ -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

View File

@@ -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: ['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 --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.11' }}

View File

@@ -1,70 +1,60 @@
LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}')
LIBRARY_NAME=$(shell grep name library/setup.cfg | 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 <target>, where target is one of:\n"
@echo "install: install the library locally from source"
@echo "uninstall: uninstall the local library"
@echo "check: peform basic integrity checks on the codebase"
@echo "python-readme: generate library/README.md from README.md + library/CHANGELOG.txt"
@echo "python-wheels: build python .whl files for distribution"
@echo "python-sdist: build python source distribution"
@echo "python-clean: clean python build and dist directories"
@echo "python-dist: build all python distribution files"
@echo "python-testdeploy: build all and deploy to test PyPi"
@echo "tag: tag the repository with the current version"
@echo "install: install the library locally from source"
@echo "uninstall: uninstall the local library"
@echo "dev-deps: install Python dev dependencies"
@echo "check: perform basic integrity checks on the codebase"
@echo "qa: run linting and package QA"
@echo "pytest: run Python test fixtures"
@echo "clean: clean Python build and dist directories"
@echo "build: build Python distribution files"
@echo "testdeploy: build and upload to test PyPi"
@echo "deploy: build and upload to PyPi"
@echo "tag: tag the repository with the current version\n"
install:
./install.sh
./install.sh --unstable
uninstall:
./uninstall.sh
dev-deps:
python3 -m pip install -r requirements-dev.txt
sudo apt install dos2unix
check:
@echo "Checking for trailing whitespace"
@! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO
@echo "Checking for DOS line-endings"
@! grep -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"
@bash check.sh
qa:
tox -e qa
pytest:
tox -e py
nopost:
@bash check.sh --nopost
tag:
git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}"
build: check
@hatch build
clean:
-rm -r dist
library/README.md: README.md library/CHANGELOG.txt
cp README.md library/README.md
printf "\n# Changelog\n" >> library/README.md
testdeploy: build
twine upload --repository testpypi dist/*
library/LICENSE.txt: LICENSE
cp LICENSE library/LICENSE.txt
python-wheels: python-readme python-license
cd library; python3 setup.py bdist_wheel
cd library; python setup.py bdist_wheel
python-sdist: python-readme python-license
cd library; python setup.py sdist
python-clean:
-rm -r library/dist
-rm -r library/build
-rm -r library/*.egg-info
python-dist: python-clean python-wheels python-sdist
ls library/dist
python-testdeploy: python-dist
twine upload --repository-url https://test.pypi.org/legacy/ library/dist/*
python-deploy: check python-dist
deploy: nopost build
twine upload dist/*

View File

@@ -1,50 +1,22 @@
# Python ST7735
[![Build Status](https://travis-ci.com/pimoroni/st7735-python.svg?branch=master)](https://travis-ci.com/pimoroni/st7735-python)
[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/st7735-python/test.yml?branch=main)](https://github.com/pimoroni/st7735-python/actions/workflows/test.yml)
[![Coverage Status](https://coveralls.io/repos/github/pimoroni/st7735-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/st7735-python?branch=master)
[![PyPi Package](https://img.shields.io/pypi/v/st7735.svg)](https://pypi.python.org/pypi/st7735)
[![Python Versions](https://img.shields.io/pypi/pyversions/st7735.svg)](https://pypi.python.org/pypi/st7735)
Python library to control an ST7735 TFT LCD display. Allows simple drawing on the display without installing a kernel module.
Designed specifically to work with a ST7735 based 160x80 pixel TFT SPI display. (Specifically the 0.96" SPI LCD from Pimoroni).
## Installing
### Python 2
Make sure you have the following dependencies:
````
sudo apt update
sudo apt install python-rpi.gpio python-spidev python-pip python-pil python-numpy
````
Install this library by running:
````
sudo pip install st7735
````
### Python 3
Make sure you have the following dependencies:
````
sudo apt update
sudo apt install python3-rpi.gpio python3-spidev python3-pip python3-pil python3-numpy
````
Install this library by running:
````
sudo python3 -m pip install st7735
pip install st7735
````
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.

5
ST7735.py Normal file
View File

@@ -0,0 +1,5 @@
from warnings import warn
from st7735 import * # noqa F403
warn("Using \"import ST7735\" is deprecated. Please \"import st7735\" (all lowercase)!", DeprecationWarning, stacklevel=2)

87
check.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/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))}'`
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

View File

@@ -18,15 +18,15 @@
# 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.
import time
import math
import sys
import time
from PIL import Image
from PIL import ImageDraw
import ST7735 as ST7735
from PIL import Image, ImageDraw
SPI_SPEED_MHZ = 10 # Higher speed = higher framerate
import st7735
SPI_SPEED_MHZ = 4 # Higher speed = higher framerate
if len(sys.argv) > 1:
SPI_SPEED_MHZ = int(sys.argv[1])
@@ -41,11 +41,11 @@ Running at: {}MHz
""".format(SPI_SPEED_MHZ))
# Create ST7735 LCD display class.
disp = ST7735.ST7735(
disp = st7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc=9,
backlight=19, # 18 for back BG slot, 19 for front BG slot.
cs=st7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc="PIN21",
backlight="PIN35", # 18 for back BG slot, 19 for front BG slot.
rotation=90,
spi_speed_hz=SPI_SPEED_MHZ * 1000000
)

View File

@@ -18,10 +18,12 @@
# 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 PIL import Image
import ST7735
import time
import sys
import time
from PIL import Image
import st7735
print("""
gif.py - Display a gif on the LCD.
@@ -37,11 +39,11 @@ else:
sys.exit(0)
# Create TFT LCD display class.
disp = ST7735.ST7735(
disp = st7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc=9,
backlight=19, # 18 for back BG slot, 19 for front BG slot.
cs=st7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc="GPIO9",
backlight="GPIO19", # 18 for back BG slot, 19 for front BG slot.
spi_speed_hz=4000000
)

View File

@@ -21,7 +21,8 @@
import sys
from PIL import Image
import ST7735 as ST7735
import st7735
print("""
image.py - Display an image on the LCD.
@@ -37,11 +38,11 @@ if len(sys.argv) < 2:
image_file = sys.argv[1]
# Create ST7735 LCD display class.
disp = ST7735.ST7735(
disp = st7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc=9,
backlight=19, # 18 for back BG slot, 19 for front BG slot.
cs=st7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc="GPIO9",
backlight="GPIO19", # 18 for back BG slot, 19 for front BG slot.
rotation=90,
spi_speed_hz=4000000
)

View File

@@ -1,19 +1,17 @@
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import time
import ST7735
from PIL import Image, ImageDraw, ImageFont
import st7735
MESSAGE = "Hello World! How are you today?"
# Create ST7735 LCD display class.
disp = ST7735.ST7735(
disp = st7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc=9,
backlight=19, # 18 for back BG slot, 19 for front BG slot.
cs=st7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc="GPIO9",
backlight="GPIO19", # 18 for back BG slot, 19 for front BG slot.
rotation=90,
spi_speed_hz=10000000
)

View File

@@ -18,11 +18,9 @@
# 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 PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from PIL import Image, ImageDraw, ImageFont
import ST7735
import st7735
print("""
shapes.py - Display test shapes on the LCD using PIL.
@@ -33,11 +31,11 @@ breakout into the rear slot.
""")
# Create ST7735 LCD display class.
disp = ST7735.ST7735(
disp = st7735.ST7735(
port=0,
cs=ST7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc=9,
backlight=19, # 18 for back BG slot, 19 for front BG slot.
cs=st7735.BG_SPI_CS_FRONT, # BG_SPI_CSB_BACK or BG_SPI_CS_FRONT
dc="GPIO9",
backlight="GPIO19", # 18 for back BG slot, 19 for front BG slot.
rotation=90,
spi_speed_hz=4000000
)

View File

@@ -1,26 +1,29 @@
#!/bin/bash
LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'`
CONFIG=/boot/config.txt
DATESTAMP=`date "+%Y-%M-%d-%H-%M-%S"`
DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"`
CONFIG_BACKUP=false
APT_HAS_UPDATED=false
USER_HOME=/home/$SUDO_USER
RESOURCES_TOP_DIR=$USER_HOME/Pimoroni
RESOURCES_TOP_DIR=$HOME/Pimoroni
VENV_BASH_SNIPPET=$RESOURCES_DIR/auto_venv.sh
VENV_DIR=$HOME/.virtualenvs/pimoroni
WD=`pwd`
USAGE="sudo ./install.sh (--unstable)"
USAGE="./install.sh (--unstable)"
POSITIONAL_ARGS=()
FORCE=false
UNSTABLE=false
PYTHON3=`which python3`
PYTHON="python"
user_check() {
if [ $(id -u) -ne 0 ]; then
printf "Script must be run as root. Try 'sudo ./install.sh'\n"
if [ $(id -u) -eq 0 ]; then
printf "Script should not be run as root. Try './install.sh'\n"
exit 1
fi
}
confirm() {
if [ "$FORCE" == '-y' ]; then
if $FORCE; then
true
else
read -r -p "$1 [y/N] " response < /dev/tty
@@ -53,12 +56,52 @@ warning() {
echo -e "$(tput setaf 1)$1$(tput sgr0)"
}
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
else
exit 1
fi
fi
}
function do_config_backup {
if [ ! $CONFIG_BACKUP == true ]; then
CONFIG_BACKUP=true
FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt"
inform "Backing up $CONFIG to /boot/$FILENAME\n"
cp $CONFIG /boot/$FILENAME
sudo cp $CONFIG /boot/$FILENAME
mkdir -p $RESOURCES_TOP_DIR/config-backups/
cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME
if [ -f "$UNINSTALLER" ]; then
@@ -83,16 +126,20 @@ function apt_pkg_install {
if ! [ "$PACKAGES" == "" ]; then
echo "Installing missing packages: $PACKAGES"
if [ ! $APT_HAS_UPDATED ]; then
apt update
sudo apt update
APT_HAS_UPDATED=true
fi
apt install -y $PACKAGES
sudo apt install -y $PACKAGES
if [ -f "$UNINSTALLER" ]; then
echo "apt uninstall -y $PACKAGES"
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
@@ -100,6 +147,15 @@ while [[ $# -gt 0 ]]; do
UNSTABLE=true
shift
;;
-f|--force)
FORCE=true
shift
;;
-p|--python)
PYTHON=$2
shift
shift
;;
*)
if [[ $1 == -* ]]; then
printf "Unrecognised option: $1\n";
@@ -112,31 +168,33 @@ while [[ $# -gt 0 ]]; do
done
user_check
venv_check
apt_pkg_install python-configparser
if [ ! -f `which $PYTHON` ]; then
printf "Python path $PYTHON not found!\n"
exit 1
fi
CONFIG_VARS=`python - <<EOF
from configparser import ConfigParser
c = ConfigParser()
c.read('library/setup.cfg')
p = dict(c['pimoroni'])
# Convert multi-line config entries into bash arrays
for k in p.keys():
fmt = '"{}"'
if '\n' in p[k]:
p[k] = "'\n\t'".join(p[k].split('\n')[1:])
fmt = "('{}')"
p[k] = fmt.format(p[k])
PYTHON_VER=`$PYTHON --version`
printf "$LIBRARY_NAME Python Library: Installer\n\n"
inform "Checking Dependencies. Please wait..."
pip_pkg_install toml
CONFIG_VARS=`$PYTHON - <<EOF
import toml
config = toml.load("pyproject.toml")
p = dict(config['tool']['pimoroni'])
# Convert list config entries into bash arrays
for k, v in p.items():
v = "'\n\t'".join(v)
p[k] = f"('{v}')"
print("""
LIBRARY_NAME="{name}"
LIBRARY_VERSION="{version}"
""".format(**c['metadata']))
print("""
PY3_DEPS={py3deps}
PY2_DEPS={py2deps}
APT_PACKAGES={apt_packages}
SETUP_CMDS={commands}
CONFIG_TXT={configtxt}
PY3_ONLY={py3only}
""".format(**p))
EOF`
@@ -150,6 +208,13 @@ eval $CONFIG_VARS
RESOURCES_DIR=$RESOURCES_TOP_DIR/$LIBRARY_NAME
UNINSTALLER=$RESOURCES_DIR/uninstall.sh
RES_DIR_OWNER=`stat -c "%U" $RESOURCES_TOP_DIR`
if [[ "$RES_DIR_OWNER" == "root" ]]; then
warning "\n\nFixing $RESOURCES_TOP_DIR permissions!\n\n"
sudo chown -R $USER:$USER $RESOURCES_TOP_DIR
fi
mkdir -p $RESOURCES_DIR
cat << EOF > $UNINSTALLER
@@ -157,49 +222,25 @@ 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 "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n"
if $UNSTABLE; then
warning "Installing unstable library from source.\n\n"
else
printf "Installing stable library from pypi.\n\n"
fi
cd library
if ! $PY3_ONLY; then
printf "Installing for Python 2..\n"
apt_pkg_install "${PY2_DEPS[@]}"
if $UNSTABLE; then
python setup.py install > /dev/null
else
pip install --upgrade $LIBRARY_NAME
fi
if [ $? -eq 0 ]; then
success "Done!\n"
echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER
fi
fi
if [ -f "$PYTHON3" ]; then
printf "Installing for Python 3..\n"
apt_pkg_install "${PY3_DEPS[@]}"
if $UNSTABLE; then
python3 setup.py install > /dev/null
else
pip3 install --upgrade $LIBRARY_NAME
fi
if [ $? -eq 0 ]; then
success "Done!\n"
echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER
fi
inform "Installing for $PYTHON_VER...\n"
apt_pkg_install "${APT_PACKAGES[@]}"
if $UNSTABLE; then
pip_pkg_install .
else
if $PY3_ONLY; then
warning "Python 3 is required to install this library...\n"
exit 1
fi
pip_pkg_install $LIBRARY_NAME
fi
if [ $? -eq 0 ]; then
success "Done!\n"
echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER
fi
cd $WD
@@ -218,9 +259,9 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do
if ! [ "$CONFIG_LINE" == "" ]; then
do_config_backup
inform "Adding $CONFIG_LINE to $CONFIG\n"
sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG
sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG
if ! grep -q "^$CONFIG_LINE" $CONFIG; then
printf "$CONFIG_LINE\n" >> $CONFIG
printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG
fi
fi
done
@@ -236,13 +277,12 @@ fi
printf "\n"
if [ -f "/usr/bin/pydoc" ]; then
if confirm "Would you like to generate documentation?"; then
pip_pkg_install pdoc
printf "Generating documentation.\n"
pydoc -w $LIBRARY_NAME > /dev/null
if [ -f "$LIBRARY_NAME.html" ]; then
cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html
rm -f $LIBRARY_NAME.html
inform "Documentation saved to $RESOURCES_DIR/docs.html"
$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."

View File

@@ -1,4 +0,0 @@
[run]
source = ST7735
omit =
.tox/*

View File

@@ -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.

View File

@@ -1,5 +0,0 @@
include CHANGELOG.txt
include LICENSE.txt
include README.md
include setup.py
recursive-include ST7735 *.py

View File

@@ -1,100 +0,0 @@
# Python ST7735
[![Build Status](https://travis-ci.com/pimoroni/st7735-python.svg?branch=master)](https://travis-ci.com/pimoroni/st7735-python)
[![Coverage Status](https://coveralls.io/repos/github/pimoroni/st7735-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/st7735-python?branch=master)
[![PyPi Package](https://img.shields.io/pypi/v/st7735.svg)](https://pypi.python.org/pypi/st7735)
[![Python Versions](https://img.shields.io/pypi/pyversions/st7735.svg)](https://pypi.python.org/pypi/st7735)
Python library to control an ST7735 TFT LCD display. Allows simple drawing on the display without installing a kernel module.
Designed specifically to work with a ST7735 based 160x80 pixel TFT SPI display. (Specifically the 0.96" SPI LCD from Pimoroni).
## Installing
### Python 2
Make sure you have the following dependencies:
````
sudo apt update
sudo apt install python-rpi.gpio python-spidev python-pip python-pil python-numpy
````
Install this library by running:
````
sudo pip install st7735
````
### Python 3
Make sure you have the following dependencies:
````
sudo apt update
sudo apt install python3-rpi.gpio python3-spidev python3-pip python3-pil python3-numpy
````
Install this library by running:
````
sudo python3 -m pip install st7735
````
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.
It has been modified by Pimoroni to include support for their 160x80 SPI LCD breakout, and hopefully also generalised enough so that it will support other ST7735-powered displays.
## 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-post1
-----------
* Repackage with Markdown README/setup.cfg
* Fix `__version__` to 0.0.4
* Update dependencies in README
0.0.4
-----
* Depend upon spidev==3.4.0 for stability fixes
* Switch from manual data chunking to spidev.xfer3()
0.0.3
-----
* Fixed backlight pin
* Added `set_backlight`
* Added constants BG_SPI_CS_FRONT and BG_SPI_CS_BACK
* Added module `__version__`
0.0.2
-----
* Support for multiple display sizes/orientations
0.0.1
-----
* Initial Release

View File

@@ -1,17 +0,0 @@
from tools import force_reimport
def test_128_64_0(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24, width=128, height=64, rotation=0)
assert display.width == 128
assert display.height == 64
def test_128_64_90(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24, width=128, height=64, rotation=90)
assert display.width == 64
assert display.height == 128

View File

@@ -1,25 +0,0 @@
import mock
from tools import force_reimport
def test_display(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24)
numpy.dstack().flatten().tolist.return_value = [0xff, 0x00, 0xff, 0x00]
display.display(mock.MagicMock())
spidev.SpiDev().xfer3.assert_called_with([0xff, 0x00, 0xff, 0x00])
def test_color565(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
assert ST7735.color565(255, 255, 255) == 0xFFFF
def test_image_to_data(GPIO, spidev, numpy):
force_reimport('ST7735')
numpy.dstack().flatten().tolist.return_value = []
import ST7735
assert ST7735.image_to_data(mock.MagicMock()) == []

View File

@@ -1,47 +0,0 @@
import mock
from tools import force_reimport
def test_setup(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24)
del display
GPIO.output.assert_has_calls([
mock.call(24, True),
mock.call(24, False)
], any_order=True)
def test_setup_no_invert(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24, invert=False)
del display
def test_setup_with_backlight(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24, backlight=4)
GPIO.setup.assert_called_with(4, GPIO.OUT)
display.set_backlight(GPIO.HIGH)
GPIO.output.assert_has_calls([
mock.call(4, GPIO.LOW),
mock.call(4, GPIO.HIGH),
# Dozens of falls with True/False here
# due to _init() being called and the display
# setup setting the command/data pin
mock.call(4, GPIO.HIGH)
], any_order=True)
def test_setup_with_reset(GPIO, spidev, numpy):
force_reimport('ST7735')
import ST7735
display = ST7735.ST7735(port=0, cs=0, dc=24, rst=4)
GPIO.setup.assert_called_with(4, GPIO.OUT)
del display

View File

@@ -1,23 +0,0 @@
import sys
def force_reimport(module):
"""Force the module under test to be re-imported.
Because pytest runs all tests within the same scope (this makes me cry)
we have to do some manual housekeeping to avoid tests polluting each other.
Since conftest.py already does some sys.modules mangling I see no reason not to
do the same thing here.
"""
if "." in module:
steps = module.split(".")
else:
steps = [module]
for i in range(len(steps)):
module = ".".join(steps[0:i + 1])
try:
del sys.modules[module]
except KeyError:
pass

View File

@@ -1,24 +0,0 @@
[tox]
envlist = py,qa
skip_missing_interpreters = True
[testenv]
commands =
python setup.py install
coverage run -m pytest -v -r wsx
coverage report
deps =
mock
pytest>=3.1
pytest-cov
[testenv:qa]
commands =
check-manifest --ignore tox.ini,tests/*,.coveragerc
python setup.py sdist bdist_wheel
twine check dist/*
flake8 --ignore E501
deps =
check-manifest
flake8
twine

120
pyproject.toml Normal file
View File

@@ -0,0 +1,120 @@
[build-system]
requires = ["hatchling", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build"
[project]
name = "st7735"
dynamic = ["version", "readme"]
description = "Library to control an ST7735 168x80 TFT LCD display."
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",
"displays"
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3 :: Only",
"Topic :: Software Development",
"Topic :: Software Development :: Libraries",
"Topic :: System :: Hardware",
]
dependencies = [
"spidev"
]
[project.urls]
GitHub = "https://www.github.com/pimoroni/st7735-python"
Homepage = "https://www.pimoroni.com"
[tool.hatch.version]
path = "st7735/__init__.py"
[tool.hatch.build]
include = [
"st7735",
"ST7735.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 = [
"printf \"Setting up SPI...\n\"",
"sudo raspi-config nonint do_spi 0"
]

9
requirements-dev.txt Normal file
View File

@@ -0,0 +1,9 @@
check-manifest
ruff
codespell
isort
twine
hatch
hatch-fancy-pypi-readme
tox
pdoc

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
pillow
numpy
spidev

View File

@@ -19,8 +19,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from setuptools import setup, __version__
from pkg_resources import parse_version
from setuptools import __version__, setup
minimum_version = parse_version('30.4.0')

View File

@@ -20,14 +20,17 @@
# THE SOFTWARE.
import numbers
import time
import gpiod
import gpiodevice
import numpy as np
import spidev
import RPi.GPIO as GPIO
from gpiod.line import Direction, Value
__version__ = '0.0.5'
OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE)
BG_SPI_CS_BACK = 0
BG_SPI_CS_FRONT = 1
@@ -122,9 +125,9 @@ class ST7735(object):
height=ST7735_TFTHEIGHT, rotation=90, offset_left=None, offset_top=None, invert=True, bgr=True, spi_speed_hz=4000000):
"""Create an instance of the display using SPI communication.
Must provide the GPIO pin number for the D/C pin and the SPI driver.
Must provide the GPIO pin label for the D/C pin and the SPI driver.
Can optionally provide the GPIO pin number for the reset pin as the rst parameter.
Can optionally provide the GPIO pin label for the reset pin as the rst parameter.
:param port: SPI port number
:param cs: SPI chip-select number (0 or 1 for BCM
@@ -140,22 +143,20 @@ class ST7735(object):
"""
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
self._spi = spidev.SpiDev(port, cs)
self._spi.mode = 0
self._spi.lsbfirst = False
self._spi.max_speed_hz = spi_speed_hz
self._dc = dc
self._rst = rst
self._width = width
self._height = height
self._rotation = rotation
self._invert = invert
self._bgr = bgr
self._bl = None
self._rst = None
# Default left offset to center display
if offset_left is None:
offset_left = (ST7735_COLS - width) // 2
@@ -168,24 +169,29 @@ class ST7735(object):
self._offset_top = offset_top
gpiodevice.friendly_errors = True
# Set DC as output.
GPIO.setup(dc, GPIO.OUT)
self._dc = gpiodevice.get_pin(dc, "st7735-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, "st7735-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(rst, GPIO.OUT)
self._rst = gpiodevice.get_pin(rst, "st7735-rst", OUTL)
self.reset()
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
@@ -193,7 +199,7 @@ class ST7735(object):
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]
@@ -201,8 +207,8 @@ class ST7735(object):
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)
def display_off(self):
self.command(ST7735_DISPOFF)
@@ -235,11 +241,11 @@ class ST7735(object):
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):

View File

@@ -3,21 +3,34 @@ These allow the mocking of various Python modules
that might otherwise have runtime side-effects.
"""
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']
def st7735():
import st7735
yield st7735
del sys.modules['st7735']
@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)

10
tests/test_dimensions.py Normal file
View File

@@ -0,0 +1,10 @@
def test_128_64_0(gpiodevice, gpiod, spidev, numpy, st7735):
display = st7735.ST7735(port=0, cs=0, dc=24, width=128, height=64, rotation=0)
assert display.width == 128
assert display.height == 64
def test_128_64_90(gpiodevice, gpiod, spidev, numpy, st7735):
display = st7735.ST7735(port=0, cs=0, dc=24, width=128, height=64, rotation=90)
assert display.width == 64
assert display.height == 128

18
tests/test_features.py Normal file
View File

@@ -0,0 +1,18 @@
import mock
def test_display(gpiodevice, gpiod, spidev, numpy, st7735):
display = st7735.ST7735(port=0, cs=0, dc=24)
numpy.dstack().flatten().tolist.return_value = [0xff, 0x00, 0xff, 0x00]
display.display(mock.MagicMock())
spidev.SpiDev().xfer3.assert_called_with([0xff, 0x00, 0xff, 0x00])
def test_color565(gpiodevice, gpiod, spidev, numpy, st7735):
assert st7735.color565(255, 255, 255) == 0xFFFF
def test_image_to_data(gpiodevice, gpiod, spidev, numpy, st7735):
numpy.dstack().flatten().tolist.return_value = []
assert st7735.image_to_data(mock.MagicMock()) == []

31
tests/test_setup.py Normal file
View File

@@ -0,0 +1,31 @@
import mock
def test_setup(gpiodevice, gpiod, spidev, numpy, st7735):
_ = st7735.ST7735(port=0, cs=0, dc="GPIO24")
gpiodevice.get_pin.assert_has_calls([
mock.call("GPIO24", "st7735-dc", st7735.OUTL)
], any_order=True)
def test_setup_no_invert(gpiodevice, gpiod, spidev, numpy, st7735):
_ = st7735.ST7735(port=0, cs=0, dc="GPIO24", invert=False)
def test_setup_with_backlight(gpiodevice, gpiod, spidev, numpy, st7735):
display = st7735.ST7735(port=0, cs=0, dc="GPIO24", backlight="GPIO4")
display.set_backlight(True)
gpiodevice.get_pin.assert_has_calls([
mock.call("GPIO4", "st7735-bl", st7735.OUTL)
], any_order=True)
def test_setup_with_reset(gpiodevice, gpiod, spidev, numpy, st7735):
_ = st7735.ST7735(port=0, cs=0, dc=24, rst="GPIO4")
gpiodevice.get_pin.assert_has_calls([
mock.call("GPIO4", "st7735-rst", st7735.OUTL)
], any_order=True)

34
tox.ini Normal file
View File

@@ -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

72
uninstall.sh Executable file
View File

@@ -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 "$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"