Project

General

Profile

Tutorial zum Schreiben eines Plugins

Sorry, this article is not translated !

Da ja xxv ein sehr modulares Konzept besitzt müssen nur einige Dinge beachtet werden um selber Plugins fuer XXV schreiben zu können. Ich versuche das Konzept anhand des Grab Plugins zu erklären. Dieses Plugin soll alle 5 Minuten einen grab Befehl an das svdrp Interface vom vdr schicken. Außerdem soll es möglich sein mit einem Befehl sich jederzeit das Bild ansehen zu können.
Plugin erstellen

Zu aller Erst muss ein Name fuer das Plugin festgelegt werden. Alle Plugins bei xxv hören auf folgendes Namensschema:

XXV::MODULES:: also
XXV::MODULES::GRAB

Der Pfad lautet also nach perl Konvention: lib/XXV/MODULES/GRAB.pm, alle Module und Plugins befinden sich grundsätzlich in dem lib-dir.

Standard Methoden

Um ein Plugin zu schreiben werden Standardmethoden in dem Plugin benoetigt damit xxv weiss welchen Namen das Plugin hat und welche Befehle auf diesem Plugin ausgeführt werden können. Im grunde sind das nur 2 an der Zahl: new, module. Hier mal ein winziges Beispiel ohne Funktion:

 package XXV::MODULES::GRAB;

 sub new {
    my($class, %attr) = @_;
    my $self = {};
    bless($self, $class);

    $self->{MOD} = $self->module;

    return $self;
 }

 sub module {
     my $obj = shift || return error ('No Object!' );
     my $args = {
         Name => 'GRAB',
     };
     return $args;
 }

 1;

new

typisches perlmethode damit das plugin initialisiert werden kann (XXX::MODULES::GRAB->new({options => 1}). Diese Routine wird vom startprogramm xxvd automatisch aufgerufen nach dem Schema:

    use XXV::MODULES::GRAB;
    my $auto = XXV::MODULES::GRAB->new(
        -config => $Config,
        -dbh    => $DBH,
        -realbin=> $RealBin,
    );

Dabei schaut xxvd einfach in dem Ordner mit den Plugins lib/XXV/MODULES/* und startet bei allen die new Methode. Dabei wird immer als Argumente die komplette Konfiguration von der xxvd.cfg als Hash ($Config->{name} => $value) uebergeben. Ausserdem wird das fertig konnektierte Datenbankhandle mit übergeben und sowie der genaue Pfad wo das Programm läuft.

In unserem Beispiel sieht die new Methode in dem Module Grab folgendermaßen aus:

 # ------------------
 sub new {
 # ------------------
    my($class, %attr) = @_;
    my $self = {};
    bless($self, $class);

    # who am I
    $self->{MOD} = $self->module;

    # all configvalues to $self without parents (important for ConfigModule)
    map {
        $self->{$_} = $attr{'-config'}->{$self->{MOD}->{Name}}->{$_}
    } keys %{$attr{'-config'}->{$self->{MOD}->{Name}}};

    # i need the svdrp module
    $self->{svdrp} = main::getModule('SVDRP');

    # Interval to read timers and put to DB
    Event->timer(
        interval => $self->{config}->{interval},
        cb => sub{
            $self->grab();
        },
    );

    return $self;
 }

Sieht schlimmer aus als es ist ;) Also am Anfang wird der Classenname($class) und die Argumente(%attr) übergeben. Die Klasse wird dann mit bless als Modul in Perl definiert. Am besten die ersten 3 Zeilen einfach so übernehmen, dann kann nichts schiefgehen ;)

In der folgenden Zeile ...

    # who am I
    $self->{MOD} = $self->module;

werden einfach die Metadaten über die Methode $self->module in die Variable $self->{MOD} abgespeichert, somit weiß das Hauptprogramm welche Befehle, Versionsnummer usw. zur Verfügung stehen. Zu dem genauen Syntax lese bitte im nächsten Kapitel weiter.

Die nächsten Zeilen extrahieren die wichtigsten Parameter für unseren grab Befehl.

Die Monsterzeile tut nur eins, alle Configvariablen werden in dem Schema $self ->{name} = value abgespeichert. So das das neue Configmodul in der lage ist jederzeit ein reconfigure vor zu nehmen.

War doch bis jetzt ganz einfach oder? ;) Ok weiter im Text, natürlich brauchen wir ja noch das wichtigste nämlich die SVDRP Schnittstelle damit wir dem telnet Interface vom vdr sagen können das wir jetzt einen grab machen wollen.

Wie kommt man nun an die anderen Module ran, eigentlich ganz einfach. mit der folgenden Zeile:

    # i need the svdrp module
    $self->{svdrp} = main::getModule('SVDRP');

Hier wird aus dem Hauptprogramm eine Routine mit dem Namen getModule aufgerufen. Als Parameter übergeben wir den Namen des Modules das wir benötigen. In $self ->{svdrp} steht dann hoffentlich eine Referenz auf das SVDRP Module und können über dieses Interface nach Lust und Laune Befehle an das svdrp Interface schicken und die Antworten abwarten. zB.:

 my $erg = $self->{svdrp}->command('stat disk');

Am Ende kommt noch etwas sehr spannendes, wir wollen ja alle 5 Sekunden ein Bild grabben:

    # Interval to read timers and put to DB
    Event->timer(
        interval => $self->{interval},
        cb => sub{
            $self->grab();
        },
    );

Die Macht von xxv gegenüber z.B. vdradmin ist ja sein asynchrones Eventinterface, mit diesem Interface kann man mehrere Aufgaben parallel erledigen. Zum Beispiel liest ja xxv die epg.data und stellt neue Einträge in die Datenbank, gleichzeitig ruft es aber auch die Routine grab alle 5 Sekunden auf um ein neues Bild von der DVB-Karte zu erhalten. Hiermit können also Events völlig losgelöst von den anderen Events behandelt werden als das was sie sind ... nämlich als ein Ereignis ;) Nähere Informationen über dieses Klasse Modul könnt ihr bei CPAN nachlesen und nutzen:

http://search.cpan.org/~jprit/Event-1.00/lib/Event.pod

module

So das war es auch schon, upps ... stimmt nicht ganz. Wir müssen ja noch die Metainformationen fuer das Plugin aufbereiten damit das Hauptprogramm auch weiß um welches Plugin es sich handelt, wer das ganze verbrochen hat, wie es von dem user gesteuert werden kann usw.

 # This module method must exist for XXV
 # ------------------
 sub module {
 # ------------------
    my $obj = shift || return error ('No Object!' );
    my $args = {
        Name => 'GRAB',
        Description => 'This module grab a picture from livestream.',
        Version => '0.01',
        Date => '06.09.2004',
        Author => 'xpix',
        Preferences => {
            xsize => {
                description => 'The x size from picture',
                default     => 320,
                type        => 'integer',
                required    => 'This is required',
            },
            ysize => {
                description => 'The y size from picture',
                default     => 240,
                type        => 'integer',
                required    => 'This is required',
            },
            file => {
                description => 'The place to save the image file',
                default     => '/tmp/live.jpg',
                type        => 'string',
                required    => 'This is required',
            },
            interval => {
                description => 'The interval to grab the image',
                default     => 300,
                type        => 'integer',
                required    => 'This is required',
            },
        },
        Commands => {
            grab => {
                description => 'Grab a picture',
                short       => 'gr',
                callback    => sub{ $obj->grab(@_) },
            },
            gdisplay => {
                description => 'Display the picture',
                short       => 'gd',
                callback    => sub{ $obj->display(@_) },
            },
        },
    };
    return $args;
 }

Ganz wichtig dabei ist der Parameter Name => 'GRAB', ueber diesen Namen koennen andere Plugins dieses Grabmodul nutzen. Ohne diesen Namen wuerde getModule nicht funktionieren. Nehmen wir zum Beispiel ein Fernbedienungsinterface das nach jeder Bedienung auch einen neuen Grab machen will, damit der User die Reaktion des vdr auf seinen tastendruck sofort sehen kann. Dieses Plugin wuerde also folgendes aufrufen:

    ...
    $obj->click('menu');
    my $grabmodule = main::getModule('GRAB');
    $grabmodule->grab();
    ...

Der Rest kann angegeben werden, muss aber nicht:

        Description => 'This module grab a picture from livestream.',
        Version => '0.01',
        Date => '06.09.2004',
        Author => 'xpix',

Preferences laesst diese Werte in der Configdatei und auch im Preferences Dialog erscheinen. Somit ist auch das Modul Config in der Lage ein reconfigure von dem modul auszuloesen.

Kommen wir nun zu dem Teil wo das Hauptprogramm endlich weiss welche Befehle Grab zur Verfuegung stellt. Das macht der Commands Parameter. Jedes Plugin kann logischerweise auch mehrere Befehle besitzen und dem user zur Verfuegung stellen.

Hier also unser Beispiel:

        ....
        Commands => {
            grab => {
                description => 'Grab a picture',
                short       => 'gr',
                callback    => sub{ $obj->grab(@_) },
            },
            gdisplay => {
                description => 'Display the picture',
                short       => 'gd',
                callback    => sub{ $obj->display(@_) },
            },
        },
        ....

Das Kommando womit man eine bestimmte Aktion ausfuhert ist der Name zu einem Hash der noch die anderen Parameter beinhaltet. Sehen wir uns ein Kommando genauer an:

            grab => {
                description => 'Grab a picture',
                short       => 'gr',
                callback    => sub{ $obj->grab(@_) },
            },

description:

beschreibt was das Kommando am Ende tut ;)

short:

Dies ist eine Kurzform des Befehls grab, in diesem Fall gr. Es wird von xxvd ueberprueft ob dieser Shortcut schon existiert und gg. angemeckert. Dann solltet ihr Euch eine anderen Shortnamen einfallen lassen.

callback:

Dies ist die Aktion die am ende ausgefuehrt wird, also in unserem Fall $obj ->grab(@_). Als Parameter werden grundsaetzlich der watcher, console sowie optionale zusatzparameter uebergeben.

grab

Schauen wir uns also gleich mal die entsprechende GrabMethode genauer an:

    # ------------------
    sub grab {
    # ------------------
        my $obj = shift || return error ('No Object!' );
        my $watcher = shift;
        my $console = shift;

        # the svdrp module
        my $svdrp = $obj->{svdrp};

        # the command
        my $cmd = sprintf('grab %s jpeg 80 %d %d',
                $obj->{file},
                $obj->{xsize},
                $obj->{ysize},
        );

        my $erg = $svdrp->command($cmd);
        $console->message($erg)
            if(ref $console);
        return 1;
    }

Am Anfang seht ihr auch gleich was an diese Methode uebergeben wird, erstmal das eigentliche GrabObject dann der watcher und die entsprwechende Console (z.Zt. HTTPD,Telnet oder Interface). Dann hole ich mir noch schnell das svdrpObjekt das ich ja in der new Routine definiert habe.

In der naechsten Zeile definiere ich den GrabBefehl und schicke ihn mit der Methode command an das TelnetInterface vom vdr. Der verarbeitet den Befehl und gibt Message aus die ich dann meinerseits an die Console von dem User schicke.