Auch wenn ein USB-Spielzeug wie ein Styroporraketenwerfer nur mit einer
Windows-CD daherkommt, lässt es sich mit etwas Reverse-Engineering
unter Linux anschließen. Mit libusb
sogar ohne Treiber, vom User-Space
aus und mit Perl.
Ärger im Büro? Der muss nicht immer gleich in eine Schlacht eskalieren wie in dem Video ``The Great Office War'' [2] des Spielzeugherstellers Hasbro. Dennoch lohnt sich die Anschaffung des USB-gesteuerten Raketenwerfers ``Rocket Baby'' (Abbildung 1) der chinesischen Firma Cheeky Dream für etwa 20 Euro, denn er dient nicht nur zur Erheiterung der Kollegen sondern bietet auch Gelegenheit, das recht komplexe USB-Subsystem des Linux-Kernels zu studieren ([5]).
Abbildung 1: Der USB-Raketenwerfer "Rocket Baby" von Cheeky Dream |
In der Verpackung des Spielzeugs findet sich allerdings nur eine CD für Windows XP, keine Spur von einem Linux-Treiber. Dies spornte einige spielverliebte Entwickler offensichtlich dazu an, das verwendete USB- Protokoll unter Windows mit USB-Schnüffelwerkzeugen wie USBsniff zu ermitteln und mittels Reverse-Engineering Schnittstellen für Sprachen wie Python oder andere Operationsysteme zu basteln (zum Beispiel [3]).
Die Ubuntu-Distribution Hardy Heron erkennt das Spielzeug automatisch nach dem Anstöpseln des USB-Steckers. Die Nachrichten des Kernels sind in der Logdatei /var/log/messages nachzulesen (Abbildung 2) und zeigen an, dass die Stalinorgel nun am UHCI-Controller meines Intel-basierten PCs hängt.
Abbildung 2: Nach dem Einstöpseln des Raketenwerfers erkennt der Kernel das Device und weist ihm einen USB-Eintrag zu. |
Das USB-Subsystem des Kernels hat ihn laut Logfile
unter ``usb 5-1'' aufgenommen. Details
zeigt der Sysfs-Baum unter /sys/bus/usb/devices/5-1
. Das USB-Filesystem
usbfs
projeziert hier Kernel-interne USB-Daten in den User-Space. Abbildung
3 zeigt, dass die Hersteller-ID (idVendor
) des vom Kernel erkannten
Raketenwerfers 0x0a81 ist und die Produkt-ID (idProduct
)
0x0701. Der Kernel nimmt heißgestöpselte USB-Geräte unter zufälligen
USB-Nummern auf, statt ``usb 5-1'' könnte es nächstes Mal durchaus ``usb 3-1'' sein.
Doch hängt nur ein Gerät mit den gerade ermittelten Werten für
idVendor
und idProduct
am PC, kann ein Programm dessen Adresse
zuverlässig und relativ zügig ermitteln, indem es den ganzen
USB-Baum durchstöbert, bis es ein Gerät mit dieser Kombination findet.
Abbildung 3: Im /sys-Baum zeigt Linux Details des mittels Hotplug eingehängten Gerätes an. |
Um nun Kontakt mit dem USB-Gerät aufzunehmen, verwendet Linux
tradionellerweise Device-Treiber im Kernel. Diese sind allerdings schwer
zu schreiben, da der Entwickler ohne Netz und doppelten Boden arbeitet und
der kleinste Pointerfehler das gesamte Linux-System von der Klippe schubst
und einen Reboot erforderlich macht.
Außerdem müssen Device-Treiber für jeden Kernel neu kompiliert und
das entsprechende Modul mit modprobe
unter Root geladen werden. Und
gerade im Kernel ändern sich Datenstrukturen unheimlich schnell, sodass
es durchaus sein kann, dass der Source-Code eines mühevoll für den
Kernel 2.6.22 geschriebenen Treibers mit Version 2.6.24 schon nicht mehr
funktioniert.
Benötigt man allerdings keinen hohen Datendurchsatz oder Antworten in
Echtzeit, muss die Steuerungslogik nicht im Kernel laufen. Bietet der Kernel
zudem eine Schnittstelle wie usbfs an, um mit USB-Geräten auf
Hardware-Ebene zu kommunizieren, ist es durchaus möglich, den gesamten
Treiber im User-Space zu implementieren. Das Open-Source-Projekt libusb
[6] stellt eine bequeme Libary für C-Programme zur Verfügung, das Perl-Modul
Device::USB vom CPAN wickelt Perl-Funktionen drumherum.
Listing rocket-test
zeigt, wie sich der Geschützturm des Raketenwerfers
mit ein paar Zeilen Perl-Code um etwa einen Zentimeter nach oben schwenken
lässt. Zunächst sucht die Methode find_device
des Moduls Device::USB
nach einem Gerät mit den vorher ermittelten Werten für idVendor und idProduct
im USB-Baum. Die Methode open
nimmt im Erfolgsfall dann Verbindung mit
dem gefundenen Device auf.
Das USB-Subsystem des Kernels unterstützt vier verschiedene Kommunikationsmodi mit USB-Controllern: Control Transfers für Kurznachrichten, Bulk Transfers für größere Datenmengen, Interrupt Transfers für zeitkritische Daten und Isosynchronous Transfers für Echtzeitdaten. Durch reverse-engineering fanden Entwickler heraus, dass der Raketenwerfer zur Bewegung des Kanzelturms und zum Abfeuern der Styroporraketen Control-Messages mit einem Byte Länge erwartet. Kasten 1 zeigt die Codes für die verschiedenen Aktionen.
Kasten: Control-Codes für den Betrieb des Raketenwerfers
"down" 0x01, "up" 0x02, "left" 0x04, "right" 0x08, "fire" 0x10, "stop" 0x20, "start" 0x40,
Dabei setzt ein Code den Werfer so lange in Bewegung, bis ein weiterer Code entweder die Richtung ändert oder ein ``Stop''-Befehl die Bewegung abbricht. Dies ist wichtig, denn setzt ein Programm eine Bewegung in Gang und bricht dann ab, surrt der Motor des Werfers unschön weiter.
Die an die Methode control_msg
in den Zeilen 12 und 19 übergebenen
Hex-Werte legen fest, wie die USB-Schnittstelle das Kontroll-Byte aus
Kasten 1 an den Controller weiterreicht:
0x21 steht für den Request-Typ, 0x09 für USB_REQ_SET_CONFIGURATION
,
0x02 für USB_RECIP_ENDPOINT
und der Wert 0 für einen nicht benutzten
Index. Es folgt der mit der Perl-Funktion chr()
ermittelte Byte-Wert
des angegebenen Integer-Werts 0x02 zur Steuerung des Werfers nach oben.
Die letzten beiden Parameter
geben mit 1 die Länge des übermittelten Strings an (im vorliegenden Fall
genau 1 Byte) und die Wartezeit auf eine Antwort
in Millisekunden (1000) bevor das Programm einen Fehler auslöst.
Kasten 2: Parameter für control_msg()
$requesttype => 0x21 $request => 0x09 $value => 0x02 $index => 0 $bytes => chr(...) $size => 1 $timeout => 1000
Dann genehmigt sich das Testprogramm mit Hilfe des Moduls
Time::HiRes vom CPAN und dessen Funktion usleep()
ein kurzes Schläfchen
von einer Zehntelsekunde (100.000 Micro-Sekunden) und setzt anschließend das
Kontrollbyte 0x20 ab, was der Empfänger als ``stop'' interpretiert und
den Motor des Geschützturms wieder zur Ruhe bringt. Die zwei Aufrufe von
control_msg()
im Skript rocket-test haben den Geschützturm also insgesamt
eine Zehntelsekunde lang nach oben gefahren. Falls dieser nicht
eh schon am oberen Ende anlag, hat der Motor kurz aufgeheult und
die Styroporraketen haben sich um etwa 20 Grad nach oben gedreht.
Beim Feuern einer Rakete ist zu beachten, dass der Geschützmotor ungefähr zwei Sekunden lang pumpen muss, damit intern die nötige Spannung aufgebaut ist, um die Styroporgranate abzufeuern. Damit das Programm weiß, wann die Rakete abgefeuert wurde und der Motor nicht mehr benötigt wird, muss es lesend auf die USB-Schnittstelle zugreifen und Daten vom Controller des Geschützes einholen.
Dieser meldet, welche Aktionen
gerade verfügbar sind und welche nicht. Ist der Geschützturm zum Beispiel
am rechten Anschlag, liefert es einen Status-String mit dem Wert
0x08 (binär 0000_1000
) zurück, um anzuzeigen, dass alle Aktionen außer
0x08 nun verfügbar sind (0x08 symbolisiert laut Kasten 1 die Richtung ``right'').
Steht der Geschützturm hingegen am linken
unteren Anschlag, liefert die Statusmeldung 0x05 (binär 0000_0101
)
zurück, denn sowohl 0x01 (``down'') als auch 0x04 (``left'') sind jetzt
blockiert. Analog setzt das USB-Device kurz nach dem Abfeuern einer
Rakete das Flag 0x10 (binär 0001_0000
), und dann weiß die Steuerung,
dass sie jetzt den Motor mit 0x20 abstellen kann, es sei denn, sie möchte
die nächste der insgesamt drei Raketen gleich hinterherfeuern.
01 #!/usr/local/bin/perl -w 02 use strict; 03 04 use Time::HiRes qw(usleep); 05 use Device::USB; 06 my $usb = Device::USB->new; 07 my $dev = $usb->find_device(0xA81, 0x701); 08 $dev->open; 09 10 # Move Up 11 my $val = 0x02; 12 $dev->control_msg(0x21, 0x09, 0x02, 0, 13 chr($val), 1, 1000); 14 15 usleep(150_000); 16 17 # Stop 18 $val = 0x20; 19 $dev->control_msg(0x21, 0x09, 0x02, 0, 20 chr($val), 1, 1000); 21 22 # Read status 23 $val = 0x40; 24 my $buf; 25 $dev->control_msg(0x21, 0x09, 0x02, 0, 26 chr($val), 1, 1000); 27 $dev->bulk_read(1, $buf = "", 1, 1000); 28 printf "Status %08b\n", ord($buf);
Um den Status des Geschützes abzufragen, schickt die Steuerung zunächst
den Control-Code 0x40 mit control_msg()
an das USB-Device, um gleich
hinterher per Bulk-Transfer mit der Methode bulk_read()
den
bereitgestellten Datenstring abzuholen. Zeile 28 in Listing rocket-test
schreibt als Ergebnis in den meisten Fällen 00000000
aus, es sei denn,
der Turm
steht am Anschlag oder die eine Rakete wurde gerade abgefeuert.
Das Modul Device::USB::MissileLauncher::RocketBaby vom CPAN bietet eine
schöne Abstraktion der Schnittstelle, ein neu konstruiertes Objekt
verfügt über die Methoden do()
and cando(), die Aktionen als Strings wie
``left'', ``up'', ``fire'' oder ``stop'' entgegennehmen. Die Methode do()
führt
die entsprechende Aktion aus, cando()
hingegen prüft mit einer Statusabfrage
und einem Bulk-Lesevorgang, ob die Aktion gegenwärtig durchführbar ist.
Listing center-fire
illustriert den Gebrauch. Es dreht den Geschützturm
zunächst bis ganz nach links unten, damit es dessen genaue Position kennt.
Anschließend misst es die Zeit, die benötigt wird, um den Turm sowohl nach
ganz oben als auch zum rechten Anschlag zu drehen. Es halbiert dann beide
Zeiten, fährt den Turm mit den gewonnenen Werten zurück in die Mitte und
feuert eine Rakete nach der anderen ab.
01 #!/usr/local/bin/perl -w 02 use strict; 03 04 use 05 Device::USB::MissileLauncher::RocketBaby; 06 use Time::HiRes qw(usleep gettimeofday 07 tv_interval); 08 09 my $rb = 10 Device::USB::MissileLauncher::RocketBaby 11 ->new(); 12 13 do_until("left"); 14 do_until("down"); 15 16 my $right_start = [gettimeofday]; 17 do_until("right"); 18 my $right_elapsed = tv_interval( 19 $right_start, [gettimeofday] ); 20 21 my $up_start = [gettimeofday]; 22 do_until("up"); 23 my $up_elapsed = tv_interval( 24 $up_start, [gettimeofday] ); 25 26 do_until("left", $right_elapsed/2); 27 do_until("down", $up_elapsed/2); 28 29 for(1..3) { 30 do_until("fire"); 31 usleep(100_000); 32 } 33 34 ########################################### 35 sub do_until { 36 ########################################### 37 my($what, $max_time) = @_; 38 39 my $start = [gettimeofday]; 40 41 while($rb->cando( $what )) { 42 $rb->do( $what ); 43 usleep(100_000); 44 last if defined $max_time and 45 tv_interval($start, 46 [gettimeofday]) > $max_time; 47 } 48 $rb->do("stop"); 49 }
Linux benötigt das Paket libusb-dev
, um aus dem Userspace auf
USB-Geräte zugreifen zu können. Alle relativ neuen
Distributionen verfügen bereits
darüber. Die Module Device::USB und
Device::USB::MissileLauncher::RocketBaby lassen sich am besten mit einer
CPAN-Shell installieren.
Verschiedene Typen des Raketenwerfers [7] nutzen unterschiedliche Code-Kombinationen, die im Zweifelsfall aus dem Internet eingeholt und in eine Abstraktion wie das RocketBaby-Modul vom CPAN verpackt werden sollten.
Wie der Raketenwerfer mit dem Skript center-fire
gesteuert herumorgelt,
zeigt das Youtube-Video auf [8].
Und zum Schluss noch ein dringender Hinweis
des Innenministers: Ein Export des Geräts oder des Programms in
sogenannte Schurkenstaaten ist unbedingt zu vermeiden.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/07/Perl
``The Great Office War'', http://www.youtube.com/watch?v=pVKnF26qFFM
``Python Interfacing a USB Missile Launcher'', Pedram Amini, http://dvlabs.tippingpoint.com/blog/2009/02/12/python-interfacing-a-usb-missile-launcher
Pyrocket, http://code.google.com/p/pyrocket/
``Essential Linux Device Drivers'', Sreekrishnan Venkateswaran, Prentice Hall, 2007.
Das libusb-Projekt: http://libusb.sourceforge.net
Youtube-Video, das den USB Missile Launcher ``Rocket Baby'' bei der Ausführung des Skripts center-fire zeigt: http://www.youtube.com/watch?v=-6qTRhDijJc
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. |