From 060b58b7b414c20f14ec14f94acde743ad844aa9 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 11:48:01 +0100 Subject: [PATCH 1/7] Packaging: Sync to boilerplate. --- Makefile | 12 +- check.sh | 21 ++-- install.sh | 268 +++++++++++++++++++++++++++++++------------ pyproject.toml | 2 +- requirements-dev.txt | 1 + tox.ini | 2 +- uninstall.sh | 25 ++-- 7 files changed, 231 insertions(+), 100 deletions(-) diff --git a/Makefile b/Makefile index 760b4b8..56cf0df 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ endif @echo "deploy: build and upload to PyPi" @echo "tag: tag the repository with the current version\n" +version: + @hatch version + install: ./install.sh --unstable @@ -30,11 +33,14 @@ uninstall: dev-deps: python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix + sudo apt install dos2unix shellcheck check: @bash check.sh +shellcheck: + shellcheck *.sh + qa: tox -e qa @@ -44,7 +50,7 @@ pytest: nopost: @bash check.sh --nopost -tag: +tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check @@ -54,7 +60,7 @@ clean: -rm -r dist testdeploy: build - twine upload --repository-url https://test.pypi.org/legacy/ dist/* + twine upload --repository testpypi dist/* deploy: nopost build twine upload dist/* diff --git a/check.sh b/check.sh index cbb1565..38dfc3a 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,10 @@ # This script handles some basic QA checks on the source NOPOST=$1 -LIBRARY_NAME=`hatch project metadata name` -LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` -POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') +TERM=${TERM:="xterm-256color"} success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" @@ -28,7 +29,7 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; + printf "Unrecognised option: %s\n" "$1"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -39,8 +40,7 @@ done inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" inform "Checking for trailing whitespace..." -grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO -if [[ $? -eq 0 ]]; then +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then warning "Trailing whitespace found!" exit 1 else @@ -49,8 +49,7 @@ fi printf "\n" inform "Checking for DOS line-endings..." -grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile -if [[ $? -eq 0 ]]; then +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then warning "DOS line-endings found!" exit 1 else @@ -59,8 +58,7 @@ fi printf "\n" inform "Checking CHANGELOG.md..." -cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 -if [[ $? -eq 1 ]]; then +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." exit 1 else @@ -69,8 +67,7 @@ fi printf "\n" inform "Checking for git tag ${LIBRARY_VERSION}..." -git tag -l | grep -E "${LIBRARY_VERSION}$" -if [[ $? -eq 1 ]]; then +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then warning "Missing git tag for version ${LIBRARY_VERSION}" fi printf "\n" diff --git a/install.sh b/install.sh index 74278e8..3db90bc 100755 --- a/install.sh +++ b/install.sh @@ -1,22 +1,24 @@ #!/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"` +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 -WD=`pwd` +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false UNSTABLE=false -PYTHON="/usr/bin/python3" +PYTHON="python" +CMD_ERRORS=false user_check() { - if [ $(id -u) -eq 0 ]; then - printf "Script should not be run as root. Try './install.sh'\n" - exit 1 + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -33,15 +35,6 @@ confirm() { fi } -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } @@ -51,53 +44,126 @@ inform() { } warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 +} + +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + 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 to /boot/$FILENAME\n" - sudo cp $CONFIG /boot/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME + 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" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" fi fi } function apt_pkg_install { - PACKAGES=() + PACKAGES_NEEDED=() PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") + PACKAGES_NEEDED+=("$PACKAGE") fi done - PACKAGES="${PACKAGES[@]}" + PACKAGES="${PACKAGES_NEEDED[*]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then sudo apt update APT_HAS_UPDATED=true fi + # shellcheck disable=SC2086 sudo apt install -y $PACKAGES + check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" fi fi } function pip_pkg_install { - # Sadly have to run this as sudo in order to have the package be usable when running code with sudo. - # This is needed for the samples that drive the onboard LEDs using the rpi_ws281x package - PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring sudo $PYTHON -m pip install --upgrade "$@" + # 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 @@ -118,8 +184,8 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -127,119 +193,155 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing %s...\n\n" "$LIBRARY_NAME" + user_check +venv_check -if [ ! -f "$PYTHON" ]; then - printf "Python path $PYTHON not found!\n" - exit 1 +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` - -printf "$LIBRARY_NAME Python Library: Installer\n\n" +PYTHON_VER=$($PYTHON --version) inform "Checking Dependencies. Please wait..." +# Install toml and try to read pyproject.toml into bash variables + pip_pkg_install toml -CONFIG_VARS=`$PYTHON - < $UNINSTALLER +# Create a stub uninstaller file, we'll try to add the inverse of every +# install command run to here, though it's not complete. +cat << EOF > "$UNINSTALLER" printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" exit 1 +source $VIRTUAL_ENV/bin/activate EOF -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi +printf "\n" inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" + if $UNSTABLE; then + warning "Installing unstable library from source.\n" pip_pkg_install . else - pip_pkg_install $LIBRARY_NAME + inform "Installing stable library from pypi.\n" + pip_pkg_install "$LIBRARY_NAME" fi + +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag if [ $? -eq 0 ]; then success "Done!\n" - echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" fi -cd $WD +find_config + +printf "\n" +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + +inform "Running setup commands...\n" for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" - # Attempt to catch anything that touches /boot/config.txt and trigger a backup - if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then + # Attempt to catch anything that touches config.txt and trigger a backup + if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - eval $CMD + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi + eval "$CMD" + check_for_error done +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG\n" - sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG - if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG + 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 + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" success "Done!" fi fi printf "\n" +# Use pdoc to generate basic documentation from the installed module + if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." pip_pkg_install pdoc - printf "Generating documentation.\n" - $PYTHON -m pdoc $LIBRARY_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -247,6 +349,22 @@ if confirm "Would you like to generate documentation?"; then fi fi -success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" -inform "Find uninstall steps in $UNINSTALLER\n" +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi diff --git a/pyproject.toml b/pyproject.toml index 51b1dcb..f87bb77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ ignore = [ '.coveragerc' ] -[pimoroni] +[tool.pimoroni] apt_packages = ["python3-rpi.gpio", "python3-smbus"] configtxt = [] commands = [] diff --git a/requirements-dev.txt b/requirements-dev.txt index 08e9dec..525b042 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ twine hatch hatch-fancy-pypi-readme tox +pdoc diff --git a/tox.ini b/tox.ini index 090fdb0..4726cef 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ commands = python -m build --no-isolation python -m twine check dist/* isort --check . - ruff --format=github . + ruff check . codespell . deps = check-manifest diff --git a/uninstall.sh b/uninstall.sh index 31bc367..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,12 +1,21 @@ #!/bin/bash FORCE=false -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME -PYTHON="/usr/bin/python3" +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 + if [ "$(id -u)" -eq 0 ]; then printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi @@ -46,17 +55,17 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } -printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" user_check +venv_check printf "Uninstalling for Python 3...\n" -# Sadly have to run this as sudo to uninstall the packages that were installed as sudo with the install script -sudo $PYTHON -m pip uninstall $LIBRARY_NAME +$PYTHON -m pip uninstall "$LIBRARY_NAME" -if [ -d $RESOURCES_DIR ]; then +if [ -d "$RESOURCES_DIR" ]; then if confirm "Would you like to delete $RESOURCES_DIR?"; then - rm -r $RESOURCES_DIR + rm -r "$RESOURCES_DIR" fi fi From 839229fa19787550b87d5da60466f340ffa6a6d9 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 12:18:15 +0100 Subject: [PATCH 2/7] Migrate to gpiod/gpiodevice. --- inventorhatmini/__init__.py | 30 +++++++++++++++++++++--------- inventorhatmini/plasma.py | 8 ++++++-- pyproject.toml | 9 +++++++-- tests/conftest.py | 25 +++++++++++++++++-------- tests/test_setup.py | 4 +--- 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/inventorhatmini/__init__.py b/inventorhatmini/__init__.py index 29bfbdb..5420899 100644 --- a/inventorhatmini/__init__.py +++ b/inventorhatmini/__init__.py @@ -2,7 +2,9 @@ import time -import RPi.GPIO as GPIO +import gpiod +import gpiodevice +from gpiod.line import Bias, Direction, Value from ioexpander import ADC, SuperIOE from ioexpander.common import NORMAL_DIR from ioexpander.encoder import MMME_CPR, ROTARY_CPR, Encoder @@ -45,6 +47,10 @@ NUM_GPIOS = 4 NUM_LEDS = 8 +INPD = gpiod.LineSettings(direction=Direction.INPUT, bias=Bias.PULL_DOWN) +OUTL = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.INACTIVE) +OUTH = gpiod.LineSettings(direction=Direction.OUTPUT, output_value=Value.ACTIVE) + class InventorHATMini(): # I2C pins @@ -92,14 +98,13 @@ def __init__(self, address=IOE_ADDRESS, motor_gear_ratio=50, init_motors=True, i """ self.address = address - GPIO.setwarnings(False) - GPIO.setmode(GPIO.BCM) + gpiodevice.friendly_errors = True # Setup user button - GPIO.setup(self.PI_USER_SW_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) + self._pin_user_sw = gpiodevice.get_pin(self.PI_USER_SW_PIN, "IHM-SW", INPD) # Setup amplifier enable. This mutes the audio by default - GPIO.setup(self.PI_AMP_EN_PIN, GPIO.OUT, initial=GPIO.LOW if start_muted else GPIO.HIGH) + self._pin_amp_en = gpiodevice.get_pin(self.PI_AMP_EN_PIN, "IHM-AMP-En", OUTL if start_muted else OUTH) self.__cpr = MMME_CPR * motor_gear_ratio self.__init_motors = init_motors @@ -112,6 +117,14 @@ def __init__(self, address=IOE_ADDRESS, motor_gear_ratio=50, init_motors=True, i else: # Setup a dummy Plasma class, so examples don't need to check LED presence self.leds = DummyPlasma() + + def _write_pin(self, pin, state): + lines, offset = pin + lines.set_value(offset, Value.ACTIVE if state else Value.INACTIVE) + + def _read_pin(self, pin): + lines, offset = pin + return lines.get_value(offset) == Value.ACTIVE def reinit(self): try: @@ -140,10 +153,9 @@ def reinit(self): def __del__(self): self.ioe.reset() - GPIO.cleanup() def switch_pressed(self): - return GPIO.input(self.PI_USER_SW_PIN) != 0 + return self._read_pin(self._pin_user_sw) def enable_motors(self): """ Enables both motors. @@ -169,10 +181,10 @@ def read_motor_current(self, motor): return self.ioe.input(self.IOE_CURRENT_SENSES[motor]) / self.SHUNT_RESISTOR def mute_audio(self): - GPIO.output(self.PI_AMP_EN_PIN, False) + self._write_pin(self._pin_amp_en, False) def unmute_audio(self): - GPIO.output(self.PI_AMP_EN_PIN, True) + self._write_pin(self._pin_amp_en, True) def gpio_pin_mode(self, gpio, mode=None): if gpio < 0 or gpio >= NUM_GPIOS: diff --git a/inventorhatmini/plasma.py b/inventorhatmini/plasma.py index 60d311f..82c1a21 100644 --- a/inventorhatmini/plasma.py +++ b/inventorhatmini/plasma.py @@ -1,7 +1,5 @@ from colorsys import hsv_to_rgb -from rpi_ws281x import PixelStrip - from inventorhatmini.errors import LED_INIT_FAILED @@ -30,6 +28,12 @@ class Plasma(): 222, 224, 227, 229, 231, 233, 235, 237, 239, 241, 244, 246, 248, 250, 252, 255] def __init__(self, num_leds, pin): + try: + from rpi_ws281x import PixelStrip + except ImportError: + self = DummyPlasma() + return + # Setup the PixelStrip object to use with Inventor's LEDs self.leds = PixelStrip(num_leds, pin, self.LED_FREQ_HZ, self.LED_DMA, self.LED_INVERT, self.LED_BRIGHTNESS, self.LED_CHANNEL, self.LED_GAMMA) try: diff --git a/pyproject.toml b/pyproject.toml index f87bb77..61b4443 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,12 @@ classifiers = [ "Topic :: System :: Hardware", ] dependencies = [ - "pimoroni-ioexpander", + "pimoroni-ioexpander>=1.0.0", + "smbus2" +] + +[project.optional-dependencies] +leds = [ "rpi_ws281x" ] @@ -116,6 +121,6 @@ ignore = [ ] [tool.pimoroni] -apt_packages = ["python3-rpi.gpio", "python3-smbus"] +apt_packages = [] configtxt = [] commands = [] diff --git a/tests/conftest.py b/tests/conftest.py index cfe3997..3a17b65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,14 +5,23 @@ @pytest.fixture(scope='function', autouse=False) -def GPIO(): - """Mock RPi.GPIO module.""" - RPi = mock.MagicMock() - sys.modules['RPi'] = RPi - sys.modules['RPi.GPIO'] = RPi.GPIO - yield RPi.GPIO - del sys.modules['RPi'] - del sys.modules['RPi.GPIO'] +def gpiod(): + sys.modules['gpiod'] = mock.Mock() + sys.modules['gpiod.line'] = mock.Mock() + yield sys.modules['gpiod'] + del sys.modules['gpiod.line'] + del sys.modules['gpiod'] + + +@pytest.fixture(scope='function', autouse=False) +def gpiodevice(): + gpiodevice = mock.Mock() + gpiodevice.get_pins_for_platform.return_value = [(mock.Mock(), 0), (mock.Mock(), 0)] + gpiodevice.get_pin.return_value = (mock.Mock(), 0) + + sys.modules['gpiodevice'] = gpiodevice + yield gpiodevice + del sys.modules['gpiodevice'] @pytest.fixture(scope='function', autouse=False) diff --git a/tests/test_setup.py b/tests/test_setup.py index d6b51e1..dcf4a9e 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,3 @@ -def test_setup(GPIO, rpi_ws281x, ioexpander): +def test_setup(gpiod, gpiodevice, rpi_ws281x, ioexpander): import inventorhatmini inventorhatmini.InventorHATMini() - GPIO.setwarnings.assert_called_once_with(False) - GPIO.setmode.assert_called_once_with(GPIO.BCM) From 4b6b538272cc1206064dcef6f6c53ea64b7d0874 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 12:23:16 +0100 Subject: [PATCH 3/7] CI: Make codespell happy. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 271a09e..159f837 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Reboot after this for the change to take effect. To use the audio output of your Inventor HAT Mini you will need to modifying your Pi's configuration file. To do this run `sudo nano /boot/config.txt` to open a terminal text editor. -Within the editor navigate to the bottom of the file and include the lines `dtoverlay=hifiberry-dac` and `gpio=25=op,dh`. The first line switches the audio to use the GPIO header of your Pi for audio output, and the second line will cause your Pi to enable the audio output on bootup, by setting pin BCM 25 to high. Then navigate up to the line `dtparam=i2s=on`. If this says `off` or is commented out with a `#`, uncomment it and change it to `on`. +Within the editor navigate to the bottom of the file and include the lines `dtoverlay=hifiberry-dac` and `gpio=25=op,dh`. The first line switches the audio to use the GPIO header of your Pi for audio output, and the second line will cause your Pi to enable the audio output on boot, by setting pin BCM 25 to high. Then navigate up to the line `dtparam=i2s=on`. If this says `off` or is commented out with a `#`, uncomment it and change it to `on`. Depending on your setup, you may also need to disable other audio outputs of your Pi (for example audio over HDMI). Look through the file for any existing mention to `dtparam=audio=on` and change it to `dtparam=audio=off`. There may the line `dtoverlay=vc4-kms-v3d`. Modify this to be `dtoverlay=vc4-kms-v3d,noaudio`. From e8c5b84e0b9b8f7bba2f3fa84057ec783e8cd1c0 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 12:28:01 +0100 Subject: [PATCH 4/7] QA: Delete roque whitespace. --- inventorhatmini/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inventorhatmini/__init__.py b/inventorhatmini/__init__.py index 5420899..9e4a46c 100644 --- a/inventorhatmini/__init__.py +++ b/inventorhatmini/__init__.py @@ -117,7 +117,7 @@ def __init__(self, address=IOE_ADDRESS, motor_gear_ratio=50, init_motors=True, i else: # Setup a dummy Plasma class, so examples don't need to check LED presence self.leds = DummyPlasma() - + def _write_pin(self, pin, state): lines, offset = pin lines.set_value(offset, Value.ACTIVE if state else Value.INACTIVE) From 9b116c3baa941bcdf2f6ec14c64762dbe0b528b5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 12:59:13 +0100 Subject: [PATCH 5/7] Better handling of LEDs on Pi 5. --- inventorhatmini/__init__.py | 9 ++++++++- inventorhatmini/plasma.py | 8 ++------ pyproject.toml | 6 +----- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/inventorhatmini/__init__.py b/inventorhatmini/__init__.py index 9e4a46c..623a6d8 100644 --- a/inventorhatmini/__init__.py +++ b/inventorhatmini/__init__.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 import time +import warnings import gpiod import gpiodevice from gpiod.line import Bias, Direction, Value +from gpiodevice import platform from ioexpander import ADC, SuperIOE from ioexpander.common import NORMAL_DIR from ioexpander.encoder import MMME_CPR, ROTARY_CPR, Encoder @@ -111,13 +113,18 @@ def __init__(self, address=IOE_ADDRESS, motor_gear_ratio=50, init_motors=True, i self.__init_servos = init_servos self.reinit() - if init_leds: + is_pi5 = platform.get_name().startswith("Raspberry Pi 5") + + if init_leds and not is_pi5: # Setup the PixelStrip object to use with Inventor's LEDs, wrapped in a Plasma class self.leds = Plasma(NUM_LEDS, self.PI_LED_DATA_PIN) else: # Setup a dummy Plasma class, so examples don't need to check LED presence self.leds = DummyPlasma() + if is_pi5: + warnings.warn("LEDs are not yet supported on Pi 5.") + def _write_pin(self, pin, state): lines, offset = pin lines.set_value(offset, Value.ACTIVE if state else Value.INACTIVE) diff --git a/inventorhatmini/plasma.py b/inventorhatmini/plasma.py index 82c1a21..60d311f 100644 --- a/inventorhatmini/plasma.py +++ b/inventorhatmini/plasma.py @@ -1,5 +1,7 @@ from colorsys import hsv_to_rgb +from rpi_ws281x import PixelStrip + from inventorhatmini.errors import LED_INIT_FAILED @@ -28,12 +30,6 @@ class Plasma(): 222, 224, 227, 229, 231, 233, 235, 237, 239, 241, 244, 246, 248, 250, 252, 255] def __init__(self, num_leds, pin): - try: - from rpi_ws281x import PixelStrip - except ImportError: - self = DummyPlasma() - return - # Setup the PixelStrip object to use with Inventor's LEDs self.leds = PixelStrip(num_leds, pin, self.LED_FREQ_HZ, self.LED_DMA, self.LED_INVERT, self.LED_BRIGHTNESS, self.LED_CHANNEL, self.LED_GAMMA) try: diff --git a/pyproject.toml b/pyproject.toml index 61b4443..10c69bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,11 +37,7 @@ classifiers = [ ] dependencies = [ "pimoroni-ioexpander>=1.0.0", - "smbus2" -] - -[project.optional-dependencies] -leds = [ + "smbus2", "rpi_ws281x" ] From ecee406157d3f0457b2507e855953486416cb7d5 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 14:19:28 +0100 Subject: [PATCH 6/7] Bump IOE version. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 10c69bf..0bc8f8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ classifiers = [ "Topic :: System :: Hardware", ] dependencies = [ - "pimoroni-ioexpander>=1.0.0", + "pimoroni-ioexpander>=1.0.1", "smbus2", "rpi_ws281x" ] From 198423bb55bae1a9baf0b2c0bb0a979c72e66fcd Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Thu, 6 Jun 2024 14:31:34 +0100 Subject: [PATCH 7/7] Prep for v1.0.0 --- CHANGELOG.md | 6 ++++++ inventorhatmini/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f98d12..7cb19f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +1.0.0 +----- + +* Port to gpiod/gpiodevice for Pi 5 +* Use dummy LEDs for Pi 5 + 0.0.1 ----- diff --git a/inventorhatmini/__init__.py b/inventorhatmini/__init__.py index 623a6d8..dd776c3 100644 --- a/inventorhatmini/__init__.py +++ b/inventorhatmini/__init__.py @@ -16,7 +16,7 @@ from inventorhatmini.errors import NO_I2C, NO_IOE_MSG from inventorhatmini.plasma import DummyPlasma, Plasma -__version__ = '0.0.1' +__version__ = '1.0.0' # Index Constants