446 lines
14 KiB
C++
446 lines
14 KiB
C++
// 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;
|
|
}
|