/ Pi_Matrix_Cube / life.cc
life.cc
1 // SPDX-FileCopyrightText: 2022 Phillip Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 /* 6 Conway's Game of Life for 6X square RGB LED matrices. 7 Uses same physical matrix arrangement as "globe" program; see notes there. 8 9 usage: sudo ./life [options] 10 11 (You may or may not need the 'sudo' depending how the rpi-rgb-matrix 12 library is configured) 13 14 Options include all of the rpi-rgb-matrix flags, such as --led-pwm-bits=N 15 or --led-gpio-slowdown=N, and then the following: 16 17 -k <int> : Index of color palette to use. 0 = default black & white. 18 (Sorry, -c and -p are both rpi-rgb-matrix abbreviations. 19 Consider this a Monty Python Travel Agent Sketch nod.) 20 -t <float> : Run time in seconds. Program will exit after this. 21 Default is to run indefinitely, until crtl+C received. 22 -f <float> : Fade in/out time in seconds. Used in combination with the 23 -t option, this provides a nice fade-in, run for a while, 24 fade-out and exit. 25 26 rpi-rgb-matrix has the following single-character abbreviations for 27 some configurables: -b (--led-brightness), -c (--led-chain), 28 -m (--led-gpio-mapping), -p (--led-pwm-bits), -P (--led-parallel), 29 -r (--led-rows). AVOID THESE in any future configurables added to this 30 program, as some users may have "muscle memory" for those options. 31 32 This code depends on the rpi-rgb-matrix library. While this .cc file has 33 a permissive MIT licence, libraries may (or not) have restrictions on 34 commercial use, distributing precompiled binaries, etc. Check their 35 license terms if this is relevant to your situation. 36 */ 37 38 #include <getopt.h> 39 #include <led-matrix.h> 40 #include <math.h> 41 #include <signal.h> 42 #include <stdio.h> 43 #include <string.h> 44 #include <sys/time.h> 45 46 using namespace rgb_matrix; 47 48 // GLOBAL VARIABLES -------------------------------------------------------- 49 50 typedef enum { 51 EDGE_TOP = 0, 52 EDGE_LEFT, 53 EDGE_RIGHT, 54 EDGE_BOTTOM, 55 } Edge; 56 57 // Colormaps appear reversed from what one might expect. The first element 58 // of each is the 'on' pixel color, and each subsequent element is the color 59 // as a pixel 'ages,' up to the final 'background' color. Hence simple B&W 60 // on/off palette is white in index 0, black in index 1. 61 62 uint8_t map_bw[][3] = { // Simple B&W 63 {255, 255, 255}, 64 {0, 0, 0}}; 65 66 uint8_t map_gray[][3] = { // Log2 grayscale 67 {255, 255, 255}, // clang-format, 68 {127, 127, 127}, // I love you 69 {63, 63, 63}, // but 70 {31, 31, 31}, // why 71 {15, 15, 15}, // you 72 {7, 7, 7}, // gotta 73 {3, 3, 3}, // do 74 {1, 1, 1}, // this 75 {0, 0, 0}}; // to me? 76 77 uint8_t map_heat[][3] = { 78 // Heatmap (white-yellow-red-black) 79 {255, 255, 255}, // White 80 {255, 255, 127}, // Two steps to... 81 {255, 255, 0}, // Yellow 82 {255, 170, 0}, // Three steps... 83 {255, 85, 0}, // to... 84 {255, 0, 0}, // Red 85 {204, 0, 0}, // Four steps... 86 {153, 0, 0}, // 87 {102, 0, 0}, // to... 88 {51, 0, 0}, // 89 {0, 0, 0} // Black 90 }; 91 92 uint8_t map_spec[][3] = { 93 // Color spectrum 94 {255, 255, 255}, // White (100%) 95 {127, 0, 0}, // Red (50%) 96 {127, 31, 0}, // to... 97 {127, 63, 0}, // Orange (50%) 98 {127, 95, 0}, // to... 99 {127, 127, 0}, // Yellow (etc) 100 {63, 127, 0}, // to... 101 {0, 127, 0}, // Green 102 {0, 127, 127}, // Cyan 103 {0, 0, 127}, // Blue 104 {63, 0, 127}, // to... 105 {127, 0, 127}, // Magenta 106 {82, 0, 82}, // fade... 107 {41, 0, 41}, // to... 108 {0, 0, 0} // Black 109 }; 110 111 struct { 112 uint8_t *data; 113 uint8_t max; 114 } colormaps[] = { 115 {(uint8_t *)map_bw, sizeof(map_bw) / sizeof(map_bw[0]) - 1}, 116 {(uint8_t *)map_gray, sizeof(map_gray) / sizeof(map_gray[0]) - 1}, 117 {(uint8_t *)map_heat, sizeof(map_heat) / sizeof(map_heat[0]) - 1}, 118 {(uint8_t *)map_spec, sizeof(map_spec) / sizeof(map_spec[0]) - 1}, 119 }; 120 121 #define NUM_PALETTES (sizeof(colormaps) / sizeof(colormaps[0])) 122 123 struct { 124 uint8_t face; // Index of face off this edge 125 Edge edge; // Which edge of face its entering that way 126 } face[6][4] = { // Order is top, left, right, bottom 127 {{1, EDGE_LEFT}, {2, EDGE_TOP}, {4, EDGE_TOP}, {3, EDGE_RIGHT}}, 128 {{2, EDGE_LEFT}, {0, EDGE_TOP}, {5, EDGE_TOP}, {4, EDGE_RIGHT}}, 129 {{0, EDGE_LEFT}, {1, EDGE_TOP}, {3, EDGE_TOP}, {5, EDGE_RIGHT}}, 130 {{2, EDGE_RIGHT}, {5, EDGE_BOTTOM}, {0, EDGE_BOTTOM}, {4, EDGE_LEFT}}, 131 {{0, EDGE_RIGHT}, {3, EDGE_BOTTOM}, {1, EDGE_BOTTOM}, {5, EDGE_LEFT}}, 132 {{1, EDGE_RIGHT}, {4, EDGE_BOTTOM}, {2, EDGE_BOTTOM}, {3, EDGE_LEFT}}}; 133 134 135 // These globals have defaults but are runtime configurable: 136 uint16_t matrix_size = 64; // Matrix X&Y pixel count (must be square) 137 uint16_t matrix_max = matrix_size - 1; // Matrix X&Y max coord 138 float run_time = -1.0; // Time before exit (negative = run forever) 139 float fade_time = 0.0; // Fade in/out time (if run_time is set) 140 float max_brightness = 255.0; // Fade up to, down from this value 141 142 // These globals are computed or allocated at runtime after taking input: 143 uint8_t *data[2]; // Cell arrays; current and next-in-progress 144 uint8_t idx = 0; // Which data[] array is current 145 uint8_t *colormap; 146 uint8_t colormap_max; 147 148 // INTERRUPT HANDLER (to allow clearing matrix on exit) -------------------- 149 150 volatile bool interrupt_received = false; 151 static void InterruptHandler(int signo) { interrupt_received = true; } 152 153 // COMMAND-LINE HELP ------------------------------------------------------- 154 155 static int usage(const char *progname) { 156 fprintf(stderr, "usage: %s [options]\n", progname); 157 fprintf(stderr, "Options:\n"); 158 rgb_matrix::PrintMatrixFlags(stderr); 159 fprintf(stderr, "\t-k <int> : Color palette (0-%d)\n", NUM_PALETTES - 1); 160 fprintf(stderr, "\t-t <float> : Run time in seconds\n"); 161 fprintf(stderr, "\t-f <float> : Fade in/out time in seconds\n"); 162 return 1; 163 } 164 165 // SUNDRY UTILITY-LIKE FUNCTIONS ------------------------------------------- 166 167 uint8_t cross(uint8_t f, Edge e, int16_t *x, int16_t *y) { 168 switch ((e << 2) | face[f][e].edge) { 169 case (EDGE_TOP << 2) | EDGE_TOP: 170 *x = matrix_max - *x; 171 *y = 0; 172 break; 173 case (EDGE_TOP << 2) | EDGE_LEFT: 174 *y = *x; 175 *x = 0; 176 break; 177 case (EDGE_TOP << 2) | EDGE_RIGHT: 178 *y = matrix_max - *x; 179 *x = matrix_max; 180 break; 181 case (EDGE_TOP << 2) | EDGE_BOTTOM: 182 *y = matrix_max; 183 break; 184 case (EDGE_LEFT << 2) | EDGE_TOP: 185 *x = *y; 186 *y = 0; 187 break; 188 case (EDGE_LEFT << 2) | EDGE_LEFT: 189 *x = 0; 190 *y = matrix_max - *y; 191 break; 192 case (EDGE_LEFT << 2) | EDGE_RIGHT: 193 *x = matrix_max; 194 break; 195 case (EDGE_LEFT << 2) | EDGE_BOTTOM: 196 *x = matrix_max - *y; 197 *y = matrix_max; 198 break; 199 case (EDGE_RIGHT << 2) | EDGE_TOP: 200 *x = matrix_max - *y; 201 *y = 0; 202 break; 203 case (EDGE_RIGHT << 2) | EDGE_LEFT: 204 *x = 0; 205 break; 206 case (EDGE_RIGHT << 2) | EDGE_RIGHT: 207 *x = matrix_max; 208 *y = matrix_max - *y; 209 break; 210 case (EDGE_RIGHT << 2) | EDGE_BOTTOM: 211 *x = *y; 212 *y = matrix_max; 213 break; 214 case (EDGE_BOTTOM << 2) | EDGE_TOP: 215 *y = 0; 216 break; 217 case (EDGE_BOTTOM << 2) | EDGE_LEFT: 218 *y = matrix_max - *x; 219 *x = 0; 220 break; 221 case (EDGE_BOTTOM << 2) | EDGE_RIGHT: 222 *y = *x; 223 *x = matrix_max; 224 break; 225 case (EDGE_BOTTOM << 2) | EDGE_BOTTOM: 226 *x = matrix_max - *x; 227 *y = matrix_max; 228 break; 229 } 230 231 return face[f][e].face; 232 } 233 234 uint8_t getPixel(uint8_t f, int16_t x, int16_t y) { 235 if (x >= 0) { 236 if (x < matrix_size) { 237 // Pixel is within X range 238 if (y >= 0) { 239 if (y < matrix_size) { 240 // Pixel is within face bounds 241 return data[idx][(f * matrix_size + y) * matrix_size + x]; 242 } else { 243 // Pixel is off bottom edge (but within X bounds) 244 f = cross(f, EDGE_BOTTOM, &x, &y); 245 return data[idx][(f * matrix_size + y) * matrix_size + x]; 246 } 247 } else { 248 // Pixel is off top edge (but within X bounds) 249 f = cross(f, EDGE_TOP, &x, &y); 250 return data[idx][(f * matrix_size + y) * matrix_size + x]; 251 } 252 } else { 253 // Pixel is off right edge 254 if ((y >= 0) && (y < matrix_size)) { 255 // Pixel is off right edge (but within Y bounds) 256 f = cross(f, EDGE_RIGHT, &x, &y); 257 return data[idx][(f * matrix_size + y) * matrix_size + x]; 258 } 259 } 260 } else { 261 // Pixel is off left edge 262 if ((y >= 0) && (y < matrix_size)) { 263 // Pixel is off left edge (but within Y bounds) 264 f = cross(f, EDGE_LEFT, &x, &y); 265 return data[idx][(f * matrix_size + y) * matrix_size + x]; 266 } 267 } 268 269 // Pixel is off both X&Y edges. Because of cube topology, 270 // there isn't really a pixel there. 271 return 1; // 1 = dead pixel 272 } 273 274 void setPixel(uint8_t f, int16_t x, int16_t y, uint8_t value) { 275 data[1 - idx][(f * matrix_size + y) * matrix_size + x] = value; 276 } 277 278 // MAIN CODE --------------------------------------------------------------- 279 280 int main(int argc, char *argv[]) { 281 RGBMatrix *matrix; 282 FrameCanvas *canvas; 283 284 // INITIALIZE DEFAULTS and PROCESS COMMAND-LINE INPUT -------------------- 285 286 RGBMatrix::Options matrix_options; 287 rgb_matrix::RuntimeOptions runtime_opt; 288 289 matrix_options.cols = matrix_options.rows = matrix_size; 290 matrix_options.chain_length = 6; 291 runtime_opt.gpio_slowdown = 4; // For Pi 4 w/6 matrices 292 293 // Parse matrix-related command line options first 294 if (!ParseOptionsFromFlags(&argc, &argv, &matrix_options, &runtime_opt)) { 295 return usage(argv[0]); 296 } 297 298 // Validate inputs for cube-like behavior 299 if (matrix_options.cols != matrix_options.rows) { 300 fprintf(stderr, "%s: matrix columns, rows must be equal (square matrix)\n", 301 argv[0]); 302 return 1; 303 } 304 if (matrix_options.chain_length * matrix_options.parallel != 6) { 305 fprintf(stderr, "%s: total chained/parallel matrices must equal 6\n", 306 argv[0]); 307 return 1; 308 } 309 310 max_brightness = (float)matrix_options.brightness * 2.55; // 0-100 -> 0-255 311 312 // Then parse any lingering program options (filename, etc.) 313 int opt; 314 uint8_t palettenum = 0; 315 while ((opt = getopt(argc, argv, "k:t:f:")) != -1) { 316 switch (opt) { 317 case 'k': 318 palettenum = atoi(optarg); 319 if (palettenum >= NUM_PALETTES) 320 palettenum = NUM_PALETTES - 1; 321 else if (palettenum < 0) 322 palettenum = 0; 323 break; 324 case 't': 325 run_time = fabs(strtof(optarg, NULL)); 326 break; 327 case 'f': 328 fade_time = fabs(strtof(optarg, NULL)); 329 break; 330 default: // '?' 331 return usage(argv[0]); 332 } 333 } 334 335 // LOAD and ALLOCATE DATA STRUCTURES ------------------------------------- 336 337 // Allocate cell arrays 338 matrix_size = matrix_options.rows; 339 matrix_max = matrix_size - 1; 340 int num_elements = 6 * matrix_size * matrix_size; 341 if (!(data[0] = (uint8_t *)malloc(num_elements * 2 * sizeof(uint8_t)))) { 342 fprintf(stderr, "%s: can't allocate space for cell data\n", argv[0]); 343 return 1; 344 } 345 data[1] = &data[0][num_elements]; 346 347 colormap = colormaps[palettenum].data; 348 colormap_max = colormaps[palettenum].max; 349 350 // Randomize initial state; 50% chance of any pixel being set 351 int i; 352 for (i = 0; i < num_elements; i++) { 353 data[idx][i] = (rand() & 1) * colormap_max; 354 } 355 356 // INITIALIZE RGB MATRIX CHAIN and OFFSCREEN CANVAS ---------------------- 357 358 if (!(matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt))) { 359 fprintf(stderr, "%s: couldn't create matrix object\n", argv[0]); 360 return 1; 361 } 362 if (!(canvas = matrix->CreateFrameCanvas())) { 363 fprintf(stderr, "%s: couldn't create canvas object\n", argv[0]); 364 return 1; 365 } 366 367 // OTHER MINOR INITIALIZATION -------------------------------------------- 368 369 signal(SIGTERM, InterruptHandler); 370 signal(SIGINT, InterruptHandler); 371 372 struct timeval startTime, now; 373 gettimeofday(&startTime, NULL); // Program start time 374 375 // LOOP RUNS INDEFINITELY OR UNTIL CTRL+C or run_time ELAPSED ------------ 376 377 uint32_t frames = 0; 378 int prevsec = -1; 379 380 while (!interrupt_received) { 381 gettimeofday(&now, NULL); 382 double elapsed = ((now.tv_sec - startTime.tv_sec) + 383 (now.tv_usec - startTime.tv_usec) / 1000000.0); 384 if (run_time > 0.0) { // Handle time limit and fade in/out if needed... 385 if (elapsed >= run_time) 386 break; 387 if (elapsed < fade_time) { 388 matrix->SetBrightness((int)(max_brightness * elapsed / fade_time)); 389 } else if (elapsed > (run_time - fade_time)) { 390 matrix->SetBrightness( 391 (int)(max_brightness * (run_time - elapsed) / fade_time)); 392 } else { 393 matrix->SetBrightness(max_brightness); 394 } 395 } 396 // Do life stuff... 397 int newidx = 1 - idx; 398 for (uint8_t f = 0; f < 6; f++) { 399 // Upper-left corner of face in canvas space: 400 int xoffset = (f % matrix_options.chain_length) * matrix_size; 401 int yoffset = (f / matrix_options.chain_length) * matrix_size; 402 for (int y = 0; y < matrix_size; y++) { 403 for (int x = 0; x < matrix_size; x++) { 404 // Value returned by getPixel is the "age" of that pixel being 405 // empty...thus, 0 actually means pixel is currently occupied, 406 // hence all the ! here when counting neighbors... 407 uint8_t neighbors = 408 !getPixel(f, x - 1, y - 1) + !getPixel(f, x, y - 1) + 409 !getPixel(f, x + 1, y - 1) + !getPixel(f, x - 1, y) + 410 !getPixel(f, x + 1, y) + !getPixel(f, x - 1, y + 1) + 411 !getPixel(f, x, y + 1) + !getPixel(f, x + 1, y + 1); 412 // Live cell w/2 or 3 neighbors continues, else dies. 413 // Empty cell w/3 neighbors goes live. 414 uint8_t n = getPixel(f, x, y); 415 if (n == 0) { // Pixel (x,y) is 'alive' 416 if ((neighbors < 2) || (neighbors > 3)) 417 n = 1; // Pixel 'dies' 418 } else { // Pixel (x,y) is 'dead' 419 if (neighbors == 3) 420 n = 0; // Wake up! 421 else if (n < colormap_max) 422 n += 1; // Decay 423 } 424 setPixel(f, x, y, n); 425 n *= 3; // Convert color index to RGB offset 426 canvas->SetPixel(xoffset + x, yoffset + y, colormap[n], 427 colormap[n + 1], colormap[n + 2]); 428 } 429 } 430 } 431 canvas = matrix->SwapOnVSync(canvas); 432 idx = newidx; 433 frames++; 434 if (now.tv_sec != prevsec) { 435 if (prevsec >= 0) { 436 printf("%f\n", frames / elapsed); 437 } 438 prevsec = now.tv_sec; 439 } 440 } 441 442 canvas->Clear(); 443 delete matrix; 444 445 return 0; 446 }