diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddee38e..703f122 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: make dist - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v7 with: name: ${{ env.RELEASE_FILE }} path: dist/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0577f98 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +## Development environment + +A Docker-based testing image is provided so checks can be run against multiple Python versions +without installing tooling locally. Python 3.11 is the first version set up; others will follow. + +### Build the image + +Pass your host UID and GID so that files written inside the container are owned by your user, +not root: + +```bash +docker build -f Dockerfile.testing \ + --build-arg UID=$(id -u) \ + --build-arg GID=$(id -g) \ + -t inventorhatmini-dev:python3.11-v1.0.1 . +``` + +> **Image tag convention:** `inventorhatmini-dev:python-v` +> The testing version is always at least one patch ahead of the released library version +> (e.g. library `1.0.0` → testing image `v1.0.1`). + +### Run checks + +All commands below mount the repository into the container so changes are picked up without +a rebuild. Run them from the repository root. + +**Integrity checks** (trailing whitespace, DOS line-endings, CHANGELOG entry, git tag): + +```bash +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make check +``` + +**Shell script linting:** + +```bash +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make shellcheck +``` + +**QA** (ruff, isort, codespell, check-manifest, build, twine check): + +```bash +docker run --rm -v "$(pwd)":/app inventorhatmini-dev:python3.11-v1.0.1 make qa +``` + +### Dependency lock file + +The image installs from `requirements-dev.lock`. Regenerate it when `requirements-dev.txt` changes: + +```bash +uv pip compile requirements-dev.txt --output-file requirements-dev.lock --python-version 3.11 +``` + +Then rebuild the image. diff --git a/Dockerfile.testing b/Dockerfile.testing new file mode 100644 index 0000000..f650c0e --- /dev/null +++ b/Dockerfile.testing @@ -0,0 +1,52 @@ +# syntax=docker/dockerfile:1 +# +# Tagging convention: inventorhatmini-dev:python-v +# python-ver — Python minor version in the base image (e.g. 3.11) +# testing-ver — SemVer, always at least one patch ahead of the released +# inventorhatmini library version (e.g. library 1.0.0 → testing v1.0.1) +# +# Build example: +# docker build -f Dockerfile.testing -t inventorhatmini-dev:python3.11-v1.0.1 . +# +# Stage 1: UV binary source +FROM ghcr.io/astral-sh/uv:0.7.13 AS uv-base + +# Stage 2: Testing environment +FROM python:3.11-slim-bookworm AS testing + +# Copy UV binary from uv-base stage +COPY --from=uv-base /uv /uvx /usr/local/bin/ + +# Install system dependencies +# hadolint ignore=DL3008 +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + make \ + dos2unix \ + shellcheck \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Create a non-root user matching typical host UID/GID (1000:1000). +# Override at runtime with: --user $(id -u):$(id -g) +ARG UID=1000 +ARG GID=1000 +RUN groupadd -g "${GID}" appuser \ + && useradd -l -u "${UID}" -g "${GID}" -m appuser + +WORKDIR /app + +# Copy dependency lock file first for layer caching +COPY requirements-dev.lock ./ + +# Install dev dependencies from pinned lock file +RUN uv pip install --system --no-cache -r requirements-dev.lock + +# Copy project source +COPY . . + +USER appuser + +# Allow git to read the volume-mounted repo (ownership matches host UID but +# git's safe.directory check still fires on mounted directories). +RUN git config --global --add safe.directory /app diff --git a/check.sh b/check.sh index 38dfc3a..177d3b8 100755 --- a/check.sh +++ b/check.sh @@ -7,6 +7,7 @@ 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"} +export TERM success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" diff --git a/firmware/firmware_update.py b/firmware/firmware_update.py index 90fd589..669e17d 100644 --- a/firmware/firmware_update.py +++ b/firmware/firmware_update.py @@ -5,14 +5,11 @@ # Usage: python firmware_update.py filename.hex import binascii - import sys import time - from smbus2 import SMBus, i2c_msg - # Small nuvoton: bootloader_id = 0xB001 bootloader_version = 172 @@ -108,7 +105,7 @@ def write_page_to_aprom(): try: i2c_read8(bootloader_i2c_addr, 0x00) break - except: + except Exception: print("waiting for page write to finish...") def read_page_from_aprom(): @@ -126,7 +123,7 @@ def firmware_update(bin_data, i2c_address, chip_id): try: i2c_read8(bootloader_i2c_addr, 0x00) - except: + except Exception: # We're not in bootloader yet, let's enter first confirm_id(i2c_address, chip_id) enter_bootloader(i2c_address) @@ -187,7 +184,7 @@ def firmware_update(bin_data, i2c_address, chip_id): if __name__ == "__main__": - filename = sys.argv[1]; + filename = sys.argv[1] # if bin_filename.endswith(".bin"): # bin_data = open(bin_filename, "rb").read() # bin_data = [ord(x) for x in bin_data] diff --git a/firmware/set_i2c_address.py b/firmware/set_i2c_address.py index d99ec36..98f045e 100644 --- a/firmware/set_i2c_address.py +++ b/firmware/set_i2c_address.py @@ -9,18 +9,18 @@ # flash write ongoing # flash write ongoing # flash write finished -# pi@raspberrypi:~ $ -# pi@raspberrypi:~ $ +# pi@raspberrypi:~ $ +# pi@raspberrypi:~ $ # pi@raspberrypi:~ $ i2cdetect -y 1 # 0 1 2 3 4 5 6 7 8 9 a b c d e f -# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- -# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 20: -- -- 22 -- -- -- -- -- -- -- -- -- -- -- -- -- -# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 70: -- -- -- -- -- -- -- -- +# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- +# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 20: -- -- 22 -- -- -- -- -- -- -- -- -- -- -- -- -- +# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 70: -- -- -- -- -- -- -- -- # pi@raspberrypi:~ $ python set_i2c_address.py 0x22 0x23 # Waiting for flash writing to start.. # flash write ongoing @@ -33,26 +33,25 @@ # flash write finished # pi@raspberrypi:~ $ i2cdetect -y 1 # 0 1 2 3 4 5 6 7 8 9 a b c d e f -# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- -# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 20: -- -- -- 23 -- -- -- -- -- -- -- -- -- -- -- -- -# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -# 70: -- -- -- -- -- -- -- -- +# 00: -- -- -- -- -- -- -- -- -- -- -- -- -- +# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 20: -- -- -- 23 -- -- -- -- -- -- -- -- -- -- -- -- +# 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# 70: -- -- -- -- -- -- -- -- -import time -import os import sys +import time +import RPi.GPIO as GPIO from smbus2 import SMBus, i2c_msg -import RPi.GPIO as GPIO GPIO.setmode(GPIO.BCM) CHIP_ID = 0xBA11 -VERSION = 1 +VERSION = 1 # Registers specific to the trackball diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..de6e548 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,197 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile requirements-dev.txt --output-file requirements-dev.lock --python-version 3.11 +anyio==4.13.0 + # via httpx +backports-tarfile==1.2.0 + # via jaraco-context +backports-zstd==1.5.0 + # via hatch +build==1.5.0 + # via check-manifest +cachetools==7.1.4 + # via tox +certifi==2026.5.20 + # via + # httpcore + # httpx + # requests +cffi==2.0.0 + # via cryptography +charset-normalizer==3.4.7 + # via requests +check-manifest==0.51 + # via -r requirements-dev.txt +click==8.4.1 + # via + # hatch + # userpath +codespell==2.4.2 + # via -r requirements-dev.txt +colorama==0.4.6 + # via tox +cryptography==48.0.0 + # via secretstorage +distlib==0.4.0 + # via virtualenv +docutils==0.23 + # via readme-renderer +filelock==3.29.0 + # via + # python-discovery + # tox + # virtualenv +h11==0.16.0 + # via httpcore +hatch==1.16.5 + # via -r requirements-dev.txt +hatch-fancy-pypi-readme==25.1.0 + # via -r requirements-dev.txt +hatchling==1.29.0 + # via + # hatch + # hatch-fancy-pypi-readme +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via hatch +hyperlink==21.0.0 + # via hatch +id==1.6.1 + # via twine +idna==3.17 + # via + # anyio + # httpx + # hyperlink + # requests +importlib-metadata==9.0.0 + # via keyring +isort==8.0.1 + # via -r requirements-dev.txt +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.1.2 + # via keyring +jaraco-functools==4.5.0 + # via keyring +jeepney==0.9.0 + # via + # keyring + # secretstorage +jinja2==3.1.6 + # via pdoc +keyring==25.7.0 + # via + # hatch + # twine +markdown-it-py==4.2.0 + # via rich +markdown2==2.5.5 + # via pdoc +markupsafe==3.0.3 + # via + # jinja2 + # pdoc +mdurl==0.1.2 + # via markdown-it-py +more-itertools==11.1.0 + # via + # jaraco-classes + # jaraco-functools +nh3==0.3.5 + # via readme-renderer +packaging==26.2 + # via + # build + # hatch + # hatchling + # pyproject-api + # tox + # twine +pathspec==1.1.1 + # via hatchling +pdoc==16.0.0 + # via -r requirements-dev.txt +pexpect==4.9.0 + # via hatch +platformdirs==4.10.0 + # via + # hatch + # python-discovery + # tox + # virtualenv +pluggy==1.6.0 + # via + # hatchling + # tox +ptyprocess==0.7.0 + # via pexpect +pycparser==3.0 + # via cffi +pygments==2.20.0 + # via + # pdoc + # readme-renderer + # rich +pyproject-api==1.10.1 + # via tox +pyproject-hooks==1.2.0 + # via + # build + # hatch +python-discovery==1.4.0 + # via + # hatch + # tox + # virtualenv +readme-renderer==44.0 + # via twine +requests==2.34.2 + # via + # requests-toolbelt + # twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==15.0.0 + # via + # hatch + # twine +ruff==0.15.15 + # via -r requirements-dev.txt +secretstorage==3.5.0 + # via keyring +setuptools==82.0.1 + # via check-manifest +shellingham==1.5.4 + # via hatch +tomli-w==1.2.0 + # via + # hatch + # tox +tomlkit==0.15.0 + # via hatch +tox==4.55.0 + # via -r requirements-dev.txt +trove-classifiers==2026.5.22.10 + # via hatchling +twine==6.2.0 + # via -r requirements-dev.txt +typing-extensions==4.15.0 + # via anyio +urllib3==2.7.0 + # via + # id + # requests + # twine +userpath==1.9.2 + # via hatch +uv==0.11.17 + # via hatch +virtualenv==21.4.1 + # via + # hatch + # tox +zipp==4.1.0 + # via importlib-metadata diff --git a/tox.ini b/tox.ini index 4726cef..99963b2 100644 --- a/tox.ini +++ b/tox.ini @@ -5,16 +5,26 @@ isolated_build = true minversion = 4.0.0 [testenv] -commands = - coverage run -m pytest -v -r wsx - coverage report +# rpi_ws281x is a Raspberry Pi hardware library with a C extension that cannot +# be compiled on x86. All hardware deps are mocked in the test suite, so we +# skip the package install and install it manually with --no-deps. +skip_install = true deps = mock pytest>=3.1 pytest-cov build + pimoroni-ioexpander>=1.0.1 + gpiodevice + smbus2 +commands_pre = + pip install --no-deps {toxinidir} +commands = + coverage run -m pytest -v -r wsx + coverage report [testenv:qa] +skip_install = true commands = check-manifest python -m build --no-isolation