From 95ad16af7c3a16a85ac632e734648ad378d44d15 Mon Sep 17 00:00:00 2001 From: Piclaw Date: Tue, 16 Jun 2026 14:10:54 -0700 Subject: [PATCH 1/4] Convert rgb-matrix.sh to Python; bump pin for Pi 5 support Modernized 1:1 port of rgb-matrix.sh: - Drop Python 2 handling (py2 is EOL) - Use adafruit_shell helpers (select_n/prompt/reconfig/run_raspi_config/ prompt_reboot) and shell.get_boot_config() for the boot config path - Idempotent isolcpus cmdline editing in pure Python Also bumps the pinned hzeller/rpi-rgb-led-matrix commit from 7a503494 (May 2025) to 4e326c1b (Jun 2026), the first commit with Raspberry Pi 5 (RP1) support. The previous pin builds a register-poke driver that cannot drive the matrix on a Pi 5. Not yet hardware-tested. --- rgb-matrix.py | 248 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 rgb-matrix.py diff --git a/rgb-matrix.py b/rgb-matrix.py new file mode 100644 index 0000000..3c9ab55 --- /dev/null +++ b/rgb-matrix.py @@ -0,0 +1,248 @@ +# SPDX-FileCopyrightText: 2018 Phillip Burgess for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +# INSTALLER SCRIPT FOR ADAFRUIT RGB MATRIX BONNET OR HAT + +import os + +try: + from adafruit_shell import Shell +except ImportError: + raise RuntimeError( + "The library 'adafruit_shell' was not found. To install, try typing: " + "sudo pip3 install adafruit-python-shell" + ) + +shell = Shell() +shell.group = "RGB-Matrix" + +# hzeller/rpi-rgb-led-matrix sees lots of active development! +# That's cool and all, BUT, to avoid tutorial breakage, +# we reference a specific commit (update this as needed): +GITUSER = "https://github.com/hzeller" +REPO = "rpi-rgb-led-matrix" +# This is the first commit with Pi 5 (RP1) support; older commits build a +# driver that cannot drive the matrix on a Pi 5. +COMMIT = "4e326c1b34bed36847711e5c1a421aecb879b9bb" +# Previously: COMMIT=7a503494378a67f3baa4ac680cecbae2703cc58f +# Previously: COMMIT=a3eea997a9254b83ab2de97ae80d83588f696387 +# Previously: COMMIT=45d3ab5d6cff6e0c14da58930d662822627471fc +# Previously: COMMIT=21410d2b0bac006b4a1661594926af347b3ce334 +# Previously: COMMIT=e3dd56dcc0408862f39cccc47c1d9dea1b0fb2d2 + +INTERFACES = ( + "Adafruit RGB Matrix Bonnet", + "Adafruit RGB Matrix HAT + RTC", +) + +QUALITY_OPTS = ( + "Quality (disables sound, requires soldering on single matrix Bonnet/HAT)", + "Convenience (sound on, no soldering)", +) + +ISOLCPUS_OPTS = ( + "Do not reserve a core for driving the display", + "Reserve a core for driving the display (recommended)", +) + + +def set_isolcpus(cmdline_file, reserve, isolcpu_token): + """Idempotently set the isolcpus=N token in cmdline.txt. + + cmdline.txt is a single line; strip any previously-added isolcpus token + first (re-running the installer, or moving the SD card to a Pi with a + different core count), then append the new one if a core is reserved. + """ + line = shell.read_text_file(cmdline_file).strip() + tokens = [t for t in line.split() if not t.startswith("isolcpus=")] + if reserve: + tokens.append(isolcpu_token) + shell.write_text_file(cmdline_file, " ".join(tokens) + "\n", append=False) + + +def main(): + shell.require_root() + + num_cores = os.cpu_count() or 1 + # Reserve the highest-numbered core (isolcpus is 0-indexed). + isolcpu_token = f"isolcpus={num_cores - 1}" + + config = shell.get_boot_config() + if config is None: + shell.bail("No Device Tree Detected, not supported") + + # Boot config locations. Bookworm and later use /boot/firmware/; + # pre-Bookworm releases use /boot/. + cmdline_file = "/boot/firmware/cmdline.txt" + if not shell.exists(cmdline_file): + cmdline_file = "/boot/cmdline.txt" + + shell.clear() + print("This script installs software for the Adafruit") + print("RGB Matrix Bonnet or HAT for Raspberry Pi.") + print("Steps include:") + print("- Update package index files (apt-get update)") + print("- Install prerequisite software") + print("- Install RGB matrix driver software and examples") + print("- Configure boot options") + print("Run time ~15 minutes. Some options require reboot.") + print("EXISTING INSTALLATION, IF ANY, WILL BE OVERWRITTEN.") + print("") + if not shell.prompt("CONTINUE?", default="n"): + print("Canceled.") + shell.exit(0) + + # FEATURE PROMPTS ------------------------------------------------------ + # Installation doesn't begin until after all user input is taken. + + print("") + print("Select interface board type:") + interface_type = shell.select_n("", INTERFACES) + + install_rtc = False + if interface_type == 2: + # For matrix HAT, ask about RTC install. + print("") + install_rtc = shell.prompt("Install realtime clock support?") + + print("") + print("Now you must choose between QUALITY and CONVENIENCE.") + print("") + print("QUALITY: best output from the LED matrix requires") + print("commandeering hardware normally used for sound, plus") + print("some soldering on the single matrix Bonnet/HAT. If") + print("you choose this option, there will") + print("be NO sound from the audio jack or HDMI (USB audio") + print("adapters will work and sound best anyway), AND you") + print("must SOLDER a wire between GPIO4 and GPIO18 on the") + print("single matrix Bonnet or HAT board. For the Triple LED") + print("Matrix Bonnet choose QUALITY, and No soldering is") + print("required.") + print("") + print("CONVENIENCE: sound works normally, no extra soldering.") + print("Images on the LED matrix are not quite as steady, but") + print("maybe OK for most uses. If eager to get started, use") + print("'CONVENIENCE' for now, you can make the change and") + print("reinstall using this script later!") + print("") + print("What is thy bidding?") + quality_mod = shell.select_n("", QUALITY_OPTS) + + # Default: don't reserve a core (e.g. single-core Pi where the menu is + # skipped). select_n() returns 1 = "Do not reserve", 2 = "Reserve". + isol_cpu = 1 + if num_cores >= 2: + print("") + print(f"Your Pi has {num_cores} CPU cores. You can dedicate one") + print("to driving the display. This reduces flicker when the") + print("system is busy with other work, at the cost of one core") + print("being unavailable for general use. This is the upstream") + print("recommendation from hzeller/rpi-rgb-led-matrix.") + isol_cpu = shell.select_n("", ISOLCPUS_OPTS) + + # VERIFY SELECTIONS BEFORE CONTINUING ---------------------------------- + + print("") + print(f"Interface board type: {INTERFACES[interface_type - 1]}") + if interface_type == 2: + print(f"Install RTC support: {'YES' if install_rtc else 'NO'}") + print(f"Optimize: {QUALITY_OPTS[quality_mod - 1]}") + if quality_mod == 1: + print("Reminder: you must SOLDER a wire between GPIO4") + print("and GPIO18, and internal sound is DISABLED!") + if num_cores >= 2: + print(f"Isolate CPU for display driving: {ISOLCPUS_OPTS[isol_cpu - 1]}") + print("") + if not shell.prompt("CONTINUE?", default="n"): + print("Canceled.") + shell.exit(0) + + # START INSTALL -------------------------------------------------------- + # All selections are validated at this point. + + print("") + print("Starting installation...") + print("Updating package index files...") + shell.run_command("apt-get update") + + print("Downloading prerequisites...") + shell.run_command( + "apt-get install -y python3-dev python3-pillow cython3 python3-setuptools" + ) + + print("Downloading RGB matrix software...") + shell.run_command( + f"curl -L {GITUSER}/{REPO}/archive/{COMMIT}.zip -o {REPO}-{COMMIT}.zip" + ) + shell.run_command(f"unzip -q {REPO}-{COMMIT}.zip") + shell.remove(f"{REPO}-{COMMIT}.zip") + shell.remove("rpi-rgb-led-matrix") + shell.run_command(f"mv {REPO}-{COMMIT} rpi-rgb-led-matrix") + + print("Building RGB matrix software...") + shell.chdir("rpi-rgb-led-matrix") + user_defines = "" + if quality_mod == 2: + user_defines = " -DDISABLE_HARDWARE_PULSES" + shell.run_command("make clean") + shell.run_command(f'make build-python USER_DEFINES="{user_defines}"') + + # Change ownership to the user who called sudo. + sudo_user = os.environ.get("SUDO_USER") + if sudo_user: + shell.run_command(f"chown -R {sudo_user}:{sudo_user} {os.getcwd()}") + + # CONFIG --------------------------------------------------------------- + + print("Configuring system...") + + if install_rtc: + # Enable I2C for the RTC. + shell.run_raspi_config("do_i2c 0") + # Blank any existing dtoverlay=i2c-rtc line, then add the DS1307. + shell.pattern_replace(config, r"^dtoverlay\s*=\s*i2c-rtc.*$") + shell.write_text_file(config, "dtoverlay=i2c-rtc,ds1307", append=True) + shell.run_command("apt-get -y remove fake-hwclock") + shell.run_command("update-rc.d -f fake-hwclock remove") + if shell.exists("/lib/udev/hwclock-set"): + # Comment out the systemd-guarded block so the kernel hwclock sync runs. + shell.run_command( + "sed --in-place " + "'/if \\[ -e \\/run\\/systemd\\/system \\] ; then/,+2 s/^#*/#/' " + "/lib/udev/hwclock-set" + ) + + if quality_mod == 1: + # Quality: disable onboard sound via blacklist. + shell.write_text_file( + "/etc/modprobe.d/blacklist-rgb-matrix.conf", + "blacklist snd_bcm2835", + append=False, + ) + else: + # Convenience: remove the blacklist if present. + shell.remove("/etc/modprobe.d/blacklist-rgb-matrix.conf") + + # Reserve a core for the matrix driver (upstream recommendation). + set_isolcpus(cmdline_file, isol_cpu == 2, isolcpu_token) + + # PROMPT FOR REBOOT ---------------------------------------------------- + + print("Done.") + print("") + print("Settings take effect on next boot.") + if install_rtc: + print("RTC will be enabled then but time must be set") + print("up using the 'date' and 'hwclock' commands.") + print( + "ref: https://learn.adafruit.com/adding-a-real-time-clock-to-" + "raspberry-pi/set-rtc-time#sync-time-from-pi-to-rtc" + ) + print("") + shell.prompt_reboot() + + +# Main function +if __name__ == "__main__": + main() From 1f585b4eb96181190f36fa3133666b1e8f02239a Mon Sep 17 00:00:00 2001 From: Piclaw Date: Tue, 16 Jun 2026 14:54:53 -0700 Subject: [PATCH 2/4] rgb-matrix: fix Pi 5 Python build; add slowdown guidance The pinned upstream commit replaced 'make build-python' with a scikit-build-core/Cython package, so build the bindings with 'pip install .' into the active environment. Add cmake to the apt prerequisites (required by scikit-build-core). Print the runtime --led-gpio-mapping matching the build choice and a note recommending --led-slowdown-gpio=5 to avoid flicker on solid white (worst case) on Pi 3/4; harmless on the Pi 5 RP1 backend. --- rgb-matrix.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/rgb-matrix.py b/rgb-matrix.py index 3c9ab55..a09bf2a 100644 --- a/rgb-matrix.py +++ b/rgb-matrix.py @@ -5,6 +5,7 @@ # INSTALLER SCRIPT FOR ADAFRUIT RGB MATRIX BONNET OR HAT import os +import sys try: from adafruit_shell import Shell @@ -167,8 +168,10 @@ def main(): shell.run_command("apt-get update") print("Downloading prerequisites...") + # cmake is required by scikit-build-core, the build backend the upstream + # repo now uses for its Python bindings. shell.run_command( - "apt-get install -y python3-dev python3-pillow cython3 python3-setuptools" + "apt-get install -y python3-dev python3-pillow cython3 python3-setuptools cmake" ) print("Downloading RGB matrix software...") @@ -180,13 +183,14 @@ def main(): shell.remove("rpi-rgb-led-matrix") shell.run_command(f"mv {REPO}-{COMMIT} rpi-rgb-led-matrix") - print("Building RGB matrix software...") + print("Building and installing RGB matrix Python bindings...") shell.chdir("rpi-rgb-led-matrix") - user_defines = "" - if quality_mod == 2: - user_defines = " -DDISABLE_HARDWARE_PULSES" - shell.run_command("make clean") - shell.run_command(f'make build-python USER_DEFINES="{user_defines}"') + # The upstream repo is now a scikit-build-core/Cython package installed + # with `pip install .` (the old `make build-python` target is gone). + # Install into the same Python environment the installer is running in so + # `from rgbmatrix import RGBMatrix` works for the user's project. + shell.run_command(f'"{sys.executable}" -m pip install --upgrade pip') + shell.run_command(f'"{sys.executable}" -m pip install .') # Change ownership to the user who called sudo. sudo_user = os.environ.get("SUDO_USER") @@ -229,8 +233,28 @@ def main(): # PROMPT FOR REBOOT ---------------------------------------------------- + # Tell the user which gpio mapping matches their build choice. With the + # current upstream the hardware mapping is a runtime option, not a + # compile-time define. + gpio_mapping = "adafruit-hat-pwm" if quality_mod == 1 else "adafruit-hat" + print("Done.") print("") + print("The 'rgbmatrix' Python package is installed in:") + print(f" {sys.executable}") + print("Run your programs with that interpreter so 'import rgbmatrix' works.") + print("") + print("Use this LED GPIO mapping for your board/quality choice:") + print(f" --led-gpio-mapping={gpio_mapping}") + print(f'(In python: RGBMatrixOptions().hardware_mapping = "{gpio_mapping}")') + print("") + # gpio_slowdown is a runtime option; 5 is a safe default that avoids + # flicker on solid white (worst case) on Pi 3/4. No effect on Pi 5's RP1 + # backend. Lower it for higher refresh if you see no flicker. + print("If you see flicker (e.g. on solid white), increase the GPIO slowdown:") + print(" --led-slowdown-gpio=5") + print("(In python: RGBMatrixOptions().gpio_slowdown = 5)") + print("") print("Settings take effect on next boot.") if install_rtc: print("RTC will be enabled then but time must be set") From 854e0e1099df7438d5b774abb38219da7563ecfe Mon Sep 17 00:00:00 2001 From: makermelissa-piclaw Date: Tue, 16 Jun 2026 15:07:46 -0700 Subject: [PATCH 3/4] rgb-matrix: address Copilot review feedback - Add build-essential, python3-pip, and unzip to apt prereqs so the Cython/scikit-build pip build and zip extraction work on a minimal image. - chown with the caller's actual primary GID (SUDO_GID) instead of assuming the group name matches the username. - curl -fL so a bad commit hash / 404 fails fast instead of saving an HTML page that later breaks unzip. - Reword 'rgbmatrix installed in ' to 'installed for this interpreter' (it's the interpreter path, not an install dir). - Update set_isolcpus docstring to match the actual behavior (strips any existing isolcpus= token, including user-managed ranges). --- rgb-matrix.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/rgb-matrix.py b/rgb-matrix.py index a09bf2a..95953ee 100644 --- a/rgb-matrix.py +++ b/rgb-matrix.py @@ -51,9 +51,11 @@ def set_isolcpus(cmdline_file, reserve, isolcpu_token): """Idempotently set the isolcpus=N token in cmdline.txt. - cmdline.txt is a single line; strip any previously-added isolcpus token - first (re-running the installer, or moving the SD card to a Pi with a - different core count), then append the new one if a core is reserved. + cmdline.txt is a single line. This installer manages the isolcpus token, + so any existing isolcpus= entry (including a user-managed range) is + stripped first, then the installer's token is appended when a core is + reserved. This keeps re-runs idempotent and avoids stacking multiple + isolcpus= entries. """ line = shell.read_text_file(cmdline_file).strip() tokens = [t for t in line.split() if not t.startswith("isolcpus=")] @@ -168,15 +170,19 @@ def main(): shell.run_command("apt-get update") print("Downloading prerequisites...") - # cmake is required by scikit-build-core, the build backend the upstream - # repo now uses for its Python bindings. + # build-essential + python3-pip are needed to compile and pip-install the + # Cython bindings; cmake is required by scikit-build-core (the upstream + # build backend); unzip extracts the downloaded source archive. shell.run_command( - "apt-get install -y python3-dev python3-pillow cython3 python3-setuptools cmake" + "apt-get install -y build-essential python3-dev python3-pip " + "python3-pillow cython3 python3-setuptools cmake unzip" ) print("Downloading RGB matrix software...") + # -f makes curl fail (non-zero) on HTTP errors instead of saving a 404 + # HTML page that would later cause a confusing unzip failure. shell.run_command( - f"curl -L {GITUSER}/{REPO}/archive/{COMMIT}.zip -o {REPO}-{COMMIT}.zip" + f"curl -fL {GITUSER}/{REPO}/archive/{COMMIT}.zip -o {REPO}-{COMMIT}.zip" ) shell.run_command(f"unzip -q {REPO}-{COMMIT}.zip") shell.remove(f"{REPO}-{COMMIT}.zip") @@ -192,10 +198,13 @@ def main(): shell.run_command(f'"{sys.executable}" -m pip install --upgrade pip') shell.run_command(f'"{sys.executable}" -m pip install .') - # Change ownership to the user who called sudo. + # Change ownership to the user who called sudo, using their actual primary + # group (which isn't always the same name as the username). sudo_user = os.environ.get("SUDO_USER") if sudo_user: - shell.run_command(f"chown -R {sudo_user}:{sudo_user} {os.getcwd()}") + sudo_gid = os.environ.get("SUDO_GID", "") + owner = f"{sudo_user}:{sudo_gid}" if sudo_gid else sudo_user + shell.run_command(f"chown -R {owner} {os.getcwd()}") # CONFIG --------------------------------------------------------------- @@ -240,7 +249,7 @@ def main(): print("Done.") print("") - print("The 'rgbmatrix' Python package is installed in:") + print("The 'rgbmatrix' Python package is installed for this interpreter:") print(f" {sys.executable}") print("Run your programs with that interpreter so 'import rgbmatrix' works.") print("") From 35fa2e669a5f441c56f057d91085e35cf1876145 Mon Sep 17 00:00:00 2001 From: makermelissa-piclaw Date: Tue, 16 Jun 2026 15:14:50 -0700 Subject: [PATCH 4/4] rgb-matrix: guard against PEP 668 externally-managed interpreter On Bookworm/Trixie the system python is externally managed, so 'pip install .' of the rgbmatrix bindings fails mid-install. Detect a non-venv externally-managed interpreter up front and bail with guidance to create/activate a venv first, instead of failing after apt + download. Older (non-PEP-668) systems and venvs are unaffected. Verified on test-pi (Pi 4/Trixie): system python -> bail, venv -> pass. --- rgb-matrix.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/rgb-matrix.py b/rgb-matrix.py index 95953ee..df84a60 100644 --- a/rgb-matrix.py +++ b/rgb-matrix.py @@ -48,6 +48,41 @@ ) +def in_virtualenv(): + """True if the running interpreter is a virtual environment.""" + return sys.prefix != getattr(sys, "base_prefix", sys.prefix) + + +def externally_managed(): + """True if this interpreter is PEP 668 externally-managed (Bookworm+).""" + stdlib = os.path.dirname(os.__file__) + return os.path.exists(os.path.join(stdlib, "EXTERNALLY-MANAGED")) + + +def check_pip_environment(): + """Bail early if `pip install .` would fail on a PEP 668 system python. + + On Bookworm/Trixie the system interpreter is externally managed, so + installing the rgbmatrix bindings into it fails with + 'externally-managed-environment'. The supported flow is to run this + installer from an activated virtual environment, e.g.: + + python3 -m venv --system-site-packages env + source env/bin/activate + sudo -E env PATH=$PATH python3 rgb-matrix.py + """ + if in_virtualenv() or not externally_managed(): + return + shell.bail( + "This interpreter is externally managed (PEP 668), so the rgbmatrix\n" + "bindings can't be pip-installed into it. Create and activate a\n" + "virtual environment first, then re-run this installer inside it:\n\n" + " python3 -m venv --system-site-packages env\n" + " source env/bin/activate\n" + " sudo -E env PATH=$PATH python3 rgb-matrix.py\n" + ) + + def set_isolcpus(cmdline_file, reserve, isolcpu_token): """Idempotently set the isolcpus=N token in cmdline.txt. @@ -66,6 +101,8 @@ def set_isolcpus(cmdline_file, reserve, isolcpu_token): def main(): shell.require_root() + # Fail fast with clear guidance if pip would hit PEP 668 later. + check_pip_environment() num_cores = os.cpu_count() or 1 # Reserve the highest-numbered core (isolcpus is 0-indexed).