Jeder kennt es aus dem Supermarkt: Die Kassiererinnen scannen Barcodes ein. Diese Technik hat aber noch ganz andere Anwendungsmöglichkeiten, jeder kann davon profitieren: Mit einem Lesegerät für 25 Euro lässt sich auch die hauseigene Bibliothek, CD- oder DVD-Sammlung erfassen.
Die Hongkonger Firma Dealextreme bietet allerhand Artikel aus chinesischer Billigproduktion zu, wie ich finde, absoluten Schlagerpreisen an. Der Kunde zahlt per Paypal, der Versand ist kostenlos. Soll\’s ein Laserpointer für anderthalb Dollar sein oder ein SATA/IDE-Adapter für nur acht Dollar? Wer es nicht eilig hat (der Versand dauert bis zu zwei Wochen), der wird bei Dealextreme gut bedient. Auf den CCD-Barcodeleser für 42 Dollar (etwa 25 Euro, eines der teuersten Produkte dort, [2]) hatte ich schon geraume Zeit ein Auge geworfen und eines Tages drückte ich gut gelaunt den »Buy«-Button.
Post aus Hongkong
Als der Postbote dann endlich das Paket brachte, gab es kein Halten mehr: Was lag näher, als eine Applikation zu schreiben, die die Barcodes aller Ausgaben meiner umfangreichen Fachbuchsammlung erfasst und in einer Datenbank ablegt? Die Barcodes sind dort – je nach Herkunft des Buches – im Format UPC (Universal Product Number) oder EAN (Europäische Artikelnummer) aufgedruckt und Amazon bietet einen kostenlosen Webservice, der detaillierte Produktinformationen liefert, sobald man ihm die Nummer aus einem solchen Barcode übergibt. Ein Perl-Skript kann so Autor und Titel eines Buches bestimmen oder den Interpreten einer gerade eingescannten CD herausfinden. Auch Bilddateien der CD- und Buchcover gehören zum Lieferumfang. Läuft die Applikation als grafische Oberfläche, kann sie die Buchdeckel und die Hüllen eingescannter CDs sofort farbig auf dem Bildschirm anzeigen.
Als zweite Tastatur
Das Lesegerät verfügt über einen USB-Stecker, Linux erkennt es sofort als zweite Tastatur. Wer den Lesekopf wie ein Kassierer über den Barcode auf einem Fachbuch, einer CD oder DVD hält und den Knopf drückt, der schaltet das rote Licht ein, aktiviert den CCD-Sensor – und der eingebaute Kleinstcomputer versucht anhand der verschieden dicken Striche den dargestellten Barcode zu erkennen (Abbildungen 1 und 2).

Abbildung 1: Der Barcode-Scanner erfasst den UPC- oder EAN-Code eines Buchs. Die Software fragt Amazon anhand der Nummer nach weiteren Informationen.

Abbildung 2: Der Scanner arbeitet mit einem CCD-Sensor und schaltet auf Knopfdruck ein Rotlicht der Leuchtdioden an.
Der Barcode-Leser arbeitet sehr zuverlässig, piept, wenn er fertig ist, und sendet die Ziffernfolge per USB an den Rechner, ganz so, als hätte der User jede Ziffer einzeln über die Tastatur eingegeben und anschließend [Return] gedrückt. Sollte der Leser einen Barcode einmal nicht erkennen, was bei meinen Versuchen nicht vorkam, dann kann der Benutzer die Nummern auch in das Eingabefeld der unten vorgestellten Applikation tippen – der Effekt wäre der gleiche.
Farbbild inklusive
Das heute vorgestellte Skript »upcscan« (Listing 1) benutzt eine auf dem Toolkit TK aufbauende grafische Oberfläche, deren Texteingabefeld sich sofort nach dem Start den Tastaturfokus schnappt. Falls der Barcode-Reader einen Code erkennt, landen dessen Ziffern im Eingabefeld. Auf das Return-Zeichen, das der Leser abschließend sendet, reagiert das GUI mit dem Aufruf einer Callback-Funktion »scan_done()«. Die schickt den Barcode an den Webservice von Amazon und bekommt nach etwa einer Sekunde nicht nur Titel und Autor oder Interpret des Buches beziehungsweise der CD/DVD zurück, sondern auch eine URL, hinter der sich ein JPG-Bild des Buchdeckels oder der CD-Hülle versteckt. Abbildung 3 zeigt die Applikation kurz nach dem Einscannen des Barcode auf dem Rücken eines Javascript-Buches. Die Datenfelder sind korrekt ausgefüllt und das Programm hat von Amazon das Bild des richtigen Buchdeckels erhalten. In Abbildung 4 ist das Ergebnis einer eingescannten CD des Beach-Boys-Sängers Brian Wilson zu sehen. In beiden Fällen hinterlegt das Skript die eingeholten Daten in einer lokalen SQLite-Datenbank, in der sich nach Herzenslust stöbern lässt (Abbildung 5).
Scheinsynchron
Mit dem TK-Paket vom CPAN bereiten ansehnliche GUIs Perl-Skripten keine Schwierigkeiten. Gerade bei der geplanten Anwendung aber ergibt sich ein Problem mit lange laufenden Operationen wie den Webrequests, die die Oberfläche einfrieren lassen würden. Eine Anfrage an Amazon kann schon mal einige Sekunden dauern, in der Zwischenzeit wäre die Oberfläche tot.
Einen Ausweg bietet das ebenfalls vom CPAN erhältliche POE-Modul. Es lässt das GUI in einem Event-orientierten Kernel ticken und stellt Mechanismen für ein kooperatives Multitasking zur Verfügung. Webrequests arbeitet ein Skript in dieser Umgebung nicht mehr synchron ab. Stattdessen setzt es zunächst einen Request an den Webserver ab und gibt danach die Kontrolle sofort an den POE-Kernel ab. Liegt die Antwort aus dem Internet später vor, weckt der Kernel die wartende Task wieder und übermittelt die vorliegenden Daten.

Abbildung 3: Das Skript hat den eingelesenen Code an Amazon.com geschickt und die zugehörigen Produktdaten eingeholt.

Abbildung 4: Auch CDs und DVDs erkennt der Scanner am Barcode und holt Produktdaten und Coverbild von Amazon.
Special Agent
Die Kommunikation mit Amazon übernimmt das CPAN-Modul Net::Amazon, das eine Vielzahl von Anfragen an den Webservice des Warengiganten unterstützt. Es verwendet allerdings intern nicht das asynchrone POE-Modul für die Anfragen an die Amazon-Datenbank, sondern das synchrone LWP::UserAgent. Doch es lässt sich mit dem Parameter »ua« dazu überreden, einen hereingereichten Useragenten zu benutzen. Auf dem CPAN steht LWP::UserAgent::POE bereit, ein Agent mit der LWP-Schnittstelle, aber mit besonderer Berücksichtigung der asynchronen Bedürfnisse des POE-Kernels. Während das Modul scheinbar Webrequests absetzt und synchron auf das Ergebnis wartet, ist in seinem Inneren schwarze Magie am Werke, die den POE-Kernel immer wieder ein paar Ticks weiterlaufen lässt, damit auch andere Tasks drankommen.
Ab in die Bank
Das Programm »upcscan« nutzt den Datenbankwrapper Rose::DB vom CPAN, um das Schema der Datenbank zu ermitteln und neue Records in deren Tabelle »articles« einzufügen (Abbildung 6). Zeile 19 in Listing 1 bestimmt die Datei »articles.dat« im aktuellen Verzeichnis als SQLite-Datenbank. Die folgenden Optionen »Autocommit« und »RaiseError« stellen sicher, dass neue Einträge ohne extra Commit-Befehl in die Datenbank wandern und eventuell auftretende Fehler sofort eine Exception erzeugen.
Die Methode »make_classes()« in Zeile 23 importiert dann die Datenbankobjekte in den Skriptcode, sodass später ein einfacher Aufruf von »Article->new()« genügt, um einen neuen Eintrag in der Datenbanktabelle »articles« vorzubereiten.
Widgets im POE-Reigen
Die grafische Oberfläche ruht im Hauptfenster »$top«, das Zeile 25 von »$poe_main_window« übernimmt, da ja TK im Skript nicht allein vor sich hin werkelt, sondern mit POE kooperiert. Wenn die Anweisung »use Tk« im Code vor »use POE« im Quelltext steht, weiß POE, dass es eine Eventschleife für das TK-GUI bereitstellen muss, initialisiert bereits das Hauptfenster »MainWindow« und legt auch schon eine Referenz darauf in »$poe_main_window« ab.
In der Kopfzeile des Fensters speichert der »configure()«-Befehl den String »UPC Reader« und setzt die Hintergrundfarbe des GUI auf »#a2b2a3«, also helles Olivgrün. Ganz oben im Hauptfenster liegt das Entry-Widget »$ENTRY«, das die Zahlenkolonnen des Scanners entgegennimmt und in der globalen Variablen »$UPC_VAR« sichert. Weiter unten befindet sich ein Foto-Widget für die Buch- und CD-Cover, das wiederum aus organisatorischen Gründen in einem Label-Widget eingebettet ist. Es folgen vier weitere Widgets vom Typ »Label«, die den Titel (»$PRODUCT«), Autor/Interpret (»$BYWHO«), den gelesenen UPC- oder EAN-Code (»$UPC«) sowie eine Statusanzeige (»$FOOTER«) aufnehmen.
Die For-Schleife ab Zeile 46 packt die Widgets von oben nach unten in das Hauptfenster und stellt mit »-fill => \’x\’« und »-expand => 1« sicher, dass sich die Labels horizontal bis zum Fensterrand ausdehnen und sich auch beim Aufziehen des Hauptfensters automatisch mit vergrößern.
POE-Sessions
Eine kritische Rolle kommt dem Bind-Befehl in Zeile 52 des Listings zu. Das Entry-Widget ignoriert das »Return«-Zeichen des Scanners, denn es handelt sich um ein einzeiliges Eingabefeld. Die Anweisung »bind« bindet den Tastaturcode aber an die Funktion »scan_done()«, die ab Zeile 64 definiert ist und die Verarbeitung des vom Scanner eingelesenen Code veranlasst.
Zunächst löscht sie mit der Methode »blank()« des Foto-Objekts die Anzeige des alten Covers und tauscht den Titel und den Namen des Autors/Interpreten gegen einen leeren String. Am Fußende erscheint der Text »Processing …«, der Request an Amazon wird mit »amzn_fetch()« abgesetzt. Zeile 74 löscht den vom Scanner gelesenen Code dann sofort wieder aus dem Entry-Widget, um es auf den nächsten Lesevorgang vorzubereiten. Der Barcode des aktuellen Artikels ist ja im »$UPC-Widget« gesichert.
Nach diesen Vorbereitungen definiert Zeile 54 eine POE-Session und Zeile 61 startet den POE-Kernel, der von diesem Zeitpunkt an das Programm bis zu dessen Abbruch dirigiert. Er nimmt die Benutzereingaben wie etwa Mausklicks oder Tastatur- undScannereingaben entgegen und sorgt dafür, dass jede anstehende Task immer ihr Zeitscheibchen abbekommt.
POE regiert seine Sessions konsequent und mit eiserner Hand. Sobald diese nichts mehr zu tun haben, eliminiert sie der Kernel rücksichtslos. Dass eine TK-Applikation einfach nur auf Benutzereingaben wartet, begreift es nicht. Deshalb definieren die Zeilen 54 bis 59 eine POE-Session, die im 60-Sekunden-Takt wieder den anfänglich durchlaufenen »_start«-Event anspringt.
Frag nach bei Amazon
Liefert der Scanner einen UPC- oder EAN-Code, erzeugt die Funktion »amzn_fetch()« ab Zeile 78 eine Instanz eines Net::Amazon-Objekts und übergibt ihm nicht nur den Developer-Token, den sich der Skriptanwender von Amazon besorgen muss (mehr dazu im Abschnitt “Installation”), sondern auch den vorher global erzeugten Spezialagenten LWP::UserAgent::POE, der sowohl Webrequests einholt, als auch mit POE kooperiert.
Ein Request-Objekt vom Typ Net:: Amazon::Request::UPC spricht mit dem Webservice bei Amazon, der den UPC/EAN-Lookup bereitstellt. Die zurückgelieferte Antwort bietet die Methode »is_success()« an, die Auskunft darüber gibt, ob ein entsprechender Code gefunden wurde. Der Request muss vorher angeben, ob der UPC/EAN-Code im Bereich Books, Music oder DVD zu suchen ist. Da der Scanner nicht weiß, ob er gerade ein Buch oder eine CD scannt, probiert die For-Schleife ab Zeile 89 einfach alle drei unterstützten Möglichkeiten durch und bricht ab, sobald Amazon einen Erfolg meldet. Die Methode »amzn_fetch()« liefert drei Parameter zurück: das Antwort-Objekt »$resp«, den Bereich, in dem es fündig geworden ist (also »Books«, »Music« oder »DVD«) und den eingescannten UPC/EAN-Code.
Die ab Zeile 110 definierte Funktion »resp_process()« schnappt sich das Ergebnis und frischt die Felder des GUI damit auf. Die Methode »ImageUrlMedium()« des gefundenen Artikels gibt eine URL zu einem Jpeg-Bild mittlerer Größe an, das seinerseits das Cover des Produkts darstellt.
Jpegs lesen
Damit das Foto-Widget aus dem TK-Toolkit diese Jpeg-Bilder auch lesen und darstellen kann, holt Zeile 4 das Modul Tk::JPEG aus der TK-Distribution herein. Die ab Zeile 154 definierte Funktion »img_display()« nimmt die URL eines Bildes auf Amazon entgegen und besorgt es dann mit Hilfe des POE-freundlichen Useragenten.
Weil das TK-Foto-Widget – zumindest für Jpegs – stur auf Base64-kodierten Daten besteht, wandelt die Funktion »encode_base64()« aus dem Modul MIME::Base64 die mit der »content()«-Methode extrahierten Jpeg-Daten um, bis sie dem Widget schmecken. Anschließend benutzt die »configure()«-Methode des Foto-Widgets die Option »-data«, was wiederum das Widget dazu veranlasst, die Daten zu lesen, in das interne TK-Format umzuwandeln und schließlich auf dem GUI anzuzeigen.
Datenbank mit Wrapper
Zurück zur Funktion »resp_process()«: Sie zeigt die Produktdaten nicht nur an, sondern legt sie auch in der Datenbank ab. Hierzu legt die Zeile 124 mit Hilfe des Rose::DB-Wrappers ein neues Objekt vom Typ »Article« an und setzt dessen Felder »upc«, »type«, »title« und »bywho«, die sich alle auf die gleichnamigen Spalten der Datenbanktabelle beziehen.
Die Methode »load()« mit dem Attribut »speculative« versucht anschließend einen entsprechenden Eintrag in der Datenbank zu finden. Führt dies zum Erfolg, schreibt das Skript »”ALREADY EXISTS”« in die GUI-Anzeige, damit der Operator weiß, dass er den Artikel schon einmal erfolgreich gescannt hat. Schlägt »load()« dagegen fehl, sichert »save()« den neu erfassten Artikel in Zeile 145 in der Datenbank.
Installation
Bevor das Skript den Webservice von Amazon nutzen kann, muss sich der User ein Entwicklertoken von Amazon besorgen und in Zeile 65 des Skripts einfügen. Das Token gibt\’s für jeden problemlos unter [3], wenn er sich mit einer gültigen E-Mail registriert. Ohne gültigen Token würde das Skript immer nur »NOT FOUND« melden.
Die Datenbank erzeugt der SQLite-Client umgehend, wenn er die Schema-Datei (Abbildung 5) übermittelt bekommt:
sqlite3 articles.dat <schema.sql
Der Client erzeugt die Datenbank »articles.dat« und darin eine leere Tabelle »articles« mit den Spalten »id«, »upc« (dem UPC/EAN-Code), »type« (»Books«, »Music« oder »DVD«), »title« (Titel des Buchs oder der CD/DVD) und »bywho« (Autor oder Interpret). Das »UNIQUE«-Kommando im SQL der Datenbanktabelle macht aus dem Artikeltyp und der UPC-Nummer einen eindeutigen Schlüssel, sodass sich der Datenbankwrapper Rose bei einem neu eingescannten Artikel schnell mit »load()« davon überzeugen kann, ob das Produkt schon einmal gescannt wurde oder noch nicht.
Die verwendeten Module sind allesamt vom CPAN erhältlich und mit einer CPAN-Shell installierbar. Die mit dem Scanner erstellte Artikeldatenbank lässt sich anschließend auf vielerlei Weise nutzen: Als Einkaufshilfe (“Hab ich dieses Buch schon?”), als CD-Archiv oder, falls man auch noch eine Ortsbeschreibung einfügt (zum Beispiel “Zimmer 1, Regal 4, Fach 3”), als Onlinekatalog für zerstreute Bibliothekare. (jcb)
|
Listing 1: |
|---|
001 #!/usr/local/bin/perl -w
002 use strict;
003 use Tk;
004 use Tk::JPEG;
005 use POE;
006 use LWP::UserAgent::POE;
007 use Net::Amazon;
008 use Net::Amazon::Request::UPC;
009 use MIME::Base64;
010 use Rose::DB::Object::Loader;
011 use Log::Log4perl qw(:easy);
012
013 my @MODES = qw(books music dvd);
014
015 my $UA = LWP::UserAgent::POE->new();
016
017 my $loader = Rose::DB::Object::Loader->new(
018 db_dsn =>
019 "dbi:SQLite:dbname=articles.dat",
020 db_options => {
021 AutoCommit => 1, RaiseError => 1 },
022 );
023 $loader->make_classes();
024
025 my $top = $poe_main_window;
026 $top->configure(-title => "UPC Reader",
027 -background=> "#a2b2a3");
028 $top->geometry("200x300");
029
030 my $FOOTER = $top->Label();
031 $FOOTER->configure(-text =>
032 "Scan next item");
033
034 my $BYWHO = $top->Label();
035 my $UPC = $top->Label();
036 my $PHOTO = $top->Photo(-format => 'jpeg');
037 my $photolabel =
038 $top->Label(-image => $PHOTO);
039 my $entry = $top->Entry(
040 -textvariable => my $UPC_VAR);
041
042 my $PRODUCT = $top->Label();
043
044 $entry->focus();
045
046 for my $w ($entry, $photolabel, $PRODUCT,
047 $BYWHO, $UPC, $FOOTER) {
048 $w->pack(-side => 'top', -expand => 1,
049 -fill => "x" );
050 }
051
052 $entry->bind("<Return>", \&scan_done);
053
054 my $session = POE::Session->create(
055 inline_states => {
056 _start => sub{
057 $poe_kernel->delay("_start", 60);
058 }
059 });
060
061 POE::Kernel->run();
062
063 ###########################################
064 sub scan_done {
065 ###########################################
066 $PHOTO->blank();
067 $PRODUCT->configure(-text => "");
068 $FOOTER->configure(-text =>
069 "Processing ...");
070 $BYWHO->configure(-text => "");
071 $UPC->configure(-text => $UPC_VAR);
072 resp_process(
073 amzn_fetch( $UPC_VAR ) );
074 $UPC_VAR = "";
075 }
076
077 ###########################################
078 sub amzn_fetch {
079 ###########################################
080 my($upc) = @_;
081
082 my $resp;
083
084 my $amzn = Net::Amazon->new(
085 token => 'XXXXXXXXXXXXXXXXXXXX',
086 ua => $UA,
087 );
088
089 for my $mode (@MODES) {
090
091 my $req =
092 Net::Amazon::Request::UPC->new(
093 upc => $upc,
094 mode => $mode,
095 );
096
097 $resp = $amzn->request($req);
098
099 if($resp->is_success()) {
100 return($resp, $mode, $upc);
101 last;
102 }
103
104 WARN "Nothing found in mode '$mode'";
105 }
106 return $resp;
107 }
108
109 ###########################################
110 sub resp_process {
111 ###########################################
112 my($resp, $mode, $upc) = @_;
113
114 if($resp->is_error()) {
115 $PRODUCT->configure(
116 -text => "NOT FOUND");
117 return 0;
118 }
119
120 my ($property) = $resp->properties();
121 my $imgurl = $property->ImageUrlMedium();
122 img_display( $imgurl );
123
124 my $a = Article->new();
125 $a->upc($upc);
126 $a->type($mode);
127 $a->title( $property->Title() );
128
129 if($mode eq "books") {
130 $a->bywho( $property->author() );
131 } elsif( $mode eq "music") {
132 $a->bywho( $property->artist() );
133 } else {
134 $a->bywho( "" );
135 }
136
137 $BYWHO->configure(-text => $a->bywho() );
138 $PRODUCT->configure(
139 -text => $a->title() );
140
141 if($a->load( speculative => 1 )) {
142 $PRODUCT->configure(
143 -text => "ALREADY EXISTS");
144 } else {
145 $a->save();
146 }
147
148 $FOOTER->configure(
149 -text => "Scan next item");
150 return 1;
151 }
152
153 ###########################################
154 sub img_display {
155 ###########################################
156 my($imgurl) = @_;
157
158 my $imgresp = $UA->get( $imgurl );
159
160 if($imgresp->is_success()) {
161 $PHOTO->configure( -data =>
162 encode_base64( $imgresp->content() ));
163 }
164 }
|
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2008/10/Perl] [2] Dealextreme: [http://www.dealextreme.com/details.dx/sku.12559] [3] Amazon Web Service; Entwicklertoken: [http://amazon.com/soap] [4] Informationen zum Perl-TK-Toolkit:[http://w4.lns.cornell.edu/~pvhp/ptk/ptkFAQ.html] |
|
Der Autor |
|---|
|
|








