diff --git a/.gitignore b/.gitignore index ba69057..ec677e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /*.pio.h -protodemo /build *.egg-info diff --git a/docs/adafruit_blinka_raspberry_pi5_piomatter.rst b/docs/adafruit_blinka_raspberry_pi5_piomatter.rst new file mode 100644 index 0000000..afebd04 --- /dev/null +++ b/docs/adafruit_blinka_raspberry_pi5_piomatter.rst @@ -0,0 +1,8 @@ +HUB75 matrix driver for Raspberry Pi 5 using PIO +------------------------------------------------ + +.. autosummary:: + :toctree: _generate + :recursive: + + adafruit_blinka_raspberry_pi5_piomatter diff --git a/docs/adafruit_raspberry_pi5_piomatter.rst b/docs/adafruit_raspberry_pi5_piomatter.rst deleted file mode 100644 index bf2c472..0000000 --- a/docs/adafruit_raspberry_pi5_piomatter.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: adafruit_blinka_raspberry_pi5_piomatter diff --git a/docs/conf.py b/docs/conf.py index 88ad51f..3087519 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,13 +26,27 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - "sphinx.ext.autodoc", + "autoapi.extension", "sphinx.ext.intersphinx", "sphinx.ext.autosummary", "sphinx.ext.napoleon", ] -autosummary_generate = True +autoapi_keep_files = True +autoapi_dirs = ["../src/adafruit_blinka_raspberry_pi5_piomatter"] +autoapi_add_toctree_entry = True +autoapi_options = [ + "members", + "undoc-members", + "show-inheritance", + "special-members", + "show-module-summary", +] + +autoapi_python_class_content = "both" +autoapi_python_use_implicit_namespaces = True +autoapi_template_dir = "autoapi/templates" +autoapi_root = "api" # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -50,7 +64,7 @@ # General information about the project. project = "adafruit-blinka-pi5-piomatter" -copyright = "2023 Jeff Epler" +copyright = "2025 Jeff Epler" author = "Jeff Epler" # The version info for the project you're documenting, acts as replacement for diff --git a/docs/requirements.txt b/docs/requirements.txt index c2c0633..a3ba6b6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,5 +3,6 @@ # SPDX-License-Identifier: Unlicense sphinx +sphinx-autoapi sphinx-rtd-theme sphinxcontrib-jquery diff --git a/examples/fbmirror.py b/examples/fbmirror.py index 2d42632..d3eb18d 100644 --- a/examples/fbmirror.py +++ b/examples/fbmirror.py @@ -12,10 +12,12 @@ """ -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import click import numpy as np -import piomatter_click + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter +import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click +from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper with open("/sys/class/graphics/fb0/virtual_size") as f: screenx, screeny = [int(word) for word in f.read().split(",")] @@ -33,12 +35,31 @@ linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // bytes_per_pixel), dtype=dtype) +def make_pixelmap_multilane(width, height, n_addr_lines, n_lanes): + calc_height = n_lanes << n_addr_lines + if height != calc_height: + raise RuntimeError(f"Calculated height {calc_height} does not match requested height {height}") + n_addr = 1 << n_addr_lines + + m = [] + for addr in range(n_addr): + for x in range(width): + for lane in range(n_lanes): + y = addr + lane * n_addr + m.append(x + width * y) + print(m) + return m + @click.command @click.option("--x-offset", "xoffset", type=int, help="The x offset of top left corner of the region to mirror", default=0) @click.option("--y-offset", "yoffset", type=int, help="The y offset of top left corner of the region to mirror", default=0) @piomatter_click.standard_options -def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines): - geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, rotation=rotation) +def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes): + if n_lanes != 2: + pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes) + geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap) + else: + geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, rotation=rotation) framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB565, pinout=pinout, framebuffer=framebuffer, geometry=geometry) diff --git a/examples/fbmirror_scaled.py b/examples/fbmirror_scaled.py index b82e0ca..832c81f 100644 --- a/examples/fbmirror_scaled.py +++ b/examples/fbmirror_scaled.py @@ -37,11 +37,13 @@ `... video=HDMI-A-1:640x480M@60D`. """ -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import click import numpy as np import PIL.Image as Image -import piomatter_click + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter +import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click +from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper with open("/sys/class/graphics/fb0/virtual_size") as f: screenx, screeny = [int(word) for word in f.read().split(",")] @@ -65,8 +67,12 @@ @click.option("--y-offset", "yoffset", type=int, help="The y offset of top left corner of the region to mirror", default=0) @click.option("--scale", "scale", type=int, help="The scale factor to reduce the display down by.", default=3) @piomatter_click.standard_options -def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines): - geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, rotation=rotation) +def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes): + if n_lanes != 2: + pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes) + geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap) + else: + geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_temporal_planes=n_temporal_planes, n_addr_lines=n_addr_lines, rotation=rotation) matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=matrix_framebuffer, geometry=geometry) diff --git a/examples/play_gif.py b/examples/play_gif.py index 8a621ae..4f21131 100644 --- a/examples/play_gif.py +++ b/examples/play_gif.py @@ -11,10 +11,11 @@ import time -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np import PIL.Image as Image +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + width = 64 height = 32 diff --git a/examples/playframes.py b/examples/playframes.py index 9fa3bdf..373cf19 100644 --- a/examples/playframes.py +++ b/examples/playframes.py @@ -13,10 +13,11 @@ import sys import time -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np import PIL.Image as Image +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + images = sorted(glob.glob(sys.argv[1])) geometry = piomatter.Geometry(width=64, height=32, n_addr_lines=4, rotation=piomatter.Orientation.Normal) diff --git a/examples/quote_scroller.py b/examples/quote_scroller.py index 3d5639d..293386e 100644 --- a/examples/quote_scroller.py +++ b/examples/quote_scroller.py @@ -14,11 +14,12 @@ """ -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np import requests from PIL import Image, ImageDraw, ImageFont +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + # 128px for 2x1 matrices. Change to 64 if you're using a single matrix. total_width = 128 total_height = 32 diff --git a/examples/rainbow_spiral.py b/examples/rainbow_spiral.py index f4ed5e2..eb58436 100644 --- a/examples/rainbow_spiral.py +++ b/examples/rainbow_spiral.py @@ -10,11 +10,12 @@ $ python rainbow_spiral.py """ -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np import rainbowio from PIL import Image, ImageDraw +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + width = 64 height = 32 pen_radius = 1 diff --git a/examples/rainbow_spiral_active3.py b/examples/rainbow_spiral_active3.py new file mode 100644 index 0000000..555c40f --- /dev/null +++ b/examples/rainbow_spiral_active3.py @@ -0,0 +1,114 @@ +#!/usr/bin/python3 +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +""" +Display a spiral around the display drawn with a rainbow color. + +Run like this: + +$ python rainbow_spiral.py + +""" +import numpy as np +import rainbowio +from PIL import Image, ImageDraw + +import adafruit_blinka_raspberry_pi5_piomatter as piomatter +from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper + +width = 64 +n_lanes = 6 +n_addr_lines = 5 +height = n_lanes << n_addr_lines +pen_radius = 1 + +canvas = Image.new('RGB', (width, height), (0, 0, 0)) +draw = ImageDraw.Draw(canvas) + +pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes) +geometry = piomatter.Geometry(width=width, height=height, n_addr_lines=n_addr_lines, n_planes=10, n_temporal_planes=4, map=pixelmap, n_lanes=n_lanes) +framebuffer = np.asarray(canvas) + 0 # Make a mutable copy +matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, + pinout=piomatter.Pinout.Active3, + framebuffer=framebuffer, + geometry=geometry) + +color_index = 0 + +update_interval = 3 +update_counter = 0 +def update_matrix(): + global update_counter + if (update_counter := update_counter + 1) >= update_interval: + framebuffer[:] = np.asarray(canvas) + matrix.show() + update_counter = 0 + +def darken_color(hex_color, darkness_factor): + # Convert hex color number to RGB + r = (hex_color >> 16) & 0xFF + g = (hex_color >> 8) & 0xFF + b = hex_color & 0xFF + + # Apply darkness factor + r = int(r * (1 - darkness_factor)) + g = int(g * (1 - darkness_factor)) + b = int(b * (1 - darkness_factor)) + + # Ensure values are within the valid range + r = max(0, min(255, r)) + g = max(0, min(255, g)) + b = max(0, min(255, b)) + + # Convert RGB back to hex number + darkened_hex_color = (r << 16) + (g << 8) + b + + return darkened_hex_color + +step_count = 4 +darkness_factor = 0.5 + +clearing = False + +try: + # step_down_size = pen_radius * 2 + 2 + + while True: + for step in range(step_count): + step_down_size = step * (pen_radius* 2) + (2 * step) + for x in range(pen_radius + step_down_size, width - pen_radius - step_down_size - 1): + color_index = (color_index + 2) % 256 + color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000 + draw.circle((x, pen_radius + step_down_size), pen_radius, color) + update_matrix() + for y in range(pen_radius + step_down_size, height - pen_radius - step_down_size - 1): + color_index = (color_index + 2) % 256 + color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000 + draw.circle((width - pen_radius - step_down_size -1, y), pen_radius, color) + update_matrix() + for x in range(width - pen_radius - step_down_size - 1, pen_radius + step_down_size, -1): + color_index = (color_index + 2) % 256 + color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000 + draw.circle((x, height - pen_radius - step_down_size - 1), pen_radius, color) + update_matrix() + for y in range(height - pen_radius - step_down_size - 1, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1))) -1, -1): + color_index = (color_index + 2) % 256 + color = darken_color(rainbowio.colorwheel(color_index), darkness_factor) if not clearing else 0x000000 + draw.circle((pen_radius + step_down_size, y), pen_radius, color) + update_matrix() + + if step != step_count-1: + # connect to next iter + for x in range(pen_radius + step_down_size, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1)))): + color_index = (color_index + 2) % 256 + color = darken_color(rainbowio.colorwheel(color_index), + darkness_factor) if not clearing else 0x000000 + draw.circle((x, pen_radius + ((step+1) * (pen_radius* 2) + (2 * (step+1)))), pen_radius, color) + update_matrix() + + print(matrix.fps) + clearing = not clearing + +except KeyboardInterrupt: + print("Exiting") diff --git a/examples/simpletest.py b/examples/simpletest.py index 64c048b..c36beb6 100644 --- a/examples/simpletest.py +++ b/examples/simpletest.py @@ -13,10 +13,11 @@ import pathlib -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np import PIL.Image as Image +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + geometry = piomatter.Geometry(width=64, height=64, n_addr_lines=4, rotation=piomatter.Orientation.Normal) framebuffer = np.asarray(Image.open(pathlib.Path(__file__).parent / "blinka64x64.png")) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, diff --git a/examples/simpletest_addre_bgr.py b/examples/simpletest_addre_bgr.py index cf13d41..3425a68 100644 --- a/examples/simpletest_addre_bgr.py +++ b/examples/simpletest_addre_bgr.py @@ -13,10 +13,11 @@ import pathlib -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np import PIL.Image as Image +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + geometry = piomatter.Geometry(width=64, height=64, n_addr_lines=5, rotation=piomatter.Orientation.Normal, n_planes=8) framebuffer = np.asarray(Image.open(pathlib.Path(__file__).parent / "blinka64x64.png")) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=piomatter.Pinout.AdafruitMatrixBonnetBGR, framebuffer=framebuffer, geometry=geometry) diff --git a/examples/single_panel_simpletest.py b/examples/single_panel_simpletest.py index 6830dfd..3b20fcf 100644 --- a/examples/single_panel_simpletest.py +++ b/examples/single_panel_simpletest.py @@ -11,10 +11,11 @@ """ -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import numpy as np from PIL import Image, ImageDraw +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + width = 64 height = 32 diff --git a/examples/virtualdisplay.py b/examples/virtualdisplay.py index 06469dc..139444f 100644 --- a/examples/virtualdisplay.py +++ b/examples/virtualdisplay.py @@ -26,12 +26,13 @@ import shlex from subprocess import Popen -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import click import numpy as np -import piomatter_click from pyvirtualdisplay.smartdisplay import SmartDisplay +import adafruit_blinka_raspberry_pi5_piomatter as piomatter +import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click + @click.command @click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1) diff --git a/examples/virtualdisplay_keyboard.py b/examples/virtualdisplay_keyboard.py index ee7476a..fcc1f48 100644 --- a/examples/virtualdisplay_keyboard.py +++ b/examples/virtualdisplay_keyboard.py @@ -29,12 +29,14 @@ import tty from subprocess import Popen, run -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import click import numpy as np -import piomatter_click from pyvirtualdisplay.smartdisplay import SmartDisplay +import adafruit_blinka_raspberry_pi5_piomatter as piomatter +import adafruit_blinka_raspberry_pi5_piomatter.click as piomatter_click +from adafruit_blinka_raspberry_pi5_piomatter.pixelmappers import simple_multilane_mapper + keyboard_debug = False keys_down = set() basic_characters = string.ascii_letters + string.digits @@ -91,8 +93,8 @@ @click.option("--ctrl-c-interrupt/--no-ctrl-c-interrupt", help="If Ctrl+C should be handled as an interrupt.", default=True) @piomatter_click.standard_options @click.argument("command", nargs=-1) -def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, - n_addr_lines, ctrl_c_interrupt, command): +def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, + n_addr_lines, n_lanes, ctrl_c_interrupt, command): def handle_key_event(evt_data): if evt_data in key_map.keys(): keys_down.add(key_map[evt_data]) @@ -132,8 +134,11 @@ def handle_key_event(evt_data): if extra_args: kwargs['extra_args'] = shlex.split(extra_args) print("xauth", use_xauth) - geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, - rotation=rotation) + if n_lanes != 2: + pixelmap = simple_multilane_mapper(width, height, n_addr_lines, n_lanes) + geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, n_lanes=n_lanes, map=pixelmap) + else: + geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_temporal_planes=n_temporal_planes, n_addr_lines=n_addr_lines, rotation=rotation) framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8) matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer, geometry=geometry) diff --git a/pyproject.toml b/pyproject.toml index 112a689..f90c314 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,15 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] [tool.ruff] -extend-select = [ +lint.extend-select = [ "B", # flake8-bugbear "I", # isort "PGH", # pygrep-hooks "RUF", # Ruff-specific "UP", # pyupgrade ] -extend-ignore = [ +lint.extend-ignore = [ "E501", # Line too long + "RUF002", # Yes I meant to type 'multiplication sign'! ] target-version = "py311" diff --git a/setup.py b/setup.py index d55cacd..c7c9e4b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # say from a submodule. ext_modules = [ - Pybind11Extension("adafruit_blinka_raspberry_pi5_piomatter", + Pybind11Extension("adafruit_blinka_raspberry_pi5_piomatter._piomatter", ["src/pymain.cpp", "src/piolib/piolib.c", "src/piolib/pio_rp1.c"], define_macros = [('VERSION_INFO', __version__)], include_dirs = ['./src/include', './src/piolib/include'], @@ -33,6 +33,8 @@ cmdclass={"build_ext": build_ext}, zip_safe=False, python_requires=">=3.11", + packages=['adafruit_blinka_raspberry_pi5_piomatter'], + package_dir={'adafruit_blinka_raspberry_pi5_piomatter': 'src/adafruit_blinka_raspberry_pi5_piomatter'}, extras_require={ 'docs': ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-jquery"], }, diff --git a/src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py b/src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py new file mode 100644 index 0000000..76c4b19 --- /dev/null +++ b/src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py @@ -0,0 +1,33 @@ +""" +HUB75 matrix driver for Raspberry Pi 5 using PIO +------------------------------------------------ + +.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter + +.. autosummary:: + :toctree: _generate + :recursive: + :class: Orientation Pinout Colorspace Geometry PioMatter + + Orientation + Pinout + Colorspace + Geometry + PioMatter +""" + +from ._piomatter import ( + Colorspace, + Geometry, + Orientation, + Pinout, + PioMatter, +) + +__all__ = [ + 'Colorspace', + 'Geometry', + 'Orientation', + 'Pinout', + 'PioMatter', +] diff --git a/examples/piomatter_click.py b/src/adafruit_blinka_raspberry_pi5_piomatter/click.py similarity index 74% rename from examples/piomatter_click.py rename to src/adafruit_blinka_raspberry_pi5_piomatter/click.py index 197b6d3..9aef1cb 100644 --- a/examples/piomatter_click.py +++ b/src/adafruit_blinka_raspberry_pi5_piomatter/click.py @@ -5,11 +5,12 @@ from collections.abc import Callable from typing import Any -import adafruit_blinka_raspberry_pi5_piomatter as piomatter import click +import adafruit_blinka_raspberry_pi5_piomatter as piomatter + -class PybindEnumChoice(click.Choice): +class _PybindEnumChoice(click.Choice): def __init__(self, enum, case_sensitive=False): self.enum = enum choices = [k for k, v in enum.__dict__.items() if isinstance(v, enum)] @@ -25,6 +26,11 @@ def convert( r = getattr(self.enum, value) return r +def _validate_temporal_planes(ctx, param, value): + if value not in (0, 2, 4): + raise click.BadParameter("must be 0, 2, or 4") + return value + def standard_options( f: click.decorators.FC | None = None, *, @@ -34,7 +40,9 @@ def standard_options( rotation=piomatter.Orientation.Normal, pinout=piomatter.Pinout.AdafruitMatrixBonnet, n_planes=10, + n_temporal_planes=0, n_addr_lines=4, + n_lanes=2, ) -> Callable[[], None]: """Add standard commandline flags, with the defaults given @@ -61,7 +69,7 @@ def wrapper(f: click.decorators.FC): f = click.option( "--pinout", default=pinout, - type=PybindEnumChoice(piomatter.Pinout), + type=_PybindEnumChoice(piomatter.Pinout), help="The details of the electrical connection to the panels" )(f) if rotation is not None: @@ -69,13 +77,17 @@ def wrapper(f: click.decorators.FC): "--orientation", "rotation", default=rotation, - type=PybindEnumChoice(piomatter.Orientation), + type=_PybindEnumChoice(piomatter.Orientation), help="The overall orientation (rotation) of the panels" )(f) if n_planes is not None: - f = click.option("--num-planes", "n_planes", default=n_planes, help="The number of bit planes (color depth. Lower values can improve refresh rate in frames per second")(f) + f = click.option("--num-planes", "n_planes", default=n_planes, help="The number of bit planes (color depth). Lower values can improve refresh rate in frames per second")(f) + if n_temporal_planes is not None: + f = click.option("--num-temporal-planes", "n_temporal_planes", default=n_temporal_planes, callback=_validate_temporal_planes, help="The number of temporal bit-planes. May be 0, 2, or 4. Nonzero values improve frame rate but can cause some shimmer")(f) if n_addr_lines is not None: f = click.option("--num-address-lines", "n_addr_lines", default=n_addr_lines, help="The number of address lines used by the panels")(f) + if n_lanes is not None: + f = click.option("--num-lanes", "n_lanes", default=n_lanes, help="The number of lanes used by the panels. One 16-pin connector has two lanes (6 RGB pins)")(f) return f if f is None: return wrapper diff --git a/src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py b/src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py new file mode 100644 index 0000000..3ca06c8 --- /dev/null +++ b/src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py @@ -0,0 +1,31 @@ +"""Functions to define the layout of complex setups, particularly multi-connector matrices""" + +def simple_multilane_mapper(width, height, n_addr_lines, n_lanes): + """A simple mapper for 4+ pixel lanes + + A framebuffer (width × height) is mapped onto a matrix where the lanes are stacked + top-to-bottom. Panels within a lane may be cascaded left-to-right. + + Rotation is not supported, and neither are more complicated arrangements of panels + within a single chain (no support for serpentine or stacked panels within a segment) + + .. code-block:: + + 0 -> [panel] -> [panel] + 1 -> [panel] -> [panel] + 2 -> [panel] -> [panel] + """ + + calc_height = n_lanes << n_addr_lines + if height != calc_height: + raise RuntimeError(f"Calculated height {calc_height} does not match requested height {height}") + n_addr = 1 << n_addr_lines + + m = [] + for addr in range(n_addr): + for x in range(width): + for lane in range(n_lanes): + y = addr + lane * n_addr + m.append(x + width * y) + print(m) + return m diff --git a/src/include/piomatter/matrixmap.h b/src/include/piomatter/matrixmap.h index 58455e7..f721a88 100644 --- a/src/include/piomatter/matrixmap.h +++ b/src/include/piomatter/matrixmap.h @@ -48,7 +48,8 @@ matrix_map make_matrixmap(size_t width, size_t height, size_t n_addr_lines, size_t panel_height = 2 << n_addr_lines; if (height % panel_height != 0) { - throw std::range_error("Height does not evenly divide panel height"); + throw std::range_error( + "Overall height does not evenly divide calculated panel height"); } size_t half_panel_height = 1u << n_addr_lines; @@ -80,23 +81,136 @@ matrix_map make_matrixmap(size_t width, size_t height, size_t n_addr_lines, return result; } +struct schedule_entry { + uint32_t shift, active_time; +}; + +using schedule = std::vector; +using schedule_sequence = std::vector; + +schedule_sequence rescale_schedule(schedule_sequence ss, size_t pixels_across) { + uint32_t max_active_time = 0; + for (auto &s : ss) { + for (auto &ent : s) { + max_active_time = std::max(ent.active_time, max_active_time); + } + } + if (max_active_time == 0 || max_active_time >= pixels_across) { + return ss; + } + int scale = (pixels_across + max_active_time - 1) / max_active_time; + for (auto &s : ss) { + for (auto &ent : s) { + ent.active_time *= scale; + } + } + return ss; +} + +schedule_sequence make_simple_schedule(int n_planes, size_t pixels_across) { + if (n_planes < 1 || n_planes > 10) { + throw std::range_error("n_planes out of range"); + } + schedule result; + for (int i = 0; i < n_planes; i++) { + result.emplace_back(9 - i, (1 << (n_planes - i - 1))); + } + return rescale_schedule({result}, pixels_across); +} + +// Make a temporal dither schedule. All the top `n_planes` are shown everytime, +// but the lowest planes are done in a cycle of `n_temporal_planes`: +// 2: {0, 1}; 4: {0, 1, 2, 3} +schedule_sequence make_temporal_dither_schedule(int n_planes, + size_t pixels_across, + int n_temporal_planes) { + if (n_planes < 1 || n_planes > 10) { + throw std::range_error("n_planes out of range"); + } + if (n_temporal_planes < 2) { + // either 0 or 1 temporal planes are not really temporal at all + return make_simple_schedule(n_planes, pixels_across); + } + if (n_temporal_planes >= n_planes) { + throw std::range_error("n_temporal_planes can't exceed n_planes"); + } + if (n_temporal_planes != 2 && n_temporal_planes != 4) { + // the code can generate a schedule for 8 temporal planes, but it + // flickers intolerably + throw std::range_error("n_temporal_planes must be 0, 1, 2, or 4"); + } + + int n_real_planes = n_planes - n_temporal_planes; + + schedule base_sched; + for (int j = 0; j < n_real_planes; j++) { + base_sched.emplace_back( + 9 - j, (1 << (n_temporal_planes + n_real_planes - j - 1)) / + n_temporal_planes); + } + + schedule_sequence result; + + auto add_sched = [&result, &base_sched](int plane, int count) { + auto sched = base_sched; + sched.emplace_back(9 - plane, count); + result.emplace_back(sched); + }; + + for (int i = 0; i < n_temporal_planes; i++) { + add_sched(n_real_planes + i, 1 << (n_temporal_planes - i - 1)); + } +#if 0 + std::vector counts(10, 0); + for (auto s : result) { + for(auto t: s) { + counts[t.shift] += t.active_time; + } + } + for (auto s : counts) { + printf("%d ", s); + } + printf("\n"); +#endif + + return rescale_schedule(result, pixels_across); + ; +} + struct matrix_geometry { template matrix_geometry(size_t pixels_across, size_t n_addr_lines, int n_planes, - size_t width, size_t height, bool serpentine, const Cb &cb) + int n_temporal_planes, size_t width, size_t height, + bool serpentine, const Cb &cb) + : matrix_geometry( + pixels_across, n_addr_lines, n_planes, n_temporal_planes, width, + height, + make_matrixmap(width, height, n_addr_lines, serpentine, cb), 2) {} + + matrix_geometry(size_t pixels_across, size_t n_addr_lines, int n_planes, + int n_temporal_planes, size_t width, size_t height, + matrix_map map, size_t n_lanes) + : matrix_geometry(pixels_across, n_addr_lines, width, height, map, + n_lanes, + make_temporal_dither_schedule(n_planes, pixels_across, + n_temporal_planes)) {} + + matrix_geometry(size_t pixels_across, size_t n_addr_lines, size_t width, + size_t height, matrix_map map, size_t n_lanes, + const schedule_sequence &schedules) : pixels_across(pixels_across), n_addr_lines(n_addr_lines), - n_planes(n_planes), width(width), - height(height), map{make_matrixmap(width, height, n_addr_lines, - serpentine, cb)} { - size_t pixels_down = 2u << n_addr_lines; + n_lanes(n_lanes), width(width), height(height), + map(map), schedules{schedules} { + size_t pixels_down = n_lanes << n_addr_lines; if (map.size() != pixels_down * pixels_across) { throw std::range_error( "map size does not match calculated pixel count"); } } - size_t pixels_across, n_addr_lines; - int n_planes; + + size_t pixels_across, n_addr_lines, n_lanes; size_t width, height; matrix_map map; + schedule_sequence schedules; }; } // namespace piomatter diff --git a/src/include/piomatter/pins.h b/src/include/piomatter/pins.h index ba61903..9c2435b 100644 --- a/src/include/piomatter/pins.h +++ b/src/include/piomatter/pins.h @@ -40,4 +40,42 @@ struct adafruit_matrix_bonnet_pinout_bgr { static constexpr uint32_t post_addr_delay = 500; }; +struct active3_pinout { + static constexpr pin_t PIN_RGB[] = {7, 27, 11, 10, 9, 8, 6, 5, 12, + 20, 13, 19, 3, 2, 14, 21, 16, 26}; + static constexpr pin_t PIN_ADDR[] = {22, 23, 24, 25, 15}; + static constexpr pin_t PIN_OE = 18; // /OE: output enable when LOW + static constexpr pin_t PIN_CLK = 17; // SRCLK: clocks on RISING edge + static constexpr pin_t PIN_LAT = 4; // RCLK: latches on RISING edge + + static constexpr uint32_t clk_bit = 1u << PIN_CLK; + static constexpr uint32_t lat_bit = 1u << PIN_LAT; + static constexpr uint32_t oe_bit = 1u << PIN_OE; + static constexpr uint32_t oe_active = 0; + static constexpr uint32_t oe_inactive = oe_bit; + + static constexpr uint32_t post_oe_delay = 0; + static constexpr uint32_t post_latch_delay = 0; + static constexpr uint32_t post_addr_delay = 500; +}; + +struct active3_pinout_bgr { + static constexpr pin_t PIN_RGB[] = {11, 27, 7, 8, 9, 10, 12, 5, 6, + 19, 13, 20, 14, 2, 3, 26, 16, 21}; + static constexpr pin_t PIN_ADDR[] = {22, 23, 24, 25, 15}; + static constexpr pin_t PIN_OE = 18; // /OE: output enable when LOW + static constexpr pin_t PIN_CLK = 17; // SRCLK: clocks on RISING edge + static constexpr pin_t PIN_LAT = 4; // RCLK: latches on RISING edge + + static constexpr uint32_t clk_bit = 1u << PIN_CLK; + static constexpr uint32_t lat_bit = 1u << PIN_LAT; + static constexpr uint32_t oe_bit = 1u << PIN_OE; + static constexpr uint32_t oe_active = 0; + static constexpr uint32_t oe_inactive = oe_bit; + + static constexpr uint32_t post_oe_delay = 0; + static constexpr uint32_t post_latch_delay = 0; + static constexpr uint32_t post_addr_delay = 500; +}; + } // namespace piomatter diff --git a/src/include/piomatter/piomatter.h b/src/include/piomatter/piomatter.h index 4f91fc3..ba9bcae 100644 --- a/src/include/piomatter/piomatter.h +++ b/src/include/piomatter/piomatter.h @@ -49,22 +49,31 @@ template struct piomatter : piomatter_base { using buffer_type = std::vector; + using bufseq_type = std::vector; piomatter(std::span framebuffer, const matrix_geometry &geometry) : framebuffer(framebuffer), geometry{geometry}, converter{}, - blitter_thread{&piomatter::blit_thread, this} { + blitter_thread{} { if (geometry.n_addr_lines > std::size(pinout::PIN_ADDR)) { throw std::runtime_error("too many address lines requested"); } program_init(); + blitter_thread = std::move(std::thread{&piomatter::blit_thread, this}); show(); } void show() override { int buffer_idx = manager.get_free_buffer(); - auto &buffer = buffers[buffer_idx]; + auto &bufseq = buffers[buffer_idx]; + bufseq.resize(geometry.schedules.size()); auto converted = converter.convert(framebuffer); - protomatter_render_rgb10(buffer, geometry, converted.data()); + auto old_active_time = geometry.schedules.back().back().active_time; + for (size_t i = 0; i < geometry.schedules.size(); i++) { + protomatter_render_rgb10(bufseq[i], geometry, + geometry.schedules[i], + old_active_time, converted.data()); + old_active_time = geometry.schedules[i].back().active_time; + } manager.put_filled_buffer(buffer_idx); } @@ -160,26 +169,27 @@ struct piomatter : piomatter_base { } void blit_thread() { - const uint32_t *databuf = nullptr; - size_t datasize = 0; - int old_buffer_idx = buffer_manager::no_buffer; + int cur_buffer_idx = buffer_manager::no_buffer; int buffer_idx; + int seq_idx = -1; uint64_t t0, t1; t0 = monotonicns64(); while ((buffer_idx = manager.get_filled_buffer()) != buffer_manager::exit_request) { if (buffer_idx != buffer_manager::no_buffer) { - const auto &buffer = buffers[buffer_idx]; - databuf = &buffer[0]; - datasize = buffer.size() * sizeof(*databuf); - if (old_buffer_idx != buffer_manager::no_buffer) { - manager.put_free_buffer(old_buffer_idx); + if (cur_buffer_idx != buffer_manager::no_buffer) { + manager.put_free_buffer(cur_buffer_idx); } - old_buffer_idx = buffer_idx; + cur_buffer_idx = buffer_idx; } - if (datasize) { + if (cur_buffer_idx != buffer_manager::no_buffer) { + const auto &cur_buf = buffers[cur_buffer_idx]; + seq_idx = (seq_idx + 1) % cur_buf.size(); + const auto &data = cur_buf[seq_idx]; + auto datasize = sizeof(uint32_t) * data.size(); + auto dataptr = const_cast(&data[0]); pio_sm_xfer_data_large(pio, sm, PIO_DIR_TO_SM, datasize, - (uint32_t *)databuf); + dataptr); t1 = monotonicns64(); if (t0 != t1) { fps = 1e9 / (t1 - t0); @@ -194,7 +204,7 @@ struct piomatter : piomatter_base { PIO pio = NULL; int sm = -1; std::span framebuffer; - buffer_type buffers[3]; + bufseq_type buffers[3]; buffer_manager manager{}; matrix_geometry geometry; colorspace converter; diff --git a/src/include/piomatter/protomatter.pio.h b/src/include/piomatter/protomatter.pio.h index 73485e2..ee5e74a 100644 --- a/src/include/piomatter/protomatter.pio.h +++ b/src/include/piomatter/protomatter.pio.h @@ -3,7 +3,7 @@ const int protomatter_wrap = 4; const int protomatter_wrap_target = 0; const int protomatter_sideset_pin_count = 1; -const bool protomatter_sideset_enable = true; +const bool protomatter_sideset_enable = 1; const uint16_t protomatter[] = { // ; data format (out-shift-right): // ; MSB ... LSB diff --git a/src/include/piomatter/render.h b/src/include/piomatter/render.h index 12c49c4..d6e6e7a 100644 --- a/src/include/piomatter/render.h +++ b/src/include/piomatter/render.h @@ -132,6 +132,7 @@ struct colorspace_rgb10 { template void protomatter_render_rgb10(std::vector &result, const matrix_geometry &matrixmap, + const schedule &sched, uint32_t old_active_time, const uint32_t *pixels) { result.clear(); @@ -153,7 +154,7 @@ void protomatter_render_rgb10(std::vector &result, data_count = n; }; - int32_t active_time; + int32_t active_time = old_active_time; auto do_data_clk_active = [&active_time, &data_count, &result](uint32_t d) { bool active = active_time > 0; @@ -183,68 +184,42 @@ void protomatter_render_rgb10(std::vector &result, return data; }; - auto add_pixels = [&do_data_clk_active, - &result](uint32_t addr_bits, bool r0, bool g0, bool b0, - bool r1, bool g1, bool b1) { - uint32_t data = addr_bits; - if (r0) - data |= (1 << pinout::PIN_RGB[0]); - if (g0) - data |= (1 << pinout::PIN_RGB[1]); - if (b0) - data |= (1 << pinout::PIN_RGB[2]); - if (r1) - data |= (1 << pinout::PIN_RGB[3]); - if (g1) - data |= (1 << pinout::PIN_RGB[4]); - if (b1) - data |= (1 << pinout::PIN_RGB[5]); - - do_data_clk_active(data); - }; - - int last_bit = 0; // illuminate the right row for data in the shift register (the previous // address) const size_t n_addr = 1u << matrixmap.n_addr_lines; - const int n_planes = matrixmap.n_planes; - constexpr size_t n_bits = 10u; - unsigned offset = n_bits - n_planes; const size_t pixels_across = matrixmap.pixels_across; size_t prev_addr = n_addr - 1; uint32_t addr_bits = calc_addr_bits(prev_addr); for (size_t addr = 0; addr < n_addr; addr++) { - // printf("addr=%zu/%zu\n", addr, n_addr); - for (int bit = n_planes - 1; bit >= 0; bit--) { - // printf("bit=%d/%d\n", bit, n_planes); - uint32_t r = 1 << (20 + offset + bit); - uint32_t g = 1 << (10 + offset + bit); - uint32_t b = 1 << (0 + offset + bit); - - // the shortest /OE we can do is one DATA_OVERHEAD... - // TODO: should make sure desired duration of MSB is at least - // `pixels_across` - active_time = 1 << last_bit; - last_bit = bit; + for (auto &schedule_ent : sched) { + uint32_t r_mask = 1 << (20 + schedule_ent.shift); + uint32_t g_mask = 1 << (10 + schedule_ent.shift); + uint32_t b_mask = 1 << (0 + schedule_ent.shift); prep_data(pixels_across); - auto mapiter = matrixmap.map.begin() + 2 * addr * pixels_across; + auto mapiter = matrixmap.map.begin() + + matrixmap.n_lanes * addr * pixels_across; for (size_t x = 0; x < pixels_across; x++) { - assert(mapiter != matrixmap.map.end()); - auto pixel0 = pixels[*mapiter++]; - auto r0 = pixel0 & r; - auto g0 = pixel0 & g; - auto b0 = pixel0 & b; - assert(mapiter != matrixmap.map.end()); - auto pixel1 = pixels[*mapiter++]; - auto r1 = pixel1 & r; - auto g1 = pixel1 & g; - auto b1 = pixel1 & b; - - add_pixels(addr_bits, r0, g0, b0, r1, g1, b1); + uint32_t data = addr_bits; + for (size_t px = 0; px < matrixmap.n_lanes; px++) { + assert(mapiter != matrixmap.map.end()); + auto pixel0 = pixels[*mapiter++]; + auto r_bit = pixel0 & r_mask; + auto g_bit = pixel0 & g_mask; + auto b_bit = pixel0 & b_mask; + + if (r_bit) + data |= (1 << pinout::PIN_RGB[px * 3 + 0]); + if (g_bit) + data |= (1 << pinout::PIN_RGB[px * 3 + 1]); + if (b_bit) + data |= (1 << pinout::PIN_RGB[px * 3 + 2]); + } + + do_data_clk_active(data); } do_data_delay(addr_bits | pinout::oe_active, @@ -256,6 +231,8 @@ void protomatter_render_rgb10(std::vector &result, do_data_delay(addr_bits | pinout::oe_inactive | pinout::lat_bit, pinout::post_latch_delay); + active_time = schedule_ent.active_time; + // with oe inactive, set address bits to illuminate THIS line if (addr != prev_addr) { addr_bits = calc_addr_bits(addr); diff --git a/src/protodemo.c b/src/protodemo.c index 4dab4ec..b5899b3 100644 --- a/src/protodemo.c +++ b/src/protodemo.c @@ -95,10 +95,56 @@ static uint64_t monotonicns64() { return tp.tv_sec * UINT64_C(1000000000) + tp.tv_nsec; } +static void print_dither_schedule(const piomatter::schedule_sequence &ss) { + for (auto s : ss) { + for (auto i : s) { + printf("{%d %d} ", i.shift, i.active_time); + } + printf("\n"); + } + printf("\n"); +} + +static void test_simple_dither_schedule(int n_planes, int pixels_across) { + auto ss = piomatter::make_simple_schedule(n_planes, pixels_across); + print_dither_schedule(ss); +} +static void test_temporal_dither_schedule(int n_planes, int pixels_across, + int n_temporal_frames) { + auto ss = piomatter::make_temporal_dither_schedule(n_planes, pixels_across, + n_temporal_frames); + print_dither_schedule(ss); +} + int main(int argc, char **argv) { int n = argc > 1 ? atoi(argv[1]) : 0; - piomatter::matrix_geometry geometry(128, 4, 10, 64, 64, true, + test_simple_dither_schedule(5, 1); + test_temporal_dither_schedule(5, 1, 0); + test_temporal_dither_schedule(5, 1, 2); + test_temporal_dither_schedule(5, 1, 4); + + test_simple_dither_schedule(6, 1); + test_temporal_dither_schedule(6, 1, 0); + test_temporal_dither_schedule(6, 1, 2); + test_temporal_dither_schedule(6, 1, 4); + + test_simple_dither_schedule(5, 16); + test_temporal_dither_schedule(5, 16, 2); + test_temporal_dither_schedule(5, 16, 4); + + test_simple_dither_schedule(5, 24); + test_temporal_dither_schedule(5, 24, 2); + test_temporal_dither_schedule(5, 24, 4); + + test_simple_dither_schedule(10, 24); + test_temporal_dither_schedule(10, 24, 8); + + test_temporal_dither_schedule(5, 128, 4); + test_temporal_dither_schedule(5, 192, 4); + return 0; + + piomatter::matrix_geometry geometry(128, 4, 10, 0, 64, 64, true, piomatter::orientation_normal); piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry); diff --git a/src/pymain.cpp b/src/pymain.cpp index d21d68d..4793471 100644 --- a/src/pymain.cpp +++ b/src/pymain.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include "piomatter/piomatter.h" @@ -33,6 +34,14 @@ make_piomatter_pc(py::buffer buffer, const py::buffer_info info = buffer.request(); const size_t buffer_size_in_bytes = info.size * info.itemsize; + if (geometry.n_lanes * 3 > std::size(pinout::PIN_RGB)) { + throw std::runtime_error( + py::str("Geometry lane count {} exceeds the pinout with {} rgb " + "pins ({} lanes)") + .attr("format")(geometry.n_lanes, std::size(pinout::PIN_RGB), + std::size(pinout::PIN_RGB) / 3) + .template cast()); + } if (buffer_size_in_bytes != data_size_in_bytes) { throw std::runtime_error( py::str("Framebuffer size must be {} bytes ({} elements of {} " @@ -54,6 +63,8 @@ enum Colorspace { RGB565, RGB888, RGB888Packed }; enum Pinout { AdafruitMatrixBonnet, AdafruitMatrixBonnetBGR, + Active3, + Active3BGR, }; template @@ -70,12 +81,10 @@ make_piomatter_p(Colorspace c, py::buffer buffer, case RGB888Packed: return make_piomatter_pc( buffer, geometry); - - default: - throw std::runtime_error(py::str("Invalid colorspace {!r}") - .attr("format")(c) - .template cast()); } + throw std::runtime_error(py::str("Invalid colorspace {!r}") + .attr("format")(c) + .template cast()); } std::unique_ptr @@ -88,15 +97,19 @@ make_piomatter(Colorspace c, Pinout p, py::buffer buffer, case AdafruitMatrixBonnetBGR: return make_piomatter_p( c, buffer, geometry); - default: - throw std::runtime_error(py::str("Invalid pinout {!r}") - .attr("format")(p) - .template cast()); + case Active3: + return make_piomatter_p(c, buffer, geometry); + case Active3BGR: + return make_piomatter_p(c, buffer, + geometry); } + throw std::runtime_error(py::str("Invalid pinout {!r}") + .attr("format")(p) + .template cast()); } } // namespace -PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) { +PYBIND11_MODULE(_piomatter, m) { py::options options; options.enable_enum_members_docstring(); options.enable_function_signatures(); @@ -106,18 +119,7 @@ PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) { HUB75 matrix driver for Raspberry Pi 5 using PIO ------------------------------------------------ - .. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter - - .. autosummary:: - :toctree: _generate - - Orientation - Pinout - Colorspace - Geometry - PioMatter - AdafruitMatrixBonnetRGB888 - AdafruitMatrixBonnetRGB888Packed + .. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter._piomatter )pbdoc"; py::enum_( @@ -138,7 +140,10 @@ PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) { .value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet, "Adafruit Matrix Bonnet or Matrix Hat") .value("AdafruitMatrixHatBGR", Pinout::AdafruitMatrixBonnetBGR, - "Adafruit Matrix Bonnet or Matrix Hat with BGR color order"); + "Adafruit Matrix Bonnet or Matrix Hat with BGR color order") + .value("Active3", Pinout::Active3, "Active-3 or compatible board") + .value("Active3BGR", Pinout::Active3BGR, + "Active-3 or compatible board with BGR color order"); py::enum_( m, "Colorspace", @@ -157,19 +162,33 @@ Describe the geometry of a set of panels The number of pixels in the shift register is automatically computed from these values. +``n_planes`` controls the color depth of the panel. This is separate from the framebuffer +layout. Decreasing ``n_planes`` can increase FPS at the cost of reduced color fidelity. +The default, 10, is the maximum value. + +``n_temporal_planes`` controls temporal dithering of the panel. The acceptable values +are 0 (the default), 2, and 4. A higher setting can increase FPS at the cost of +slightly increasing the variation of brightness across subsequent frames. + +For simple panels with just 1 connector (2 color lanes), the following constructor arguments are available: + ``serpentine`` controls the arrangement of multiple panels when they are stacked in rows. If it is `True`, then each row goes in the opposite direction of the previous row. +If this is specified, ``n_lanes`` cannot be, and 2 lanes are always used. ``rotation`` controls the orientation of the panel(s). Must be one of the ``Orientation`` constants. Default is ``Orientation.Normal``. -``n_planes`` controls the color depth of the panel. This is separate from the framebuffer -layout. Decreasing ``n_planes`` can increase FPS at the cost of reduced color fidelity. -The default, 10, is the maximum value. +For panels with more than 2 lanes, or using a custom pixel mapping, the following constructor arguments are available: + +``n_lanes`` controls how many color lanes are used. A single 16-pin HUB75 connector has 2 color lanes. +If 2 or 3 connectors are used, then there are 4 or 6 lanes. + +``map`` is a Python list of integers giving the framebuffer pixel indices for each matrix pixel. )pbdoc") .def(py::init([](size_t width, size_t height, size_t n_addr_lines, bool serpentine, piomatter::orientation rotation, - size_t n_planes) { + size_t n_planes, size_t n_temporal_planes) { size_t n_lines = 2 << n_addr_lines; size_t pixels_across = width * height / n_lines; size_t odd = (width * height) % n_lines; @@ -185,30 +204,51 @@ The default, 10, is the maximum value. switch (rotation) { case piomatter::orientation::normal: return piomatter::matrix_geometry( - pixels_across, n_addr_lines, n_planes, width, height, - serpentine, piomatter::orientation_normal); + pixels_across, n_addr_lines, n_planes, + n_temporal_planes, width, height, serpentine, + piomatter::orientation_normal); case piomatter::orientation::r180: return piomatter::matrix_geometry( - pixels_across, n_addr_lines, n_planes, width, height, - serpentine, piomatter::orientation_r180); + pixels_across, n_addr_lines, n_planes, + n_temporal_planes, width, height, serpentine, + piomatter::orientation_r180); case piomatter::orientation::ccw: return piomatter::matrix_geometry( - pixels_across, n_addr_lines, n_planes, width, height, - serpentine, piomatter::orientation_ccw); + pixels_across, n_addr_lines, n_planes, + n_temporal_planes, width, height, serpentine, + piomatter::orientation_ccw); case piomatter::orientation::cw: return piomatter::matrix_geometry( - pixels_across, n_addr_lines, n_planes, width, height, - serpentine, piomatter::orientation_cw); + pixels_across, n_addr_lines, n_planes, + n_temporal_planes, width, height, serpentine, + piomatter::orientation_cw); } throw std::runtime_error("invalid rotation"); }), py::arg("width"), py::arg("height"), py::arg("n_addr_lines"), py::arg("serpentine") = true, py::arg("rotation") = piomatter::orientation::normal, - py::arg("n_planes") = 10u) + py::arg("n_planes") = 10u, py::arg("n_temporal_planes") = 2) + .def(py::init([](size_t width, size_t height, size_t n_addr_lines, + piomatter::matrix_map map, size_t n_planes, + size_t n_temporal_planes, size_t n_lanes) { + size_t n_lines = n_lanes << n_addr_lines; + size_t pixels_across = width * height / n_lines; + for (auto el : map) { + if ((size_t)el >= width * height) { + throw std::out_of_range("Map element out of range"); + } + } + return piomatter::matrix_geometry(pixels_across, n_addr_lines, + n_planes, n_temporal_planes, + width, height, map, n_lanes); + }), + py::arg("width"), py::arg("height"), py::arg("n_addr_lines"), + py::arg("map"), py::arg("n_planes") = 10u, + py::arg("n_temporal_planes") = 0u, py::arg("n_lanes") = 2) .def_readonly("width", &piomatter::matrix_geometry::width) .def_readonly("height", &piomatter::matrix_geometry::height); @@ -216,16 +256,16 @@ The default, 10, is the maximum value. HUB75 matrix driver for Raspberry Pi 5 using PIO ``colorspace`` controls the colorspace that will be used for data to be displayed. -It must be one of the ``Colorspace`` constants. Which to use depends on what data +It must be one of the `Colorspace` constants. Which to use depends on what data your displaying and how it is processed before copying into the framebuffer. ``pinout`` defines which pins the panels are wired to. Different pinouts can support different hardware breakouts and panels with different color order. The -value must be one of the ``Pinout`` constants. +value must be one of the `Pinout` constants. ``framebuffer`` a numpy array that holds pixel data in the appropriate colorspace. -``geometry`` controls the size and shape of the panel. The value must be a ``Geometry`` +``geometry`` controls the size and shape of the panel. The value must be a `Geometry` instance. )pbdoc") .def(py::init(&make_piomatter), py::arg("colorspace"), @@ -239,55 +279,5 @@ data is triple-buffered to prevent tearing. )pbdoc") .def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc( The approximate number of matrix refreshes per second. -)pbdoc"); - - m.def( - "AdafruitMatrixBonnetRGB565", - [](py::buffer buffer, const piomatter::matrix_geometry &geometry) { - return make_piomatter(Colorspace::RGB565, - Pinout::AdafruitMatrixBonnet, buffer, - geometry); - }, - py::arg("buffer"), py::arg("geometry"), - R"pbdoc( -Construct a PioMatter object to drive panels connected to an -Adafruit Matrix Bonnet using the RGB565 memory layout (2 bytes per -pixel) - -This is deprecated shorthand for `PioMatter(Colorspace.RGB565, Pinout.AdafruitMatrixBonnet, ...)`. -)pbdoc"); - - m.def( - "AdafruitMatrixBonnetRGB888", - [](py::buffer buffer, const piomatter::matrix_geometry &geometry) { - return make_piomatter(Colorspace::RGB888, - Pinout::AdafruitMatrixBonnet, buffer, - geometry); - }, - py::arg("framebuffer"), py::arg("geometry"), - R"pbdoc( -Construct a PioMatter object to drive panels connected to an -Adafruit Matrix Bonnet using the RGB888 memory layout (4 bytes per -pixel) - -This is deprecated shorthand for `PioMatter(Colorspace.RGB888, Pinout.AdafruitMatrixBonnet, ...)`. -)pbdoc") - //.doc() = - ; - - m.def( - "AdafruitMatrixBonnetRGB888Packed", - [](py::buffer buffer, const piomatter::matrix_geometry &geometry) { - return make_piomatter(Colorspace::RGB888Packed, - Pinout::AdafruitMatrixBonnet, buffer, - geometry); - }, - py::arg("framebuffer"), py::arg("geometry"), - R"pbdoc( -Construct a PioMatter object to drive panels connected to an -Adafruit Matrix Bonnet using the RGB888 packed memory layout (3 -bytes per pixel) - -This is deprecated shorthand for `PioMatter(Colorspace.RGB888Packed, Pinout.AdafruitMatrixBonnet, ...)`. )pbdoc"); }