LCD-Newsticker für RSS mit Perl
In meinem letzten Beitrag habe ich ein LCD/LED-Gerät gebastelt. In diesem Artikel geht es um einen möglichen Anwendungsfall: Das Lesen von RSS-Feeds und deren Anzeige auf dem LCD-Bildschirm.
Die Bauanleitung für das LCD/LED-Gadget habe ich im letzten Artikel vorgestellt. Eigentlich wollte ich ein Tool zum Abrufen von E-Mails schreiben, leider führte ein Testskript von mir zu einem schweren Fehler (Segfault), da das Perl-Modul Mail::POP3Client im verschlüsselten Betrieb wiederum auf das SSL-Modul von Perl zurückgreift, welches offenbar irgendwo einen Null-Pointer verwendet (in seiner Verbindung zu openssl). Aus diesem Grund habe ich einen Newsreader in Perl implementiert, den Mail-Leser werde ich mit Java umsetzen.
Vorbereitungen
Die D2XX-Treiber und die entsprechenden Perl-Bindings müssen installiert sein, damit das Perl-Skript unten funktioniert. Eine detaillierte Anleitung findet man in meinem Artikel LED am USB-Port: Neue E-Mails anzeigen lassen unter den Überschriften „D2XX-Treiber installieren” und „FTD2XX-Bindings für Perl installieren”.
Ansonsten benötigt man eigentlich lediglich das unten stehende Skript. Individuelle Einstellungen lassen sich jederzeit im Kopfbereich (Zeilen 22–56) des Skripts vornehmen.
Aufruf und Ergebnis
Mann muss das Skript ausführbar machen (unter Linux z.B. mit chmod +x feedreader.pl). Einen beispielhaften Aufruf des Skripts sieht man in folgendem Video:
Erläuterungen zum Skript
Das Skript ruft eine Server-Subroutine auf, welche zwei weitere Threads startet: Einem für die LED und einen für die LCD-Anzeige. Der Hauptthread kümmert sich selbst um die Abfrage des Feeds.
Demzufolge teilt sich das Skript in mehrere logische Teile:
- FTDI-Steuerung allgemein (Zeilen 61–112): Hier wird der FTDI-Kontext geladen bzw. geschlossen.
- LED-Funktionen (Zeilen 118–124): Diese kapseln die FTDI-Aufrufe in einfache Subroutinen.
- LCD-Routinen (Zeilen 130–248): Bei diesen handelt es sich um allgemeine Routinen zum Ansprechen des LCDs. Bei den Routinen habe ich mich von jenen im Arduino-Playground inspirieren lassen.
- Blink-Thread (Zeilen 254–275): Der Blink-Thread kümmert sich um die LED im Gerät. Es gibt zwei globale Variablen, die gesetzt werden können:
blinkLEDlässt die Diode immer blinken.blinkUntilkann auf einen Unix-Timestamp (Anzahl der Sekunden seit 1.1.1970) gesetzt werden, um das Blinken an einem bestimmten Zeitpunkt einzustellen.blinkLEDbesitzt Priorität gegenüberblinkUntil. - LCD-Thread (Zeilen 281–348): Der Thread kümmert sich um die Anzeige im Gerät. Als wichtigste globale Variable dient der Array
@content, in dem die Titelüberschriften des Feeds gespeichert werden. Weitere globale Variablen verhindern konkurrente Zugriffe von Threads auf diese Variable und steuern die Beleuchtung der Anzeige (ähnlich wie für die LED). Der Thread selbst initialisiert zuerst die LCD-Anzeige und fährt einen Test, um Fehler zu prüfen. Dann geht es in die Hauptschleife. Falls Nachrichten vorliegen, wird zunächst geprüft, ob es neue Nachrichten gab, indem der erste Eintrag des Arrays mit einer gespeicherten Variable verglichen wird. Falls es neue Beiträge gab, wird das Blinken gesetzt und die LCD-Anzeige beleuchtet. Die restlichen Aufrufe im Thread kümmern sich um die Hintergrundbeleuchtung, die Position der Anzeige und schließlich die Anzeige selbst. Der Titel wird dabei abgeschnitten und ins ASCII-Format transformiert, damit die Anzeige keine fehlerhaften Zeichen darstellt. Die Schleife zählt am Ende immer einen Positionsanzeiger hoch und wartet eine Weile. Es werden also die Nachrichten hintereinander auf der LCD-Anzeige dargestellt. - Server-Thread (Zeilen 354–454): Der Server initialisiert anfangs den FTDI-Kontext und startet die beiden anderen Threads. Dann ruft er in regelmäßigen Abständen die Routine
getRSSauf. Diese lädt den Feed aus dem Internet und parst ihn mit Hilfe des Perl-ModulsXML::RSS. Falls keine Fehler auftreten, wird der globale Array@contentmit den Titelzeilen des Feeds überschrieben. - Hilfsfunktionen und Start (Zeilen 460–551): Am Ende folgen einige Hilfsfunktionen und der Start des eigentlichen Servers.
Es ist im Übrigen recht einfach, das Skript in Linux beim Laden des Systems als Daemon zu starten. Als Basis dient in der Regel eine „leere” Daemon-Datei namens /etc/init.d/skeleton.
Ansonsten wünsche ich viel Spaß mit dem Skript!
- Quelltext: Alles auswählen | Zeilennummerierung an/aus
-
- #!/usr/bin/perl -w
- # RSS-Feedreader für FTDI LCD/LED
- # Beschrieben unter http://auxc.de/lo
- # Installation: d2xx-Pakte sind notwendig (s. Link oben).
- # Debian/Ubuntu-Pakete: libxml-rss-perl, libwww-perl,
- # libtext-iconv-perl
- use strict;
- use warnings;
- use ExtUtils::testlib; #nur nötig, wenn man FTDI-Bindings nicht global installiert
- use XML::RSS;
- use LWP::UserAgent;
- use Text::Iconv;
- use threads;
- use threads::shared;
- #fängt Signal auf und schließt sauber
- use sigtrap 'handler' => \&cleanAndExit, 'INT', 'ABRT', 'QUIT', 'TERM';
- ################################################################
- # Konfiguration - ändern!
- ################################################################
- #Seriennummer des Geräts - wird genommen, falls nicht leer
- my $dev_serial = '';
- #Nummer des Geräts - wird ignoriert, falls $dev_serial gesetzt
- my $dev_device_number = 0;
- #Name des Benutzers
- my $myName = 'Max';
- #RSS-Feed, der abgefragt wird, z.B. Spiegel Online oder Heise
- my $url = 'http://www.spiegel.de/schlagzeilen/index.rss';
- #my $url = 'http://www.heise.de/mobil/newsticker/heise.rdf';
- #Anzahl der Nachrichten, die maximal angezeigt werden
- my $maxNews = 10;
- #Intervall in Sekunden, in der die Nachrichten gewechselt werden
- my $showSpeed = 5;
- #Intervall der Abfrage in Sekunden, 60*5 ist z.B. alle 5 Minuten
- my $interval = 60*5;
- #Anzahl der Sekunden, die der Blinker bei neuen Nachrichten blinken soll
- my $blinkFor = 60*5;
- #Anzahl der Sekunden, die das LCD bei neuen Nachrichten beleuchtet bleiben soll
- my $lightFor = 60;
- ################################################################
- # Konfiguration - Ende
- ################################################################
- ################################################################
- # FTDI-Einstellungen kümmern sich um die FTDI-Verbindung
- ################################################################
- my $ftdi_dev;
- my $text_econding = ''; #speichert Encoding des Feeds
- #FTDI start
- sub ftdiStart {
- &FTDI::D2XX::FT_CreateDeviceInfoList(my $numDevices);
- if ($numDevices == 0) {
- }
- #Gerät suchen - nach Seriennummer
- if ($dev_serial ne '') {
- my $ftStatus =
- &FTDI::D2XX::FT_OpenEx($dev_serial,
- FTDI::D2XX::FT_OPEN_BY_SERIAL_NUMBER, $ftdi_dev);
- if ($ftStatus != 0) {
- } else {
- }
- } else { #nach Nummer
- my $ftStatus =
- &FTDI::D2XX::FT_Open($dev_device_number, $ftdi_dev);
- if ($ftStatus != 0) {
- } else { #Dummy-Variablen
- my $pftType = my $lpdwID = 0;
- my $pcSerialNumber = my $pcDescription = '';
- &FTDI::D2XX::FT_GetDeviceInfo($ftdi_dev, $pftType,
- $lpdwID, $pcSerialNumber, $pcDescription, 0);
- }
- }
- #Modus für serielle Verbindung zum LCD setzen
- FTDI::D2XX::FT_SetBitMode($ftdi_dev, 0xFF, FT_BITMODE_MPSSE);
- FTDI::D2XX::FT_SetBaudRate($ftdi_dev, 9600);
- FTDI::D2XX::FT_SetDataCharacteristics($ftdi_dev, FT_BITS_8,
- FT_STOP_BITS_1, FT_PARITY_NONE);
- FTDI::D2XX::FT_SetFlowControl($ftdi_dev, FT_FLOW_NONE, 0, 0);
- }
- #FTDI ende
- sub ftdiStop {
- &FTDI::D2XX::FT_Close($ftdi_dev);
- }
- ################################################################
- # LED-Einstellungen - einfach
- ################################################################
- sub setLED {
- FTDI::D2XX::FT_SetDtr($ftdi_dev);
- }
- sub clearLED {
- FTDI::D2XX::FT_ClrDtr($ftdi_dev);
- }
- ################################################################
- # LCD-Einstellungen - etwas komplexer
- ################################################################
- #Schreibt einen Text ins LCD-Display
- sub writeLCD {
- my $text = $_[0];
- #Text konvertierten
- my $converter = Text::Iconv->new($text_econding, "ASCII//TRANSLIT");
- my $converted = $converter->convert($text);
- #in char-Array umwandeln
- my $length = $#data + 1;
- #an LCD schicken
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, $length, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #Lösche LCD-Anzeige
- sub clearLCD {
- my @data;
- $data[0]=0xFE;
- $data[1]=1;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #Hintergrundlicht an
- sub backLightOn {
- my @data;
- $data[0]=0x7C;
- $data[1]=157;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #Hintergrundlicht an
- sub backLightOff {
- my @data;
- $data[0]=0x7C;
- $data[1]=128;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #Hintergrundlicht auf 50%
- sub backLightHalf {
- my @data;
- $data[0]=0x7C;
- $data[1]=143;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #in erste Zeile springen
- sub selectLineOne {
- my @data;
- $data[0]=0xFE;
- $data[1]=128;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #in zweite Zeile springen
- sub selectLineTwo {
- my @data;
- $data[0]=0xFE;
- $data[1]=192;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #in zweite Zeile springen
- sub selectLineThree {
- my @data;
- $data[0]=0xFE;
- $data[1]=148;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #in zweite Zeile springen
- sub selectLineFour {
- my @data;
- $data[0]=0xFE;
- $data[1]=212;
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- #zu einer bestimmten Cursor-Position springen
- sub goTo {
- my $pos = $_[0];
- my @data;
- $data[0]=0xFE;
- #Fehler abfangen
- #erste Zeile
- elsif ($pos < 20) {
- $data[1]= 128 + $pos;
- } elsif ($pos < 40) {
- $data[1]= 172 + $pos; # pos + 128 + 64 - 20
- } elsif ($pos < 60) {
- $data[1]= 108 + $pos; # pos + 128 + 20 - 40
- } elsif ($pos < 80) {
- $data[1]= 152 + $pos; # pos + 128 + 84 - 60
- FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
- sleepMilli(0.02); #Puffer
- }
- ################################################################
- # Blink-Thread für das Gerät
- ################################################################
- #1, falls das LED blinken soll
- my $blinkLED :shared = 0;
- #wie lange soll ich blinken?
- my $blinkUntil :shared = 0;
- sub blinkThread {
- while (1) {
- #blinken, falls Ende nicht gesetzt oder unter der Zeit
- &setLED;
- }
- sleepMilli(0.5);
- &clearLED;
- sleepMilli(0.5);
- }
- }
- #Starte das Blinken
- sub startBlinking {
- #neues Blinkende vereinbaren
- }
- ################################################################
- # LCD-Thread für das Gerät
- ################################################################
- #enthält Nachrichten-Köpfe
- my @content :shared;
- #1, falls gerade ein Update des contents gefahren wird
- my $updating :shared = 0;
- #1, falls LCD gerade upgedated wird
- my $using :shared = 0;
- #wie lange soll Display beleuchtet bleiben
- my $lightedUntil :shared = 0;
- sub lcdThread {
- my $needClean = 1;
- my $pos = 0;
- my $firstEntry = '';
- while (1) {
- #Falls Content leer ist...
- if ($#content < 1) {
- if ($needClean == 1) { #ggf. Bildschim leeren)
- clearLCD();
- writeLCD("Warten...");
- $needClean = 0;
- }
- } else { #es sind Einträge vorhanden
- #auf Server-Thread warten
- while($updating == 1) { #falls im Update-Prozess
- sleepMilli(100);
- }
- $using = 1; #reservieren
- #gab es Änderungen?
- if ($firstEntry ne $content[0]) {
- #Blinker für eine Weile starten
- &startBlinking;
- #neuen Eintrag speichern
- $firstEntry = $content[0];
- #Position zurücksetzen
- $pos = 0;
- #LCD einschalten
- &backLightOn;
- #Backlight timer setzen
- }
- #LCD nach Ende des Timers ausschalten
- &backLightOff;
- }
- #Anzeigen durchlaufen und nun zurücksetzen?
- if ($pos > $#content) {
- $pos = 0;
- }
- #Anzeigen durchlaufen - hole eine
- my $text = $content[$pos];
- #Formatieren
- my $converter = Text::Iconv->new($text_econding, "ASCII//TRANSLIT");
- $text = $converter->convert($text);
- #ggf. Kürzen
- }
- #an LCD
- clearLCD();
- writeLCD($text);
- $pos++; #nächstes Mal eine weitere
- $using = 0; #freigeben
- }
- }
- }
- ################################################################
- # Hauptthread - Serverfunktionen
- ################################################################
- #Initialisierung
- sub initServer {
- #FTDI starten
- &ftdiStart;
- #FTDI-Funktionstest
- &functionTest;
- #Blink-Thread starten
- my $blinker = threads->new(\&blinkThread);
- $blinker->detach;
- #LCD-Thread starten
- my $lcdviewer = threads->new(\&lcdThread);
- $lcdviewer->detach;
- }
- #Server-Schleife
- sub startServer {
- #initialiseren des Servers + der anderen Threads
- &initServer;
- my $lastcheck = 0; #letzter check
- while (1) {
- if ($lastcheck + $interval <= $now) {
- #auf LCD-Thread warten
- while($using == 1) { #falls im Using-Prozess
- sleepMilli(100);
- }
- #Prüfroutine aufrufen
- $updating = 1;
- @content = &getRSS;
- $updating = 0;
- #letzten Check hochsetzen
- $lastcheck = $now;
- }
- }
- }
- #Hole RSS-Feed aus dem Netz
- sub getRSS {
- # create UserAgent object
- my $ua = new LWP::UserAgent;
- # Timeout
- $ua->timeout(15);
- # Request losschicken
- my $request = HTTP::Request->new('GET');
- $request->url($url);
- my $response = $ua->request($request);
- #Nur Response 200 akzeptieren
- #TODO: Umleitungen automatisch auflösen
- if ($response->code != 200) {
- }
- #Hole Daten - Typ und Encoding
- my ($type, $encoding) =
- $response->header("Content-Type") =~ m/(.*);\s*charset=(.*)$/i;
- #Prüfe Content-Typ
- #TODO: evtl. andere Typen akzeptieren
- if (!($type =~ m/text\/xml/i)) {
- }
- #encoding global übernehmen
- $text_econding = $encoding;
- #RSS-Parser anwerfen
- my $rss = XML::RSS->new(version => '1.0', encoding => $encoding);
- #Parsen
- $rss->parse($response->content);
- #Maximal $maxNews Nachrichten anzeigen
- while (@{$rss->{'items'}} > $maxNews) {
- }
- #Rückgabe vorbereiten
- my @list = {};
- my $i = 0;
- foreach my $item (@{$rss->{'items'}}) {
- #print "Titel: ".$item->{'title'}."\n";
- $list[$i++] = $item->{'title'};
- }
- }
- ################################################################
- # Hilfsfunktionen
- ################################################################
- #Für Millisekunden schlafen
- sub sleepMilli {
- my $sleep = $_[0];
- }
- #Funktionstest
- sub functionTest {
- clearLCD;
- clearLCD; #doppelt, da evt. Artefakte am Anfang existieren
- backLightOn;
- selectLineTwo;
- writeLCD("Hallo ".$myName."!");
- setLED;
- sleepMilli(0.5);
- clearLED;
- backLightOff;
- sleepMilli(0.5);
- setLED;
- backLightOn;
- sleepMilli(0.5);
- selectLineFour;
- writeLCD("Initialisierung ok.");
- goTo(19);
- writeLCD("*");
- goTo(39);
- writeLCD("*");
- goTo(59);
- writeLCD("*");
- goTo(79);
- writeLCD("*");
- sleepMilli(0.5);
- clearLED;
- backLightOff;
- clearLCD;
- }
- #sauber beenden
- sub cleanAndExit(){
- #LCD und LED löschen
- &backLightOff;
- &clearLED;
- &clearLCD;
- #FTDI beenden
- &ftdiStop;
- }
- ################################################################
- #Main: Server starten
- &startServer;
- 0 Kommentare