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.