Merge pull request #27 from adafruit/make-package
Allow defining geometries (including more than 2 color lanes) via Python code
This commit is contained in:
commit
8d3355fca3
29 changed files with 628 additions and 199 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
||||||
/*.pio.h
|
/*.pio.h
|
||||||
protodemo
|
|
||||||
/build
|
/build
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
|
|
||||||
8
docs/adafruit_blinka_raspberry_pi5_piomatter.rst
Normal file
8
docs/adafruit_blinka_raspberry_pi5_piomatter.rst
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||||
|
------------------------------------------------
|
||||||
|
|
||||||
|
.. autosummary::
|
||||||
|
:toctree: _generate
|
||||||
|
:recursive:
|
||||||
|
|
||||||
|
adafruit_blinka_raspberry_pi5_piomatter
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.. automodule:: adafruit_blinka_raspberry_pi5_piomatter
|
|
||||||
20
docs/conf.py
20
docs/conf.py
|
|
@ -26,13 +26,27 @@
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
"sphinx.ext.autodoc",
|
"autoapi.extension",
|
||||||
"sphinx.ext.intersphinx",
|
"sphinx.ext.intersphinx",
|
||||||
"sphinx.ext.autosummary",
|
"sphinx.ext.autosummary",
|
||||||
"sphinx.ext.napoleon",
|
"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.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ["_templates"]
|
templates_path = ["_templates"]
|
||||||
|
|
@ -50,7 +64,7 @@ master_doc = "index"
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = "adafruit-blinka-pi5-piomatter"
|
project = "adafruit-blinka-pi5-piomatter"
|
||||||
copyright = "2023 Jeff Epler"
|
copyright = "2025 Jeff Epler"
|
||||||
author = "Jeff Epler"
|
author = "Jeff Epler"
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@
|
||||||
# SPDX-License-Identifier: Unlicense
|
# SPDX-License-Identifier: Unlicense
|
||||||
|
|
||||||
sphinx
|
sphinx
|
||||||
|
sphinx-autoapi
|
||||||
sphinx-rtd-theme
|
sphinx-rtd-theme
|
||||||
sphinxcontrib-jquery
|
sphinxcontrib-jquery
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,12 @@ For help with commandline arguments, run `python fbmirror.py --help`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import click
|
import click
|
||||||
import numpy as np
|
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:
|
with open("/sys/class/graphics/fb0/virtual_size") as f:
|
||||||
screenx, screeny = [int(word) for word in f.read().split(",")]
|
screenx, screeny = [int(word) for word in f.read().split(",")]
|
||||||
|
|
@ -33,12 +35,31 @@ with open("/sys/class/graphics/fb0/stride") as f:
|
||||||
|
|
||||||
linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // bytes_per_pixel), dtype=dtype)
|
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.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("--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)
|
@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
|
@piomatter_click.standard_options
|
||||||
def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines):
|
def main(xoffset, yoffset, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes):
|
||||||
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_addr_lines=n_addr_lines, n_temporal_planes=n_temporal_planes, rotation=rotation)
|
||||||
framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype)
|
framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype)
|
||||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB565, pinout=pinout, framebuffer=framebuffer, geometry=geometry)
|
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB565, pinout=pinout, framebuffer=framebuffer, geometry=geometry)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,13 @@ or if `/boot/firmware/cmdline.txt` specifies a resolution such as
|
||||||
`... video=HDMI-A-1:640x480M@60D`.
|
`... video=HDMI-A-1:640x480M@60D`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import click
|
import click
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import PIL.Image as Image
|
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:
|
with open("/sys/class/graphics/fb0/virtual_size") as f:
|
||||||
screenx, screeny = [int(word) for word in f.read().split(",")]
|
screenx, screeny = [int(word) for word in f.read().split(",")]
|
||||||
|
|
@ -65,8 +67,12 @@ linux_framebuffer = np.memmap('/dev/fb0',mode='r', shape=(screeny, stride // byt
|
||||||
@click.option("--y-offset", "yoffset", type=int, help="The y 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)
|
||||||
@click.option("--scale", "scale", type=int, help="The scale factor to reduce the display down by.", default=3)
|
@click.option("--scale", "scale", type=int, help="The scale factor to reduce the display down by.", default=3)
|
||||||
@piomatter_click.standard_options
|
@piomatter_click.standard_options
|
||||||
def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_addr_lines):
|
def main(xoffset, yoffset, scale, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes, n_addr_lines, n_lanes):
|
||||||
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)
|
||||||
matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
|
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)
|
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=matrix_framebuffer, geometry=geometry)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@ The animated gif is played repeatedly until interrupted with ctrl-c.
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import PIL.Image as Image
|
import PIL.Image as Image
|
||||||
|
|
||||||
|
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||||
|
|
||||||
width = 64
|
width = 64
|
||||||
height = 32
|
height = 32
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ import glob
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import PIL.Image as Image
|
import PIL.Image as Image
|
||||||
|
|
||||||
|
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||||
|
|
||||||
images = sorted(glob.glob(sys.argv[1]))
|
images = sorted(glob.glob(sys.argv[1]))
|
||||||
|
|
||||||
geometry = piomatter.Geometry(width=64, height=32, n_addr_lines=4, rotation=piomatter.Orientation.Normal)
|
geometry = piomatter.Geometry(width=64, height=32, n_addr_lines=4, rotation=piomatter.Orientation.Normal)
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,12 @@ $ python quote_scroller.py
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
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.
|
# 128px for 2x1 matrices. Change to 64 if you're using a single matrix.
|
||||||
total_width = 128
|
total_width = 128
|
||||||
total_height = 32
|
total_height = 32
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,12 @@ Run like this:
|
||||||
$ python rainbow_spiral.py
|
$ python rainbow_spiral.py
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import rainbowio
|
import rainbowio
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||||
|
|
||||||
width = 64
|
width = 64
|
||||||
height = 32
|
height = 32
|
||||||
pen_radius = 1
|
pen_radius = 1
|
||||||
|
|
|
||||||
114
examples/rainbow_spiral_active3.py
Normal file
114
examples/rainbow_spiral_active3.py
Normal file
|
|
@ -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")
|
||||||
|
|
@ -13,10 +13,11 @@ The image is displayed until the user hits enter to exit.
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import PIL.Image as Image
|
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)
|
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"))
|
framebuffer = np.asarray(Image.open(pathlib.Path(__file__).parent / "blinka64x64.png"))
|
||||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed,
|
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed,
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,11 @@ The image is displayed until the user hits enter to exit.
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import PIL.Image as Image
|
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)
|
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"))
|
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)
|
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=piomatter.Pinout.AdafruitMatrixBonnetBGR, framebuffer=framebuffer, geometry=geometry)
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@ $ python simpletest.py
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
||||||
|
|
||||||
width = 64
|
width = 64
|
||||||
height = 32
|
height = 32
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,13 @@ Here's an example for running an emulator using a rom stored in "/tmp/snesrom.sm
|
||||||
import shlex
|
import shlex
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import click
|
import click
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import piomatter_click
|
|
||||||
from pyvirtualdisplay.smartdisplay import SmartDisplay
|
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.command
|
||||||
@click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1)
|
@click.option("--scale", type=float, help="The scale factor, larger numbers mean more virtual pixels", default=1)
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,14 @@ import termios
|
||||||
import tty
|
import tty
|
||||||
from subprocess import Popen, run
|
from subprocess import Popen, run
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import click
|
import click
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import piomatter_click
|
|
||||||
from pyvirtualdisplay.smartdisplay import SmartDisplay
|
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
|
keyboard_debug = False
|
||||||
keys_down = set()
|
keys_down = set()
|
||||||
basic_characters = string.ascii_letters + string.digits
|
basic_characters = string.ascii_letters + string.digits
|
||||||
|
|
@ -91,8 +93,8 @@ ctrl_modified_range = (1, 26)
|
||||||
@click.option("--ctrl-c-interrupt/--no-ctrl-c-interrupt", help="If Ctrl+C should be handled as an interrupt.", default=True)
|
@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
|
@piomatter_click.standard_options
|
||||||
@click.argument("command", nargs=-1)
|
@click.argument("command", nargs=-1)
|
||||||
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes,
|
def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpentine, rotation, pinout, n_planes, n_temporal_planes,
|
||||||
n_addr_lines, ctrl_c_interrupt, command):
|
n_addr_lines, n_lanes, ctrl_c_interrupt, command):
|
||||||
def handle_key_event(evt_data):
|
def handle_key_event(evt_data):
|
||||||
if evt_data in key_map.keys():
|
if evt_data in key_map.keys():
|
||||||
keys_down.add(key_map[evt_data])
|
keys_down.add(key_map[evt_data])
|
||||||
|
|
@ -132,8 +134,11 @@ def main(scale, backend, use_xauth, extra_args, rfbport, width, height, serpenti
|
||||||
if extra_args:
|
if extra_args:
|
||||||
kwargs['extra_args'] = shlex.split(extra_args)
|
kwargs['extra_args'] = shlex.split(extra_args)
|
||||||
print("xauth", use_xauth)
|
print("xauth", use_xauth)
|
||||||
geometry = piomatter.Geometry(width=width, height=height, n_planes=n_planes, n_addr_lines=n_addr_lines,
|
if n_lanes != 2:
|
||||||
rotation=rotation)
|
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)
|
framebuffer = np.zeros(shape=(geometry.height, geometry.width, 3), dtype=np.uint8)
|
||||||
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer,
|
matrix = piomatter.PioMatter(colorspace=piomatter.Colorspace.RGB888Packed, pinout=pinout, framebuffer=framebuffer,
|
||||||
geometry=geometry)
|
geometry=geometry)
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@ build-backend = "setuptools.build_meta"
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
extend-select = [
|
lint.extend-select = [
|
||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
"I", # isort
|
"I", # isort
|
||||||
"PGH", # pygrep-hooks
|
"PGH", # pygrep-hooks
|
||||||
"RUF", # Ruff-specific
|
"RUF", # Ruff-specific
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
]
|
]
|
||||||
extend-ignore = [
|
lint.extend-ignore = [
|
||||||
"E501", # Line too long
|
"E501", # Line too long
|
||||||
|
"RUF002", # Yes I meant to type 'multiplication sign'!
|
||||||
]
|
]
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
|
|
|
||||||
4
setup.py
4
setup.py
|
|
@ -11,7 +11,7 @@ __version__ = get_version()
|
||||||
# say from a submodule.
|
# say from a submodule.
|
||||||
|
|
||||||
ext_modules = [
|
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"],
|
["src/pymain.cpp", "src/piolib/piolib.c", "src/piolib/pio_rp1.c"],
|
||||||
define_macros = [('VERSION_INFO', __version__)],
|
define_macros = [('VERSION_INFO', __version__)],
|
||||||
include_dirs = ['./src/include', './src/piolib/include'],
|
include_dirs = ['./src/include', './src/piolib/include'],
|
||||||
|
|
@ -33,6 +33,8 @@ setup(
|
||||||
cmdclass={"build_ext": build_ext},
|
cmdclass={"build_ext": build_ext},
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
python_requires=">=3.11",
|
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={
|
extras_require={
|
||||||
'docs': ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-jquery"],
|
'docs': ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-jquery"],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
33
src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py
Normal file
33
src/adafruit_blinka_raspberry_pi5_piomatter/__init__.py
Normal file
|
|
@ -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',
|
||||||
|
]
|
||||||
|
|
@ -5,11 +5,12 @@
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import adafruit_blinka_raspberry_pi5_piomatter as piomatter
|
|
||||||
import click
|
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):
|
def __init__(self, enum, case_sensitive=False):
|
||||||
self.enum = enum
|
self.enum = enum
|
||||||
choices = [k for k, v in enum.__dict__.items() if isinstance(v, enum)]
|
choices = [k for k, v in enum.__dict__.items() if isinstance(v, enum)]
|
||||||
|
|
@ -25,6 +26,11 @@ class PybindEnumChoice(click.Choice):
|
||||||
r = getattr(self.enum, value)
|
r = getattr(self.enum, value)
|
||||||
return r
|
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(
|
def standard_options(
|
||||||
f: click.decorators.FC | None = None,
|
f: click.decorators.FC | None = None,
|
||||||
*,
|
*,
|
||||||
|
|
@ -34,7 +40,9 @@ def standard_options(
|
||||||
rotation=piomatter.Orientation.Normal,
|
rotation=piomatter.Orientation.Normal,
|
||||||
pinout=piomatter.Pinout.AdafruitMatrixBonnet,
|
pinout=piomatter.Pinout.AdafruitMatrixBonnet,
|
||||||
n_planes=10,
|
n_planes=10,
|
||||||
|
n_temporal_planes=0,
|
||||||
n_addr_lines=4,
|
n_addr_lines=4,
|
||||||
|
n_lanes=2,
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
"""Add standard commandline flags, with the defaults given
|
"""Add standard commandline flags, with the defaults given
|
||||||
|
|
||||||
|
|
@ -61,7 +69,7 @@ def standard_options(
|
||||||
f = click.option(
|
f = click.option(
|
||||||
"--pinout",
|
"--pinout",
|
||||||
default=pinout,
|
default=pinout,
|
||||||
type=PybindEnumChoice(piomatter.Pinout),
|
type=_PybindEnumChoice(piomatter.Pinout),
|
||||||
help="The details of the electrical connection to the panels"
|
help="The details of the electrical connection to the panels"
|
||||||
)(f)
|
)(f)
|
||||||
if rotation is not None:
|
if rotation is not None:
|
||||||
|
|
@ -69,13 +77,17 @@ def standard_options(
|
||||||
"--orientation",
|
"--orientation",
|
||||||
"rotation",
|
"rotation",
|
||||||
default=rotation,
|
default=rotation,
|
||||||
type=PybindEnumChoice(piomatter.Orientation),
|
type=_PybindEnumChoice(piomatter.Orientation),
|
||||||
help="The overall orientation (rotation) of the panels"
|
help="The overall orientation (rotation) of the panels"
|
||||||
)(f)
|
)(f)
|
||||||
if n_planes is not None:
|
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:
|
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)
|
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
|
return f
|
||||||
if f is None:
|
if f is None:
|
||||||
return wrapper
|
return wrapper
|
||||||
31
src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py
Normal file
31
src/adafruit_blinka_raspberry_pi5_piomatter/pixelmappers.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -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;
|
size_t panel_height = 2 << n_addr_lines;
|
||||||
if (height % panel_height != 0) {
|
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;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct schedule_entry {
|
||||||
|
uint32_t shift, active_time;
|
||||||
|
};
|
||||||
|
|
||||||
|
using schedule = std::vector<schedule_entry>;
|
||||||
|
using schedule_sequence = std::vector<schedule>;
|
||||||
|
|
||||||
|
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<uint32_t> 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 {
|
struct matrix_geometry {
|
||||||
template <typename Cb>
|
template <typename Cb>
|
||||||
matrix_geometry(size_t pixels_across, size_t n_addr_lines, int n_planes,
|
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),
|
: pixels_across(pixels_across), n_addr_lines(n_addr_lines),
|
||||||
n_planes(n_planes), width(width),
|
n_lanes(n_lanes), width(width), height(height),
|
||||||
height(height), map{make_matrixmap(width, height, n_addr_lines,
|
map(map), schedules{schedules} {
|
||||||
serpentine, cb)} {
|
size_t pixels_down = n_lanes << n_addr_lines;
|
||||||
size_t pixels_down = 2u << n_addr_lines;
|
|
||||||
if (map.size() != pixels_down * pixels_across) {
|
if (map.size() != pixels_down * pixels_across) {
|
||||||
throw std::range_error(
|
throw std::range_error(
|
||||||
"map size does not match calculated pixel count");
|
"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;
|
size_t width, height;
|
||||||
matrix_map map;
|
matrix_map map;
|
||||||
|
schedule_sequence schedules;
|
||||||
};
|
};
|
||||||
} // namespace piomatter
|
} // namespace piomatter
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,42 @@ struct adafruit_matrix_bonnet_pinout_bgr {
|
||||||
static constexpr uint32_t post_addr_delay = 500;
|
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
|
} // namespace piomatter
|
||||||
|
|
|
||||||
|
|
@ -49,22 +49,31 @@ template <class pinout = adafruit_matrix_bonnet_pinout,
|
||||||
class colorspace = colorspace_rgb888>
|
class colorspace = colorspace_rgb888>
|
||||||
struct piomatter : piomatter_base {
|
struct piomatter : piomatter_base {
|
||||||
using buffer_type = std::vector<uint32_t>;
|
using buffer_type = std::vector<uint32_t>;
|
||||||
|
using bufseq_type = std::vector<buffer_type>;
|
||||||
piomatter(std::span<typename colorspace::data_type const> framebuffer,
|
piomatter(std::span<typename colorspace::data_type const> framebuffer,
|
||||||
const matrix_geometry &geometry)
|
const matrix_geometry &geometry)
|
||||||
: framebuffer(framebuffer), geometry{geometry}, converter{},
|
: framebuffer(framebuffer), geometry{geometry}, converter{},
|
||||||
blitter_thread{&piomatter::blit_thread, this} {
|
blitter_thread{} {
|
||||||
if (geometry.n_addr_lines > std::size(pinout::PIN_ADDR)) {
|
if (geometry.n_addr_lines > std::size(pinout::PIN_ADDR)) {
|
||||||
throw std::runtime_error("too many address lines requested");
|
throw std::runtime_error("too many address lines requested");
|
||||||
}
|
}
|
||||||
program_init();
|
program_init();
|
||||||
|
blitter_thread = std::move(std::thread{&piomatter::blit_thread, this});
|
||||||
show();
|
show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void show() override {
|
void show() override {
|
||||||
int buffer_idx = manager.get_free_buffer();
|
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);
|
auto converted = converter.convert(framebuffer);
|
||||||
protomatter_render_rgb10<pinout>(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<pinout>(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);
|
manager.put_filled_buffer(buffer_idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,26 +169,27 @@ struct piomatter : piomatter_base {
|
||||||
}
|
}
|
||||||
|
|
||||||
void blit_thread() {
|
void blit_thread() {
|
||||||
const uint32_t *databuf = nullptr;
|
int cur_buffer_idx = buffer_manager::no_buffer;
|
||||||
size_t datasize = 0;
|
|
||||||
int old_buffer_idx = buffer_manager::no_buffer;
|
|
||||||
int buffer_idx;
|
int buffer_idx;
|
||||||
|
int seq_idx = -1;
|
||||||
uint64_t t0, t1;
|
uint64_t t0, t1;
|
||||||
t0 = monotonicns64();
|
t0 = monotonicns64();
|
||||||
while ((buffer_idx = manager.get_filled_buffer()) !=
|
while ((buffer_idx = manager.get_filled_buffer()) !=
|
||||||
buffer_manager::exit_request) {
|
buffer_manager::exit_request) {
|
||||||
if (buffer_idx != buffer_manager::no_buffer) {
|
if (buffer_idx != buffer_manager::no_buffer) {
|
||||||
const auto &buffer = buffers[buffer_idx];
|
if (cur_buffer_idx != buffer_manager::no_buffer) {
|
||||||
databuf = &buffer[0];
|
manager.put_free_buffer(cur_buffer_idx);
|
||||||
datasize = buffer.size() * sizeof(*databuf);
|
|
||||||
if (old_buffer_idx != buffer_manager::no_buffer) {
|
|
||||||
manager.put_free_buffer(old_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<uint32_t *>(&data[0]);
|
||||||
pio_sm_xfer_data_large(pio, sm, PIO_DIR_TO_SM, datasize,
|
pio_sm_xfer_data_large(pio, sm, PIO_DIR_TO_SM, datasize,
|
||||||
(uint32_t *)databuf);
|
dataptr);
|
||||||
t1 = monotonicns64();
|
t1 = monotonicns64();
|
||||||
if (t0 != t1) {
|
if (t0 != t1) {
|
||||||
fps = 1e9 / (t1 - t0);
|
fps = 1e9 / (t1 - t0);
|
||||||
|
|
@ -194,7 +204,7 @@ struct piomatter : piomatter_base {
|
||||||
PIO pio = NULL;
|
PIO pio = NULL;
|
||||||
int sm = -1;
|
int sm = -1;
|
||||||
std::span<typename colorspace::data_type const> framebuffer;
|
std::span<typename colorspace::data_type const> framebuffer;
|
||||||
buffer_type buffers[3];
|
bufseq_type buffers[3];
|
||||||
buffer_manager manager{};
|
buffer_manager manager{};
|
||||||
matrix_geometry geometry;
|
matrix_geometry geometry;
|
||||||
colorspace converter;
|
colorspace converter;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
const int protomatter_wrap = 4;
|
const int protomatter_wrap = 4;
|
||||||
const int protomatter_wrap_target = 0;
|
const int protomatter_wrap_target = 0;
|
||||||
const int protomatter_sideset_pin_count = 1;
|
const int protomatter_sideset_pin_count = 1;
|
||||||
const bool protomatter_sideset_enable = true;
|
const bool protomatter_sideset_enable = 1;
|
||||||
const uint16_t protomatter[] = {
|
const uint16_t protomatter[] = {
|
||||||
// ; data format (out-shift-right):
|
// ; data format (out-shift-right):
|
||||||
// ; MSB ... LSB
|
// ; MSB ... LSB
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ struct colorspace_rgb10 {
|
||||||
template <typename pinout>
|
template <typename pinout>
|
||||||
void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
||||||
const matrix_geometry &matrixmap,
|
const matrix_geometry &matrixmap,
|
||||||
|
const schedule &sched, uint32_t old_active_time,
|
||||||
const uint32_t *pixels) {
|
const uint32_t *pixels) {
|
||||||
result.clear();
|
result.clear();
|
||||||
|
|
||||||
|
|
@ -153,7 +154,7 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
||||||
data_count = n;
|
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) {
|
auto do_data_clk_active = [&active_time, &data_count, &result](uint32_t d) {
|
||||||
bool active = active_time > 0;
|
bool active = active_time > 0;
|
||||||
|
|
@ -183,68 +184,42 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
||||||
return data;
|
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
|
// illuminate the right row for data in the shift register (the previous
|
||||||
// address)
|
// address)
|
||||||
|
|
||||||
const size_t n_addr = 1u << matrixmap.n_addr_lines;
|
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;
|
const size_t pixels_across = matrixmap.pixels_across;
|
||||||
|
|
||||||
size_t prev_addr = n_addr - 1;
|
size_t prev_addr = n_addr - 1;
|
||||||
uint32_t addr_bits = calc_addr_bits(prev_addr);
|
uint32_t addr_bits = calc_addr_bits(prev_addr);
|
||||||
|
|
||||||
for (size_t addr = 0; addr < n_addr; addr++) {
|
for (size_t addr = 0; addr < n_addr; addr++) {
|
||||||
// printf("addr=%zu/%zu\n", addr, n_addr);
|
for (auto &schedule_ent : sched) {
|
||||||
for (int bit = n_planes - 1; bit >= 0; bit--) {
|
uint32_t r_mask = 1 << (20 + schedule_ent.shift);
|
||||||
// printf("bit=%d/%d\n", bit, n_planes);
|
uint32_t g_mask = 1 << (10 + schedule_ent.shift);
|
||||||
uint32_t r = 1 << (20 + offset + bit);
|
uint32_t b_mask = 1 << (0 + schedule_ent.shift);
|
||||||
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;
|
|
||||||
|
|
||||||
prep_data(pixels_across);
|
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++) {
|
for (size_t x = 0; x < pixels_across; x++) {
|
||||||
assert(mapiter != matrixmap.map.end());
|
uint32_t data = addr_bits;
|
||||||
auto pixel0 = pixels[*mapiter++];
|
for (size_t px = 0; px < matrixmap.n_lanes; px++) {
|
||||||
auto r0 = pixel0 & r;
|
assert(mapiter != matrixmap.map.end());
|
||||||
auto g0 = pixel0 & g;
|
auto pixel0 = pixels[*mapiter++];
|
||||||
auto b0 = pixel0 & b;
|
auto r_bit = pixel0 & r_mask;
|
||||||
assert(mapiter != matrixmap.map.end());
|
auto g_bit = pixel0 & g_mask;
|
||||||
auto pixel1 = pixels[*mapiter++];
|
auto b_bit = pixel0 & b_mask;
|
||||||
auto r1 = pixel1 & r;
|
|
||||||
auto g1 = pixel1 & g;
|
|
||||||
auto b1 = pixel1 & b;
|
|
||||||
|
|
||||||
add_pixels(addr_bits, r0, g0, b0, r1, g1, b1);
|
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,
|
do_data_delay(addr_bits | pinout::oe_active,
|
||||||
|
|
@ -256,6 +231,8 @@ void protomatter_render_rgb10(std::vector<uint32_t> &result,
|
||||||
do_data_delay(addr_bits | pinout::oe_inactive | pinout::lat_bit,
|
do_data_delay(addr_bits | pinout::oe_inactive | pinout::lat_bit,
|
||||||
pinout::post_latch_delay);
|
pinout::post_latch_delay);
|
||||||
|
|
||||||
|
active_time = schedule_ent.active_time;
|
||||||
|
|
||||||
// with oe inactive, set address bits to illuminate THIS line
|
// with oe inactive, set address bits to illuminate THIS line
|
||||||
if (addr != prev_addr) {
|
if (addr != prev_addr) {
|
||||||
addr_bits = calc_addr_bits(addr);
|
addr_bits = calc_addr_bits(addr);
|
||||||
|
|
|
||||||
|
|
@ -95,10 +95,56 @@ static uint64_t monotonicns64() {
|
||||||
return tp.tv_sec * UINT64_C(1000000000) + tp.tv_nsec;
|
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 main(int argc, char **argv) {
|
||||||
int n = argc > 1 ? atoi(argv[1]) : 0;
|
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::orientation_normal);
|
||||||
piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry);
|
piomatter::piomatter p(std::span(&pixels[0][0], 64 * 64), geometry);
|
||||||
|
|
||||||
|
|
|
||||||
168
src/pymain.cpp
168
src/pymain.cpp
|
|
@ -1,5 +1,6 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <pybind11/pybind11.h>
|
#include <pybind11/pybind11.h>
|
||||||
|
#include <pybind11/stl.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "piomatter/piomatter.h"
|
#include "piomatter/piomatter.h"
|
||||||
|
|
@ -33,6 +34,14 @@ make_piomatter_pc(py::buffer buffer,
|
||||||
const py::buffer_info info = buffer.request();
|
const py::buffer_info info = buffer.request();
|
||||||
const size_t buffer_size_in_bytes = info.size * info.itemsize;
|
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<std::string>());
|
||||||
|
}
|
||||||
if (buffer_size_in_bytes != data_size_in_bytes) {
|
if (buffer_size_in_bytes != data_size_in_bytes) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
py::str("Framebuffer size must be {} bytes ({} elements of {} "
|
py::str("Framebuffer size must be {} bytes ({} elements of {} "
|
||||||
|
|
@ -54,6 +63,8 @@ enum Colorspace { RGB565, RGB888, RGB888Packed };
|
||||||
enum Pinout {
|
enum Pinout {
|
||||||
AdafruitMatrixBonnet,
|
AdafruitMatrixBonnet,
|
||||||
AdafruitMatrixBonnetBGR,
|
AdafruitMatrixBonnetBGR,
|
||||||
|
Active3,
|
||||||
|
Active3BGR,
|
||||||
};
|
};
|
||||||
|
|
||||||
template <class pinout>
|
template <class pinout>
|
||||||
|
|
@ -70,12 +81,10 @@ make_piomatter_p(Colorspace c, py::buffer buffer,
|
||||||
case RGB888Packed:
|
case RGB888Packed:
|
||||||
return make_piomatter_pc<pinout, piomatter::colorspace_rgb888_packed>(
|
return make_piomatter_pc<pinout, piomatter::colorspace_rgb888_packed>(
|
||||||
buffer, geometry);
|
buffer, geometry);
|
||||||
|
|
||||||
default:
|
|
||||||
throw std::runtime_error(py::str("Invalid colorspace {!r}")
|
|
||||||
.attr("format")(c)
|
|
||||||
.template cast<std::string>());
|
|
||||||
}
|
}
|
||||||
|
throw std::runtime_error(py::str("Invalid colorspace {!r}")
|
||||||
|
.attr("format")(c)
|
||||||
|
.template cast<std::string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<PyPiomatter>
|
std::unique_ptr<PyPiomatter>
|
||||||
|
|
@ -88,15 +97,19 @@ make_piomatter(Colorspace c, Pinout p, py::buffer buffer,
|
||||||
case AdafruitMatrixBonnetBGR:
|
case AdafruitMatrixBonnetBGR:
|
||||||
return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout_bgr>(
|
return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout_bgr>(
|
||||||
c, buffer, geometry);
|
c, buffer, geometry);
|
||||||
default:
|
case Active3:
|
||||||
throw std::runtime_error(py::str("Invalid pinout {!r}")
|
return make_piomatter_p<piomatter::active3_pinout>(c, buffer, geometry);
|
||||||
.attr("format")(p)
|
case Active3BGR:
|
||||||
.template cast<std::string>());
|
return make_piomatter_p<piomatter::active3_pinout_bgr>(c, buffer,
|
||||||
|
geometry);
|
||||||
}
|
}
|
||||||
|
throw std::runtime_error(py::str("Invalid pinout {!r}")
|
||||||
|
.attr("format")(p)
|
||||||
|
.template cast<std::string>());
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) {
|
PYBIND11_MODULE(_piomatter, m) {
|
||||||
py::options options;
|
py::options options;
|
||||||
options.enable_enum_members_docstring();
|
options.enable_enum_members_docstring();
|
||||||
options.enable_function_signatures();
|
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
|
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter
|
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter._piomatter
|
||||||
|
|
||||||
.. autosummary::
|
|
||||||
:toctree: _generate
|
|
||||||
|
|
||||||
Orientation
|
|
||||||
Pinout
|
|
||||||
Colorspace
|
|
||||||
Geometry
|
|
||||||
PioMatter
|
|
||||||
AdafruitMatrixBonnetRGB888
|
|
||||||
AdafruitMatrixBonnetRGB888Packed
|
|
||||||
)pbdoc";
|
)pbdoc";
|
||||||
|
|
||||||
py::enum_<piomatter::orientation>(
|
py::enum_<piomatter::orientation>(
|
||||||
|
|
@ -138,7 +140,10 @@ PYBIND11_MODULE(adafruit_blinka_raspberry_pi5_piomatter, m) {
|
||||||
.value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet,
|
.value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet,
|
||||||
"Adafruit Matrix Bonnet or Matrix Hat")
|
"Adafruit Matrix Bonnet or Matrix Hat")
|
||||||
.value("AdafruitMatrixHatBGR", Pinout::AdafruitMatrixBonnetBGR,
|
.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_<Colorspace>(
|
py::enum_<Colorspace>(
|
||||||
m, "Colorspace",
|
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.
|
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.
|
``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 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``
|
``rotation`` controls the orientation of the panel(s). Must be one of the ``Orientation``
|
||||||
constants. Default is ``Orientation.Normal``.
|
constants. Default is ``Orientation.Normal``.
|
||||||
|
|
||||||
``n_planes`` controls the color depth of the panel. This is separate from the framebuffer
|
For panels with more than 2 lanes, or using a custom pixel mapping, the following constructor arguments are available:
|
||||||
layout. Decreasing ``n_planes`` can increase FPS at the cost of reduced color fidelity.
|
|
||||||
The default, 10, is the maximum value.
|
``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")
|
)pbdoc")
|
||||||
.def(py::init([](size_t width, size_t height, size_t n_addr_lines,
|
.def(py::init([](size_t width, size_t height, size_t n_addr_lines,
|
||||||
bool serpentine, piomatter::orientation rotation,
|
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 n_lines = 2 << n_addr_lines;
|
||||||
size_t pixels_across = width * height / n_lines;
|
size_t pixels_across = width * height / n_lines;
|
||||||
size_t odd = (width * height) % n_lines;
|
size_t odd = (width * height) % n_lines;
|
||||||
|
|
@ -185,30 +204,51 @@ The default, 10, is the maximum value.
|
||||||
switch (rotation) {
|
switch (rotation) {
|
||||||
case piomatter::orientation::normal:
|
case piomatter::orientation::normal:
|
||||||
return piomatter::matrix_geometry(
|
return piomatter::matrix_geometry(
|
||||||
pixels_across, n_addr_lines, n_planes, width, height,
|
pixels_across, n_addr_lines, n_planes,
|
||||||
serpentine, piomatter::orientation_normal);
|
n_temporal_planes, width, height, serpentine,
|
||||||
|
piomatter::orientation_normal);
|
||||||
|
|
||||||
case piomatter::orientation::r180:
|
case piomatter::orientation::r180:
|
||||||
return piomatter::matrix_geometry(
|
return piomatter::matrix_geometry(
|
||||||
pixels_across, n_addr_lines, n_planes, width, height,
|
pixels_across, n_addr_lines, n_planes,
|
||||||
serpentine, piomatter::orientation_r180);
|
n_temporal_planes, width, height, serpentine,
|
||||||
|
piomatter::orientation_r180);
|
||||||
|
|
||||||
case piomatter::orientation::ccw:
|
case piomatter::orientation::ccw:
|
||||||
return piomatter::matrix_geometry(
|
return piomatter::matrix_geometry(
|
||||||
pixels_across, n_addr_lines, n_planes, width, height,
|
pixels_across, n_addr_lines, n_planes,
|
||||||
serpentine, piomatter::orientation_ccw);
|
n_temporal_planes, width, height, serpentine,
|
||||||
|
piomatter::orientation_ccw);
|
||||||
|
|
||||||
case piomatter::orientation::cw:
|
case piomatter::orientation::cw:
|
||||||
return piomatter::matrix_geometry(
|
return piomatter::matrix_geometry(
|
||||||
pixels_across, n_addr_lines, n_planes, width, height,
|
pixels_across, n_addr_lines, n_planes,
|
||||||
serpentine, piomatter::orientation_cw);
|
n_temporal_planes, width, height, serpentine,
|
||||||
|
piomatter::orientation_cw);
|
||||||
}
|
}
|
||||||
throw std::runtime_error("invalid rotation");
|
throw std::runtime_error("invalid rotation");
|
||||||
}),
|
}),
|
||||||
py::arg("width"), py::arg("height"), py::arg("n_addr_lines"),
|
py::arg("width"), py::arg("height"), py::arg("n_addr_lines"),
|
||||||
py::arg("serpentine") = true,
|
py::arg("serpentine") = true,
|
||||||
py::arg("rotation") = piomatter::orientation::normal,
|
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("width", &piomatter::matrix_geometry::width)
|
||||||
.def_readonly("height", &piomatter::matrix_geometry::height);
|
.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
|
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
||||||
|
|
||||||
``colorspace`` controls the colorspace that will be used for data to be displayed.
|
``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.
|
your displaying and how it is processed before copying into the framebuffer.
|
||||||
|
|
||||||
``pinout`` defines which pins the panels are wired to. Different pinouts can
|
``pinout`` defines which pins the panels are wired to. Different pinouts can
|
||||||
support different hardware breakouts and panels with different color order. The
|
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.
|
``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.
|
instance.
|
||||||
)pbdoc")
|
)pbdoc")
|
||||||
.def(py::init(&make_piomatter), py::arg("colorspace"),
|
.def(py::init(&make_piomatter), py::arg("colorspace"),
|
||||||
|
|
@ -239,55 +279,5 @@ data is triple-buffered to prevent tearing.
|
||||||
)pbdoc")
|
)pbdoc")
|
||||||
.def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc(
|
.def_property_readonly("fps", &PyPiomatter::fps, R"pbdoc(
|
||||||
The approximate number of matrix refreshes per second.
|
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");
|
)pbdoc");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue