Add Pi_Matrix_Cube initial files

This commit is contained in:
Phillip Burgess 2022-05-02 12:54:02 -07:00
parent 5587b67a40
commit 9510829735
12 changed files with 2118 additions and 0 deletions

21
Pi_Matrix_Cube/Makefile Normal file
View file

@ -0,0 +1,21 @@
RGB_LIB_DISTRIBUTION=../rpi-rgb-led-matrix
RGB_INCDIR=$(RGB_LIB_DISTRIBUTION)/include
RGB_LIBDIR=$(RGB_LIB_DISTRIBUTION)/lib
RGB_LIBRARY_NAME=rgbmatrix
CXXFLAGS=-Wall -Ofast -fomit-frame-pointer -I$(RGB_INCDIR)
LDFLAGS+=-L$(RGB_LIBDIR) -l$(RGB_LIBRARY_NAME) -lrt -lm -lpthread
EXECS = globe life
all: $(EXECS)
globe: globe.cc
$(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS) -ljpeg
strip $@
life: life.cc
$(CXX) $(CXXFLAGS) $< -o $@ $(LDFLAGS)
strip $@
clean:
rm -f $(EXECS)

28
Pi_Matrix_Cube/cycle.sh Normal file
View file

@ -0,0 +1,28 @@
#!/bin/sh
# Fades/cycles the globe program through different scenes
# Show all files in the maps directory in alphabetical order
FILES=maps/*.jpg
# If you'd prefer a subset of files, different order or location,
# you can instead make a list of filenames, e.g.:
# FILES="
# maps/earth.jpg
# maps/moon.jpg
# maps/jupiter.jpg
# "
# Flags passed to globe program each time.
# --led-pwm-bits=9 because long chain is otherwise flickery (default is 11)
# -a 3 for 3x3 antialiasing (use 2 or 1 for slower Pi)
# -r 6 is run time in seconds before exiting
# -f 1 fades in/out for 1 second at either end
# Can add "-p" to this list if you want poles at cube vertices,
# or --led-rgb-sequence="BRG" with certain RGB matrices, etc.
set -- --led-pwm-bits=9 -a 3 -r 6 -f 1
while true; do
for f in $FILES; do
./globe $@ -i $f
done
done

418
Pi_Matrix_Cube/globe.cc Normal file
View file

@ -0,0 +1,418 @@
// SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*
IF GLOBES WERE SQUARE: a revolving "globe" for 6X square RGB LED matrices on
Raspberry Pi w/Adafruit Matrix Bonnet or HAT.
usage: sudo ./globe [options]
(You may or may not need the 'sudo' depending how the rpi-rgb-matrix
library is configured)
Options include all of the rpi-rgb-matrix flags, such as --led-pwm-bits=N
or --led-gpio-slowdown=N, and then the following:
-i <filename> : Image filename for texture map. MUST be JPEG image format.
Default is maps/earth.jpg
-v : Orient cube with vertices at top & bottom, rather than
flat faces on top & bottom. No accompanying value.
-s <float> : Spin time in seconds (per revolution). Positive values
will revolve in the correct direction for the Earth map.
Negative values spin the opposite direction (magnitude
specifies seconds), maybe useful for text, logos or Uranus.
-a <int> : Antialiasing samples, per-axis. Range 1-8. Default is 2,
for 2x2 supersampling. Fast hardware can go higher, slow
devices should use 1.
-t <float> : Run time in seconds. Program will exit after this.
Default is to run indefinitely, until crtl+C received.
-f <float> : Fade in/out time in seconds. Used in combination with the
-t option, this provides a nice fade-in, revolve for a
while, fade-out and exit. Combined with a simple shell
script, it provides a classy way to cycle among different
planetoids/scenes/etc. without having to explicitly
implement such a feature here.
-e <float> : Edge-to-edge physical measure of LED matrix. Combined
with -E below, provides spatial compensation for edge
bezels when matrices are arranged in a cube (i.e. pixels
don't "jump" across the seam -- has a bit of dead space).
-E <float> : Edge-to-edge measure of opposite faces of assembled cube,
used in combination with -e above. This will be a little
larger than the -e value (lower/upper case is to emphasize
this relationship). Units for both are arbitrary; use
millimeters, inches, whatever, it's the ratio that's
important.
rpi-rgb-matrix has the following single-character abbreviations for
some configurables: -b (--led-brightness), -c (--led-chain),
-m (--led-gpio-mapping), -p (--led-pwm-bits), -P (--led-parallel),
-r (--led-rows). AVOID THESE in any future configurables added to this
program, as some users may have "muscle memory" for those options.
This code depends on libjpeg and rpi-rgb-matrix libraries. While this
.cc file has a permissive MIT licence, those libraries may (or not) have
restrictions on commercial use, distributing precompiled binaries, etc.
Check their license terms if this is relevant to your situation.
*/
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <getopt.h>
#include <jpeglib.h>
#include <led-matrix.h>
using namespace rgb_matrix;
// GLOBAL VARIABLES --------------------------------------------------------
// Some constants first...
const char default_filename[] = "maps/earth.jpg";
/* Cube coordinates use a right-handed coordinate system;
+X is to right, +Y is up, +Z is toward your face
Default is a flat/square/axis-aligned cube.
Poles are at center of top & bottom faces.
Numbers here are vertex indices (0-7):
1-------------2
/| +Y /|
/ | ^ / |
/ | | / |
0-------------3 |
| | | | |
| | 0----|->+X
| | / | |
| 5--/------|---6
| / L | /
| / +Z | /
|/ |/
4-------------7
*/
const float square_coords[8][3] = {{-1, 1, 1}, {-1, 1, -1}, {1, 1, -1},
{1, 1, 1}, {-1, -1, 1}, {-1, -1, -1},
{1, -1, -1}, {1, -1, 1}};
const uint8_t verts[6][3] = {
{0, 1, 3}, // Vertex indices for UL, UR, LL of top face matrix
{0, 4, 1}, // " left
{0, 3, 4}, // " front face
{7, 3, 6}, // " right
{2, 1, 6}, // " back
{5, 4, 6}, // " bottom matrix
};
// Alternate coordinates for a rotated cube with points at poles.
// Vertex indices are the same (does not need a new verts[] array),
// relationships are the same, the whole thing is just pivoted to
// "hang" from vertex 3 at top. I will NOT attempt ASCII art of this.
const float xx = sqrt(26.0 / 9.0);
const float yy = sqrt(3.0) / 3.0;
const float cc = -0.5; // cos(120.0 * M_PI / 180.0);
const float ss = sqrt(0.75); // sin(120.0 * M_PI / 180.0);
const float pointy_coords[8][3] = {
{-xx, yy, 0.0}, // Vertex 0 = leftmost point
{xx * cc, -yy, -xx *ss}, // 1
{-xx * cc, yy, -xx *ss}, // 2
{0.0, sqrt(3.0), 0.0}, // 3 = top
{xx * cc, -yy, xx *ss}, // 4
{0.0, -sqrt(3.0), 0.0}, // 5 = bottom
{xx, -yy, 0.0}, // 6 = rightmost point
{-xx * cc, yy, xx *ss} // 7
};
// These globals have defaults but are runtime configurable:
uint8_t aa = 2; // Antialiasing samples, per axis
uint16_t matrix_size = 64; // Matrix X&Y pixel count (must be square)
bool pointy = false; // Cube orientation has a vertex at top
float matrix_measure = 1.0; // Edge-to-edge dimension of LED matrix
float cube_measure = 1.0; // Face-to-face dimension of assembled cube
float spin_time = 10.0; // Seconds per rotation
char *map_filename = (char *)default_filename; // Planet image file
float run_time = -1.0; // Time before exit (negative = run forever)
float fade_time = 0.0; // Fade in/out time (if run_time is set)
float max_brightness = 255.0; // Fade up to, down from this value
// These globals are computed or allocated at runtime after taking input:
uint16_t samples_per_pixel; // Total antialiasing samples per pixel
int map_width; // Map image width in pixels
int map_height; // Map image height in pixels
uint8_t *map_data; // Map image pixel data in RAM
float *longitude; // Table of longitude values
uint16_t *latitude; // Table of latitude values
// INTERRUPT HANDLER (to allow clearing matrix on exit) --------------------
volatile bool interrupt_received = false;
static void InterruptHandler(int signo) { interrupt_received = true; }
// IMAGE LOADING -----------------------------------------------------------
// Barebones JPEG reader; no verbose error handling, etc.
int read_jpeg(const char *filename) {
FILE *file;
if ((file = fopen(filename, "rb"))) {
struct jpeg_decompress_struct info;
struct jpeg_error_mgr err;
info.err = jpeg_std_error(&err);
jpeg_create_decompress(&info);
jpeg_stdio_src(&info, file);
jpeg_read_header(&info, TRUE);
jpeg_start_decompress(&info);
map_width = info.image_width;
map_height = info.image_height;
if ((map_data = (uint8_t *)malloc(map_width * map_height * 3))) {
unsigned char *rowptr[1] = {map_data};
while (info.output_scanline < info.output_height) {
(void)jpeg_read_scanlines(&info, rowptr, 1);
if (info.output_components == 1) { // Convert gray to RGB if needed
for (int x = map_width - 1; x >= 0; x--) {
rowptr[0][x * 3] = rowptr[0][x * 3 + 1] = rowptr[0][x * 3 + 2] =
rowptr[0][x];
}
}
rowptr[0] += map_width * 3;
}
}
jpeg_finish_decompress(&info);
jpeg_destroy_decompress(&info);
fclose(file);
return (map_data != NULL);
}
return 0;
}
// COMMAND-LINE HELP -------------------------------------------------------
static int usage(const char *progname) {
fprintf(stderr, "usage: %s [options]\n", progname);
fprintf(stderr, "Options:\n");
rgb_matrix::PrintMatrixFlags(stderr);
fprintf(stderr, "\t-i <filename> : Image filename for texture map\n");
fprintf(stderr, "\t-v : Orient cube with vertex at top\n");
fprintf(stderr, "\t-s <float> : Spin time in seconds (per revolution)\n");
fprintf(stderr, "\t-a <int> : Antialiasing samples, per-axis\n");
fprintf(stderr, "\t-t <float> : Run time in seconds\n");
fprintf(stderr, "\t-f <float> : Fade in/out time in seconds\n");
fprintf(stderr, "\t-e <float> : Edge-to-edge measure of LED matrix\n");
fprintf(stderr, "\t-E <float> : Edge-to-edge measure of assembled cube\n");
return 1;
}
// MAIN CODE ---------------------------------------------------------------
int main(int argc, char *argv[]) {
RGBMatrix *matrix;
FrameCanvas *canvas;
// INITIALIZE DEFAULTS and PROCESS COMMAND-LINE INPUT --------------------
RGBMatrix::Options matrix_options;
rgb_matrix::RuntimeOptions runtime_opt;
matrix_options.cols = matrix_options.rows = matrix_size;
matrix_options.chain_length = 6;
runtime_opt.gpio_slowdown = 4; // For Pi 4 w/6 matrices
// Parse matrix-related command line options first
if (!ParseOptionsFromFlags(&argc, &argv, &matrix_options, &runtime_opt)) {
return usage(argv[0]);
}
// Validate inputs for cube-like behavior
if (matrix_options.cols != matrix_options.rows) {
fprintf(stderr, "%s: matrix columns, rows must be equal (square matrix)\n",
argv[0]);
return 1;
}
if (matrix_options.chain_length * matrix_options.parallel != 6) {
fprintf(stderr, "%s: total chained/parallel matrices must equal 6\n",
argv[0]);
return 1;
}
max_brightness = (float)matrix_options.brightness * 2.55; // 0-100 -> 0-255
// Then parse any lingering program options (filename, etc.)
int opt;
while ((opt = getopt(argc, argv, "i:vs:a:t:f:e:E:")) != -1) {
switch (opt) {
case 'i':
map_filename = strdup(optarg);
break;
case 'v':
pointy = true;
break;
case 's':
spin_time = strtof(optarg, NULL);
break;
case 'a':
aa = abs(atoi(optarg));
if (aa < 1)
aa = 1;
else if (aa > 8)
aa = 8;
break;
case 't':
run_time = fabs(strtof(optarg, NULL));
break;
case 'f':
fade_time = fabs(strtof(optarg, NULL));
break;
case 'e':
matrix_measure = strtof(optarg, NULL);
break;
case 'E':
cube_measure = strtof(optarg, NULL);
break;
default: /* '?' */
return usage(argv[0]);
}
}
// LOAD and ALLOCATE DATA STRUCTURES -------------------------------------
// Load map image; initializes globals map_width, map_height, map_data:
if (!read_jpeg(map_filename)) {
fprintf(stderr, "%s: error loading image '%s'\n", argv[0], map_filename);
return 1;
}
// Allocate huge arrays for longitude & latitude values
matrix_size = matrix_options.rows;
samples_per_pixel = aa * aa;
int num_elements = 6 * matrix_size * matrix_size * samples_per_pixel;
if (!(longitude = (float *)malloc(num_elements * sizeof(float))) ||
!(latitude = (uint16_t *)malloc(num_elements * sizeof(uint16_t)))) {
fprintf(stderr, "%s: can't allocate space for lat/long tables\n", argv[0]);
return 1;
}
// PRECOMPUTE LONGITUDE, LATITUDE TABLES ---------------------------------
float *coords = (float *)(pointy ? pointy_coords : square_coords);
// Longitude & latitude tables have one entry for each pixel (or subpixel,
// if supersampling) on each face of the cube. e.g. 64x64x2x2x6 pairs of
// values when using 64x64 matrices and 2x2 supersampling. Although some
// of the interim values through here could be computed and stored (e.g.
// corner-to-corner distances, matrix_size * aa, etc.), it's the 21st
// century and optimizing compilers are really dang good at this now, so
// let it do its job and keep the code relatively short.
int i = 0; // Index into longitude[] and latitude[] arrays
float mr = matrix_measure / cube_measure; // Scale ratio
float mo = ((1.0 - mr) + mr / (matrix_size * aa)) * 0.5; // Axis offset
for (uint8_t face = 0; face < 6; face++) {
float *ul = &coords[verts[face][0] * 3]; // 3D coordinates of matrix's
float *ur = &coords[verts[face][1] * 3]; // upper-left, upper-right
float *ll = &coords[verts[face][2] * 3]; // and lower-left corners.
for (int py = 0; py < matrix_size; py++) { // For each pixel Y...
for (int px = 0; px < matrix_size; px++) { // For each pixel X...
for (uint8_t ay = 0; ay < aa; ay++) { // " antialiased sample Y...
float yfactor =
mo + mr * (float)(py * aa + ay) / (float)(matrix_size * aa);
for (uint8_t ax = 0; ax < aa; ax++) { // " antialiased sample X...
float xfactor =
mo + mr * (float)(px * aa + ax) / (float)(matrix_size * aa);
float x, y, z;
// Figure out the pixel's 3D position in space...
x = ul[0] + (ll[0] - ul[0]) * yfactor + (ur[0] - ul[0]) * xfactor;
y = ul[1] + (ll[1] - ul[1]) * yfactor + (ur[1] - ul[1]) * xfactor;
z = ul[2] + (ll[2] - ul[2]) * yfactor + (ur[2] - ul[2]) * xfactor;
// Then use trigonometry to convert to polar coords on a sphere...
longitude[i] =
fmod((M_PI + atan2(-z, x)) / (M_PI * 2.0) * (float)map_width,
(float)map_width);
latitude[i] = (int)((M_PI * 0.5 - atan2(y, sqrt(x * x + z * z))) /
M_PI * (float)map_height);
i++;
}
}
}
}
}
// INITIALIZE RGB MATRIX CHAIN and OFFSCREEN CANVAS ----------------------
if (!(matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt))) {
fprintf(stderr, "%s: couldn't create matrix object\n", argv[0]);
return 1;
}
if (!(canvas = matrix->CreateFrameCanvas())) {
fprintf(stderr, "%s: couldn't create canvas object\n", argv[0]);
return 1;
}
// OTHER MINOR INITIALIZATION --------------------------------------------
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
struct timeval startTime, now;
gettimeofday(&startTime, NULL); // Program start time
// LOOP RUNS INDEFINITELY OR UNTIL CTRL+C or run_time ELAPSED ------------
uint32_t frames = 0;
int prevsec = -1;
while (!interrupt_received) {
gettimeofday(&now, NULL);
double elapsed = ((now.tv_sec - startTime.tv_sec) +
(now.tv_usec - startTime.tv_usec) / 1000000.0);
if (run_time > 0.0) { // Handle time limit and fade in/out if needed...
if (elapsed >= run_time)
break;
if (elapsed < fade_time) {
matrix->SetBrightness((int)(max_brightness * elapsed / fade_time));
} else if (elapsed > (run_time - fade_time)) {
matrix->SetBrightness(
(int)(max_brightness * (run_time - elapsed) / fade_time));
} else {
matrix->SetBrightness(max_brightness);
}
}
float loffset =
fmod(elapsed, fabs(spin_time)) / fabs(spin_time) * (float)map_width;
if (spin_time > 0)
loffset = map_width - loffset;
i = 0; // Index into longitude[] and latitude[] arrays
for (uint8_t face = 0; face < 6; face++) {
int xoffset = (face % matrix_options.chain_length) * matrix_size;
int yoffset = (face / matrix_options.chain_length) * matrix_size;
for (int py = 0; py < matrix_size; py++) {
for (int px = 0; px < matrix_size; px++) {
uint16_t r = 0, g = 0, b = 0;
for (uint16_t s = 0; s < samples_per_pixel; s++) {
int sx = (int)(longitude[i] + loffset) % map_width;
int sy = latitude[i];
uint8_t *src = &map_data[(sy * map_width + sx) * 3];
r += src[0];
g += src[1];
b += src[2];
i++;
}
canvas->SetPixel(xoffset + px, yoffset + py, r / samples_per_pixel,
g / samples_per_pixel, b / samples_per_pixel);
}
}
}
canvas = matrix->SwapOnVSync(canvas);
frames++;
if (now.tv_sec != prevsec) {
if (prevsec >= 0) {
printf("%f\n", frames / elapsed);
}
prevsec = now.tv_sec;
}
}
canvas->Clear();
delete matrix;
return 0;
}

555
Pi_Matrix_Cube/globe.py Normal file
View file

@ -0,0 +1,555 @@
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
IF GLOBES WERE SQUARE: a revolving "globe" for 6X square RGB LED matrices on
Raspberry Pi w/Adafruit Matrix Bonnet or HAT.
usage: sudo ./globe [options]
usage: sudo python globe.py [options]
(You may or may not need the 'sudo' depending how the rpi-rgb-matrix
library is configured)
Options include all of the rpi-rgb-matrix flags, such as --led-pwm-bits=N
or --led-gpio-slowdown=N, and then the following:
-i <filename> : Image filename for texture map. MUST be JPEG image format.
Default is maps/earth.jpg
-v : Orient cube with vertices at top & bottom, rather than
flat faces on top & bottom. No accompanying value.
-s <float> : Spin time in seconds (per revolution). Positive values
will revolve in the correct direction for the Earth map.
Negative values spin the opposite direction (magnitude
specifies seconds), maybe useful for text, logos or Uranus.
-a <int> : Antialiasing samples, per-axis. Range 1-8. Default is 1,
no supersampling. Fast hardware can sometimes go higher,
most should stick with 1.
-t <float> : Run time in seconds. Program will exit after this.
Default is to run indefinitely, until crtl+C received.
-f <float> : Fade in/out time in seconds. Used in combination with the
-t option, this provides a nice fade-in, revolve for a
while, fade-out and exit. Combined with a simple shell
script, it provides a classy way to cycle among different
planetoids/scenes/etc. without having to explicitly
implement such a feature here.
-e <float> : Edge-to-edge physical measure of LED matrix. Combined
with -E below, provides spatial compensation for edge
bezels when matrices are arranged in a cube (i.e. pixels
don't "jump" across the seam -- has a bit of dead space).
-E <float> : Edge-to-edge measure of opposite faces of assembled cube,
used in combination with -e above. This will be a little
larger than the -e value (lower/upper case is to emphasize
this relationship). Units for both are arbitrary; use
millimeters, inches, whatever, it's the ratio that's
important.
-rgb-matrix has the following single-character abbreviations for
some configurables: -b (--led-brightness), -c (--led-chain),
-m (--led-gpio-mapping), -p (--led-pwm-bits), -P (--led-parallel),
-r (--led-rows). AVOID THESE in any future configurables added to this
program, as some users may have "muscle memory" for those options.
This is not great learning-from code, being a fairly direct mash-up of
code taken from life.py and adapted from globe.cc. It's mostly here to
fulfill a need to offer these demos in both C++ and Python versions.
The C++ code is a little better commented.
This code depends on the rpi-rgb-matrix library. While this .py file has
a permissive MIT licence, libraries may (or not) have restrictions on
commercial use, distributing precompiled binaries, etc. Check their
license terms if this is relevant to your situation.
"""
import argparse
import math
import os
import sys
import time
from rgbmatrix import RGBMatrix, RGBMatrixOptions
from PIL import Image
VERTS = (
(0, 1, 3), # Vertex indices for UL, UR, LL of top face matrix
(0, 4, 1), # " left
(0, 3, 4), # " front face
(7, 3, 6), # " right
(2, 1, 6), # " back
(5, 4, 6), # " bottom matrix
)
SQUARE_COORDS = (
(-1, 1, 1),
(-1, 1, -1),
(1, 1, -1),
(1, 1, 1),
(-1, -1, 1),
(-1, -1, -1),
(1, -1, -1),
(1, -1, 1),
)
# Alternate coordinates for a rotated cube with points at poles.
# Vertex indices are the same (does not need a new VERTS array),
# relationships are the same, the whole thing is just pivoted to
# "hang" from vertex 3 at top. I will NOT attempt ASCII art of this.
XX = (26.0 / 9.0) ** 0.5
YY = (3.0 ** 0.5) / 3.0
CC = -0.5 # cos(120.0 * M_PI / 180.0);
SS = 0.75 ** 0.5 # sin(120.0 * M_PI / 180.0);
POINTY_COORDS = (
(-XX, YY, 0.0), # Vertex 0 = leftmost point
(XX * CC, -YY, -XX * SS), # 1
(-XX * CC, YY, -XX * SS), # 2
(0.0, 3.0 ** 0.5, 0.0), # 3 = top
(XX * CC, -YY, XX * SS), # 4
(0.0, -(3.0 ** 0.5), 0.0), # 5 = bottom
(XX, -YY, 0.0), # 6 = rightmost point
(-XX * CC, YY, XX * SS), # 7
)
class Globe:
"""
Revolving globe on a cube.
"""
# pylint: disable=too-many-instance-attributes, too-many-locals
def __init__(self):
self.matrix = None # RGB matrix object (initialized after inputs)
self.canvas = None # Offscreen canvas (after inputs)
self.matrix_size = 0 # Matrix width/height in pixels (after inputs)
self.run_time = -1.0 # If >0 (input can override), limit run time
self.fade_time = 0.0 # Fade in/out time (input can override)
self.max_brightness = 255 # Matrix brightness (input can override)
self.samples_per_pixel = 1 # Total antialiasing samples per pixel
self.map_width = 0 # Map image width in pixels
self.map_data = None # Map image pixel data in RAM
self.longitude = None # Table of longitude values
self.latitude = None # Table of latitude values
self.imgbuf = None # Image is rendered to this RGB buffer
self.spin_time = 10.0
self.chain_length = 6
# pylint: disable=too-many-branches, too-many-statements
def setup(self):
""" Returns False on success, True on error """
parser = argparse.ArgumentParser()
# RGB matrix standards
parser.add_argument(
"-r",
"--led-rows",
action="store",
help="Display rows. 32 for 32x32, 64 for 64x64. Default: 64",
default=64,
type=int,
)
parser.add_argument(
"--led-cols",
action="store",
help="Panel columns. Typically 32 or 64. (Default: 64)",
default=64,
type=int,
)
parser.add_argument(
"-c",
"--led-chain",
action="store",
help="Daisy-chained boards. Default: 6.",
default=6,
type=int,
)
parser.add_argument(
"-P",
"--led-parallel",
action="store",
help="For Plus-models or RPi2: parallel chains. 1..3. Default: 1",
default=1,
type=int,
)
parser.add_argument(
"-p",
"--led-pwm-bits",
action="store",
help="Bits used for PWM. Something between 1..11. Default: 11",
default=11,
type=int,
)
parser.add_argument(
"-b",
"--led-brightness",
action="store",
help="Sets brightness level. Default: 100. Range: 1..100",
default=100,
type=int,
)
parser.add_argument(
"-m",
"--led-gpio-mapping",
help="Hardware Mapping: regular, adafruit-hat, adafruit-hat-pwm",
choices=["regular", "regular-pi1", "adafruit-hat", "adafruit-hat-pwm"],
type=str,
)
parser.add_argument(
"--led-scan-mode",
action="store",
help="Progressive or interlaced scan. 0 Progressive, 1 Interlaced (default)",
default=1,
choices=range(2),
type=int,
)
parser.add_argument(
"--led-pwm-lsb-nanoseconds",
action="store",
help="Base time-unit for the on-time in the lowest "
"significant bit in nanoseconds. Default: 130",
default=130,
type=int,
)
parser.add_argument(
"--led-show-refresh",
action="store_true",
help="Shows the current refresh rate of the LED panel",
)
parser.add_argument(
"--led-slowdown-gpio",
action="store",
help="Slow down writing to GPIO. Range: 0..4. Default: 3",
default=4, # For Pi 4 w/6 matrices
type=int,
)
parser.add_argument(
"--led-no-hardware-pulse",
action="store",
help="Don't use hardware pin-pulse generation",
)
parser.add_argument(
"--led-rgb-sequence",
action="store",
help="Switch if your matrix has led colors swapped. Default: RGB",
default="RGB",
type=str,
)
parser.add_argument(
"--led-pixel-mapper",
action="store",
help='Apply pixel mappers. e.g "Rotate:90"',
default="",
type=str,
)
parser.add_argument(
"--led-row-addr-type",
action="store",
help="0 = default; 1=AB-addressed panels; 2=row direct; "
"3=ABC-addressed panels; 4 = ABC Shift + DE direct",
default=0,
type=int,
choices=[0, 1, 2, 3, 4],
)
parser.add_argument(
"--led-multiplexing",
action="store",
help="Multiplexing type: 0=direct; 1=strip; 2=checker; 3=spiral; "
"4=ZStripe; 5=ZnMirrorZStripe; 6=coreman; 7=Kaler2Scan; "
"8=ZStripeUneven... (Default: 0)",
default=0,
type=int,
)
parser.add_argument(
"--led-panel-type",
action="store",
help="Needed to initialize special panels. Supported: 'FM6126A'",
default="",
type=str,
)
parser.add_argument(
"--led-no-drop-privs",
dest="drop_privileges",
help="Don't drop privileges from 'root' after initializing the hardware.",
action="store_false",
)
# Extra args unique to this program
parser.add_argument(
"-i",
action="store",
help="Image filename for texture map. Default: maps/earth.jpg",
default="maps/earth.jpg",
type=str,
)
parser.add_argument(
"-v",
dest="pointy",
help="Orient cube with vertices at top & bottom.",
action="store_true",
)
parser.add_argument(
"-s",
action="store",
help="Spin time in seconds/revolution. Default: 10.0",
default=10.0,
type=float,
)
parser.add_argument(
"-a",
action="store",
help="Antialiasing samples/axis. Default: 1",
default=1,
type=int,
)
parser.add_argument(
"-t",
action="store",
help="Run time in seconds. Default: run indefinitely",
default=-1.0,
type=float,
)
parser.add_argument(
"-f",
action="store",
help="Fade in/out time in seconds. Default: 0.0",
default=0.0,
type=float,
)
parser.add_argument(
"-e",
action="store",
help="Edge-to-edge measure of matrix.",
default=1.0,
type=float,
)
parser.add_argument(
"-E",
action="store",
help="Edge-to-edge measure of opposite cube faces.",
default=1.0,
type=float,
)
parser.set_defaults(drop_privileges=True)
parser.set_defaults(pointy=False)
args = parser.parse_args()
if args.led_rows != args.led_cols:
print(
os.path.basename(__file__) + ": error: led rows and columns must match"
)
return True
if args.led_chain * args.led_parallel != 6:
print(
os.path.basename(__file__)
+ ": error: total chained * parallel matrices must equal 6"
)
return True
options = RGBMatrixOptions()
if args.led_gpio_mapping is not None:
options.hardware_mapping = args.led_gpio_mapping
options.rows = args.led_rows
options.cols = args.led_cols
options.chain_length = args.led_chain
options.parallel = args.led_parallel
options.row_address_type = args.led_row_addr_type
options.multiplexing = args.led_multiplexing
options.pwm_bits = args.led_pwm_bits
options.brightness = args.led_brightness
options.pwm_lsb_nanoseconds = args.led_pwm_lsb_nanoseconds
options.led_rgb_sequence = args.led_rgb_sequence
options.pixel_mapper_config = args.led_pixel_mapper
options.panel_type = args.led_panel_type
if args.led_show_refresh:
options.show_refresh_rate = 1
if args.led_slowdown_gpio is not None:
options.gpio_slowdown = args.led_slowdown_gpio
if args.led_no_hardware_pulse:
options.disable_hardware_pulsing = True
if not args.drop_privileges:
options.drop_privileges = False
self.matrix = RGBMatrix(options=options)
self.canvas = self.matrix.CreateFrameCanvas()
self.matrix_size = args.led_rows
self.chain_length = args.led_chain
self.max_brightness = args.led_brightness
self.run_time = args.t
self.fade_time = args.f
self.samples_per_pixel = args.a * args.a
matrix_measure = args.e
cube_measure = args.E
self.spin_time = args.s
try:
image = Image.open(args.i)
except FileNotFoundError:
print(
os.path.basename(__file__)
+ ": error: image file "
+ args.i
+ " not found"
)
return True
self.map_width = image.size[0]
map_height = image.size[1]
self.map_data = image.tobytes()
# Longitude and latitude tables are 1-dimensional,
# can do that because we iterate every pixel every frame.
pixels = self.matrix.width * self.matrix.height
subpixels = pixels * self.samples_per_pixel
self.longitude = [0.0 for _ in range(subpixels)]
self.latitude = [0 for _ in range(subpixels)]
# imgbuf holds result for one face of cube
self.imgbuf = bytearray(self.matrix_size * self.matrix_size * 3)
coords = POINTY_COORDS if args.pointy else SQUARE_COORDS
# Fill the longitude & latitude tables, one per subpixel.
ll_index = 0 # Index into longitude[] and latitude[] arrays
ratio = matrix_measure / cube_measure # Scale ratio
offset = ((1.0 - ratio) + ratio / (self.matrix_size * args.a)) * 0.5
# Axis offset
for face in range(6):
upper_left = coords[VERTS[face][0]]
upper_right = coords[VERTS[face][1]]
lower_left = coords[VERTS[face][2]]
for ypix in range(self.matrix_size): # For each pixel Y...
for xpix in range(self.matrix_size): # For each pixel X...
for yaa in range(args.a): # " antialiased sample Y...
yfactor = offset + ratio * (ypix * args.a + yaa) / (
self.matrix_size * args.a
)
for xaa in range(args.a): # " antialiased sample X...
xfactor = offset + ratio * (xpix * args.a + xaa) / (
self.matrix_size * args.a
)
# Figure out the pixel's 3D position in space...
x3d = (
upper_left[0]
+ (lower_left[0] - upper_left[0]) * yfactor
+ (upper_right[0] - upper_left[0]) * xfactor
)
y3d = (
upper_left[1]
+ (lower_left[1] - upper_left[1]) * yfactor
+ (upper_right[1] - upper_left[1]) * xfactor
)
z3d = (
upper_left[2]
+ (lower_left[2] - upper_left[2]) * yfactor
+ (upper_right[2] - upper_left[2]) * xfactor
)
# Then convert to polar coords on a sphere...
self.longitude[ll_index] = (
(math.pi + math.atan2(-z3d, x3d))
/ (math.pi * 2.0)
* self.map_width
) % self.map_width
self.latitude[ll_index] = int(
(
math.pi * 0.5
- math.atan2(y3d, math.sqrt(x3d * x3d + z3d * z3d))
)
/ math.pi
* map_height
)
ll_index += 1
return False
def run(self):
"""Main loop."""
start_time, frames = time.monotonic(), 0
while True:
elapsed = time.monotonic() - start_time
if self.run_time > 0: # Handle fade in / fade out
if elapsed >= self.run_time:
break
if elapsed < self.fade_time:
self.matrix.brightness = int(
self.max_brightness * elapsed / self.fade_time
)
elif elapsed > (self.run_time - self.fade_time):
self.matrix.brightness = int(
self.max_brightness * (self.run_time - elapsed) / self.fade_time
)
else:
self.matrix.brightness = self.max_brightness
loffset = (
(elapsed % abs(self.spin_time)) / abs(self.spin_time) * self.map_width
)
if self.spin_time > 0:
loffset = self.map_width - loffset
self.render(loffset)
# Swap double-buffered canvas, show frames per second
self.canvas = self.matrix.SwapOnVSync(self.canvas)
frames += 1
print(frames / (time.monotonic() - start_time))
# pylint: disable=too-many-locals
def render(self, loffset):
"""Render one frame of the globe animation, taking latitude offset
as input."""
# Certain instance variables (ones referenced in inner loop) are
# copied to locals to speed up access. This is kind of a jerk thing
# to do and not "Pythonic," but anything for a boost in this code.
imgbuf = self.imgbuf
map_data = self.map_data
lon = self.longitude
lat = self.latitude
samples = self.samples_per_pixel
map_width = self.map_width
ll_index = 0 # Index into longitude/latitude tables
for face in range(6):
img_index = 0 # Index into imgbuf[]
for _ in range(self.matrix_size * self.matrix_size):
red = green = blue = 0
for _ in range(samples):
map_index = (
lat[ll_index] * map_width
+ (int(lon[ll_index] + loffset) % map_width)
) * 3
red += map_data[map_index]
green += map_data[map_index + 1]
blue += map_data[map_index + 2]
ll_index += 1
imgbuf[img_index] = red // samples
imgbuf[img_index + 1] = green // samples
imgbuf[img_index + 2] = blue // samples
img_index += 3
image = Image.frombuffer(
"RGB",
(self.matrix_size, self.matrix_size),
bytes(imgbuf),
"raw",
"RGB",
0,
1,
)
# Upper-left corner of face in canvas space:
xoffset = (face % self.chain_length) * self.matrix_size
yoffset = (face // self.chain_length) * self.matrix_size
self.canvas.SetImage(image, offset_x=xoffset, offset_y=yoffset)
# pylint: disable=superfluous-parens
if __name__ == "__main__":
globe = Globe()
if not (status := globe.setup()):
try:
print("Press CTRL-C to stop")
globe.run()
except KeyboardInterrupt:
print("Exiting\n")
sys.exit(status)

View file

@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Very minimal image viewer for 6X square RGB LED matrices.
usage: sudo python imageviewer.py [filename]
"""
import time
import sys
from rgbmatrix import RGBMatrix, RGBMatrixOptions
from PIL import Image
if len(sys.argv) < 2:
sys.exit("Requires an image argument")
else:
image_file = sys.argv[1]
image = Image.open(image_file).convert("RGB")
# Hardcoded matrix config; commandline args ignored
options = RGBMatrixOptions()
options.rows = 64
options.cols = 64
options.chain_length = 6
options.parallel = 1
options.hardware_mapping = "adafruit-hat-pwm"
options.gpio_slowdown = 4
matrix = RGBMatrix(options=options)
# Scale image to fit 6X matrix chain
image.thumbnail((matrix.width, matrix.height), Image.ANTIALIAS)
matrix.SetImage(image)
try:
print("Press CTRL-C to stop.")
while True:
time.sleep(100)
except KeyboardInterrupt:
sys.exit(0)

446
Pi_Matrix_Cube/life.cc Normal file
View file

@ -0,0 +1,446 @@
// SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/*
Conway's Game of Life for 6X square RGB LED matrices.
Uses same physical matrix arrangement as "globe" program; see notes there.
usage: sudo ./life [options]
(You may or may not need the 'sudo' depending how the rpi-rgb-matrix
library is configured)
Options include all of the rpi-rgb-matrix flags, such as --led-pwm-bits=N
or --led-gpio-slowdown=N, and then the following:
-k <int> : Index of color palette to use. 0 = default black & white.
(Sorry, -c and -p are both rpi-rgb-matrix abbreviations.
Consider this a Monty Python Travel Agent Sketch nod.)
-t <float> : Run time in seconds. Program will exit after this.
Default is to run indefinitely, until crtl+C received.
-f <float> : Fade in/out time in seconds. Used in combination with the
-t option, this provides a nice fade-in, run for a while,
fade-out and exit.
rpi-rgb-matrix has the following single-character abbreviations for
some configurables: -b (--led-brightness), -c (--led-chain),
-m (--led-gpio-mapping), -p (--led-pwm-bits), -P (--led-parallel),
-r (--led-rows). AVOID THESE in any future configurables added to this
program, as some users may have "muscle memory" for those options.
This code depends on the rpi-rgb-matrix library. While this .cc file has
a permissive MIT licence, libraries may (or not) have restrictions on
commercial use, distributing precompiled binaries, etc. Check their
license terms if this is relevant to your situation.
*/
#include <getopt.h>
#include <led-matrix.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
using namespace rgb_matrix;
// GLOBAL VARIABLES --------------------------------------------------------
typedef enum {
EDGE_TOP = 0,
EDGE_LEFT,
EDGE_RIGHT,
EDGE_BOTTOM,
} Edge;
// Colormaps appear reversed from what one might expect. The first element
// of each is the 'on' pixel color, and each subsequent element is the color
// as a pixel 'ages,' up to the final 'background' color. Hence simple B&W
// on/off palette is white in index 0, black in index 1.
uint8_t map_bw[][3] = { // Simple B&W
{255, 255, 255},
{0, 0, 0}};
uint8_t map_gray[][3] = { // Log2 grayscale
{255, 255, 255}, // clang-format,
{127, 127, 127}, // I love you
{63, 63, 63}, // but
{31, 31, 31}, // why
{15, 15, 15}, // you
{7, 7, 7}, // gotta
{3, 3, 3}, // do
{1, 1, 1}, // this
{0, 0, 0}}; // to me?
uint8_t map_heat[][3] = {
// Heatmap (white-yellow-red-black)
{255, 255, 255}, // White
{255, 255, 127}, // Two steps to...
{255, 255, 0}, // Yellow
{255, 170, 0}, // Three steps...
{255, 85, 0}, // to...
{255, 0, 0}, // Red
{204, 0, 0}, // Four steps...
{153, 0, 0}, //
{102, 0, 0}, // to...
{51, 0, 0}, //
{0, 0, 0} // Black
};
uint8_t map_spec[][3] = {
// Color spectrum
{255, 255, 255}, // White (100%)
{127, 0, 0}, // Red (50%)
{127, 31, 0}, // to...
{127, 63, 0}, // Orange (50%)
{127, 95, 0}, // to...
{127, 127, 0}, // Yellow (etc)
{63, 127, 0}, // to...
{0, 127, 0}, // Green
{0, 127, 127}, // Cyan
{0, 0, 127}, // Blue
{63, 0, 127}, // to...
{127, 0, 127}, // Magenta
{82, 0, 82}, // fade...
{41, 0, 41}, // to...
{0, 0, 0} // Black
};
struct {
uint8_t *data;
uint8_t max;
} colormaps[] = {
{(uint8_t *)map_bw, sizeof(map_bw) / sizeof(map_bw[0]) - 1},
{(uint8_t *)map_gray, sizeof(map_gray) / sizeof(map_gray[0]) - 1},
{(uint8_t *)map_heat, sizeof(map_heat) / sizeof(map_heat[0]) - 1},
{(uint8_t *)map_spec, sizeof(map_spec) / sizeof(map_spec[0]) - 1},
};
#define NUM_PALETTES (sizeof(colormaps) / sizeof(colormaps[0]))
struct {
uint8_t face; // Index of face off this edge
Edge edge; // Which edge of face its entering that way
} face[6][4] = { // Order is top, left, right, bottom
{{1, EDGE_LEFT}, {2, EDGE_TOP}, {4, EDGE_TOP}, {3, EDGE_RIGHT}},
{{2, EDGE_LEFT}, {0, EDGE_TOP}, {5, EDGE_TOP}, {4, EDGE_RIGHT}},
{{0, EDGE_LEFT}, {1, EDGE_TOP}, {3, EDGE_TOP}, {5, EDGE_RIGHT}},
{{2, EDGE_RIGHT}, {5, EDGE_BOTTOM}, {0, EDGE_BOTTOM}, {4, EDGE_LEFT}},
{{0, EDGE_RIGHT}, {3, EDGE_BOTTOM}, {1, EDGE_BOTTOM}, {5, EDGE_LEFT}},
{{1, EDGE_RIGHT}, {4, EDGE_BOTTOM}, {2, EDGE_BOTTOM}, {3, EDGE_LEFT}}};
// These globals have defaults but are runtime configurable:
uint16_t matrix_size = 64; // Matrix X&Y pixel count (must be square)
uint16_t matrix_max = matrix_size - 1; // Matrix X&Y max coord
float run_time = -1.0; // Time before exit (negative = run forever)
float fade_time = 0.0; // Fade in/out time (if run_time is set)
float max_brightness = 255.0; // Fade up to, down from this value
// These globals are computed or allocated at runtime after taking input:
uint8_t *data[2]; // Cell arrays; current and next-in-progress
uint8_t idx = 0; // Which data[] array is current
uint8_t *colormap;
uint8_t colormap_max;
// INTERRUPT HANDLER (to allow clearing matrix on exit) --------------------
volatile bool interrupt_received = false;
static void InterruptHandler(int signo) { interrupt_received = true; }
// COMMAND-LINE HELP -------------------------------------------------------
static int usage(const char *progname) {
fprintf(stderr, "usage: %s [options]\n", progname);
fprintf(stderr, "Options:\n");
rgb_matrix::PrintMatrixFlags(stderr);
fprintf(stderr, "\t-k <int> : Color palette (0-%d)\n", NUM_PALETTES - 1);
fprintf(stderr, "\t-t <float> : Run time in seconds\n");
fprintf(stderr, "\t-f <float> : Fade in/out time in seconds\n");
return 1;
}
// SUNDRY UTILITY-LIKE FUNCTIONS -------------------------------------------
uint8_t cross(uint8_t f, Edge e, int16_t *x, int16_t *y) {
switch ((e << 2) | face[f][e].edge) {
case (EDGE_TOP << 2) | EDGE_TOP:
*x = matrix_max - *x;
*y = 0;
break;
case (EDGE_TOP << 2) | EDGE_LEFT:
*y = *x;
*x = 0;
break;
case (EDGE_TOP << 2) | EDGE_RIGHT:
*y = matrix_max - *x;
*x = matrix_max;
break;
case (EDGE_TOP << 2) | EDGE_BOTTOM:
*y = matrix_max;
break;
case (EDGE_LEFT << 2) | EDGE_TOP:
*x = *y;
*y = 0;
break;
case (EDGE_LEFT << 2) | EDGE_LEFT:
*x = 0;
*y = matrix_max - *y;
break;
case (EDGE_LEFT << 2) | EDGE_RIGHT:
*x = matrix_max;
break;
case (EDGE_LEFT << 2) | EDGE_BOTTOM:
*x = matrix_max - *y;
*y = matrix_max;
break;
case (EDGE_RIGHT << 2) | EDGE_TOP:
*x = matrix_max - *y;
*y = 0;
break;
case (EDGE_RIGHT << 2) | EDGE_LEFT:
*x = 0;
break;
case (EDGE_RIGHT << 2) | EDGE_RIGHT:
*x = matrix_max;
*y = matrix_max - *y;
break;
case (EDGE_RIGHT << 2) | EDGE_BOTTOM:
*x = *y;
*y = matrix_max;
break;
case (EDGE_BOTTOM << 2) | EDGE_TOP:
*y = 0;
break;
case (EDGE_BOTTOM << 2) | EDGE_LEFT:
*y = matrix_max - *x;
*x = 0;
break;
case (EDGE_BOTTOM << 2) | EDGE_RIGHT:
*y = *x;
*x = matrix_max;
break;
case (EDGE_BOTTOM << 2) | EDGE_BOTTOM:
*x = matrix_max - *x;
*y = matrix_max;
break;
}
return face[f][e].face;
}
uint8_t getPixel(uint8_t f, int16_t x, int16_t y) {
if (x >= 0) {
if (x < matrix_size) {
// Pixel is within X range
if (y >= 0) {
if (y < matrix_size) {
// Pixel is within face bounds
return data[idx][(f * matrix_size + y) * matrix_size + x];
} else {
// Pixel is off bottom edge (but within X bounds)
f = cross(f, EDGE_BOTTOM, &x, &y);
return data[idx][(f * matrix_size + y) * matrix_size + x];
}
} else {
// Pixel is off top edge (but within X bounds)
f = cross(f, EDGE_TOP, &x, &y);
return data[idx][(f * matrix_size + y) * matrix_size + x];
}
} else {
// Pixel is off right edge
if ((y >= 0) && (y < matrix_size)) {
// Pixel is off right edge (but within Y bounds)
f = cross(f, EDGE_RIGHT, &x, &y);
return data[idx][(f * matrix_size + y) * matrix_size + x];
}
}
} else {
// Pixel is off left edge
if ((y >= 0) && (y < matrix_size)) {
// Pixel is off left edge (but within Y bounds)
f = cross(f, EDGE_LEFT, &x, &y);
return data[idx][(f * matrix_size + y) * matrix_size + x];
}
}
// Pixel is off both X&Y edges. Because of cube topology,
// there isn't really a pixel there.
return 1; // 1 = dead pixel
}
void setPixel(uint8_t f, int16_t x, int16_t y, uint8_t value) {
data[1 - idx][(f * matrix_size + y) * matrix_size + x] = value;
}
// MAIN CODE ---------------------------------------------------------------
int main(int argc, char *argv[]) {
RGBMatrix *matrix;
FrameCanvas *canvas;
// INITIALIZE DEFAULTS and PROCESS COMMAND-LINE INPUT --------------------
RGBMatrix::Options matrix_options;
rgb_matrix::RuntimeOptions runtime_opt;
matrix_options.cols = matrix_options.rows = matrix_size;
matrix_options.chain_length = 6;
runtime_opt.gpio_slowdown = 4; // For Pi 4 w/6 matrices
// Parse matrix-related command line options first
if (!ParseOptionsFromFlags(&argc, &argv, &matrix_options, &runtime_opt)) {
return usage(argv[0]);
}
// Validate inputs for cube-like behavior
if (matrix_options.cols != matrix_options.rows) {
fprintf(stderr, "%s: matrix columns, rows must be equal (square matrix)\n",
argv[0]);
return 1;
}
if (matrix_options.chain_length * matrix_options.parallel != 6) {
fprintf(stderr, "%s: total chained/parallel matrices must equal 6\n",
argv[0]);
return 1;
}
max_brightness = (float)matrix_options.brightness * 2.55; // 0-100 -> 0-255
// Then parse any lingering program options (filename, etc.)
int opt;
uint8_t palettenum = 0;
while ((opt = getopt(argc, argv, "k:t:f:")) != -1) {
switch (opt) {
case 'k':
palettenum = atoi(optarg);
if (palettenum >= NUM_PALETTES)
palettenum = NUM_PALETTES - 1;
else if (palettenum < 0)
palettenum = 0;
break;
case 't':
run_time = fabs(strtof(optarg, NULL));
break;
case 'f':
fade_time = fabs(strtof(optarg, NULL));
break;
default: // '?'
return usage(argv[0]);
}
}
// LOAD and ALLOCATE DATA STRUCTURES -------------------------------------
// Allocate cell arrays
matrix_size = matrix_options.rows;
matrix_max = matrix_size - 1;
int num_elements = 6 * matrix_size * matrix_size;
if (!(data[0] = (uint8_t *)malloc(num_elements * 2 * sizeof(uint8_t)))) {
fprintf(stderr, "%s: can't allocate space for cell data\n", argv[0]);
return 1;
}
data[1] = &data[0][num_elements];
colormap = colormaps[palettenum].data;
colormap_max = colormaps[palettenum].max;
// Randomize initial state; 50% chance of any pixel being set
int i;
for (i = 0; i < num_elements; i++) {
data[idx][i] = (rand() & 1) * colormap_max;
}
// INITIALIZE RGB MATRIX CHAIN and OFFSCREEN CANVAS ----------------------
if (!(matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt))) {
fprintf(stderr, "%s: couldn't create matrix object\n", argv[0]);
return 1;
}
if (!(canvas = matrix->CreateFrameCanvas())) {
fprintf(stderr, "%s: couldn't create canvas object\n", argv[0]);
return 1;
}
// OTHER MINOR INITIALIZATION --------------------------------------------
signal(SIGTERM, InterruptHandler);
signal(SIGINT, InterruptHandler);
struct timeval startTime, now;
gettimeofday(&startTime, NULL); // Program start time
// LOOP RUNS INDEFINITELY OR UNTIL CTRL+C or run_time ELAPSED ------------
uint32_t frames = 0;
int prevsec = -1;
while (!interrupt_received) {
gettimeofday(&now, NULL);
double elapsed = ((now.tv_sec - startTime.tv_sec) +
(now.tv_usec - startTime.tv_usec) / 1000000.0);
if (run_time > 0.0) { // Handle time limit and fade in/out if needed...
if (elapsed >= run_time)
break;
if (elapsed < fade_time) {
matrix->SetBrightness((int)(max_brightness * elapsed / fade_time));
} else if (elapsed > (run_time - fade_time)) {
matrix->SetBrightness(
(int)(max_brightness * (run_time - elapsed) / fade_time));
} else {
matrix->SetBrightness(max_brightness);
}
}
// Do life stuff...
int newidx = 1 - idx;
for (uint8_t f = 0; f < 6; f++) {
// Upper-left corner of face in canvas space:
int xoffset = (f % matrix_options.chain_length) * matrix_size;
int yoffset = (f / matrix_options.chain_length) * matrix_size;
for (int y = 0; y < matrix_size; y++) {
for (int x = 0; x < matrix_size; x++) {
// Value returned by getPixel is the "age" of that pixel being
// empty...thus, 0 actually means pixel is currently occupied,
// hence all the ! here when counting neighbors...
uint8_t neighbors =
!getPixel(f, x - 1, y - 1) + !getPixel(f, x, y - 1) +
!getPixel(f, x + 1, y - 1) + !getPixel(f, x - 1, y) +
!getPixel(f, x + 1, y) + !getPixel(f, x - 1, y + 1) +
!getPixel(f, x, y + 1) + !getPixel(f, x + 1, y + 1);
// Live cell w/2 or 3 neighbors continues, else dies.
// Empty cell w/3 neighbors goes live.
uint8_t n = getPixel(f, x, y);
if (n == 0) { // Pixel (x,y) is 'alive'
if ((neighbors < 2) || (neighbors > 3))
n = 1; // Pixel 'dies'
} else { // Pixel (x,y) is 'dead'
if (neighbors == 3)
n = 0; // Wake up!
else if (n < colormap_max)
n += 1; // Decay
}
setPixel(f, x, y, n);
n *= 3; // Convert color index to RGB offset
canvas->SetPixel(xoffset + x, yoffset + y, colormap[n],
colormap[n + 1], colormap[n + 2]);
}
}
}
canvas = matrix->SwapOnVSync(canvas);
idx = newidx;
frames++;
if (now.tv_sec != prevsec) {
if (prevsec >= 0) {
printf("%f\n", frames / elapsed);
}
prevsec = now.tv_sec;
}
}
canvas->Clear();
delete matrix;
return 0;
}

606
Pi_Matrix_Cube/life.py Normal file
View file

@ -0,0 +1,606 @@
# SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Conway's Game of Life for 6X square RGB LED matrices.
Uses same physical matrix arrangement as "globe" program; see notes there.
usage: sudo python life.py [options]
(You may or may not need the 'sudo' depending how the rpi-rgb-matrix
library is configured)
Options include all of the rpi-rgb-matrix flags, such as --led-pwm-bits=N
or --led-gpio-slowdown=N, and then the following:
-k <int> : Index of color palette to use. 0 = default black & white
(sorry, -c and -p already taken by matrix configurables).
-t <float> : Run time in seconds. Program will exit after this.
Default is to run indefinitely, until crtl+C received.
-f <float> : Fade in/out time in seconds. Used in combination with the
-t option, this provides a nice fade-in, run for a
while, fade-out and exit.
rpi-rgb-matrix has the following single-character abbreviations for
some configurables: -b (--led-brightness), -c (--led-chain),
-m (--led-gpio-mapping), -p (--led-pwm-bits), -P (--led-parallel),
-r (--led-rows). AVOID THESE in any future configurables added to this
program, as some users may have "muscle memory" for those options.
This code depends on the rpi-rgb-matrix library. While this .py file has
a permissive MIT licence, libraries may (or not) have restrictions on
commercial use, distributing precompiled binaries, etc. Check their
license terms if this is relevant to your situation.
"""
import argparse
import os
import sys
import time
import random
from rgbmatrix import RGBMatrix, RGBMatrixOptions
from PIL import Image
# import cProfile # Used only when profiling
EDGE_TOP = 0
EDGE_LEFT = 1
EDGE_RIGHT = 2
EDGE_BOTTOM = 3
FACE = ( # Topology for 6 faces of cube; constant, not runtime-configurable.
# Sequence within each face is top, left, right, bottom.
# Top, left, etc. are with respect to exterior view of LED face sides.
( # For face[0]...
(1, EDGE_LEFT), # Top edge connects to left of face[1]
(2, EDGE_TOP), # Left edge connects to top of face[2]
(4, EDGE_TOP), # Right edge connects to top of face[4]
(3, EDGE_RIGHT), # etc...
),
( # face[1]...
(2, EDGE_LEFT), # Top edge connects to left of face[2]
(0, EDGE_TOP), # etc...
(5, EDGE_TOP),
(4, EDGE_RIGHT),
),
(
(0, EDGE_LEFT),
(1, EDGE_TOP),
(3, EDGE_TOP),
(5, EDGE_RIGHT),
),
(
(2, EDGE_RIGHT),
(5, EDGE_BOTTOM),
(0, EDGE_BOTTOM),
(4, EDGE_LEFT),
),
(
(0, EDGE_RIGHT),
(3, EDGE_BOTTOM),
(1, EDGE_BOTTOM),
(5, EDGE_LEFT),
),
(
(1, EDGE_RIGHT),
(4, EDGE_BOTTOM),
(2, EDGE_BOTTOM),
(3, EDGE_LEFT),
),
)
# Colormaps appear reversed from what one might expect. The first element
# of each is the 'on' pixel color, and each subsequent element is the color
# as a pixel 'ages,' up to the final 'background' color. Hence simple B&W
# on/off palette is white in index 0, black in index 1.
COLORMAP = (
((255, 255, 255), (0, 0, 0)), # Simple B&W
( # Log2 Grayscale
(255, 255, 255),
(127, 127, 127),
(63, 63, 63),
(31, 31, 31),
(15, 15, 15),
(7, 7, 7),
(3, 3, 3),
(1, 1, 1),
(0, 0, 0),
),
( # Heatmap (white-yellow-red-black)
(255, 255, 255), # White
(255, 255, 127), # Two steps to...
(255, 255, 0), # Yellow
(255, 170, 0), # Three steps...
(255, 85, 0),
(255, 0, 0), # Red
(204, 0, 0), # Four steps...
(153, 0, 0),
(102, 0, 0),
(51, 0, 0),
(0, 0, 0), # Black
),
( # Spectrum
(255, 255, 255), # White (100%)
(127, 0, 0), # Red (50%)
(127, 31, 0),
(127, 63, 0), # Orange (50%)
(127, 95, 0),
(127, 127, 0), # Yellow (etc)
(63, 127, 0),
(0, 127, 0), # Green
(0, 127, 127), # Cyan
(0, 0, 127), # Blue
(63, 0, 127),
(127, 0, 127), # Magenta
(82, 0, 82),
(41, 0, 41),
(0, 0, 0), # Black
),
)
# pylint: disable=too-many-instance-attributes
class Life:
"""
Conway's Game of Life, mapped on a cube. See, the trick is that you
can't just treat it as a big 2D rectangle...faces may be arranged in
different orientations, and the space is discontiguous...the edges
and corners create shenanigans.
"""
def __init__(self):
self.matrix = None # RGB matrix object (initialized after inputs)
self.canvas = None # Offscreen canvas (after inputs)
self.matrix_size = 0 # Matrix width/height in pixels (after inputs)
self.matrix_max = 0 # Maximum column/row (after inputs)
self.data = None # Pixel 'age' data (after inputs)
self.direct = None # Table of 'OK to read pixel data directly' flags
self.idx = 0 # Currently active data index (0/1, double-buffered)
self.run_time = -1.0 # If >0 (input can override), limit run time
self.fade_time = 0.0 # Fade in/out time (input can override)
self.max_brightness = 255 # Matrix brightness (input can override)
self.chain_length = 6 # Matrix chain length
self.colormap = COLORMAP[0] # Input can override
self.colormap_max = None # Initialized after inputs
self.imgbuf = None # PIL image buffer (initialized after inputs)
# pylint: disable=too-many-statements
def setup(self):
""" Returns False on success, True on error """
parser = argparse.ArgumentParser()
# RGB matrix standards
parser.add_argument(
"-r",
"--led-rows",
action="store",
help="Display rows. 32 for 32x32, 64 for 64x64. Default: 64",
default=64,
type=int,
)
parser.add_argument(
"--led-cols",
action="store",
help="Panel columns. Typically 32 or 64. (Default: 64)",
default=64,
type=int,
)
parser.add_argument(
"-c",
"--led-chain",
action="store",
help="Daisy-chained boards. Default: 6.",
default=6,
type=int,
)
parser.add_argument(
"-P",
"--led-parallel",
action="store",
help="For Plus-models or RPi2: parallel chains. 1..3. Default: 1",
default=1,
type=int,
)
parser.add_argument(
"-p",
"--led-pwm-bits",
action="store",
help="Bits used for PWM. Something between 1..11. Default: 11",
default=11,
type=int,
)
parser.add_argument(
"-b",
"--led-brightness",
action="store",
help="Sets brightness level. Default: 100. Range: 1..100",
default=100,
type=int,
)
parser.add_argument(
"-m",
"--led-gpio-mapping",
help="Hardware Mapping: regular, adafruit-hat, adafruit-hat-pwm",
choices=["regular", "regular-pi1", "adafruit-hat", "adafruit-hat-pwm"],
type=str,
)
parser.add_argument(
"--led-scan-mode",
action="store",
help="Progressive or interlaced scan. 0 Progressive, 1 Interlaced (default)",
default=1,
choices=range(2),
type=int,
)
parser.add_argument(
"--led-pwm-lsb-nanoseconds",
action="store",
help="Base time-unit for the on-time in the lowest "
"significant bit in nanoseconds. Default: 130",
default=130,
type=int,
)
parser.add_argument(
"--led-show-refresh",
action="store_true",
help="Shows the current refresh rate of the LED panel",
)
parser.add_argument(
"--led-slowdown-gpio",
action="store",
help="Slow down writing to GPIO. Range: 0..4. Default: 3",
default=4, # For Pi 4 w/6 matrices
type=int,
)
parser.add_argument(
"--led-no-hardware-pulse",
action="store",
help="Don't use hardware pin-pulse generation",
)
parser.add_argument(
"--led-rgb-sequence",
action="store",
help="Switch if your matrix has led colors swapped. Default: RGB",
default="RGB",
type=str,
)
parser.add_argument(
"--led-pixel-mapper",
action="store",
help='Apply pixel mappers. e.g "Rotate:90"',
default="",
type=str,
)
parser.add_argument(
"--led-row-addr-type",
action="store",
help="0 = default; 1=AB-addressed panels; 2=row direct; "
"3=ABC-addressed panels; 4 = ABC Shift + DE direct",
default=0,
type=int,
choices=[0, 1, 2, 3, 4],
)
parser.add_argument(
"--led-multiplexing",
action="store",
help="Multiplexing type: 0=direct; 1=strip; 2=checker; 3=spiral; "
"4=ZStripe; 5=ZnMirrorZStripe; 6=coreman; 7=Kaler2Scan; "
"8=ZStripeUneven... (Default: 0)",
default=0,
type=int,
)
parser.add_argument(
"--led-panel-type",
action="store",
help="Needed to initialize special panels. Supported: 'FM6126A'",
default="",
type=str,
)
parser.add_argument(
"--led-no-drop-privs",
dest="drop_privileges",
help="Don't drop privileges from 'root' after initializing the hardware.",
action="store_false",
)
# Extra args unique to this program
parser.add_argument(
"-k",
action="store",
help="Index of color palette to use. Default: 0",
default=0,
type=int,
)
parser.add_argument(
"-t",
action="store",
help="Run time in seconds. Default: run indefinitely",
default=-1.0,
type=float,
)
parser.add_argument(
"-f",
action="store",
help="Fade in/out time in seconds. Default: 0.0",
default=0.0,
type=float,
)
parser.set_defaults(drop_privileges=True)
args = parser.parse_args()
if args.led_rows != args.led_cols:
print(
os.path.basename(__file__) + ": error: led rows and columns must match"
)
return True
if args.led_chain * args.led_parallel != 6:
print(
os.path.basename(__file__)
+ ": error: total chained * parallel matrices must equal 6"
)
return True
options = RGBMatrixOptions()
if args.led_gpio_mapping is not None:
options.hardware_mapping = args.led_gpio_mapping
options.rows = args.led_rows
options.cols = args.led_cols
options.chain_length = args.led_chain
options.parallel = args.led_parallel
options.row_address_type = args.led_row_addr_type
options.multiplexing = args.led_multiplexing
options.pwm_bits = args.led_pwm_bits
options.brightness = args.led_brightness
options.pwm_lsb_nanoseconds = args.led_pwm_lsb_nanoseconds
options.led_rgb_sequence = args.led_rgb_sequence
options.pixel_mapper_config = args.led_pixel_mapper
options.panel_type = args.led_panel_type
if args.led_show_refresh:
options.show_refresh_rate = 1
if args.led_slowdown_gpio is not None:
options.gpio_slowdown = args.led_slowdown_gpio
if args.led_no_hardware_pulse:
options.disable_hardware_pulsing = True
if not args.drop_privileges:
options.drop_privileges = False
self.matrix = RGBMatrix(options=options)
self.canvas = self.matrix.CreateFrameCanvas()
self.matrix_size = args.led_rows
self.matrix_max = self.matrix_size - 1
self.chain_length = args.led_chain
self.max_brightness = args.led_brightness * 2.55 # 0-100 -> 0-255
self.run_time = args.t
self.fade_time = args.f
self.colormap = COLORMAP[min(max(args.k, 0), len(COLORMAP) - 1)]
self.colormap_max = len(self.colormap) - 1
# Alloc & randomize initial state; 50% chance of any pixel being set
self.data = [
[
[
[
random.randrange(2) * self.colormap_max
for x in range(self.matrix_size)
]
for y in range(self.matrix_size)
]
for face in range(6)
]
for i in range(2)
]
# Rather than testing X & Y to see if we should use get_edge_pixel
# or access the data array directly, this table pre-stores which
# pixel-getting approach to use for each row/column of one face.
self.direct = (
[[False] * self.matrix_size]
+ [[False] + [True] * (self.matrix_size - 2) + [False]]
* (self.matrix_size - 2)
+ [[False] * self.matrix_size]
)
self.imgbuf = bytearray(self.matrix_size * self.matrix_size * 3)
return False
# NOTE: if the code starts looking super atrocious from here down,
# that's no coincidence. To keep the animation smooth and appealing,
# this was written to be fast, not Pythonic. Tons of A/B testing was
# performed against different approaches to each piece, using cProfile
# and/or the displayed FPS values. Some of this looks REALLY bad. If
# you're wondering "why didn't they just [X]?", that's why.
# NOT GOOD CODE TO LEARN FROM, except maybe for setting a bad example.
# pylint: disable=too-many-branches
def cross(self, face, col, row, edge):
"""Given a face index and a column & row known to be ONE pixel
off ONE edge, return a new face index and a corresponding
column & row within that face's native coordinate system.
"""
to_edge = FACE[face][edge][1]
if edge == EDGE_TOP:
if to_edge == EDGE_TOP:
col, row = self.matrix_max - col, 0
elif to_edge == EDGE_LEFT:
col, row = 0, col
elif to_edge == EDGE_RIGHT:
col, row = self.matrix_max, self.matrix_max - col
else:
row = self.matrix_max
elif edge == EDGE_LEFT:
if to_edge == EDGE_TOP:
col, row = row, 0
elif to_edge == EDGE_LEFT:
col, row = 0, self.matrix_max - row
elif to_edge == EDGE_RIGHT:
col = self.matrix_max
else:
col, row = self.matrix_max - row, self.matrix_max
elif edge == EDGE_RIGHT:
if to_edge == EDGE_TOP:
col, row = self.matrix_max - row, 0
elif to_edge == EDGE_LEFT:
col = 0
elif to_edge == EDGE_RIGHT:
col, row = self.matrix_max, self.matrix_max - row
else:
col, row = row, self.matrix_max
else:
if to_edge == EDGE_TOP:
row = 0
elif to_edge == EDGE_LEFT:
col, row = 0, self.matrix_max - col
elif to_edge == EDGE_RIGHT:
col, row = self.matrix_max, col
else:
col, row = self.matrix_max - col, self.matrix_max
return FACE[face][edge][0], col, row
def get_edge_pixel(self, face, col, row):
"""Given a face index and a column & row that might be in-bounds
OR one pixel off one or two edges, return 'age' of pixel, wrapping
around edges as appropriate.
"""
if 0 <= col <= self.matrix_max: # Pixel in X bounds
if 0 <= row <= self.matrix_max: # Pixel in Y bounds
return self.data[self.idx][face][row][col]
# Else pixel in X bounds, but out of Y bounds
edge = EDGE_TOP if row < 0 else EDGE_BOTTOM
elif 0 <= row <= self.matrix_max: # Pixel in Y bounds, off left/right
edge = EDGE_LEFT if col < 0 else EDGE_RIGHT
else: # Pixel off two edges; treat corners as "dead"
return 1
face, col, row = self.cross(face, col, row, edge)
return self.data[self.idx][face][row][col]
def run(self):
"""Main loop of Life simulation."""
start_time, frames = time.monotonic(), 0
while True:
if self.run_time > 0: # Handle fade in / fade out
elapsed = time.monotonic() - start_time
if elapsed >= self.run_time:
break
if elapsed < self.fade_time:
self.matrix.brightness = int(
self.max_brightness * elapsed / self.fade_time
)
elif elapsed > (self.run_time - self.fade_time):
self.matrix.brightness = int(
self.max_brightness * (self.run_time - elapsed) / self.fade_time
)
else:
self.matrix.brightness = self.max_brightness
self.iterate() # Process and render one frame
# Swap double-buffered canvas, show frames per second
self.canvas = self.matrix.SwapOnVSync(self.canvas)
frames += 1
print(frames / (time.monotonic() - start_time))
# pylint: disable=too-many-locals
def iterate(self):
"""Run one cycle of the Life simulation, drawing to offscreen canvas."""
next_idx = 1 - self.idx # Destination
# Certain instance variables (ones referenced in inner loop) are
# copied to locals to speed up access. This is kind of a jerk thing
# to do and not "Pythonic," but anything for a boost in this code.
imgbuf = self.imgbuf
colormap = self.colormap
colormap_max = self.colormap_max
get_edge_pixel = self.get_edge_pixel
for face in range(6):
offset = 0
for row in range(0, self.matrix_size):
row_data = self.data[self.idx][face][row]
row_data_next = self.data[next_idx][face][row]
rm1 = row - 1
rp1 = row + 1
if row > 0:
above_data = self.data[self.idx][face][rm1]
if row < self.matrix_max:
below_data = self.data[self.idx][face][rp1]
cm1 = -1
col = 0
direct = self.direct[row]
for cp1 in range(1, self.matrix_size + 1):
neighbors = (
(
above_data[cm1],
above_data[col],
above_data[cp1],
row_data[cm1],
row_data[cp1],
below_data[cm1],
below_data[col],
below_data[cp1],
)
if direct[col]
else (
get_edge_pixel(face, cm1, rm1),
get_edge_pixel(face, col, rm1),
get_edge_pixel(face, cp1, rm1),
get_edge_pixel(face, cm1, row),
get_edge_pixel(face, cp1, row),
get_edge_pixel(face, cm1, rp1),
get_edge_pixel(face, col, rp1),
get_edge_pixel(face, cp1, rp1),
)
).count(0)
# Live cell w/2 or 3 neighbors continues, else dies.
# Empty cell w/3 neighbors goes live.
age = row_data[col]
if age == 0: # Pixel (col,row) is active
if not neighbors in (2, 3):
age = 1 # Pixel aging starts
else: # Pixel (col,row) is aged
if neighbors == 3:
age = 0 # Arise!
elif age < colormap_max:
age += 1 # Decay
row_data_next[col] = age
rgb = colormap[age]
imgbuf[offset] = rgb[0]
imgbuf[offset + 1] = rgb[1]
imgbuf[offset + 2] = rgb[2]
offset += 3
cm1 = col
col = cp1
image = Image.frombuffer(
"RGB",
(self.matrix_size, self.matrix_size),
bytes(imgbuf),
"raw",
"RGB",
0,
1,
)
# Upper-left corner of face in canvas space:
xoffset = (face % self.chain_length) * self.matrix_size
yoffset = (face // self.chain_length) * self.matrix_size
self.canvas.SetImage(image, offset_x=xoffset, offset_y=yoffset)
self.idx = next_idx
# pylint: disable=superfluous-parens
if __name__ == "__main__":
life = Life()
if not (status := life.setup()):
try:
print("Press CTRL-C to stop")
life.run()
# cProfile.run('life.run()') # Used only when profiling
except KeyboardInterrupt:
print("Exiting\n")
sys.exit(status)

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB