| 
18.03.2010
 | 
15:40

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:

  1. FTDI-Steuerung allgemein (Zeilen 61–112): Hier wird der FTDI-Kontext geladen bzw. geschlossen.
  2. LED-Funktionen (Zeilen 118–124): Diese kapseln die FTDI-Aufrufe in einfache Subroutinen.
  3. 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.
  4. 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: blinkLED lässt die Diode immer blinken. blinkUntil kann auf einen Unix-Timestamp (Anzahl der Sekunden seit 1.1.1970) gesetzt werden, um das Blinken an einem bestimmten Zeitpunkt einzustellen. blinkLED besitzt Priorität gegenüber blinkUntil.
  5. 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.
  6. 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 getRSS auf. Diese lädt den Feed aus dem Internet und parst ihn mit Hilfe des Perl-Moduls XML::RSS. Falls keine Fehler auftreten, wird der globale Array @content mit den Titelzeilen des Feeds überschrieben.
  7. 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
  1. #!/usr/bin/perl -w
  2. # RSS-Feedreader für FTDI LCD/LED
  3. # Beschrieben unter http://auxc.de/lo
  4.  
  5. # Installation: d2xx-Pakte sind notwendig (s. Link oben).
  6. # Debian/Ubuntu-Pakete: libxml-rss-perl, libwww-perl,
  7. #    libtext-iconv-perl
  8.  
  9. use strict;
  10. use warnings;
  11.  
  12. use ExtUtils::testlib; #nur nötig, wenn man FTDI-Bindings nicht global installiert
  13. use FTDI::D2XX qw(FT_BITS_8 FT_STOP_BITS_1 FT_PARITY_NONE FT_FLOW_NONE FT_BITMODE_MPSSE);
  14. use XML::RSS;
  15. use LWP::UserAgent;
  16. use Text::Iconv;
  17. use threads;
  18. use threads::shared;
  19. #fängt Signal auf und schließt sauber
  20. use sigtrap 'handler' => \&cleanAndExit, 'INT', 'ABRT', 'QUIT', 'TERM';
  21.  
  22. ################################################################
  23. # Konfiguration - ändern!
  24. ################################################################
  25.  
  26. #Seriennummer des Geräts - wird genommen, falls nicht leer
  27. my $dev_serial = '';
  28. #Nummer des Geräts - wird ignoriert, falls $dev_serial gesetzt
  29. my $dev_device_number = 0;
  30.  
  31. #Name des Benutzers
  32. my $myName = 'Max';
  33.  
  34. #RSS-Feed, der abgefragt wird, z.B. Spiegel Online oder Heise
  35. my $url = 'http://www.spiegel.de/schlagzeilen/index.rss';
  36. #my $url = 'http://www.heise.de/mobil/newsticker/heise.rdf';
  37.  
  38. #Anzahl der Nachrichten, die maximal angezeigt werden
  39. my $maxNews = 10;
  40.  
  41. #Intervall in Sekunden, in der die Nachrichten gewechselt werden
  42. my $showSpeed = 5;
  43.  
  44. #Intervall der Abfrage in Sekunden, 60*5 ist z.B. alle 5 Minuten
  45. my $interval = 60*5;
  46.  
  47. #Anzahl der Sekunden, die der Blinker bei neuen Nachrichten blinken soll
  48. my $blinkFor = 60*5;
  49.  
  50. #Anzahl der Sekunden, die das LCD bei neuen Nachrichten beleuchtet bleiben soll
  51. my $lightFor = 60;
  52.  
  53. ################################################################
  54. # Konfiguration - Ende
  55. ################################################################
  56.  
  57. ################################################################
  58. # FTDI-Einstellungen kümmern sich um die FTDI-Verbindung
  59. ################################################################
  60. my $ftdi_dev;
  61. my $text_econding = ''; #speichert Encoding des Feeds
  62.  
  63. #FTDI start
  64. sub ftdiStart {
  65.     &FTDI::D2XX::FT_CreateDeviceInfoList(my $numDevices);
  66.     print $numDevices." Gerät(e) gefunden. ";
  67.     if ($numDevices == 0) {
  68.         print "Beende, da kein Gerät bereit.\n";
  69.         exit(1);
  70.     }
  71.    
  72.     #Gerät suchen - nach Seriennummer
  73.     if ($dev_serial ne '') {
  74.         print "Suche Gerät mit Seriennummer ".$dev_serial.".\n";
  75.         my $ftStatus =
  76.             &FTDI::D2XX::FT_OpenEx($dev_serial,
  77.                 FTDI::D2XX::FT_OPEN_BY_SERIAL_NUMBER, $ftdi_dev);
  78.         if ($ftStatus != 0) {
  79.             print "Gerät wurde nicht gefunden!\n";
  80.             exit(2);
  81.         } else {
  82.             print "Gerät wurde gefunden - alles ok!\n";
  83.         }
  84.     } else { #nach Nummer
  85.         print "Öffne Gerät #".$dev_device_number.".\n";
  86.         my $ftStatus =
  87.             &FTDI::D2XX::FT_Open($dev_device_number, $ftdi_dev);
  88.         if ($ftStatus != 0) {
  89.             print "Gerät wurde nicht gefunden!\n";
  90.             exit(2);
  91.         } else { #Dummy-Variablen
  92.             my $pftType = my $lpdwID = 0;
  93.             my $pcSerialNumber = my $pcDescription = '';
  94.             &FTDI::D2XX::FT_GetDeviceInfo($ftdi_dev, $pftType,
  95.                 $lpdwID, $pcSerialNumber, $pcDescription, 0);
  96.             print "Gerät [SN: ".$pcSerialNumber."] wurde gefunden - alles ok!\n";
  97.         }
  98.     }
  99.  
  100.     #Modus für serielle Verbindung zum LCD setzen
  101.     FTDI::D2XX::FT_SetBitMode($ftdi_dev, 0xFF, FT_BITMODE_MPSSE);
  102.     FTDI::D2XX::FT_SetBaudRate($ftdi_dev, 9600);
  103.     FTDI::D2XX::FT_SetDataCharacteristics($ftdi_dev, FT_BITS_8,
  104.         FT_STOP_BITS_1, FT_PARITY_NONE);
  105.     FTDI::D2XX::FT_SetFlowControl($ftdi_dev, FT_FLOW_NONE, 0, 0);
  106. }
  107.  
  108. #FTDI ende
  109. sub ftdiStop {
  110.     &FTDI::D2XX::FT_Close($ftdi_dev);
  111. }
  112.  
  113.  
  114. ################################################################
  115. # LED-Einstellungen - einfach
  116. ################################################################
  117. sub setLED {
  118.     FTDI::D2XX::FT_SetDtr($ftdi_dev);
  119. }
  120.  
  121. sub clearLED {
  122.     FTDI::D2XX::FT_ClrDtr($ftdi_dev);
  123. }
  124.  
  125. ################################################################
  126. # LCD-Einstellungen - etwas komplexer
  127. ################################################################
  128. #Schreibt einen Text ins LCD-Display
  129. sub writeLCD {
  130.     my $text = $_[0];
  131.     #Text konvertierten
  132.     my $converter = Text::Iconv->new($text_econding, "ASCII//TRANSLIT");
  133.     my $converted = $converter->convert($text);
  134.    
  135.     #in char-Array umwandeln
  136.     my @data = unpack("C*", $converted);
  137.     my $length = $#data + 1;
  138.  
  139.     #an LCD schicken
  140.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, $length, my $fb);
  141.     sleepMilli(0.02); #Puffer
  142. }
  143.  
  144. #Lösche LCD-Anzeige
  145. sub clearLCD {
  146.     my @data;
  147.     $data[0]=0xFE;
  148.     $data[1]=1;
  149.  
  150.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  151.     sleepMilli(0.02); #Puffer
  152. }
  153.  
  154. #Hintergrundlicht an
  155. sub backLightOn {
  156.     my @data;
  157.     $data[0]=0x7C;
  158.     $data[1]=157;
  159.  
  160.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  161.     sleepMilli(0.02); #Puffer
  162. }
  163.  
  164. #Hintergrundlicht an
  165. sub backLightOff {
  166.     my @data;
  167.     $data[0]=0x7C;
  168.     $data[1]=128;
  169.  
  170.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  171.     sleepMilli(0.02); #Puffer
  172. }
  173.  
  174. #Hintergrundlicht auf 50%
  175. sub backLightHalf {
  176.     my @data;
  177.     $data[0]=0x7C;
  178.     $data[1]=143;
  179.  
  180.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  181.     sleepMilli(0.02); #Puffer
  182. }
  183.  
  184. #in erste Zeile springen
  185. sub selectLineOne {
  186.     my @data;
  187.     $data[0]=0xFE;
  188.     $data[1]=128;
  189.  
  190.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  191.     sleepMilli(0.02); #Puffer
  192. }
  193.  
  194. #in zweite Zeile springen
  195. sub selectLineTwo {
  196.     my @data;
  197.     $data[0]=0xFE;
  198.     $data[1]=192;
  199.  
  200.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  201.     sleepMilli(0.02); #Puffer
  202. }
  203.  
  204.  
  205. #in zweite Zeile springen
  206. sub selectLineThree {
  207.     my @data;
  208.     $data[0]=0xFE;
  209.     $data[1]=148;
  210.  
  211.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  212.     sleepMilli(0.02); #Puffer
  213. }
  214.  
  215.  
  216. #in zweite Zeile springen
  217. sub selectLineFour {
  218.     my @data;
  219.     $data[0]=0xFE;
  220.     $data[1]=212;
  221.  
  222.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  223.     sleepMilli(0.02); #Puffer
  224. }
  225.  
  226. #zu einer bestimmten Cursor-Position springen
  227. sub goTo {
  228.     my $pos = $_[0];
  229.     my @data;
  230.     $data[0]=0xFE;
  231.  
  232.     #Fehler abfangen
  233.     if ($pos < 0) { return; }
  234.     #erste Zeile
  235.     elsif ($pos < 20) {
  236.         $data[1]= 128 + $pos;
  237.     } elsif  ($pos < 40) {
  238.         $data[1]= 172 + $pos; # pos + 128 + 64 - 20
  239.     } elsif  ($pos < 60) {
  240.         $data[1]= 108 + $pos; # pos + 128 + 20 - 40
  241.     } elsif  ($pos < 80) {
  242.         $data[1]= 152 + $pos; # pos + 128 + 84 - 60
  243.     }  else { return; }
  244.  
  245.     FTDI::D2XX::FT_Write($ftdi_dev,\@data, 2, my $fb);
  246.     sleepMilli(0.02); #Puffer
  247. }
  248.  
  249. ################################################################
  250. # Blink-Thread für das Gerät
  251. ################################################################
  252. #1, falls das LED blinken soll
  253. my $blinkLED :shared = 0;
  254. #wie lange soll ich blinken?
  255. my $blinkUntil :shared = 0;
  256.  
  257. sub blinkThread {
  258.     print "Starte Blink-Thread.\n";
  259.     while (1) {
  260.     #blinken, falls Ende nicht gesetzt oder unter der Zeit
  261.         if ($blinkLED == 1 || $blinkUntil >= time) {
  262.             &setLED;
  263.         }
  264.         sleepMilli(0.5);
  265.         &clearLED;
  266.         sleepMilli(0.5);
  267.     }
  268. }
  269.  
  270. #Starte das Blinken
  271. sub startBlinking {
  272.     #neues Blinkende vereinbaren
  273.     $blinkUntil = time + $blinkFor;
  274. }
  275.  
  276. ################################################################
  277. # LCD-Thread für das Gerät
  278. ################################################################
  279. #enthält Nachrichten-Köpfe
  280. my @content :shared;
  281. #1, falls gerade ein Update des contents gefahren wird
  282. my $updating :shared = 0;
  283. #1, falls LCD gerade upgedated wird
  284. my $using :shared = 0;
  285. #wie lange soll Display beleuchtet bleiben
  286. my $lightedUntil :shared = 0;
  287.  
  288. sub lcdThread {
  289.     my $needClean = 1;
  290.     my $pos = 0;
  291.     my $firstEntry = '';
  292.     print "Starte LCD-Thread.\n";
  293.     while (1) {
  294.         #Falls Content leer ist...
  295.         if ($#content < 1) {
  296.             if ($needClean == 1) { #ggf. Bildschim leeren)
  297.                 clearLCD();
  298.                 writeLCD("Warten...");
  299.                 $needClean = 0;
  300.             }
  301.         } else { #es sind Einträge vorhanden
  302.             #auf Server-Thread warten
  303.             while($updating == 1) { #falls im Update-Prozess
  304.                 sleepMilli(100);
  305.             }
  306.             $using = 1; #reservieren
  307.             #gab es Änderungen?
  308.             if ($firstEntry ne $content[0]) {
  309.                 print "Neue Nachricht heruntergeladen!\n";
  310.                 #Blinker für eine Weile starten
  311.                 &startBlinking;
  312.                 #neuen Eintrag speichern
  313.                 $firstEntry = $content[0];
  314.                 #Position zurücksetzen
  315.                 $pos = 0;
  316.                 #LCD einschalten
  317.                 &backLightOn;
  318.                 #Backlight timer setzen
  319.                 $lightedUntil = time + $lightFor;
  320.             }
  321.             #LCD nach Ende des Timers ausschalten
  322.             if ($lightedUntil <= time) {
  323.                 &backLightOff;
  324.             }
  325.             #Anzeigen durchlaufen und nun zurücksetzen?
  326.             if ($pos > $#content) {
  327.                 $pos = 0;
  328.             }
  329.             #Anzeigen durchlaufen - hole eine
  330.             my $text = $content[$pos];
  331.             #Formatieren
  332.             my $converter = Text::Iconv->new($text_econding, "ASCII//TRANSLIT");
  333.             $text = $converter->convert($text);
  334.             #ggf. Kürzen
  335.             if (length($text) > 80) {
  336.                 $text = substr($text,0,77).'...';
  337.             }
  338.             #an LCD
  339.             clearLCD();
  340.             writeLCD($text);
  341.            
  342.             $pos++; #nächstes Mal eine weitere
  343.             $using = 0; #freigeben
  344.         }
  345.         sleep($showSpeed); #$showSpeed Sekunden schlafen
  346.     }
  347. }
  348.  
  349. ################################################################
  350. # Hauptthread - Serverfunktionen
  351. ################################################################
  352. #Initialisierung
  353. sub initServer {
  354.     #FTDI starten
  355.     &ftdiStart;
  356.  
  357.     #FTDI-Funktionstest
  358.     &functionTest;
  359.  
  360.     #Blink-Thread starten
  361.     my $blinker = threads->new(\&blinkThread);
  362.     $blinker->detach;
  363.  
  364.     #LCD-Thread starten
  365.     my $lcdviewer = threads->new(\&lcdThread);
  366.     $lcdviewer->detach;
  367. }
  368.  
  369. #Server-Schleife
  370. sub startServer {
  371.     #initialiseren des Servers + der anderen Threads
  372.     &initServer;
  373.  
  374.     my $lastcheck = 0; #letzter check
  375.     while (1) {
  376.         my $now = time; #Zeit in Sekunden
  377.         if ($lastcheck + $interval <= $now) {
  378.             #auf LCD-Thread warten
  379.             while($using == 1) { #falls im Using-Prozess
  380.                 sleepMilli(100);
  381.             }
  382.             #Prüfroutine aufrufen
  383.             $updating = 1;
  384.             @content = &getRSS;
  385.             $updating = 0;
  386.             #letzten Check hochsetzen
  387.             $lastcheck = $now;
  388.         }
  389.         sleep($interval); #$interval Sekunden Warten
  390.     }
  391. }
  392.  
  393. #Hole RSS-Feed aus dem Netz
  394. sub getRSS {
  395.     # create UserAgent object
  396.     my $ua = new LWP::UserAgent;
  397.  
  398.     # Timeout
  399.     $ua->timeout(15);
  400.      
  401.     # Request losschicken
  402.     print "Verbinde mit $url...\n";
  403.     my $request = HTTP::Request->new('GET');
  404.     $request->url($url);
  405.  
  406.     my $response = $ua->request($request);
  407.     print "Antwort war: ".$response->code."\n";
  408.    
  409.     #Nur Response 200 akzeptieren
  410.     #TODO: Umleitungen automatisch auflösen
  411.     if ($response->code != 200) {
  412.         print "Fehler: Inkorrekte Antwort - Abbruch der Abfrage.\n";
  413.         return;
  414.     }
  415.    
  416.     #Hole Daten - Typ und Encoding
  417.     print "Content-Typ: ".$response->header("Content-Type")."\n";
  418.  
  419.     my ($type, $encoding) =
  420.         $response->header("Content-Type") =~ m/(.*);\s*charset=(.*)$/i;
  421.    
  422.     #Prüfe Content-Typ
  423.     #TODO: evtl. andere Typen akzeptieren
  424.     if (!($type =~ m/text\/xml/i)) {
  425.         print "Fehler: Falscher Contenttyp - Abbruch der Abfrage.\n";
  426.         return;
  427.     }
  428.    
  429.     #encoding global übernehmen
  430.     $text_econding = $encoding;
  431.    
  432.     #RSS-Parser anwerfen
  433.     my $rss = XML::RSS->new(version => '1.0', encoding => $encoding);
  434.     #Parsen
  435.     $rss->parse($response->content);
  436.  
  437.     #Maximal $maxNews Nachrichten anzeigen
  438.     while (@{$rss->{'items'}} > $maxNews) {
  439.         pop(@{$rss->{'items'}});
  440.     }
  441.  
  442.     #Rückgabe vorbereiten
  443.     my @list = {};
  444.     my $i = 0;
  445.     foreach my $item (@{$rss->{'items'}}) {
  446.         #print "Titel: ".$item->{'title'}."\n";
  447.         $list[$i++] = $item->{'title'};
  448.     }
  449.    
  450.     print "$i Einträge gefunden und extrahiert.\n";
  451.    
  452.     return @list;
  453. }
  454.  
  455. ################################################################
  456. # Hilfsfunktionen
  457. ################################################################
  458. #Für Millisekunden schlafen
  459. sub sleepMilli {
  460.     my $sleep = $_[0];
  461.     select(undef, undef, undef, $sleep);
  462. }
  463.  
  464. #Funktionstest
  465. sub functionTest {
  466.     clearLCD;
  467.     clearLCD; #doppelt, da evt. Artefakte am Anfang existieren
  468.     backLightOn;
  469.     selectLineTwo;
  470.     writeLCD("Hallo ".$myName."!");
  471.     setLED;
  472.     sleepMilli(0.5);
  473.     clearLED;
  474.     backLightOff;
  475.     sleepMilli(0.5);
  476.     setLED;
  477.     backLightOn;
  478.     sleepMilli(0.5);
  479.     selectLineFour;
  480.     writeLCD("Initialisierung ok.");
  481.     goTo(19);
  482.     writeLCD("*");
  483.     goTo(39);
  484.     writeLCD("*");
  485.     goTo(59);
  486.     writeLCD("*");
  487.     goTo(79);
  488.     writeLCD("*");
  489.     sleepMilli(0.5);
  490.     clearLED;
  491.     backLightOff;
  492.     clearLCD;
  493. }
  494.  
  495. #sauber beenden
  496. sub cleanAndExit(){
  497.     #LCD und LED löschen
  498.     &backLightOff;
  499.     &clearLED;
  500.     &clearLCD;
  501.  
  502.     #FTDI beenden
  503.     &ftdiStop;
  504.     print "Kill-Signal erhalten - beende OnlineChecker.\n";
  505.     exit(1);
  506. }
  507.  
  508. ################################################################
  509. #Main: Server starten
  510. &startServer;
  •  
  • 0 Kommentare
  •  
Mein Kommentar
Ich möchte über jeden weiteren Kommentar benachrichtigt werden.

Zurück