296 lines
13 KiB
C++
296 lines
13 KiB
C++
#include <iostream>
|
|
#include <pybind11/pybind11.h>
|
|
#include <pybind11/stl.h>
|
|
#include <string>
|
|
|
|
#include "piomatter/piomatter.h"
|
|
|
|
#define STRINGIFY(x) #x
|
|
#define MACRO_STRINGIFY(x) STRINGIFY(x)
|
|
|
|
namespace py = pybind11;
|
|
|
|
namespace {
|
|
struct PyPiomatter {
|
|
PyPiomatter(py::buffer buffer,
|
|
std::unique_ptr<piomatter::piomatter_base> &&matter)
|
|
: buffer{buffer}, matter{std::move(matter)} {}
|
|
py::buffer buffer;
|
|
std::unique_ptr<piomatter::piomatter_base> matter;
|
|
|
|
void show() {
|
|
int err = matter->show();
|
|
if (err != 0) {
|
|
errno = err;
|
|
PyErr_SetFromErrno(PyExc_OSError);
|
|
throw py::error_already_set();
|
|
}
|
|
}
|
|
double fps() const { return matter->fps; }
|
|
int width() const { return matter->geometry.width; }
|
|
int height() const { return matter->geometry.height; }
|
|
};
|
|
|
|
template <typename pinout, typename colorspace>
|
|
std::unique_ptr<PyPiomatter>
|
|
make_piomatter_pc(py::buffer buffer,
|
|
const piomatter::matrix_geometry &geometry) {
|
|
using cls = piomatter::piomatter<pinout, colorspace>;
|
|
using data_type = colorspace::data_type;
|
|
|
|
const auto n_pixels = geometry.width * geometry.height;
|
|
const auto data_size_in_bytes = colorspace::data_size_in_bytes(n_pixels);
|
|
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<std::string>());
|
|
}
|
|
if (buffer_size_in_bytes != data_size_in_bytes) {
|
|
throw std::runtime_error(
|
|
py::str("Framebuffer size must be {} bytes ({} elements of {} "
|
|
"bytes each), got a buffer of {} bytes")
|
|
.attr("format")(data_size_in_bytes, n_pixels,
|
|
colorspace::data_size_in_bytes(1),
|
|
buffer_size_in_bytes)
|
|
.template cast<std::string>());
|
|
}
|
|
|
|
std::span<data_type> framebuffer(reinterpret_cast<data_type *>(info.ptr),
|
|
data_size_in_bytes / sizeof(data_type));
|
|
return std::make_unique<PyPiomatter>(
|
|
buffer, std::move(std::make_unique<cls>(framebuffer, geometry)));
|
|
}
|
|
|
|
enum Colorspace { RGB565, RGB888, RGB888Packed };
|
|
|
|
enum Pinout {
|
|
AdafruitMatrixBonnet,
|
|
AdafruitMatrixBonnetBGR,
|
|
Active3,
|
|
Active3BGR,
|
|
};
|
|
|
|
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);
|
|
}
|
|
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);
|
|
case Active3:
|
|
return make_piomatter_p<piomatter::active3_pinout>(c, buffer, geometry);
|
|
case Active3BGR:
|
|
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
|
|
|
|
PYBIND11_MODULE(_piomatter, m) {
|
|
py::options options;
|
|
options.enable_enum_members_docstring();
|
|
options.enable_function_signatures();
|
|
options.enable_user_defined_docstrings();
|
|
|
|
m.doc() = R"pbdoc(
|
|
HUB75 matrix driver for Raspberry Pi 5 using PIO
|
|
------------------------------------------------
|
|
|
|
.. currentmodule:: adafruit_blinka_raspberry_pi5_piomatter._piomatter
|
|
)pbdoc";
|
|
|
|
py::enum_<piomatter::orientation>(
|
|
m, "Orientation", "Describe the orientation of a set of panels")
|
|
.value("Normal", piomatter::orientation::normal, "Normal orientation")
|
|
.value("R180", piomatter::orientation::r180, "Rotated 180 degrees")
|
|
.value("CCW", piomatter::orientation::ccw,
|
|
"Rotated 90 degress counterclockwise")
|
|
.value("CW", piomatter::orientation::cw,
|
|
"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::AdafruitMatrixBonnetBGR,
|
|
"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>(
|
|
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(
|
|
Describe the geometry of a set of panels
|
|
|
|
``width`` and ``height`` give the panel resolution in pixels.
|
|
|
|
``n_addr_lines`` gives the number of connected address lines.
|
|
|
|
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.
|
|
0 or 1 behave the same: All `n_planes` are transmitted every time.
|
|
A higher value sets the number of planes that are only transmitted every `1/n_temporal_frames` times.
|
|
A higher setting can increase FPS at the cost of slightly increasing the variation of brightness across subsequent frames.
|
|
Settings above 4 are allowed, but generally do not give good results.
|
|
|
|
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``.
|
|
|
|
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_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;
|
|
if (odd) {
|
|
throw std::runtime_error(
|
|
py::str(
|
|
"Total pixel count {} must be a multiple of {}, "
|
|
"the number of distinct row addresses for {}")
|
|
.attr("format")(width * height, n_lines,
|
|
n_addr_lines)
|
|
.cast<std::string>());
|
|
}
|
|
switch (rotation) {
|
|
case piomatter::orientation::normal:
|
|
return piomatter::matrix_geometry(
|
|
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,
|
|
n_temporal_planes, width, height, serpentine,
|
|
piomatter::orientation_r180);
|
|
|
|
case piomatter::orientation::ccw:
|
|
return piomatter::matrix_geometry(
|
|
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,
|
|
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_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);
|
|
|
|
py::class_<PyPiomatter>(m, "PioMatter", R"pbdoc(
|
|
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
|
|
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.
|
|
|
|
``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`
|
|
instance.
|
|
)pbdoc")
|
|
.def(py::init(&make_piomatter), py::arg("colorspace"),
|
|
py::arg("pinout"), py::arg("framebuffer"), py::arg("geometry"))
|
|
.def("show", &PyPiomatter::show, R"pbdoc(
|
|
Update the displayed image
|
|
|
|
After modifying the content of the framebuffer, call this method to
|
|
update the data actually displayed on the panel. Internally, the
|
|
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")
|
|
.def_property_readonly("width", &PyPiomatter::width)
|
|
.def_property_readonly("height", &PyPiomatter::height);
|
|
}
|