// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*- // Copyright (C) 2015 Henner Zeller // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation version 2. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see #include "led-matrix.h" #include "graphics.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace rgb_matrix; volatile bool interrupt_received = false; static void InterruptHandler(int signo) { interrupt_received = true; } static int usage(const char *progname) { fprintf(stderr, "usage: %s [options] [| -i ]\n", progname); fprintf(stderr, "Takes text and scrolls it with speed -s\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, "\t-f : Path to *.bdf-font to be used.\n" "\t-i : Input from file.\n" "\t-s : Approximate letters per second. \n" "\t Positive: scroll right to left; Negative: scroll left to right\n" "\t (Zero for no scrolling)\n" "\t-l : Number of loops through the text. " "-1 for endless (default)\n" "\t-b , : Blink while scrolling. Keep " "on and off for these amount of scrolled pixels.\n" "\t-x : Shift X-Origin of displaying text (Default: 0)\n" "\t-y : Shift Y-Origin of displaying text (Default: 0)\n" "\t-t : Spacing pixels between letters (Default: 0)\n" "\n" "\t-C : Text Color. Default 255,255,255 (white)\n" "\t-B : Background-Color. Default 0,0,0\n" "\t-O : Outline-Color, e.g. to increase contrast.\n" ); fprintf(stderr, "\nGeneral LED matrix options:\n"); rgb_matrix::PrintMatrixFlags(stderr); return 1; } static bool parseColor(Color *c, const char *str) { return sscanf(str, "%hhu,%hhu,%hhu", &c->r, &c->g, &c->b) == 3; } static bool FullSaturation(const Color &c) { return (c.r == 0 || c.r == 255) && (c.g == 0 || c.g == 255) && (c.b == 0 || c.b == 255); } static void add_micros(struct timespec *accumulator, long micros) { const long billion = 1000000000; const int64_t nanos = (int64_t) micros * 1000; accumulator->tv_sec += nanos / billion; accumulator->tv_nsec += nanos % billion; while (accumulator->tv_nsec > billion) { accumulator->tv_nsec -= billion; accumulator->tv_sec += 1; } } // Read line and return if it changed. typedef uint64_t stat_fingerprint_t; static bool ReadLineOnChange(const char *filename, std::string *out, stat_fingerprint_t *last_file_status) { struct stat sb; if (stat(filename, &sb) < 0) { perror("Couldn't determine file change"); return false; } const stat_fingerprint_t fp = ((uint64_t)sb.st_mtime << 32) + sb.st_size; if (fp == *last_file_status) { return false; // no change according to stat() } *last_file_status = fp; std::ifstream fs(filename); std::string str((std::istreambuf_iterator(fs)), std::istreambuf_iterator()); std::replace(str.begin(), str.end(), '\n', ' '); if (*out == str) { return false; // no content change } *out = str; return true; } int main(int argc, char *argv[]) { RGBMatrix::Options matrix_options; rgb_matrix::RuntimeOptions runtime_opt; // If started with 'sudo': make sure to drop privileges to same user // we started with, which is the most expected (and allows us to read // files as that user). runtime_opt.drop_priv_user = getenv("SUDO_UID"); runtime_opt.drop_priv_group = getenv("SUDO_GID"); if (!rgb_matrix::ParseOptionsFromFlags(&argc, &argv, &matrix_options, &runtime_opt)) { return usage(argv[0]); } Color color(255, 255, 255); Color bg_color(0, 0, 0); Color outline_color(0,0,0); bool with_outline = false; const char *bdf_font_file = NULL; const char *input_file = NULL; std::string line; bool xorigin_configured = false; int x_orig = 0; int y_orig = 0; int letter_spacing = 0; float speed = 7.0f; int loops = -1; int blink_on = 0; int blink_off = 0; int opt; while ((opt = getopt(argc, argv, "x:y:f:C:B:O:t:s:l:b:i:")) != -1) { switch (opt) { case 's': speed = atof(optarg); break; case 'b': if (sscanf(optarg, "%d,%d", &blink_on, &blink_off) == 1) { blink_off = blink_on; } fprintf(stderr, "hz: on=%d off=%d\n", blink_on, blink_off); break; case 'l': loops = atoi(optarg); break; case 'x': x_orig = atoi(optarg); xorigin_configured = true; break; case 'y': y_orig = atoi(optarg); break; case 'f': bdf_font_file = strdup(optarg); break; case 'i': input_file = strdup(optarg); break; case 't': letter_spacing = atoi(optarg); break; case 'C': if (!parseColor(&color, optarg)) { fprintf(stderr, "Invalid color spec: %s\n", optarg); return usage(argv[0]); } break; case 'B': if (!parseColor(&bg_color, optarg)) { fprintf(stderr, "Invalid background color spec: %s\n", optarg); return usage(argv[0]); } break; case 'O': if (!parseColor(&outline_color, optarg)) { fprintf(stderr, "Invalid outline color spec: %s\n", optarg); return usage(argv[0]); } with_outline = true; break; default: return usage(argv[0]); } } stat_fingerprint_t last_change = 0; if (input_file) { if (!ReadLineOnChange(input_file, &line, &last_change)) { fprintf(stderr, "Couldn't read file '%s'\n", input_file); return usage(argv[0]); } } else { for (int i = optind; i < argc; ++i) { line.append(argv[i]).append(" "); } if (line.empty()) { fprintf(stderr, "Add the text you want to print on the command-line or -i for input file.\n"); return usage(argv[0]); } } if (bdf_font_file == NULL) { fprintf(stderr, "Need to specify BDF font-file with -f\n"); return usage(argv[0]); } /* * Load font. This needs to be a filename with a bdf bitmap font. */ rgb_matrix::Font font; if (!font.LoadFont(bdf_font_file)) { fprintf(stderr, "Couldn't load font '%s'\n", bdf_font_file); return 1; } /* * If we want an outline around the font, we create a new font with * the original font as a template that is just an outline font. */ rgb_matrix::Font *outline_font = NULL; if (with_outline) { outline_font = font.CreateOutlineFont(); } RGBMatrix *canvas = RGBMatrix::CreateFromOptions(matrix_options, runtime_opt); if (canvas == NULL) return 1; const bool all_extreme_colors = (matrix_options.brightness == 100) && FullSaturation(color) && FullSaturation(bg_color) && FullSaturation(outline_color); if (all_extreme_colors) canvas->SetPWMBits(1); signal(SIGTERM, InterruptHandler); signal(SIGINT, InterruptHandler); printf("CTRL-C for exit.\n"); // Create a new canvas to be used with led_matrix_swap_on_vsync FrameCanvas *offscreen_canvas = canvas->CreateFrameCanvas(); const int scroll_direction = (speed >= 0) ? -1 : 1; speed = fabs(speed); int delay_speed_usec = 1000000; if (speed > 0) { delay_speed_usec = 1000000 / speed / font.CharacterWidth('W'); } if (!xorigin_configured) { if (speed == 0) { // There would be no scrolling, so text would never appear. Move to front. x_orig = with_outline ? 1 : 0; } else { x_orig = scroll_direction < 0 ? canvas->width() : 0; } } int x = x_orig; int y = y_orig; int length = 0; struct timespec next_frame = {0, 0}; uint64_t frame_counter = 0; while (!interrupt_received && loops != 0) { if (input_file && ReadLineOnChange(input_file, &line, &last_change)) { x = x_orig; } ++frame_counter; offscreen_canvas->Fill(bg_color.r, bg_color.g, bg_color.b); const bool draw_on_frame = (blink_on <= 0) || (frame_counter % (blink_on + blink_off) < (uint64_t)blink_on); if (draw_on_frame) { if (outline_font) { // The outline font, we need to write with a negative (-2) text-spacing, // as we want to have the same letter pitch as the regular text that // we then write on top. rgb_matrix::DrawText(offscreen_canvas, *outline_font, x - 1, y + font.baseline(), outline_color, NULL, line.c_str(), letter_spacing - 2); } // length = holds how many pixels our text takes up length = rgb_matrix::DrawText(offscreen_canvas, font, x, y + font.baseline(), color, NULL, line.c_str(), letter_spacing); } x += scroll_direction; if ((scroll_direction < 0 && x + length < 0) || (scroll_direction > 0 && x > canvas->width())) { x = x_orig + ((scroll_direction > 0) ? -length : 0); if (loops > 0) --loops; } // Make sure render-time delays are not influencing scroll-time if (speed > 0) { if (next_frame.tv_sec == 0 && next_frame.tv_nsec == 0) { // First time. Start timer, but don't wait. clock_gettime(CLOCK_MONOTONIC, &next_frame); } else { add_micros(&next_frame, delay_speed_usec); clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_frame, NULL); } } // Swap the offscreen_canvas with canvas on vsync, avoids flickering offscreen_canvas = canvas->SwapOnVSync(offscreen_canvas); if (speed <= 0) pause(); // Nothing to scroll. } // Finished. Shut down the RGB matrix. canvas->Clear(); delete canvas; return 0; }