// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- // // This code is public domain // (but note, once linked against the led-matrix library, this is // covered by the GPL v2) // // This is a grab-bag of various demos and not very readable. #include "led-matrix.h" #include "pixel-mapper.h" #include "graphics.h" #include #include #include #include #include #include #include #include #include #include using std::min; using std::max; #define TERM_ERR "\033[1;31m" #define TERM_NORM "\033[0m" using namespace rgb_matrix; volatile bool interrupt_received = false; static void InterruptHandler(int signo) { interrupt_received = true; } class DemoRunner { protected: DemoRunner(Canvas *canvas) : canvas_(canvas) {} inline Canvas *canvas() { return canvas_; } public: virtual ~DemoRunner() {} virtual void Run() = 0; private: Canvas *const canvas_; }; /* * The following are demo image generators. They all use the utility * class DemoRunner to generate new frames. */ // Simple generator that pulses through RGB and White. class ColorPulseGenerator : public DemoRunner { public: ColorPulseGenerator(RGBMatrix *m) : DemoRunner(m), matrix_(m) { off_screen_canvas_ = m->CreateFrameCanvas(); } void Run() override { uint32_t continuum = 0; while (!interrupt_received) { usleep(5 * 1000); continuum += 1; continuum %= 3 * 255; int r = 0, g = 0, b = 0; if (continuum <= 255) { int c = continuum; b = 255 - c; r = c; } else if (continuum > 255 && continuum <= 511) { int c = continuum - 256; r = 255 - c; g = c; } else { int c = continuum - 512; g = 255 - c; b = c; } off_screen_canvas_->Fill(r, g, b); off_screen_canvas_ = matrix_->SwapOnVSync(off_screen_canvas_); } } private: RGBMatrix *const matrix_; FrameCanvas *off_screen_canvas_; }; // Simple generator that pulses through brightness on red, green, blue and white class BrightnessPulseGenerator : public DemoRunner { public: BrightnessPulseGenerator(RGBMatrix *m) : DemoRunner(m), matrix_(m) {} void Run() override { const uint8_t max_brightness = matrix_->brightness(); const uint8_t c = 255; uint8_t count = 0; while (!interrupt_received) { if (matrix_->brightness() < 1) { matrix_->SetBrightness(max_brightness); count++; } else { matrix_->SetBrightness(matrix_->brightness() - 1); } switch (count % 4) { case 0: matrix_->Fill(c, 0, 0); break; case 1: matrix_->Fill(0, c, 0); break; case 2: matrix_->Fill(0, 0, c); break; case 3: matrix_->Fill(c, c, c); break; } usleep(20 * 1000); } } private: RGBMatrix *const matrix_; }; class SimpleSquare : public DemoRunner { public: SimpleSquare(Canvas *m) : DemoRunner(m) {} void Run() override { const int width = canvas()->width() - 1; const int height = canvas()->height() - 1; // Borders DrawLine(canvas(), 0, 0, width, 0, Color(255, 0, 0)); DrawLine(canvas(), 0, height, width, height, Color(255, 255, 0)); DrawLine(canvas(), 0, 0, 0, height, Color(0, 0, 255)); DrawLine(canvas(), width, 0, width, height, Color(0, 255, 0)); // Diagonals. DrawLine(canvas(), 0, 0, width, height, Color(255, 255, 255)); DrawLine(canvas(), 0, height, width, 0, Color(255, 0, 255)); } }; class GrayScaleBlock : public DemoRunner { public: GrayScaleBlock(Canvas *m) : DemoRunner(m) {} void Run() override { const int sub_blocks = 16; const int width = canvas()->width(); const int height = canvas()->height(); const int x_step = max(1, width / sub_blocks); const int y_step = max(1, height / sub_blocks); uint8_t count = 0; while (!interrupt_received) { for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int c = sub_blocks * (y / y_step) + x / x_step; switch (count % 4) { case 0: canvas()->SetPixel(x, y, c, c, c); break; case 1: canvas()->SetPixel(x, y, c, 0, 0); break; case 2: canvas()->SetPixel(x, y, 0, c, 0); break; case 3: canvas()->SetPixel(x, y, 0, 0, c); break; } } } count++; sleep(2); } } }; // Simple class that generates a rotating block on the screen. class RotatingBlockGenerator : public DemoRunner { public: RotatingBlockGenerator(Canvas *m) : DemoRunner(m) {} uint8_t scale_col(int val, int lo, int hi) { if (val < lo) return 0; if (val > hi) return 255; return 255 * (val - lo) / (hi - lo); } void Run() override { const int cent_x = canvas()->width() / 2; const int cent_y = canvas()->height() / 2; // The square to rotate (inner square + black frame) needs to cover the // whole area, even if diagonal. Thus, when rotating, the outer pixels from // the previous frame are cleared. const int rotate_square = min(canvas()->width(), canvas()->height()) * 1.41; const int min_rotate = cent_x - rotate_square / 2; const int max_rotate = cent_x + rotate_square / 2; // The square to display is within the visible area. const int display_square = min(canvas()->width(), canvas()->height()) * 0.7; const int min_display = cent_x - display_square / 2; const int max_display = cent_x + display_square / 2; const float deg_to_rad = 2 * 3.14159265 / 360; int rotation = 0; while (!interrupt_received) { ++rotation; usleep(15 * 1000); rotation %= 360; for (int x = min_rotate; x < max_rotate; ++x) { for (int y = min_rotate; y < max_rotate; ++y) { float rot_x, rot_y; Rotate(x - cent_x, y - cent_x, deg_to_rad * rotation, &rot_x, &rot_y); if (x >= min_display && x < max_display && y >= min_display && y < max_display) { // within display square canvas()->SetPixel(rot_x + cent_x, rot_y + cent_y, scale_col(x, min_display, max_display), 255 - scale_col(y, min_display, max_display), scale_col(y, min_display, max_display)); } else { // black frame. canvas()->SetPixel(rot_x + cent_x, rot_y + cent_y, 0, 0, 0); } } } } } private: void Rotate(int x, int y, float angle, float *new_x, float *new_y) { *new_x = x * cosf(angle) - y * sinf(angle); *new_y = x * sinf(angle) + y * cosf(angle); } }; class ImageScroller : public DemoRunner { public: // Scroll image with "scroll_jumps" pixels every "scroll_ms" milliseconds. // If "scroll_ms" is negative, don't do any scrolling. ImageScroller(RGBMatrix *m, int scroll_jumps, int scroll_ms = 30) : DemoRunner(m), scroll_jumps_(scroll_jumps), scroll_ms_(scroll_ms), horizontal_position_(0), matrix_(m) { offscreen_ = matrix_->CreateFrameCanvas(); } // _very_ simplified. Can only read binary P6 PPM. Expects newlines in headers // Not really robust. Use at your own risk :) // This allows reload of an image while things are running, e.g. you can // live-update the content. bool LoadPPM(const char *filename) { FILE *f = fopen(filename, "r"); // check if file exists if (f == NULL && access(filename, F_OK) == -1) { fprintf(stderr, "File \"%s\" doesn't exist\n", filename); return false; } if (f == NULL) return false; char header_buf[256]; const char *line = ReadLine(f, header_buf, sizeof(header_buf)); #define EXIT_WITH_MSG(m) { fprintf(stderr, "%s: %s |%s", filename, m, line); \ fclose(f); return false; } if (sscanf(line, "P6 ") == EOF) EXIT_WITH_MSG("Can only handle P6 as PPM type."); line = ReadLine(f, header_buf, sizeof(header_buf)); int new_width, new_height; if (!line || sscanf(line, "%d %d ", &new_width, &new_height) != 2) EXIT_WITH_MSG("Width/height expected"); int value; line = ReadLine(f, header_buf, sizeof(header_buf)); if (!line || sscanf(line, "%d ", &value) != 1 || value != 255) EXIT_WITH_MSG("Only 255 for maxval allowed."); const size_t pixel_count = new_width * new_height; Pixel *new_image = new Pixel [ pixel_count ]; assert(sizeof(Pixel) == 3); // we make that assumption. if (fread(new_image, sizeof(Pixel), pixel_count, f) != pixel_count) { line = ""; EXIT_WITH_MSG("Not enough pixels read."); } #undef EXIT_WITH_MSG fclose(f); fprintf(stderr, "Read image '%s' with %dx%d\n", filename, new_width, new_height); horizontal_position_ = 0; MutexLock l(&mutex_new_image_); new_image_.Delete(); // in case we reload faster than is picked up new_image_.image = new_image; new_image_.width = new_width; new_image_.height = new_height; return true; } void Run() override { const int screen_height = offscreen_->height(); const int screen_width = offscreen_->width(); while (!interrupt_received) { { MutexLock l(&mutex_new_image_); if (new_image_.IsValid()) { current_image_.Delete(); current_image_ = new_image_; new_image_.Reset(); } } if (!current_image_.IsValid()) { usleep(100 * 1000); continue; } for (int x = 0; x < screen_width; ++x) { for (int y = 0; y < screen_height; ++y) { const Pixel &p = current_image_.getPixel( (horizontal_position_ + x) % current_image_.width, y); offscreen_->SetPixel(x, y, p.red, p.green, p.blue); } } offscreen_ = matrix_->SwapOnVSync(offscreen_); horizontal_position_ += scroll_jumps_; if (horizontal_position_ < 0) horizontal_position_ = current_image_.width; if (scroll_ms_ <= 0) { // No scrolling. We don't need the image anymore. current_image_.Delete(); } else { usleep(scroll_ms_ * 1000); } } } private: struct Pixel { Pixel() : red(0), green(0), blue(0){} uint8_t red; uint8_t green; uint8_t blue; }; struct Image { Image() : width(-1), height(-1), image(NULL) {} ~Image() { Delete(); } void Delete() { delete [] image; Reset(); } void Reset() { image = NULL; width = -1; height = -1; } inline bool IsValid() { return image && height > 0 && width > 0; } const Pixel &getPixel(int x, int y) { static Pixel black; if (x < 0 || x >= width || y < 0 || y >= height) return black; return image[x + width * y]; } int width; int height; Pixel *image; }; // Read line, skip comments. char *ReadLine(FILE *f, char *buffer, size_t len) { char *result; do { result = fgets(buffer, len, f); } while (result != NULL && result[0] == '#'); return result; } const int scroll_jumps_; const int scroll_ms_; // Current image is only manipulated in our thread. Image current_image_; // New image can be loaded from another thread, then taken over in main thread Mutex mutex_new_image_; Image new_image_; int32_t horizontal_position_; RGBMatrix* matrix_; FrameCanvas* offscreen_; }; // Abelian sandpile // Contributed by: Vliedel class Sandpile : public DemoRunner { public: Sandpile(Canvas *m, int delay_ms=50) : DemoRunner(m), delay_ms_(delay_ms) { width_ = canvas()->width() - 1; // We need an odd width height_ = canvas()->height() - 1; // We need an odd height // Allocate memory values_ = new int*[width_]; for (int x=0; xSetPixel(x, y, 0, 0, 0); break; case 1: canvas()->SetPixel(x, y, 0, 0, 200); break; case 2: canvas()->SetPixel(x, y, 0, 200, 0); break; case 3: canvas()->SetPixel(x, y, 150, 100, 0); break; default: canvas()->SetPixel(x, y, 200, 0, 0); } } } usleep(delay_ms_ * 1000); // ms } } private: void updateValues() { // Copy values to newValues for (int x=0; x 3) { // Collapse if (x>0) newValues_[x-1][y]++; if (x0) newValues_[x][y-1]++; if (ywidth(); height_ = canvas()->height(); // Allocate memory values_ = new int*[width_]; for (int x=0; xSetPixel(x, y, r_, g_, b_); else canvas()->SetPixel(x, y, 0, 0, 0); } } usleep(delay_ms_ * 1000); // ms } } private: int numAliveNeighbours(int x, int y) { int num=0; if (torus_) { // Edges are connected (torus) num += values_[(x-1+width_)%width_][(y-1+height_)%height_]; num += values_[(x-1+width_)%width_][y ]; num += values_[(x-1+width_)%width_][(y+1 )%height_]; num += values_[(x+1 )%width_][(y-1+height_)%height_]; num += values_[(x+1 )%width_][y ]; num += values_[(x+1 )%width_][(y+1 )%height_]; num += values_[x ][(y-1+height_)%height_]; num += values_[x ][(y+1 )%height_]; } else { // Edges are not connected (no torus) if (x>0) { if (y>0) num += values_[x-1][y-1]; if (y0) num += values_[x+1][y-1]; if (y<31) num += values_[x+1][y+1]; num += values_[x+1][y]; } if (y>0) num += values_[x][y-1]; if (y 3) newValues_[x][y] = 0; } else { // cell is dead if (num == 3) newValues_[x][y] = 1; } } } // copy newValues to values for (int x=0; xwidth(); height_ = canvas()->height(); values_ = new int*[width_]; for (int x=0; x= width_ || antY_ < 0 || antY_ >= height_) return; updatePixel(antX_, antY_); usleep(delay_ms_ * 1000); } } private: void updatePixel(int x, int y) { switch (values_[x][y]) { case 0: canvas()->SetPixel(x, y, 200, 0, 0); break; case 1: canvas()->SetPixel(x, y, 0, 200, 0); break; case 2: canvas()->SetPixel(x, y, 0, 0, 200); break; case 3: canvas()->SetPixel(x, y, 150, 100, 0); break; } if (x == antX_ && y == antY_) canvas()->SetPixel(x, y, 0, 0, 0); } int numColors_; int** values_; int antX_; int antY_; int antDir_; // 0 right, 1 up, 2 left, 3 down int delay_ms_; int width_; int height_; }; // Imitation of volume bars // Purely random height doesn't look realistic // Contributed by: Vliedel class VolumeBars : public DemoRunner { public: VolumeBars(Canvas *m, int delay_ms=50, int numBars=8) : DemoRunner(m), delay_ms_(delay_ms), numBars_(numBars), t_(0) { } ~VolumeBars() { delete [] barHeights_; delete [] barFreqs_; delete [] barMeans_; } void Run() override { const int width = canvas()->width(); height_ = canvas()->height(); barWidth_ = width/numBars_; barHeights_ = new int[numBars_]; barMeans_ = new int[numBars_]; barFreqs_ = new int[numBars_]; heightGreen_ = height_*4/12; heightYellow_ = height_*8/12; heightOrange_ = height_*10/12; heightRed_ = height_*12/12; // Array of possible bar means int numMeans = 10; int means[10] = {1,2,3,4,5,6,7,8,16,32}; for (int i=0; i= numMeans) barMeans_[i] = numMeans-1; if (barMeans_[i] < 0) barMeans_[i] = 0; } } // Update bar heights t_++; for (int i=0; iSetPixel(x, height_-1-y, r, g, b); } } int delay_ms_; int numBars_; int* barHeights_; int barWidth_; int height_; int heightGreen_; int heightYellow_; int heightOrange_; int heightRed_; int* barFreqs_; int* barMeans_; int t_; }; /// Genetic Colors /// A genetic algorithm to evolve colors /// by bbhsu2 + anonymous class GeneticColors : public DemoRunner { public: GeneticColors(Canvas *m, int delay_ms = 200) : DemoRunner(m), delay_ms_(delay_ms) { width_ = canvas()->width(); height_ = canvas()->height(); popSize_ = width_ * height_; // Allocate memory children_ = new citizen[popSize_]; parents_ = new citizen[popSize_]; srand(time(NULL)); } ~GeneticColors() { delete [] children_; delete [] parents_; } static int rnd (int i) { return rand() % i; } void Run() override { // Set a random target_ target_ = rand() & 0xFFFFFF; // Create the first generation of random children_ for (int i = 0; i < popSize_; ++i) { children_[i].dna = rand() & 0xFFFFFF; } while (!interrupt_received) { swap(); sort(); mate(); std::random_shuffle (children_, children_ + popSize_, rnd); // Draw citizens to canvas for(int i=0; i < popSize_; i++) { int c = children_[i].dna; int x = i % width_; int y = (int)(i / width_); canvas()->SetPixel(x, y, R(c), G(c), B(c)); } // When we reach the 85% fitness threshold... if(is85PercentFit()) { // ...set a new random target_ target_ = rand() & 0xFFFFFF; // Randomly mutate everyone for sake of new colors for (int i = 0; i < popSize_; ++i) { mutate(children_[i]); } } usleep(delay_ms_ * 1000); } } private: /// citizen will hold dna information, a 24-bit color value. struct citizen { citizen() { } citizen(int chrom) : dna(chrom) { } int dna; }; /// for sorting by fitness class comparer { public: comparer(int t) : target_(t) { } inline bool operator() (const citizen& c1, const citizen& c2) { return (calcFitness(c1.dna, target_) < calcFitness(c2.dna, target_)); } private: const int target_; }; static int R(const int cit) { return at(cit, 16); } static int G(const int cit) { return at(cit, 8); } static int B(const int cit) { return at(cit, 0); } static int at(const int v, const int offset) { return (v >> offset) & 0xFF; } /// fitness here is how "similar" the color is to the target static int calcFitness(const int value, const int target) { // Count the number of differing bits int diffBits = 0; for (unsigned int diff = value ^ target; diff; diff &= diff - 1) { ++diffBits; } return diffBits; } /// sort by fitness so the most fit citizens are at the top of parents_ /// this is to establish an elite population of greatest fitness /// the most fit members and some others are allowed to reproduce /// to the next generation void sort() { std::sort(parents_, parents_ + popSize_, comparer(target_)); } /// let the elites continue to the next generation children /// randomly select 2 parents of (near)elite fitness and determine /// how they will mate. after mating, randomly mutate citizens void mate() { // Adjust these for fun and profit const float eliteRate = 0.30f; const float mutationRate = 0.20f; const int numElite = popSize_ * eliteRate; for (int i = 0; i < numElite; ++i) { children_[i] = parents_[i]; } for (int i = numElite; i < popSize_; ++i) { //select the parents randomly const float sexuallyActive = 1.0 - eliteRate; const int p1 = rand() % (int)(popSize_ * sexuallyActive); const int p2 = rand() % (int)(popSize_ * sexuallyActive); const unsigned matingMask = (~0u) << (rand() % bitsPerPixel); // Make a baby unsigned baby = (parents_[p1].dna & matingMask) | (parents_[p2].dna & ~matingMask); children_[i].dna = baby; // Mutate randomly based on mutation rate if ((rand() / (float)RAND_MAX) < mutationRate) { mutate(children_[i]); } } } /// parents make children, /// children become parents, /// and they make children... void swap() { citizen* temp = parents_; parents_ = children_; children_ = temp; } void mutate(citizen& c) { // Flip a random bit c.dna ^= 1 << (rand() % bitsPerPixel); } /// can adjust this threshold to make transition to new target seamless bool is85PercentFit() { int numFit = 0; for (int i = 0; i < popSize_; ++i) { if (calcFitness(children_[i].dna, target_) < 1) { ++numFit; } } return ((numFit / (float)popSize_) > 0.85f); } static const int bitsPerPixel = 24; int popSize_; int width_, height_; int delay_ms_; int target_; citizen* children_; citizen* parents_; }; static int usage(const char *progname) { fprintf(stderr, "usage: %s -D [optional parameter]\n", progname); fprintf(stderr, "Options:\n"); fprintf(stderr, "\t-D : Always needs to be set\n" ); rgb_matrix::PrintMatrixFlags(stderr); fprintf(stderr, "Demos, choosen with -D\n"); fprintf(stderr, "\t0 - some rotating square\n" "\t1 - forward scrolling an image (-m )\n" "\t2 - backward scrolling an image (-m )\n" "\t3 - test image: a square\n" "\t4 - Pulsing color\n" "\t5 - Grayscale Block\n" "\t6 - Abelian sandpile model (-m )\n" "\t7 - Conway's game of life (-m )\n" "\t8 - Langton's ant (-m )\n" "\t9 - Volume bars (-m )\n" "\t10 - Evolution of color (-m )\n" "\t11 - Brightness pulse generator\n"); fprintf(stderr, "Example:\n\t%s -D 1 runtext.ppm\n" "Scrolls the runtext until Ctrl-C is pressed\n", progname); return 1; } int main(int argc, char *argv[]) { int demo = -1; int scroll_ms = 30; const char *demo_parameter = NULL; RGBMatrix::Options matrix_options; rgb_matrix::RuntimeOptions runtime_opt; // These are the defaults when no command-line flags are given. matrix_options.rows = 32; matrix_options.chain_length = 1; matrix_options.parallel = 1; // First things first: extract the command line flags that contain // relevant matrix options. if (!ParseOptionsFromFlags(&argc, &argv, &matrix_options, &runtime_opt)) { return usage(argv[0]); } int opt; while ((opt = getopt(argc, argv, "dD:r:P:c:p:b:m:LR:")) != -1) { switch (opt) { case 'D': demo = atoi(optarg); break; case 'm': scroll_ms = atoi(optarg); break; default: /* '?' */ return usage(argv[0]); } } if (optind < argc) { demo_parameter = argv[optind]; } if (demo < 0) { fprintf(stderr, TERM_ERR "Expected required option -D \n" TERM_NORM); return usage(argv[0]); } RGBMatrix *matrix = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt); if (matrix == NULL) return 1; printf("Size: %dx%d. Hardware gpio mapping: %s\n", matrix->width(), matrix->height(), matrix_options.hardware_mapping); Canvas *canvas = matrix; // The DemoRunner objects are filling // the matrix continuously. DemoRunner *demo_runner = NULL; switch (demo) { case 0: demo_runner = new RotatingBlockGenerator(canvas); break; case 1: case 2: if (demo_parameter) { ImageScroller *scroller = new ImageScroller(matrix, demo == 1 ? 1 : -1, scroll_ms); if (!scroller->LoadPPM(demo_parameter)) return 1; demo_runner = scroller; } else { fprintf(stderr, "Demo %d Requires PPM image as parameter\n", demo); return 1; } break; case 3: demo_runner = new SimpleSquare(canvas); break; case 4: demo_runner = new ColorPulseGenerator(matrix); break; case 5: demo_runner = new GrayScaleBlock(canvas); break; case 6: demo_runner = new Sandpile(canvas, scroll_ms); break; case 7: demo_runner = new GameLife(canvas, scroll_ms); break; case 8: demo_runner = new Ant(canvas, scroll_ms); break; case 9: demo_runner = new VolumeBars(canvas, scroll_ms, canvas->width()/2); break; case 10: demo_runner = new GeneticColors(canvas, scroll_ms); break; case 11: demo_runner = new BrightnessPulseGenerator(matrix); break; } if (demo_runner == NULL) return usage(argv[0]); // Set up an interrupt handler to be able to stop animations while they go // on. Each demo tests for while (!interrupt_received) {}, // so they exit as soon as they get a signal. signal(SIGTERM, InterruptHandler); signal(SIGINT, InterruptHandler); printf("Press to exit and reset LEDs\n"); // Now, run our particular demo; it will exit when it sees interrupt_received. demo_runner->Run(); delete demo_runner; delete canvas; printf("Received CTRL-C. Exiting.\n"); return 0; }