You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
10 KiB
C++

// -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
// Copyright (C) 2015 Henner Zeller <h.zeller@acm.org>
//
// 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 <http://gnu.org/licenses/gpl-2.0.txt>
#include "led-matrix.h"
#include "graphics.h"
#include <algorithm>
#include <fstream>
#include <streambuf>
#include <string>
#include <getopt.h>
#include <math.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
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] [<text>| -i <filename>]\n", progname);
fprintf(stderr, "Takes text and scrolls it with speed -s\n");
fprintf(stderr, "Options:\n");
fprintf(stderr,
"\t-f <font-file> : Path to *.bdf-font to be used.\n"
"\t-i <textfile> : Input from file.\n"
"\t-s <speed> : 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 <loop-count> : Number of loops through the text. "
"-1 for endless (default)\n"
"\t-b <on-time>,<off-time> : Blink while scrolling. Keep "
"on and off for these amount of scrolled pixels.\n"
"\t-x <x-origin> : Shift X-Origin of displaying text (Default: 0)\n"
"\t-y <y-origin> : Shift Y-Origin of displaying text (Default: 0)\n"
"\t-t <track-spacing>: Spacing pixels between letters (Default: 0)\n"
"\n"
"\t-C <r,g,b> : Text Color. Default 255,255,255 (white)\n"
"\t-B <r,g,b> : Background-Color. Default 0,0,0\n"
"\t-O <r,g,b> : 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<char>(fs)),
std::istreambuf_iterator<char>());
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;
}