Leitfaden zur Programmierung mit dem UPnP-Stack

Werbung
Leitfaden zur Programmierung mit dem UPnP-Stack
Alexander König, Fraunhofer FOKUS
1
Einleitung
Der Java UPnP-Stack von Fraunhofer FOKUS dient zur Erstellung und Kontrolle
beliebiger UPnP-fähiger Geräte. Er implementiert alle dafür notwendigen
Protokolle wie SSDP, SOAP und GENA. Für den Anwendungsentwickler
beschränkt sich die Arbeit auf die Erstellung angepasster Services sowie die
Programmierung der dahinter befindlichen Logik. Die UPnP-Mechanismen bleiben
ihm
verborgen.
Es existieren
bereits
verschiedene
Geräte
für die
Heimautomatisierung oder den AV-Bereich.
2
2.1
Installation
Installation über eine Archiv-Datei
Der Stack liegt als Archiv vor, dieses kann man in ein beliebiges Verzeichnis
entpacken und anschließend in Eclipse ein neues Projekt anlegen. Dort wählt man
„Create project from existing source“ und navigiert zum entsprechenden
Verzeichnis. Den Source-Ordner noch auf src/ setzen und anschließend sollte das
Projekt fehlerfrei kompilieren.
2.2
Weitere Einstellungen
Ein möglicher Fehler beim Kompilieren sind nicht gefunden Libraries. Diese liegen
im Unterordner lib (notfalls unter Properties -> Java Build Path -> Libraries
manuell eintragen).
Hinweis: Der Java-Stack benutzt UTF-8 als Kodierung. Diese Kodierung muss auch
in Eclipse eingestellt werden, um eine fehlerhafte Darstellung zu vermeiden. Dazu
im Package Explorer rechts auf den Projektnamen (z.B. UPnP) klicken und unter
Properties -> Info -> Text file encoding
UTF-8 auswählen.
2.3
Systemtest
Um das System zu testen:
1. Navigation nach
src/de/fraunhofer/fokus/upnp/core/examples/gui_control_point
2. Rechtsklick auf GUIControl.java -> Run as -> Java Application. Wenn keine
Fehlermeldungen kommen und eine Benutzeroberfläche erscheint, ist alles
klar.
3
Allgemeine Hinweise
Der komplette Stack liegt unter de.fraunhofer.fokus.upnp. Er ist folgendermaßen
unterteilt:
•
gena: UPnP-Eventing
•
http: HTTPServer und -Client Code
•
soap: UPnP-Control
•
ssdp: UPnP-Discovery
•
core: Geräte und Kontrollpunkt-Code
•
core_av: Geräte und Kontrollpunkt-Code für Audio/Video
•
core_security: UPnP-Security Implementierung
•
gateway: Code für Internet- und andere Gateways
•
util: Hilfsklassen für verschiedenste Aufgaben
Es existieren ein paar Konventionen für das Anlegen neuer Packages und Klassen.
3.1
•
examples enthält ausführbare Klassen (Java-Applikationen)
•
templates enthält Klassen, die für eigene Zwecke erweitert werden können
•
Klassennamen für Interfaces beginnen immer mit I
Formatter
Um den Code leichter lesbar zu halten, sollte der vorgegebene Formatter benutzt
werden. Dieser entspricht nicht ganz dem Java-Standard (z.B. nur 2 statt 4
Leerzeichen pro Einrückung). Den Formatter aktiviert man, indem man in Eclipse
unter
Preferences -> Java -> Code Style -> Formatter
die Datei formatter.xml aus dem Hauptverzeichnis des Projekts importiert.
3.2
Code Templates
Angepasste CodeTemplates für Eclipse (z.B. für Kommentare sowie Getter/Setter)
findet man ebenfalls im Hauptverzeichnis in der Datei codetemplates.xml. Diese
importiert man über
Preferences -> Java -> Code Style -> Code Templates
3.3
Editor Templates
EditorTemplates dienen der schnelleren Quellcode-Generierung (z.B. forSchleifen). Zusätzliche Vorlagen findet man in der Datei editortemplates.xml.
Diese importiert man über
Preferences -> Java -> Editor -> Templates
4
4.1
Erstellen eigener Geräte
Allgemeine Hinweise
Neue UPnP-Geräte sollten in einem der Packages
•
core.examples
•
core_av.examples
•
gateway.examples
implementiert werden.
Alle Geräte werden von den folgenden Klassen abgeleitet:
•
TemplateEntity ist die Basisklasse für alle Geräte. Diese enthält bei Bedarf
die main()-Methode bzw. wird von anderen Klassen zum Starten von
Geräten instantiiert. Eine Entity kann ein oder mehrere Geräte sowie einen
Kontrollpunkt enthalten.
•
TemplateDevice ist die Basisklasse für ein Gerät. An dieser Stelle findet die
Verknüpfung von Gerät und den zur Verfügung gestellten Services statt.
•
TemplateService ist die Basisklasse für neue Dienste/Services. Sie enthält
sowohl die Dienstdefinition als auch die Dienst-Logik.
Für diesen Leitfaden wird die Erstellung eines Gerätes beschrieben, welches eine
einfache Uhrzeitabfrage zur Verfügung stellt. Der Code für dieses Gerät findet sich
im Stack unter core.examples.stress .
4.2
Erstellen der ClockEntity
Die ClockEntity hat die Aufgabe, beim Aufruf des Konstruktors oder der main()Methode das UPnP-Gerät zu starten. Deshalb ist der Code an dieser Stelle sehr
übersichtlich und muss für andere Geräte nur geringfügig geändert werden.
4.3
Erstellen des ClockDevice
Die Uhr besteht nur aus einem Gerät, nämlich dem ClockDevice. Dieses enthält im
Minimalfall auch nur einen Dienst (ClockService). Im Beispiel enthält das
ClockDevice noch weitere Dienste (u.a. Einen TranslationService), welche die
Benutzung des Gerätes komfortabler machen, für das Funktionieren jedoch nicht
erforderlich sind. Den Quellcode für diese Dienste findet man unter
core.device.common.
Das ClockDevice enthält außerdem zusätzlichen Code für den geräteinternen
Webserver. Dieser kann einen Teil oder alle Informationen des Geräts auch über
einen Browser zur Verfügung stellen. In diesem Fall zeigt der Browser die aktuelle
Uhrzeit sowie die Sekunden des Tages an. Der Code generiert dazu eine HTMLSeite mit den gewünschten Informationen.
4.3.1
Funktionen für Fortgeschrittene
Für Geräte mit fortgeschrittenem Funktionsumfang kann es notwendig sein,
Initialisierungen vor dem Start des Gerätes auszuführen. Dafür gibt es zwei
Möglichkeiten:
•
Überschreiben der Methode forceRunDelayed() mit dem Rückgabewert
true. Das Gerät wird dann erst durch den Aufruf von runDelayed()
vollständig gestartet. Der Konstruktor sieht dann ungefähr so aus:
{
super();
Eigene Initialisierung des Geräts
runDelayed()
}
•
Überschreiben der Methoden setupDeviceVariables(), initDeviceContent()
und/oder runDevice(). Diese werden im Konstruktor automatisch
aufgerufen. Es ist unbedingt notwendig, in den eigenen Implementierungen
mit super() die ursprüngliche Methode auch aufzurufen
4.4
4.4.1
Erstellen des ClockService
Dienstdefinition
Ein Dienst (=Service) eines Gerätes enthält im einfachsten Fall sowohl die UPnPRepräsentation (Zustandsvariablen und Aktionen) als auch die Logik eines
Dienstes (z.B. das Ansteuern einer Lampe nach Aufruf der entsprechenden UPnPAktion).
Für bekannte Dienste wie z.B. Medienserver, Temperatursensoren und Lampen
sind die Zustandsvariablen, Aktionen sowie deren Ein- und Ausgabeparameter
standardisiert. In diesen Fällen beschränkt sich die Arbeit also auf das Abtippen
der definierten Namen sowie die Implementierung der Logik.
Für den ClockService existiert kein Standard; die Namen sind also frei wählbar
und sehen z.B. folgendermaßen aus:
private StateVariable
private StateVariable
private StateVariable
seconds;
A_ARG_TYPE_int;
A_ARG_TYPE_string;
Die Zustandsvariablen dienen der Modellierung des Zustandes. Variablen, die mit
A_ARG_TYPE_ beginnen, werden nur für Aktionen benutzt.
seconds = new StateVariable("Seconds", getCurrentTime(), true);
...
StateVariable[] stateVariableList = { seconds, A_ARG_TYPE_int, ... };
setStateVariableTable(stateVariableList);
Aktionen werden auf ähnliche Weise definiert:
private Action
private Action
getSeconds;
getTime;
getSeconds = new Action("GetSeconds");
getSeconds.setArgumentList(new Argument[]
{
new Argument("Seconds",UPnPConstant.DIRECTION_OUT,A_ARG_TYPE_int)
});
...
Action[] actionList = { getSeconds, getTime };
setActionList(actionList);
Der Java-Stack kümmert sich automatisch um die Generierung der Geräte- und
Dienstbeschreibungen, um die definierten Strukturen UPnP-Kontrollpunkten
zugänglich zu machen.
4.4.2
Dienst-Logik
Der wichtigste Punkt jedes Gerätes ist die Implementierung der definierten
Aktionen. Der Java-Stack nutzt Reflection, um zur Laufzeit die Methode für einen
empfangenen Aufruf zu finden und auszuführen. Deshalb muss der Name der
Methode dem definierten Aktionsnamen mit kleinem Startbuchstaben entsprechen.
public void getSeconds(Argument[] args) throws ActionFailedException
{
try
{
args[0].setNumericValue(getCurrentTime());
} catch (Exception ex)
{
logger.warn(ex.getMessage());
throw new ActionFailedException(402,"Invalid args");
}
}
Dies ist die Implementierung für die Methode getSeconds(). Sie enthält eine
einfache Fehlerbehandlung und setzt die Ausgangsvariable „Seconds“ auf den
aktuellen Wert. Für komplexere Geräte ist an dieser Stelle natürlich mehr Aufwand
nötig.
4.5
Geräteressourcen
Geräte und Kontrollpunkte benötigen oft Ressourcen in Form von Textdateien,
Bildern, Icons und ähnlichen Objekten, die als Datei im System abgelegt sind. Um
diese zur Laufzeit zugreifbar zu machen, kann für jede Entity ein entsprechendes
Ressource-Verzeichnis angelegt werden. Alle Ressourcen müssen im Projekt unter
res/ abgelegt werden. Der Pfad entspricht dabei dem verkürzten Pfad zu den
Source-Dateien des Geräts oder Kontrollpunkts. Beispiel:
Der korrekte Ressource-Pfad für den Uhr-Service ist
res/core/examples/stress
da die Source-Dateien unter
de/fraunhofer/fokus/upnp/core/examples/stress
liegen. Eine Besonderheit ist das Verzeichnis res/web_server_common. Dieses
enthält unter anderem Ressourcen für die Geräte-Webseite. Auch Icons für Geräte
können hier abgelegt werden. Dies erspart das Anlegen von RessourceVerzeichnissen für Geräte, die keine weiteren Dateien benötigen.
4.6
Gerätestart
Alle gerätespezifischen Eigenschaften wie Name, Hersteller etc. werden in einer
XML-Datei gespeichert. Für existierende Geräte sind diese Dateien bereits erstellt
worden, sie finden sich im Verzeichnis res/.
Um den Startprozess so weit wie möglich zu automatisieren, benutzt der Stack für
jede Entity die Konfigurationsdatei mit dem Namen der Entity. Deshalb muss die
Konfigurationsdatei für neue Entities den gleichen Namen wie die Entity besitzen
und sich auch im Verzeichnis /res befinden. Für den Clock-Dienst wäre die
entsprechende Datei z.B. ClockEntity.xml.
Die Klasse StartupConfiguration.java ist für das Parsen der Konfiguration
verantwortlich. Sie sucht beim Start einer Entity die entsprechende XML-Datei
und benutzt die darin enthaltenen Daten. Die Klasse TemplateEntity unterstützt
auch den Start mit einer vorgegebenen StartupConfiguration oder einem
vorgegebenen Dateinamen, so dass man ohne großen Aufwand z.B. über
Kommandozeilenparameter andere Konfigurationen testen kann.
Konfigurationsdateien mit dem Namen EntityName_HostAddresse.xml oder
EntityName_HostName.xml können benutzt werden, um spezielle Eigenschaften
für Geräte auf bestimmten Rechnern zu erhalten (z.B. für rechnerspezifische
Verzeichnisse). Dadurch benutzt der Stack z.B. automatisch verschiedene
Medienverzeichnisse auf verschiedenen Rechnern, ohne das ein manueller Eingriff
in den Start notwendig ist. Der Konfigurationsparser sucht bevorzugt nach diesen
spezifischen Startdateien, nur wenn diese nicht gefunden werden, wird die
allgemeine Startkonfiguration benutzt.
Zur Zeit sind für die Startup-Konfiguration die folgenden Tags definiert:
Tag
Bedeutung
WorkingDirectory
Das Arbeitsverzeichnis für die Entity. In diesem
Verzeichnis werden z.B. Konfigurationsdateien
gesucht.
SSDPMulticastAddress
Multicast-Adresse für Discovery. Kann geändert
werden, um verschiedene UPnP-Domänen zu erzeugen.
Tag
Bedeutung
SSDPMulticastPort
Multicast-Port für Discovery. Kann geändert werden,
um verschiedene UPnP-Domänen zu erzeugen.
StartKeyboardThread
Startet einen Thread, um eine Entity über die
Kommandozeile zu beenden. Ist standardmäßig TRUE.
IgnoreIPAddress
Kann benutzt werden, um lokale Netzwerkinterfaces
von der Benutzung auszuschließen. Kann mehrfach
auftreten, um mehrere Interfaces auszuschließen.
Für Geräte sind die folgenden Tags spezifiziert:
Tag
Bedeutung
WorkingDirectory
Das Arbeitsverzeichnis für das Gerät. Kann benutzt
werden, um das Arbeitsverzeichnis der Entity zu
überschreiben.
DeviceType
Der Typ des UPnP-Geräts.
ModelName
Notwendiger Modell-Name des UPnP-Geräts.
Manufacturer
Notwendiger Hersteller.
FriendlyName
Name des UPnP-Geräts für die Anzeige.
UDN
Eindeutige UUID für das Gerät. Wird automatisch
durch die Host-Adresse des Servers ergänzt und in
eine gültige UUID umgewandelt.
HTTPServerPort
Port für den HTTP-Server. Muss für jedes Gerät
eindeutig sein. Bei Fehlen des Tags wird ein beliebiger
freier Port verwendet, diese Vorgehensweise ist jedoch
unerwünscht.
SSDPUnicastPort
Port für den Socket, der M-SEARCH Nachrichten
empfängt und beantwortet. Muss für jedes Gerät
eindeutig sein. Bei Fehlen des Tags wird ein beliebiger
freier Port verwendet, diese Vorgehensweise ist jedoch
unerwünscht.
WebServerDirectory
Verzeichnis für Ressourcen, welche über den
Webserver abgerufen werden können. Es können
mehrere Verzeichnisse definiert werden, die
nacheinander durchsucht werden.
DeviceKeyFile
Datei mit dem RSA-Schlüssel für Geräte, die
DeviceSecurity implementieren.
Für Kontrollpunkte sind die folgenden Tags spezifiziert:
Tag
FriendlyName
Bedeutung
Name des Kontrollpunkts. Wird hauptsächlich für
Debugging benutzt.
EventCallbackServerPort Port für den HTTP-Server, der Events empfängt. Muss
für jeden Kontrollpunkt eindeutig sein. Bei Fehlen des
Tags wird ein beliebiger freier Port verwendet, diese
Vorgehensweise ist jedoch unerwünscht.
Tag
Bedeutung
UDPEventCallbackServer Port für den HTTP-Server, der Events über UDP
Port
empfängt. Muss für jeden Kontrollpunkt eindeutig sein.
Diese proprietäre Erweiterung kann auch weggelassen
werden und funktioniert nur innerhalb des FOKUS
UPnP-Stacks.
SSDPUnicastPort
Port für den Socket, der M-SEARCH Nachrichten
aussendet und die Antworten empfängt. Muss für
jeden Kontrollpunkt eindeutig sein. Bei Fehlen des
Tags wird ein beliebiger freier Port verwendet, diese
Vorgehensweise ist jedoch unerwünscht.
ControlPointKeyFile
Datei mit dem RSA-Schlüssel für Kontrollpunkte, die
gesicherte Geräte benutzen können.
AutomaticEventSubscript Im Standardfall registriert sich ein Kontrollpunkt erst
ion
bei Bedarf für Events. Die Angabe dieses Tags mit dem
Wert TRUE sorgt für die automatische Registrierung
für alle Events bei allen entdeckten Geräten. Diese
Option ist aus Performancesicht eher ungünstig.
Der Programmierer kann eigene Tags in die Konfigurationsdatei aufnehmen und
im Code z.B. über startupConfiguration.getProperty() abrufen. Dies ermöglicht
eine proprietäre Erweiterung von Konfigurationsdateien mit gerätespezifischen
Tags. Diese Tags sollten im Kommentar des Entity-Konstruktors beschrieben
werden.
5
Benutzung eines Kontrollpunkts
Für die meisten Aufgaben sollte es nicht nötig sein, einen eigenen Kontrollpunkt zu
programmieren. In diesen Fällen instantiiert man TemplateControlPoint
(core.templates), der bereits vielfältige Möglichkeiten zur Gerätesteuerung bietet.
5.1
Entdeckung neuer Geräte
TemplateControlPoint bietet zwei Methoden für die Entdeckung an:
public void newDevice(CPDevice newDevice)
public void deviceGone(CPDevice goneDevice)
Der Kontrollpunkt bietet die Möglichkeit, einen zusätzlichen Listener für
Geräteereignisse zu installieren. Dafür implementiert man in einer Klasse das
Interface
ICPDeviceEventListener
und
ruft
anschließend
templateControlPoint.setCPDeviceEventListener(...) auf. Dies ist der
bevorzugte Weg zum Abfangen von Gerätenachrichten. Alle Events werden auch
an die dazugehörige TemplateEntity weitergeleitet. Man kann also auch
setCPDeviceEventListener(...) für die TemplateEntity aufrufen.
Die Eigenschaften des gefundenen Gerätes können über verschiedene Methoden
abgefragt werden (z.B. getDeviceType()). Über diese Methoden erhält man auch
Auskunft über die zur Verfügung stehenden Dienste.
5.2
Ausführen von Aktionen
Für das Ausführen von Aktionen bietet der TemplateControlPoint die Methode
public Action invokeAction(CPAction action)
an. Diese Methode erstellt automatisch die entsprechenden SOAP-Nachrichten und
parst auch die Antwort des Gerätes. Die Verwendung dieser Methode hat den
Vorteil, daß für Geräte, welche UPnP-Security unterstützen, automatisch die
benötigten Signaturen erstellt werden (im Gegensatz zum Aufruf von
service.invokeAction()).
Die Instanz der Aktion erhält man durch Aufruf von service.getAction(String actionName) mit dem gewünschten Aktionsnamen.
Eingabeparameter
für
die
Aktion
setzt
man
anschließend
durch
action.getArgument(String argumentName).setValue(). Nach dem Aufruf der
Aktion erhält man die Ausgabeparameter über
returnedAction.getArgument(String argumentName).getValue().
Fehlermeldungen erhält man über ActionFailedException. Ein Beispiel für die
Verwendung des ClockService findet man unter
core.examples.gui_control_point.plugins.ClockPlugin.
5.3
Abfragen von Zustandsvariablen
Im Standardfall registriert sich der Kontrollpunkt nicht bei Geräten, um die
Netzwerklast gering zu halten. Um sich für Events zu registrieren, ruft man z.B. in
der newDevice() Methode die Funktion startManualEventSubscriptions(newDevice)
auf. Dies sorgt für den Empfang von Events für alle Services auf diesem Gerät.
endManualEventSubscriptions() beendet die Registrierung für Events wieder.
Zustandsvariablen können jederzeit über service.getCPStateVariable(String stateVariableName).getValue() abgefragt werden. Um automatisch bei sich
ändernden Variablen informiert zu werden, implementiert man das Interface
ICPStateVariableListener .
Anschließend registriert man den
templateControlPoint.setStateVariableListener(...) oder
Listener
über
templateEntity.setStateVariableListener(...).
Über
die
Methode
stateVariableChanged(CPStateVariable stateVariable), welche bei jeder
geänderten Variable aufgerufen wird, wird man automatisch über neue Werte
informiert.
Herunterladen