Merge pull request #14 from adafruit/bgr-panel-support
Some checks are pending
Pip / build (ubuntu-24.04-arm, 3.11) (push) Waiting to run
Pip / build (ubuntu-24.04-arm, 3.12) (push) Waiting to run
Pip / build (ubuntu-24.04-arm, 3.13) (push) Waiting to run
pre-commit / pre-commit (push) Waiting to run
Wheels / Build SDist (push) Waiting to run
Wheels / Wheels on ubuntu-24.04-arm (push) Waiting to run
Wheels / Upload release (push) Blocked by required conditions

Support swapped (bgr) matrices
This commit is contained in:
foamyguy 2025-02-10 09:49:04 -06:00 committed by GitHub
commit dd77450f15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 266 additions and 78 deletions

4
.gitignore vendored
View file

@ -1,4 +1,4 @@
./*.pio.h /*.pio.h
protodemo protodemo
./build /build
*.egg-info *.egg-info

View file

@ -1,59 +1,21 @@
#!/usr/bin/python3 #!/usr/bin/python3
""" """
Mirror a scaled copy of the framebuffer to a 64x32 matrix Mirror a scaled copy of the framebuffer to a matrix
The upper left corner of the framebuffer is displayed until the user hits ctrl-c. A portion of the framebuffer is displayed until the user hits ctrl-c.
Control matrix size, and orientation with command line arguments.
python fbmirror_scaled.py [width] [height] [orientation]
width int: Total width of matrices in pixels. Default is 64.
height int: Total height of matrices in pixels. Default is 32.
orientation int: Orientation in degrees, must be 0, 90, 180, or 270.
Default is 0 or Normal orientation.
The `/dev/fb0` special file will exist if a monitor is plugged in at boot time, The `/dev/fb0` special file will exist if a monitor is plugged in at boot time,
or if `/boot/firmware/cmdline.txt` specifies a resolution such as or if `/boot/firmware/cmdline.txt` specifies a resolution such as
`... video=HDMI-A-1:640x480M@60D`. `... video=HDMI-A-1:640x480M@60D`.
For help with commandline arguments, run `python fbmirror.py --help`
""" """
import sys
import adafruit_raspberry_pi5_piomatter import adafruit_raspberry_pi5_piomatter as piomatter
import click
import numpy as np import numpy as np
import piomatter_click
width = 64
height = 32
yoffset = 0
xoffset = 0
if len(sys.argv) >= 2:
width = int(sys.argv[1])
else:
width = 64
if len(sys.argv) >= 3:
height = int(sys.argv[2])
else:
height = 32
if len(sys.argv) >= 4:
rotation = int(sys.argv[3])
if rotation == 90:
rotation = adafruit_raspberry_pi5_piomatter.Orientation.CW
elif rotation == 180:
rotation = adafruit_raspberry_pi5_piomatter.Orientation.R180
elif rotation == 270:
rotation = adafruit_raspberry_pi5_piomatter.Orientation.CCW
elif rotation == 0:
rotation = adafruit_raspberry_pi5_piomatter.Orientation.Normal
else:
raise ValueError("Invalid rotation. Must be 0, 90, 180, or 270.")
else:
rotation = adafruit_raspberry_pi5_piomatter.Orientation.Normal
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(",")]
@ -71,11 +33,18 @@ 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)
@click.command
@click.option("--x-offset", "xoffset", type=int, help="The x offset of top left corner of the region to mirror")
@click.option("--y-offset", "yoffset", type=int, help="The y offset of top left corner of the region to mirror")
@piomatter_click.standard_options
def main(xoffset, yoffset, width, height, serpentine, rotation, colorspace, 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)
framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype)
matrix = piomatter.PioMatter(colorspace=colorspace, pinout=pinout, framebuffer=framebuffer, geometry=geometry)
geometry = adafruit_raspberry_pi5_piomatter.Geometry(width=width, height=height, n_addr_lines=4, rotation=rotation) while True:
matrix_framebuffer = np.zeros(shape=(geometry.height, geometry.width), dtype=dtype) framebuffer[:,:] = linux_framebuffer[yoffset:yoffset+height, xoffset:xoffset+width]
matrix = adafruit_raspberry_pi5_piomatter.AdafruitMatrixBonnetRGB565(matrix_framebuffer, geometry) matrix.show()
while True: if __name__ == '__main__':
matrix_framebuffer[:,:] = linux_framebuffer[yoffset:yoffset+height, xoffset:xoffset+width] main()
matrix.show()

View file

@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: 2025 Jeff Epler for Adafruit Industries
# SPDX-License-Identifier: Unlicense
"""A helper for parsing piomatter settings on the commandline"""
from collections.abc import Callable
from typing import Any
import adafruit_raspberry_pi5_piomatter as piomatter
import click
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)]
super().__init__(choices, case_sensitive)
def convert(
self, value: Any, param: click.Parameter | None, ctx: click.Context | None
) -> Any:
if isinstance(value, self.enum):
return value
value = super().convert(value, param, ctx)
r = getattr(self.enum, value)
return r
def standard_options(
f: click.decorators.FC | None = None,
*,
width=64,
height=32,
serpentine=True,
rotation=piomatter.Orientation.Normal,
colorspace=piomatter.Colorspace.RGB888,
pinout=piomatter.Pinout.AdafruitMatrixBonnet,
n_planes=10,
n_addr_lines=4,
) -> Callable[[], None]:
"""Add standard commandline flags, with the defaults given
Use like a click decorator:
.. code-block:: python
@click.command
@piomatter_click.standard_options()
def my_awesome_code(width, height, ...):
...
If a kwarg to this function is None, then the corresponding commandline
option is not added at all. For example, if you don't want to offer the
``--colorspace`` argument, write ``piomatter_click(..., colorspace=None)``."""
def wrapper(f: click.decorators.FC):
if width is not None:
f = click.option("--width", default=width, help="The panel width in pixels")(f)
if height is not None:
f = click.option("--height", default=height, help="The panel height in pixels")(f)
if serpentine is not None:
f = click.option("--serpentine/--no-serpentine", default=serpentine, help="The organization of multiple panels")(f)
if colorspace is not None:
f = click.option(
"--colorspace",
default=colorspace,
type=PybindEnumChoice(piomatter.Colorspace),
help="The memory organization of the framebuffer"
)(f)
if pinout is not None:
f = click.option(
"--pinout",
default=pinout,
type=PybindEnumChoice(piomatter.Pinout),
help="The details of the electrical connection to the panels"
)(f)
if rotation is not None:
f = click.option(
"--orientation",
"rotation",
default=rotation,
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)
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)
return f
if f is None:
return wrapper
return wrapper(f)

View file

@ -0,0 +1,25 @@
#!/usr/bin/python3
"""
Display a static 64x64 image
This assumes a 64x64 matrix with "BGR" pixel order, such as https://www.adafruit.com/product/5362
Run like this:
$ python simpletest.py
The image is displayed until the user hits enter to exit.
"""
import pathlib
import adafruit_raspberry_pi5_piomatter as piomatter
import numpy as np
import PIL.Image as Image
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)
matrix.show()
input("Hit enter to exit")

View file

@ -22,4 +22,22 @@ struct adafruit_matrix_bonnet_pinout {
static constexpr uint32_t post_addr_delay = 500; static constexpr uint32_t post_addr_delay = 500;
}; };
struct adafruit_matrix_bonnet_pinout_bgr {
static constexpr pin_t PIN_RGB[] = {6, 13, 5, 23, 16, 12};
static constexpr pin_t PIN_ADDR[] = {22, 26, 27, 20, 24};
static constexpr pin_t PIN_OE = 4; // /OE: output enable when LOW
static constexpr pin_t PIN_CLK = 17; // SRCLK: clocks on RISING edge
static constexpr pin_t PIN_LAT = 21; // 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

View file

@ -23,7 +23,8 @@ struct PyPiomatter {
template <typename pinout, typename colorspace> template <typename pinout, typename colorspace>
std::unique_ptr<PyPiomatter> std::unique_ptr<PyPiomatter>
make_piomatter(py::buffer buffer, const piomatter::matrix_geometry &geometry) { make_piomatter_pc(py::buffer buffer,
const piomatter::matrix_geometry &geometry) {
using cls = piomatter::piomatter<pinout, colorspace>; using cls = piomatter::piomatter<pinout, colorspace>;
using data_type = colorspace::data_type; using data_type = colorspace::data_type;
@ -34,9 +35,11 @@ make_piomatter(py::buffer buffer, const piomatter::matrix_geometry &geometry) {
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( py::str("Framebuffer size must be {} bytes ({} elements of {} "
"Framebuffer size must be {} bytes, got a buffer of {} bytes") "bytes each), got a buffer of {} bytes")
.attr("format")(data_size_in_bytes, buffer_size_in_bytes) .attr("format")(data_size_in_bytes, n_pixels,
colorspace::data_size_in_bytes(1),
buffer_size_in_bytes)
.template cast<std::string>()); .template cast<std::string>());
} }
@ -45,6 +48,52 @@ make_piomatter(py::buffer buffer, const piomatter::matrix_geometry &geometry) {
return std::make_unique<PyPiomatter>( return std::make_unique<PyPiomatter>(
buffer, std::move(std::make_unique<cls>(framebuffer, geometry))); buffer, std::move(std::make_unique<cls>(framebuffer, geometry)));
} }
enum Colorspace { RGB565, RGB888, RGB888Packed };
enum Pinout {
AdafruitMatrixBonnet,
AdafruitMatrixBonnetBGR,
};
template <class pinout>
std::unique_ptr<PyPiomatter>
make_piomatter_p(Colorspace c, py::buffer buffer,
const piomatter::matrix_geometry &geometry) {
switch (c) {
case RGB565:
return make_piomatter_pc<pinout, piomatter::colorspace_rgb565>(
buffer, geometry);
case RGB888:
return make_piomatter_pc<pinout, piomatter::colorspace_rgb888>(
buffer, geometry);
case RGB888Packed:
return make_piomatter_pc<pinout, piomatter::colorspace_rgb888_packed>(
buffer, geometry);
default:
throw std::runtime_error(py::str("Invalid colorspace {!r}")
.attr("format")(c)
.template cast<std::string>());
}
}
std::unique_ptr<PyPiomatter>
make_piomatter(Colorspace c, Pinout p, py::buffer buffer,
const piomatter::matrix_geometry &geometry) {
switch (p) {
case AdafruitMatrixBonnet:
return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout>(
c, buffer, geometry);
case AdafruitMatrixBonnetBGR:
return make_piomatter_p<piomatter::adafruit_matrix_bonnet_pinout_bgr>(
c, buffer, geometry);
default:
throw std::runtime_error(py::str("Invalid pinout {!r}")
.attr("format")(p)
.template cast<std::string>());
}
}
} // namespace } // namespace
PYBIND11_MODULE(adafruit_raspberry_pi5_piomatter, m) { PYBIND11_MODULE(adafruit_raspberry_pi5_piomatter, m) {
@ -78,6 +127,25 @@ PYBIND11_MODULE(adafruit_raspberry_pi5_piomatter, m) {
.value("CW", piomatter::orientation::cw, .value("CW", piomatter::orientation::cw,
"Rotated 90 degress clockwise"); "Rotated 90 degress clockwise");
py::enum_<Pinout>(
m, "Pinout", "Describes the pins used for the connection to the matrix")
.value("AdafruitMatrixBonnet", Pinout::AdafruitMatrixBonnet,
"Adafruit Matrix Bonnet or Matrix Hat")
.value("AdafruitMatrixBonnetBGR", Pinout::AdafruitMatrixBonnetBGR,
"Adafruit Matrix Bonnet or Matrix Hat with BGR color order")
.value("AdafruitMatrixHat", Pinout::AdafruitMatrixBonnet,
"Adafruit Matrix Bonnet or Matrix Hat")
.value("AdafruitMatrixHatBGR", Pinout::AdafruitMatrixBonnet,
"Adafruit Matrix Bonnet or Matrix Hat with BGR color order");
py::enum_<Colorspace>(
m, "Colorspace",
"Describes the organization of the graphics data in memory")
.value("RGB888Packed", Colorspace::RGB888Packed,
"3 bytes per pixel in RGB order")
.value("RGB888", Colorspace::RGB888, "4 bytes per pixel in RGB order")
.value("RGB565", Colorspace::RGB565, "2 bytes per pixel in RGB order");
py::class_<piomatter::matrix_geometry>(m, "Geometry", R"pbdoc( py::class_<piomatter::matrix_geometry>(m, "Geometry", R"pbdoc(
Describe the geometry of a set of panels Describe the geometry of a set of panels
@ -141,11 +209,9 @@ The default, 10, is the maximum value.
py::class_<PyPiomatter>(m, "PioMatter", R"pbdoc( py::class_<PyPiomatter>(m, "PioMatter", R"pbdoc(
HUB75 matrix driver for Raspberry Pi 5 using PIO HUB75 matrix driver for Raspberry Pi 5 using PIO
Do not create instances of this class directly. Instead, use one of
the constructors such as `AdafruitMatrixBonnetRGB888Packed` to
select a specific pinout & in-memory framebuffer layout.
)pbdoc") )pbdoc")
.def(py::init(&make_piomatter), py::arg("colorspace"),
py::arg("pinout"), py::arg("framebuffer"), py::arg("geometry"))
.def("show", &PyPiomatter::show, R"pbdoc( .def("show", &PyPiomatter::show, R"pbdoc(
Update the displayed image Update the displayed image
@ -157,33 +223,53 @@ data is triple-buffered to prevent tearing.
The approximate number of matrix refreshes per second. The approximate number of matrix refreshes per second.
)pbdoc"); )pbdoc");
m.def("AdafruitMatrixBonnetRGB565", m.def(
make_piomatter<piomatter::adafruit_matrix_bonnet_pinout, "AdafruitMatrixBonnetRGB565",
piomatter::colorspace_rgb565>, [](py::buffer buffer, const piomatter::matrix_geometry &geometry) {
py::arg("buffer"), py::arg("geometry")) return make_piomatter(Colorspace::RGB565,
//.doc() = "Drive panels connected to an Adafruit Matrix Bonnet using Pinout::AdafruitMatrixBonnet, buffer,
// the RGB565 memory layout (4 bytes per pixel)" 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)
m.def("AdafruitMatrixBonnetRGB888", This is deprecated shorthand for `PioMatter(Colorspace.RGB565, Pinout.AdafruitMatrixBonnet, ...)`.
make_piomatter<piomatter::adafruit_matrix_bonnet_pinout, )pbdoc");
piomatter::colorspace_rgb888>,
py::arg("framebuffer"), py::arg("geometry"), m.def(
R"pbdoc( "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 Construct a PioMatter object to drive panels connected to an
Adafruit Matrix Bonnet using the RGB888 memory layout (4 bytes per Adafruit Matrix Bonnet using the RGB888 memory layout (4 bytes per
pixel) pixel)
This is deprecated shorthand for `PioMatter(Colorspace.RGB888, Pinout.AdafruitMatrixBonnet, ...)`.
)pbdoc") )pbdoc")
//.doc() = //.doc() =
; ;
m.def("AdafruitMatrixBonnetRGB888Packed", m.def(
make_piomatter<piomatter::adafruit_matrix_bonnet_pinout, "AdafruitMatrixBonnetRGB888Packed",
piomatter::colorspace_rgb888_packed>, [](py::buffer buffer, const piomatter::matrix_geometry &geometry) {
py::arg("framebuffer"), py::arg("geometry"), return make_piomatter(Colorspace::RGB888Packed,
R"pbdoc( Pinout::AdafruitMatrixBonnet, buffer,
geometry);
},
py::arg("framebuffer"), py::arg("geometry"),
R"pbdoc(
Construct a PioMatter object to drive panels connected to an Construct a PioMatter object to drive panels connected to an
Adafruit Matrix Bonnet using the RGB888 packed memory layout (3 Adafruit Matrix Bonnet using the RGB888 packed memory layout (3
bytes per pixel) bytes per pixel)
This is deprecated shorthand for `PioMatter(Colorspace.RGB888Packed, Pinout.AdafruitMatrixBonnet, ...)`.
)pbdoc"); )pbdoc");
} }