Nicht nur Kassierer im Supermarkt scannen Produkte anhand der aufgedruckten UPC-Barcodes. Mit einem Lesegerät für 25 Euro lässt sich auch die hauseigene Bibliothek, CD- oder DVD-Sammlung erfassen.
Die in Hongkong ansässige Firma dealextreme.com bietet allerhand Artikel aus chinesischer Billigproduktion zu absoluten Schlagerpreisen an. Sie verschickt sie außerdem auch noch weltweit kostenlos, nachdem man mit Paypal bezahlt hat. Soll's ein Laserpointer für $1.50 sein oder eine SATA/IDE-Adapter für nur 8 Dollar? Wenn man es nicht eilig hat (das Verschicken dauert bis zu zwei Wochen), ist dealextreme unschlagbar. Auf den CCD-basierten Barcodeleser für $41.89 (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.
Abbildung 1: Der Barcode-Scanner erfasst den UPC-Code eines Buchs. |
Als der Postbote dann endlich das Paket brachte, gab es kein Halten mehr: Was lag näher, als eine Applikation zu schreiben, die die UPC-Codes aller Ausgaben meiner umfangreichen Fachbuchsammlung erfasst und in einer Datenbank ablegt? Amazon.com bietet einen kostenlosen Webservice an, der detaillierte Daten zu den meisten per UPC erfassten Produkten angibt. So kann ein Perlskript einfach den Autor und den Titel eines Buches bestimmen oder den Interpreten einer gerade eingescannten CD. Auch Bilddateien der CD- und Buchcover gehören zum Lieferumfang. Läuft die Applikation als graphische Oberfläche, kann sie die Buchdeckel und die Hüllen eingescannter CDs sogleich farbig auf dem Bildschirm anzeigen.
Abbildung 2: Der Scanner arbeitet mit einem CCD-Sensor und schaltet auf Knopfdruck rote Leuchtdioden an. |
Das Lesegerät verfügt über einen USB-Stecker, den Linux sofort als zweite Tastatur erkennt. Hält man den Lesekopf wie ein Kassierer über den UPC-Barcode auf dem Rücken eines Fachbuchs, einer CD oder DVD, und drückt den Knopf, schaltet der Leser das rote Licht ein, aktiviert den eingebauten CCD-Sensor und der eingebaute Kleinstcomputer versucht, anhand der verschieden dicken Striche den dargestellten UPC-Code zu erkennen.
Der Leser ermittelt den Barcode 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 einzelne Ziffer über die Tastatur eingegeben und anschließend 'Enter' gedrückt. Ich konnte zwar keinen solchen Fall finden, aber sollte der Leser einen Barcode mal wirklich nicht erkennen, kann der User die Nummern in das Eingabefeld der GUI eingeben und anschließend 'Return' drücken -- der Effekt ist der gleiche.
Das heute vorgestellte Skript upcscan
baut eine auf dem Toolkit
Tk
basierende graphische Oberfläche auf, deren Texteingabefeld
sich sofort nach dem Start den Tastaturfokus schnappt. Falls der
Barcodereader einen Code erkennt, landen dessen Ziffern im Eingabefeld.
Auf das vom Leser abschließend gesandte Return-Zeichen reagiert die
GUI mit dem Aufruf einer Callback-Funktion scan_done()
.
Diese schickt
den UPC-Code an den Webservice von amazon.com und bekommt nach etwa
einer Sekunde nicht nur Titel und Autor/Interpret des Buches/CD/DVD,
sondern auch einen URL, hinter dem sich ein JPG-Bild des Buchdeckels oder
der CD-Hülle zum Herunterladen versteckt.
Abbildung 3: Das Skript hat den eingelesenen UPC-Code an Amazon.com geschickt und die zugehörigen Produktdaten eingeholt. |
Abbildung 3 zeigt die Applikation kurz nach dem Einscannen des Barcodes auf dem Rücken eines JavaScript-Buches. Die Datenfelder sind korrekt ausgefüllt und die Abbildung zeigt den richtigen Buchdeckel. 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 von Amazon eingeholten Daten in einer lokalen SQLite-Datenbank, in der man dann anschließend nach Herzenslust programmatisch herumstöbern kann (Abbildung 5).
Abbildung 4: Auch CDs und DVDs erkennt der Scanner anhand des UPC-Codes und holt Produktdaten und Coverbild von Amazon.com. |
Abbildung 5: Die SQLite-Datenbank enthält anschließend alle eingescannten Artikel. |
Mit dem Tk-Paket vom CPAN zaubern Perl-Skripts leicht ansehnliche GUIs, allerdings stellt sich wie immer die Frage nach langlaufenden Operationen wie Web-Requests, die die Oberfläche einfrieren lassen. Denn eine Anfrage an Amazon mit einem UPC-Code kann schon mal einige Sekunden dauern, und in der Zwischenzeit wäre die Oberfläche tot.
Das ebenfalls auf dem CPAN erhältliche POE-Modul lässt die GUI aus diesem Grund in einem Event-basierten Kernel ticken und stellt Mechanismen für ein kooperatives Multitasking zur Verfügung. Webrequests arbeitet ein Skript in dieser Umgebung nicht mehr syncron ab. Statt dessen setzt es zunächst einen Request an den Webserver ab und gibt die Kontrolle sofort an den POE-Kernel weiter. Liegt die Antwort aus dem Internet dann endlich vor, weckt der Kernel die wartende Task und übermittelt die vorliegenden Daten.
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 asyncrone POE für die Anfragen an die Amazon-Datenbank,
sondern das syncrone LWP::UserAgent. Doch es lässt sich mit
dem Parameter ua
dazu überreden, einen hereingereichten
Useragenten zu verwenden. Auf dem CPAN steht LWP::UserAgent::POE bereit,
ein Agent mit der LWP-Schnittstelle, aber mit besonderer Berücksichtigung
der asyncronen Bedürfnisse des POE-Kernels. Während das Modul scheinbar
Web-Requests absetzt und syncron auf das Ergebnis wartet, ist in seinem
Innern schwarze Magie am Werke, die den POE-Kernel immer wieder ein paar
Ticks weiterlaufen lässt, damit auch andere Tasks drankommen.
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 }
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 setzt die Datei articles.dat
im aktuellen Verzeichnis als SQLite-Datenbank und die nachfolgenden
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 werfen.
Die Methode make_classes()
in Zeile 23 importiert dann die
Datenbankobjekte in den Scriptcode, sodass später ein einfaches
Article->new()
genügt, um einen neuen Eintrag in die Datenbanktabelle
articles
vorzubereiten.
Die graphische Oberfläche ruht im Hauptfenster $top
, das
Zeile 25 von $poe_main_window
übernimmt, da ja Tk im Skript nicht
allein vor sich hin orgelt, sondern den POE-Tango tanzt.
Wenn 'use Tk' im Code vor 'use POE' steht, weiß POE, dass es eine Eventschleife
für das Tk-GUI bereitstellen muss, initialisiert bereits das
Hauptfenster MainWindow, und legt eine Referenz darauf in $poe_main_window
ab.
In der Kopfzeile
des Fensters legt der configure()
-Befehl den String ``UPC Reader'' ab
und setzt und die Hintergrundfarbe der GUI auf ``#a2b2a3'', also
helles Olivgrün.
Ganz oben im Hauptfenster
liegt das Entry-Widget $ENTRY
, das die Zahlenkolonnen des Scanners
entgegennimmt und diese in der globalen Variablen $UPC_VAR
ablegt. Weiter
unten befindet sich ein Photo-Widget für die Buch- und CD-Cover,
welches wiederum
aus organisatorischen Gründen in einem Label-Widget ruht. Weiter folgen
vier Widgets vom Typ Label
, die den Titel ($PRODUCT
),
Autor/Interpret ($BYWHO
), den
gelesenen UPC-Code ($UPC
) und eine
Statusanzeige ($FOOTER
, ganz unten) 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 Rand ausdehnen und
auch beim Aufziehen des Hauptfensters automatisch mitziehen.
Eine kritische Rolle kommt dem bind()
-Befehl in Zeile 52 zu. Das
Entry-Widget ignoriert das Return
-Zeichen des Scanners, denn es handelt
sich um ein einzeiliges Eingabefeld. bind
bindet den Tastaturcode
aber an die Funktion scan_done()
, die ab Zeile 64 definiert ist und
die Verarbeitung des vom Scanner eingelesenen Codes veranlasst.
Zunächst löscht sie mit der Methode blank()
des Photo-Objektes die
Anzeige des alten Covers und auch die Anzeige des Titels und
des Autoren/Interpreten setzt sie auf den Leerstring. Im $FOOTER
erscheint der Text ``Processing ...'' und der Request an Amazon wird
mit amzn_fetch()
abgesetzt.
Zeile 74 löscht den vom Scanner gelesenen UPC-Code dann sofort wieder aus dem Entry-Widget, um es auf den nächsten Lesevorgang vorzubereiten. Die UPC 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 ab diesem Zeitpunkt das Programm bis zu dessen Abbruch steuert. Er nimmt Benutzereingaben wie Mausklicks oder Tastatur-/Scannereingaben engegeben und sorgt dafür, dass jede anstehende Task ihr Zeitscheibchen bekommt.
POE regiert seine Sessions mit eiserner Hand. Sobald diese nichts mehr zu
tun haben, eliminiert es sie rücksichtslos. Dass eine Tk-Applikation einfach
nur auf Usereingaben wartet, begreift es nicht, deshalb definieren
die Zeilen 54 bis 59 eine Session, die im 60-Sekunden-Takt wieder in den
anfänglich durchlaufenen _start
-Event anspringt.
Liefert der Scanner einen UPC-Code an, erzeugt die Funktion
amzn_fetch
ab Zeile 78 eine Instanz eines Net::Amazon-Objektes
und übergibt ihm nicht nur den Developer-Token, den der Skriptanwender
von Amazon holen muss (siehe Abschnitt Installation), sondern auch
den vorher global erzeugten Spezial-Agenten LWP::UserAgent::POE, der
nicht nur Web-Requests einholt, sondern auch den POE-Tango tanzt.
Ein Request-Objekt vom Typ Net::Amazon::Request::UPC spricht mit
dem Webservice bei Amazon, der den UPC-Lookup bereitstellt. Die
zurückgelieferte Antwort bietet die Methode is_success()
an, die
angibt, ob ein entsprechender UPC-Code gefunden wurde. Der Request
muss vorher angeben, ob der UPC-Code im Bereich ``books'', ``music'' (CDs)
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 Bereiche durch und bricht ab, sobald Amazon Erfolg
meldet. amzn_fetch
liefert drei Parameter zurück: Das Antwort-Objekt
$resp
, den Bereich (books/music/dvd
), in dem es fündig geworden
ist und den eingescannten UPC-Code.
Die ab Zeile 110 definierte Funktion resp_process()
schnappt sich
das Ergebnis und frischt die Felder der GUI damit auf. Die Methode
ImageUrlMedium()
des gefundenen Artikels $property gibt eine URL
zu einem JPEG-Bild mittlerer Größe an, das das Produkt als Buchdeckel
oder Albumcover darstellt.
Damit das Photo-Widget aus dem Tk-Toolkit auch JPG-Bilder 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 den URL eines Bildes auf amazon.com entgegen und holt selbiges mit dem POE-freundlichen Useragenten vom Netz.
Da das Photo-Widget (zumindest für JPEGs)
stur auf Base64-kodierten Daten besteht, wandelt die Funktion
encode_base64 aus dem Modul MIME::Base64 die mittels
der content()
-Methode extrahierten JPEG-Daten um,
bis sie dem Photo-Widget schmecken.
Anschließend setzt die
configure()
-Methode des Photo-Widgets die Option -data
auf
die kodierten Daten, was wiederum das Widget dazu veranlasst,
die Daten zu lesen, in das interne Tk-Format umzuwandeln und auf dem
GUI anzuzeigen.
Zurück zu resp_process()
: Es zeigt nicht nur die Produktdaten an,
sondern legt sie auch noch in der Datenbank ab. Hierzu legt es
in Zeile 124 mit Hilfe des Rose::DB-Wrappers ein neues Object vom
Typ Article
an und setzt dessen Felder upc
, type
, title
und
bywho
, die sich allesamt auf die 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()
fehl, sichert save()
den neu erfassten Artikel
in Zeile 145 in der Datenbank.
Bevor das Skript den Webservice von Amazon.com nutzen kann, muss
der User einen Entwicklertoken von Amazon holen. Dies geht problemlos
und schnell unter [3] wenn man sich mit einer gültigen Email registriert.
Ohne gültigen Token wird das Skript immer nur NOT FOUND
melden.
Abbildung 6: Das Schema der SQLite-Datenbank |
Die Datenbank richtet der SQLite-Client sqlite3
ein, wenn er die
Schema-Datei (Abbildung 5) reingeschoben bekommt:
sqlite3 articles.dat <schema.sql
Dies richtet die File-basierte Datenbank articles.dat
ein und erzeugt
darin ein leere Tabelle articles
mit den Spalten id
,
upc
(dem UPC-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 Datebanktabelle macht aus
dem Artikeltyp und der UPC-Nummer einen eindeutigen Schlüssel, sodass
der Datenbankwrapper Rose bei einem neu eingescannten Artikel schnell
mit load()
nachsehen kann, ob das Produkt schon einmal gescannt wurde
oder nicht.
Die verwendeten Module sind allesamt vom CPAN erhältlich und werden mit einer CPAN-Shell installiert. Die mit dem Scanner erstellte Artikeldatenbank lässt sich anschließend auf vielerlei Weise nutzen: Als Einkaufshilfe (``Hab ich dieses Buch schon?''), als Online-Bibliotheksindex, als CD-Archiv, oder, falls man auch noch eine Ortsbeschreibung einfügt (z.B. ``Zimmer 1, Regal 4, Fach 3''), als Orientierungshilfe für zerstreute Bibliothekare.
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |