Add Pi_Matrix_Cube initial files
This commit is contained in:
parent
5587b67a40
commit
9510829735
12 changed files with 2118 additions and 0 deletions
21
Pi_Matrix_Cube/Makefile
Normal file
21
Pi_Matrix_Cube/Makefile
Normal 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
28
Pi_Matrix_Cube/cycle.sh
Normal 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
418
Pi_Matrix_Cube/globe.cc
Normal 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
555
Pi_Matrix_Cube/globe.py
Normal 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)
|
||||
44
Pi_Matrix_Cube/imageviewer.py
Normal file
44
Pi_Matrix_Cube/imageviewer.py
Normal 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
446
Pi_Matrix_Cube/life.cc
Normal 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
606
Pi_Matrix_Cube/life.py
Normal 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)
|
||||
BIN
Pi_Matrix_Cube/maps/adafruit.jpg
Normal file
BIN
Pi_Matrix_Cube/maps/adafruit.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
Pi_Matrix_Cube/maps/boing.jpg
Normal file
BIN
Pi_Matrix_Cube/maps/boing.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 401 B |
BIN
Pi_Matrix_Cube/maps/earth.jpg
Normal file
BIN
Pi_Matrix_Cube/maps/earth.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
Pi_Matrix_Cube/maps/jupiter.jpg
Normal file
BIN
Pi_Matrix_Cube/maps/jupiter.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
Pi_Matrix_Cube/maps/moon.jpg
Normal file
BIN
Pi_Matrix_Cube/maps/moon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Loading…
Reference in a new issue