Und Action!

Starten von Sender und Empfänger

Durch das Auslagern der Grundfunktionen in einen eigenes Projekt (Kern) sind beide Startsequenzen nahezu identisch. Um neue Funktionen hinzuzufügen genügt es daher ein Modul zu implementieren und über die Methode addModule() der Applikation hinzuzufügen. Damit die GUI beim Hochfahren des Senders nicht blockiert lagern wir das Starten des Senders in einen eigenen Thread aus.

Die Startsequenz des Senders.

alt

Im Gegensatz dazu besitzt der Empfänger eine eigene main(String[] args) Methode und wird deshalb direkt von jamVM gestartet.

Die Startsequenz des Empfängers.

alt

Weil es mit Java Bordmitteln unter Windows nicht möglich ist Daten aus unserem Joystick auszulesen benutzen wir Ubuntu als Betriebssystem für den Sender. Da das Joystick Testprogramm jstest Daten auf die Konsole schreibt nutzen wir die Möglichkeit über einen InputStream darauf zuzugreifen. Diese Funktion haben wir in die Klasse JoystickReader ausgelagert, die im JoystickModule instanziert wird (Zeile 39).  Nach dem Aufbereiten des Datenstromes im JoystickModule lesen wir die Daten hier über das Standard Java Listener Konzept aus (Zeile 40 und 48). In Zeile 53 erstellen wir ein JoystickDataPacket dem wir die aktuellen Werte für Rollen und Nicken übergeben. Zum Schluss wird das Datenpaket dem Host mit Absender und Empfänger zum Versand übergeben.

Unser JoystickModule bei der Initialisierung (Auszug)

alt

Der Zugriff auf das Programm jstest

alt

Und Ab die Post!

In der Methode send() – hier verkürzt dargestellt – werden dem Paket zunächst die Adressen des Absendermodules und Empfängermodules gesetzt. Zum Schluss platzieren wir das Datenpaket in eine FIFO Queue (Zeile 116). Diese Queue ist eine Instanz der Klasse java.util.concurrent.ArrayBlockingQueue und erlaubt den konkurrierenden Zugriff mehrerer Threads.

alt

Zum Protokoll bitte!

In einem Thread unseres Senders warten wir auf eine Socket Verbindung, von der wir uns den Ausgabestrom besorgen (Zeile 217). Dem Protokoll übergeben wir einen RBBDataOutputStream der den Ausgabestrom des Sockets benutzt. In der Schleife wartet Protocol solange bis Daten zum versenden anstehen (Zeile 221).

Der Socket Thread unseres Senders.

alt

Und Tschüss!

Sobald sich ein Datenpaket in der Queue befindet holt es Protocol und schreibt als erstes die ID des Pakets. Diese ID brauchen wir später auf der Gegenseite zum erstellen eines neuen Paketes. In Zeile 122 schreibt nun das Datenpaket seine Werte in den Datenstrom. Zeile 123: Wir übergeben unsere Nachricht den “Händen” des Betriebssystems. Das Datenpaket wird jetzt nicht mehr benötigt.

alt

JoystickDataPacket schreibt seine Werte (siehe auch Zeile 122 oben).

alt

The same procedure as…  Zum Protokoll bitte! (Teil 2)

Die Daten sind beim Empfänger angekommen und werden jetzt weiterverarbeitet.

In einem Thread unseres Empfängers warten wir auf eine Socket Verbindung, von der wir uns den Eingabestrom besorgen (Zeile 325). Dem Protokoll übergeben wir einen RBBDataInputStream der den Eingabestrom des Sockets benutzt. In der Schleife wartet Protocol solange bis Daten zum auslesen anstehen (Zeile 328).

Der Thread unseres Empfängers

alt

Sie haben Post!

Sobald Daten verfügbar sind lesen wir als erstes die ID des Datenpaketes (Zeile 139). Anhand der ID lassen wir uns von der DataPacketFactory das passende Datenpaket erzeugen (Zeile 140). Anschliessend liest das Paket die Daten aus dem Eingabestrom. Es ist jetzt vollständig initialisiert und wird in der Queue (java.util.concurrent.ArrayBlockingQueue) abgelegt (Zeile 142).

alt

 

DataPacketFactory schnürt uns ein Paket in Zeile 97 und 98.

alt
JoystickDataPacket liest seine Werte.

alt

Wohin damit?

Ein weiterer Thread entnimmt aus der Queue das Paket und übergibt es der Methode _dispatch(). Wir erfragen die Adresse des Modules (Zeile 294) und suchen es anschliessend in unserer Liste (Zeile 295). Als letzte Anweisung übergeben wir das Paket dem Empfänger(modul). Wir erinnern uns an die Zeile host.send(JoystickModule.this, p, ModuleAddress.FLIGHT_LOGIC_UNIT); – somit ist klar dass ab jetzt das Modul FlightLogicUnit für die Weiterverarbeitung zuständig ist.

alt

Ja ist denn schon Weihnachten?

In setPacket() der Klasse FlightLogicUnit befindet sich der Aufruf von _handleOperandPacket(). In dieser privaten Methode ist die Bearbeitung von Daten implementiert die für Operanden bestimmt sind. Wir “öffnen” das Paket in dem wir einen Typecast auf JoystickDataPacket durchführen und weisen den entsprechenden Operanden die Roll und Pitch Werte zu (Zeile 314 bis 316). Damit hat JoystickDataPacket seinen Zweck erfüllt und wird nicht mehr benötigt.

alt

Die FlightControlUnit

“Mach’ dir keine Sorgen wegen deiner Schwierigkeiten mit der Mathematik. Ich kann dir versichern, dass meine noch größer sind” (Albert Einstein).

In der Klasse FlightControlUnit  führen wir die Berechnungen zur Fluglage von RoboDrohne durch. Beim Hochfahren besorgt sich FlightControlUnit alle für die Berechnung notwendigen Operanden (Zeile 97 bis 120). In process() (hier auszugsweise dargestellt) findet die eigentliche Berechnung statt, deren Ergebnis vor dem Verlassen der Methode den Motor Operanden zugewiesen wird.

alt
Zuweisung der Berechnungen an die vier Motor Operanden am Ende von FlightControlUnit.process()

alt

Das Modul FlightLogicUnit und die MainLoop

Wie bereits an anderer Stelle erwähnt ist FlightLogicUnit das Logikmodul. Es entscheidet welche Aktion bei welchem Betriebszustand erlaubt bzw. verboten ist. Falls beispielsweise die WLAN Verbindung zusammenbricht, übergibt die FlightLogicUnit die Flugsteuerung dem Autopilot. Dies geschieht durch setzen eines booleschen Flags das den Aufruf der Methode process() zur Folge hat.

Während der Initialisierung von FlightLogicUnit füllen wir eine Liste in einer bestimmten Reihenfolge mit den Modulen die die Methode process() implementieren. In der MainLoop wird dann dieses Array genau in dieser Reihenfolge durchlaufen. Nach dem Aufruf aller Sensoren prüft FlightLogicUnit den Zustand des gesamten Systems anhand der Operandeninhalte sowie weiterer Informationen. Entsprechend der Auswertung der Informationen setzt das Modul im Array permitted[] die Flags.

Die Liste mit der Aufrufreihenfolge.

alt

Die Endlosschleife im Thread MainLoop. Die Indizes der Arrays permitted[] und processingModules[] entsprechen den Adressen der Module. Der Thread wird beim Hochfahren des Moduls gestartet und läuft kontinuierlich bis zum herunterfahren des Empfängers.

alt

Hier noch ein Quelltextausschnitt unseres wichtigsten Sensors: SpatialPhidget. Er ist im Modul InertialMeasurementUnit (IMU) beheimatet. Die Methode setup() wird beim Hochfahren des Empfängers aufgerufen. In Zeile 131 und 132 rufen wir die aktuellen Werte des Sensors für Roll und Pitch ab.

alt

Wir geben Gas!

Nach dem kleinen Ausflug in diverse Schleifen und process() Methoden kehren wir an den Anfang – genauer an das Ende – von FlightControlUnit.process() zurück.

Das Ende von FlightControlUnit.process()

Wie wir bereits wissen ist an dieser Stelle (Zeile 228 bis 231) die Berechnung der Fluglage abgeschlossen und die Werte sind den Motor Operanden zugewiesen. Als letztes, aber entscheidendes Modul in unserer Aufrufreihenfolge tritt ServoModule in Aktion, genau genommen dessen process() Methode. Falls mindestens einer der Werte vom vorherigen abweicht wird die Variable valueChanged auf true gesetzt. Ab diesem Augenblick können die Motor Operanden beliebig oft aktualisiert werden – der Block von Zeile 242 bis 250 wird nicht mehr ausgeführt.

ServoModule.process()

alt

In der Endlosschleife eines weiteren Threads den das ServoModule startet wird das boolesche Flag ausgewertet (Zeile 375). Die Wertübergabe an den Phidget Treiber erfolgt in den Zeilen 379 bis 382. Anschliessend deklarieren wir die Werte als veraltet (Zeile 384 bis 387) und signalisieren dass ein neuer Zyklus gestartet werden kann (Zeile 389).

alt

In Zeile 151 klammern wir zuerst den Prozentwert zwischen 0 und dem Parameter der Schubbegrenzung. In der nächsten Zeile rufen wir den AdvancedServoPhidget auf und übergeben ihm den nochmals geklammerten Wert am entsprechenden Motor Index.

servo ist eine Instanz von AdvancedServoPhidget

alt

Return to Sender!

Nachdem wir den Weg eines Steuerkommandos bis hier her mehr oder weniger detailliert erklärt haben, soll jetzt der Weg eines Datenpaketes zurück zum Sender und weiter zur Darstellung auf der GUI beschrieben werden.

Das Ergebnis unseres Steuerkommandos wird in aller Regel eine Änderung der Fluglage unserer RoboDrohne zur Folge haben. Diese Änderung stellt die IMU fest und sendet ein IMUDataPacket an den Sender zurück. Wir bleiben jedoch bei den Motoren in unserem ServoModule und dem AdvancedServoPhidget. Für die Rücksendung von Daten benutzen wir ebenfalls einen eigenen Thread, aber diesmal einen einzigen für alle Daten. Die Klasse java.util.Timer ermöglicht uns periodisch Subklassen von java.util.TimerTask auszuführen. In allen Modulen ist dies als innere Klasse realisiert. Wir haben somit Zugriff auf die Variablen der umgebenden Klasse – in ServoModule beispielsweise auf die Variable servo. Den Task übergeben wir an geeigneter Stelle dem Host, der sich um den korrekten Aufruf unseres Tasks kümmert. In der run() Methode erzeugen wir ein MotorPowerDataPacket und setzen die Prozentwerte aus dem AdvancedServoPhidget (Zeile 313 bis 317) und senden das Paket an das TransmitterModule. Die beteiligten Klassen kennen wir schon: FIFO Queue, Protocol, DataPacketFactory,  RBBStreams und Sockets.

FeedbackTask ist eine Erweiterung der Klasse java.util.TimerTask

alt

Wir befinden uns jetzt wieder auf dem Sender in der Methode setPacket() der Klasse TransmitterModule. Im switch() Block der Methode casten wir als erstes auf MotorPowerDataPacket (Zeile 112). Über den EventBus versenden wir die Prozentwerte an die vier Rundinstrumente unseres Cockpits (Zeile 113 bis 116).

Ausschnitt der Methode TransmitterModule.setPacket()

alt

Los zeig dich!

Unser Cockpit ist eine ganz normale Swing Anwendung, das bedeutet dass alle Anzeigen (Komponenten) durch Layoutmanager positioniert werden. Unsere Rundinstrumente befinden sich in einem eigenen Container der seinerseits in einen übergeordneten Container eingehängt wird. Die Anordnung der Rundinstrumente symbolisiert die vier Motoren unserer Drohne. Die beiden oberen Anzeigen entsprechen den vorderen Motoren [0,1] und die darunter positionierten Instrumente die den hinteren Motoren [3,2]. RoboDrohne fliegt in X – Konfiguration. Das Layout realisieren wir dem TableLayout. Wir Initialisieren die Anzeigen mit den Topics die vom EventBus zum Versenden der Motordaten verwendet wird. Wer sich für die Implementierung des Rundinstrumentes interessiert, der findet hier weitere Informationen.

Der Container mit den vier Rundinstrumenten.

alt

Und hier die visuelle Repräsentation des Containers: Die Instrumente zeigen eine Joystickposition links vorne an..

alt

Damit sind wir am Ende unserer Beschreibung angekommen. Wir hoffen dass der geneigte Leser einen kleinen Eindruck von der Organisation und der Arbeitsweise der beiden Anwendungen bekommen konnte.

Robotik in Java.