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++
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;
|
|
}
|