Feuer frei (Linux-Magazin, Mai 2009)

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]).

Anstöpseln unter Hardy

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.

Nicht nur Chefsache

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.

Steuerung mit einem Byte

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.

Feuer frei

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.

Listing 1: rocket-test

    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);

Schütze A. meldet ...

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.

Listing 2: center-fire

    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 }

Installation

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.

Infos

[1]

Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/07/Perl

[2]

``The Great Office War'', http://www.youtube.com/watch?v=pVKnF26qFFM

[3]

``Python Interfacing a USB Missile Launcher'', Pedram Amini, http://dvlabs.tippingpoint.com/blog/2009/02/12/python-interfacing-a-usb-missile-launcher

[4]

Pyrocket, http://code.google.com/p/pyrocket/

[5]

``Essential Linux Device Drivers'', Sreekrishnan Venkateswaran, Prentice Hall, 2007.

[6]

Das libusb-Projekt: http://libusb.sourceforge.net

[7]

http://www.amazon.de/USBMis-USB-Raketenwerfer/dp/B000KC3YQK

[8]

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 Schilli

arbeitet 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.