Neben grundlegenden Funktionen, wie dem Auslesen von Sensordaten oder der Ansteuerung von Motoren, bietet leJOS die Möglichkeit erweiterte Konzepte aus der Robotik umzusetzen. Dazu werden im Folgenden ausgewählte Klassen aus dem Packet »lejos.robotics« näher behandelt:
DifferentialPilot
Neben dem »DifferentialPilot« zur Ansteuerung von zwei zusammenarbeitenden
Motoren wird vor allem die »Subsumption«-Architektur beschrieben, mit
deren Hilfe Roboter mit komplexeren Verhaltensmustern programmiert
werden können. Darüber hinaus wird noch die Klasse Stopwatch
vorgestellt, die für Zeitmessungen zuständig ist und aus dem Packet
»lejos.utility« stammt.
Viele der mobilen EV3-Roboter verwenden zur Fortbewegung zwei Räder, die direkt durch je einen großen EV3-Servomotor angetrieben werden (z.B. Roberta). Mit den bisher verfügbaren Methoden können wir zwar jede mögliche Bewegung des Roboters realisieren, allerdings kann dies sehr schnell umständlich werden, da jeder Motor einzeln angesprochen werden muss. Insbesondere wenn sich der Roboter auf Kreisbögen bewegen soll, muss für jeden Motor eine eigene Rotationsgeschwindigkeit berechnet werden. Aus diesem Grund stellt leJOS die Klasse »DifferentialPilot« aus dem Paket »lejos.robotics.navigation« zur Verfügung. Sie dient als Schnittstelle, über die dem Roboter eine gewünschte Bewegung vorgegeben werden kann. Die Ansteuerung der einzelnen Motoren erfolgt dabei automatisch.
Die notwendige Rotation der einzelnen Motoren zur Ausführung einer gewünschten Bewegung sind vom Durchmesser der verwendeten Räder und von deren Abstand abhängig. Diese Werte müssen für jeden Roboter ausgemessen und beim Erstellen eines »DifferentialPilot«-Objekts mit Hilfe folgender Konstruktoren übergeben werden.
DifferentialPilot(double wheelDiameter, double trackWidth,
RegulatedMotor leftMotor, RegulatedMotor rightMotor, boolean
reverse)
ist der Konstruktor zur Erstellung eines Differential Pilot, der einen
Roboter mit zwei gleich großen Rädern ansteuert.
wheelDiameter | – | Durchmesser der Räder |
trackWidth | – | Abstand der beiden Räder zueinander |
leftMotor | – | Der linke Motor (meist: Ein Objekt vom Typ EV3LargeRegulatedMotor) |
rightMotor | – | Der rechte Motor (meist: Ein Objekt vom Typ EV3LargeRegulatedMotor) |
reverse | – | Optionaler boolescher Parameter zur Angabe der Orientierung der Motoren (siehe oben) |
DifferentialPilot(double leftWheelDiameter, double
rightWheelDiameter, double trackWidth, RegulatedMotor
leftMotor, RegulatedMotor rightMotor, boolean reverse)
ist der Konstruktor zur Erstellung eines Differential Pilot, der einen
Roboter mit zwei verschieden großen Rädern ansteuert.
leftWheelDiameter | – | Durchmesser des linken Rads |
rightWheelDiameter | – | Durchmesser des rechten Rads |
trackWidth | – | Abstand der beiden Räder zueinander |
leftMotor | – | Der linke Motor (meist: Ein Objekt vom Typ EV3LargeRegulatedMotor) |
rightMotor | – | Der rechte Motor (meist: Ein Objekt vom Typ EV3LargeRegulatedMotor) |
reverse | – | Boolesche Variable zur
Angabe der Orientierung der
Motoren; |
Nachdem nun ein »DifferentialPilot«-Objekt zur Verfügung steht, kann dieses mit folgenden Methoden angesprochen werden. Die Einheiten der Übergabeparameter entsprechen dabei den Einheiten der im Konstruktor angegebenen Werte für Durchmesser und Abstand der Räder.
arc(double radius, double angle, boolean immediateReturn)
lässt den Roboter auf einem Kreis mit dem angegebenen Radius fahren,
bis der angegebene Winkel erreicht ist. Ein Wert von 90 Grad
entspricht dabei einem Viertelkreis, 180 Grad einem Halbkreis
usw. Wird ein positiver Wert für den Radius angegeben, befindet
sich der Mittelpunkt des Kreises links vom Roboter. Ist der Wert
negativ, befindet sich der Mittelpunkt rechts vom Roboter. Ein Wert
von null lässt den Roboter auf der Stelle rotieren. Der Roboter
bewegt sich auf dem Radius vorwärts, wenn der angegebene Winkel
positiv ist und rückwärts wenn der angegebene Winkel negativ
ist.
radius | – | Radius des Kreises |
angle | – | Winkel, den der Roboter auf dem Kreis zurücklegt |
immediateReturn | – | Optionaler Parameter;
wenn immediateReturn |
arcBackward(double radius)
lässt den Roboter solange rückwärts auf einem Kreis mit dem
angegebenen Radius fahren, bis stop() aufgerufen wird. Für die Position
des Kreismittelpunkts siehe oben (arc).
radius | – | Radius des Kreises |
arcForward(double radius)
lässt den Roboter solange vorwärts auf einem Kreis mit dem angegebenen
Radius fahren, bis stop() aufgerufen wird. Für die Position des
Kreismittelpunkts siehe oben (arc).
radius | – | Radius des Kreises |
forward()
lässt den Roboter solange vorwärts auf einer Linie fahren, bis stop()
aufgerufen wird.
backward()
lässt den Roboter solange rückwärts auf einer Linie fahren, bis stop()
aufgerufen wird.
isMoving()
prüft, ob sich der Roboter zum Zeitpunkt des Methodenaufrufs bewegt
und liefert einen entsprechenden booleschen Rückgabewert.
boolean | – |
|
isStalled()
prüft, ob die Räder des Roboters blockiert sind, während er versucht eine
Bewegung auszuführen und liefert einen entsprechenden booleschen
Rückgabewert.
boolean | – |
|
rotate(double angle, boolean immediateReturn)
lässt den Roboter auf seiner Position um den angegebenen Winkel nach
rechts oder links rotieren. Ein positiver Wert für den Winkel resultiert in
einer Drehung nach links, ein negativer Wert in einer Drehung nach
rechts.
angle | – | Winkel um den sich der Roboter drehen soll |
immediateReturn | – | Optionaler Parameter;
wenn immediateReturn |
rotateLeft()
lässt den Roboter auf seiner Position solange nach links drehen, bis
stop() aufgerufen wird.
rotateRight()
lässt Roboter auf seiner Position solange nach rechts drehen, bis stop()
aufgerufen wird.
setRotateSpeed(double rotateSpeed)
setzt die Rotationsgeschwindigkeit des Roboters in Grad pro Sekunde.
rotateSpeed | – | Rotationsgeschwindigkeit in Grad pro Sekunde |
setTravelSpeed(double travelSpeed)
setzt die Bewegungsgeschwindigkeit des Roboters. Einheit ist:
Raddurchmessers pro Sekunde.
travelSpeed | – | Bewegungsgeschwindigkeit in Einheit des Raddurchmessers pro Sekunde |
stop()
Stoppt die Bewegung des Roboters.
travel(double distance, boolean immediateReturn)
bewegt den Roboter auf einer geraden Linie um die angegebene Distanz
vorwärts (falls ein positiver Wert für die Distanz angegeben wurde)
oder rückwärts (falls ein negativer Wert für die Distanz angegeben
wurde).
distance | – | Zurückzulegende Distanz |
immediateReturn | – | Optionaler Parameter;
wenn immediateReturn |
travelArc(double radius, double distance, boolean
immediateReturn)
bewegt den Roboter auf einem Kreis mit dem angegeben Radius um die
angegebene Distanz vorwärts (falls ein positiver Distanzwert übergeben
wurde) oder rückwärts (falls ein negativer Distanzwert übergeben
wurde). Der Mittelpunkt des Kreises befindet sich auf der linken
Seite des Roboters, falls der übergebene Radius positiv ist und auf
der rechten Seite, falls der übergebene Radius negativ ist. Der
Roboter rotiert auf der Stelle, wenn der angegebene Radius null
ist.
radius | – | Radius des Kreises |
distance | – | Auf dem Kreis zurückzulegende Distanz |
immediateReturn | – | Optionaler Parameter;
wenn immediateReturn |
In folgendem Beispielprogramm 11.1 wird gezeigt, wie ein Roboter mit
Hilfe des Differential Pilots navigieren kann. Es wird dazu ein Roberta-
Robotermodell verwendet, das wieder eine quadratische Bahn abfahren
soll (vgl. Programm 10.9). Dem DifferentialPilot
-Objekt muss dazu
lediglich der Radabstand und Raddurchmesser im Konstruktor übergeben
werden. Nun kann sich Roberta ganz einfach 30cm vorwärts bewegen und
sich danach um 90 Grad auf der Stelle drehen. Dies wird noch drei mal
wiederholt, bis das Quadrat abgefahren wurde. Das Programm lässt sich
auf jeden anderen Roboter, der von zwei Motoren angetrieben wird,
übertragen. Es müssen nur die Werte für den Radabstand (und ggfs.
Raddurchmesser) angepasst werden und der Roboter fährt ein Quadrat
mit einer Seitenlänge von 30cm.
Die Klasse »DifferentialPilot« funktioniert für Roboter, deren Antrieb auf zwei unabhänging voneinander angesteuerten Motoren beruht. Jeder Motor treibt dabei direkt ein Rad an, also ohne Übersetzung zum Beispiel durch ein Getriebe, und die beiden Räder liegen auf einer gemeinsamen Achse. Die Lenkbewegung kommt durch die unterschiedlichen Umdrehungsgeschwindigkeiten der beiden Räder zustande. Es gibt aber auch noch ein anderes Fortbewegungskonzept mit zwei Motoren. Dabei ist ein Motor nur für die Vorwärtsbewegung der fest stehenden Hinterräder zuständig, der andere Motor für den Lenkeinschlag eines oder mehrerer Vorderräder. Dieser Lenkmechanismus entspricht im Prinzip dem eines normalen Pkw. Zur Ansteuerung solcher Roboter stellt leJOS die Klasse »SteeringPilot«, ebenfalls aus dem Package »lejos.robotics.navigation«, zur Verfügung. Wir wollen diese Klasse hier allerdings aufgrund des komplexeren Aufbaus von Robotern mit diesem Antriebskonzept nur erwähnen. Für eine detaillierte Beschreibung sei auf die leJOS-API verwiesen.
In diesem Abschnitt wird das Konzept der Filter näher erläutert. Sie
werden dazu verwendet, die von einem Sensor gelieferten Messwerte auf
unterschiedliche Weise zu verändern. Der Sensor liefert die »Samples«
(also Messwerte) dabei über einen SampleProvider
. Die Werte werden
vom eingesetzten Filter modifiziert und anschließend als neue »Samples«
wieder ausgegeben, weshalb Filter ebenfalls SampleProvider
darstellen.
Mehrere Filter können deswegen auch übereinander genutzt werden, um
komplexere Berechnungen zu erlauben. Jeder neue Filter nutzt das
Objekt des vorherigen Filters als Parameter usw. Das Endergebnis wird
jeweils vom »obersten« Filter-Objekt bezogen. leJOS stellt einige
Filter zur direkten Nutzung bereit. Diese befinden sich im Package
»lejos.robotics.filter«.
Zur Verwendung eines solchen Filters wird zunächst ein Objekt des
gewünschten Filters instanziiert. Dem Konstruktor des Filters müssen
dabei der SampleProvider
, auf den er angewendet werden soll, und die
Anzahl der Samples, mit denen er arbeitet (int bufferSize
), als
Parameter übergeben werden. Daraufhin kann durch Aufrufen der
Methode fetchSample(float[] sample, int offset)
des Filters das
gefilterte Sample ins angegebene Array geschrieben und weiter
verarbeitet werden. Der Filter ruft dafür immer auch die Methode
fetchSample
des zu Grunde liegenden Sensors auf und speichert den so
erhaltenen Messwert für die aktuelle und folgende Berechnungen
zwischen.
leJOS stellt unter anderem folgende Filter zur Verfügung:
MeanFilter
: Liefert den Mittelwert über eine bestimmte Anzahl von
Samples
MaximiumFilter
: Liefert das Maximum einer bestimmten Anzahl
von Samples
MinimumFilter
: Liefert das Minimum einer bestimmten Anzahl von
Samples
MedianFilter
: Liefert den Median einer bestimmten Anzahl von
SamplesDie Konstruktoren der oben beschriebenen Filter unterscheiden sich lediglich im Namen. FilterName ist dabei durch den Namen des gewünschten Filters zu ersetzen.
FilterName(SampleProvider source, int bufferSize)
source | – | »SampleProvider«-Objekt, auf dem der Filter arbeiten soll; dies kann ein Sample Provider eines Sensors oder ein anderer Filter sein |
bufferSize | – | Anzahl der Samples, die der Filter zwischenspeichern soll, um damit zu arbeiten |
Darüber hinaus gibt es auch noch den IntegrationFilter
. Dieser liefert
das Integral der Messwerte des Sample Providers, auf dem er arbeitet. Er
unterscheidet sich von den oben beschriebenen Filtern dadurch, dass ihm
keine Anzahl an Messwerten zur Zwischenspeicherung in Form einer
Integervariable übergeben wird. Er integriert jeweils zu den Zeitpunkten,
an denen seine fetchSample
-Methode aufgerufen wird. Daher benötigt
sein Konstruktor auf nur einen Parameter:
IntegrationFilter(SampleProvider source)
source | – | »SampleProvider«-Objekt, auf dem der Filter arbeiten soll; dies kann ein Sample Provider eines Sensors oder ein anderer Filter sein |
Dieser Filter kann beispielsweise verwendet werden, um die Messwerte eines Gyrosensors von Grad/s in Grad umzuwandeln oder um aus der Winkelgeschwindigkeit eines Motors den tatsächlich rotierten Winkel zu berechnen.
Um die Verwendung eines Filters zu demonstrieren wird nun eine einfache
Helligkeitssonde, bestehend aus dem EV3-Stein und einem Farbsensor
im »Umgebungslicht «-Modus, programmiert. Beim Drücken der
ENTER-Taste nimmt ein MaximiumFilter
einen Messwert über den
Sensor auf und berechnet das Maximum der fünf zuletzt aufgenommenen
Messwerte. Dieser Maximalwert wird nach jeder Messung auf dem Display
ausgegeben.
Die Subsumption-Architektur ist eine reaktive Steuerungsarchitektur für autonome mobile Roboter, die erstmals 1985 von Rodney Brooks und seinen Kollegen am MIT vorgestellt wurde [2]. Reaktiv bedeutet in diesem Zusammenhang, dass der Roboter keine interne Repräsentation seiner Umwelt zwischenspeichert, um daraus Handlungsanweisungen abzuleiten. Stattdessen werden alle nötigen Informationen zur Handlungssteuerung direkt aus den aktuellen Sensordaten gewonnen. Dieser Ansatz stand im Gegensatz zur traditionellen Robotikforschung, bei der stets auf eine symbolische Repräsentation der Umwelt zurückgegriffen wurde, und hatte großen Einfluss auf die Entwicklung autonomer Roboter.
In der Subsumption Architektur wird das Gesamtverhalten eines Roboters durch verschiedene »Unterverhalten« gebildet. Diese »Unterverhalten« sind hierarchisch in Form von übereinander liegenden Schichten (engl.: Layer) organisiert, wobei jede Schicht einer bestimmten Aufgabe des Roboters entspricht. Diese Schichten könnten beispielsweise die »Unterverhalten« »Meide andere Objekte« (Schicht 0), »Bewege dich im Raum umher« (Schicht 1), »Entdecke die Umgebung« (Schicht 2) und »Erstelle eine Karte« (Schicht 3) darstellen.
Jede dieser Schichten besteht wiederum aus Modulen, die das gewünschte Verhalten realisieren. Im Fall von Schicht 0 wären das beispielsweise die Module » Lese Sensordaten«, »Berechne Ausweg«, »Fahre berechneten Weg« und »Stoppe wenn Kollision bevorsteht«, die auf entsprechende Sensorinputs mit Anweisungen an die Motoren reagieren. Da die Anordnung der Schichten eine Hierarchie darstellt, können Module höherer Schichten Module tieferer Schichten beeinflussen. Einmal durch sog. »Suppression«-Signale; dabei wird der Input eines Moduls durch das »Suppression«-Signal des hierarchisch höheren Moduls ersetzt. Als nächstes durch sogenannte »Inhibition«-Signale; dabei wird der Output eines Moduls durch das »Inhibition«-Signal eines Moduls höherer Hierarchie unterdrückt. Das Modul niederer Hierarchie kann somit also keine Anweisungen mehr an die Motoren oder an noch tiefere Schichten senden. Auf diese Art und Weise können umfangreiche Verhaltensnetzwerke aufgebaut werden.
Die Subsumption-Architektur bietet in der Robotik die Möglichkeit, einen echtzeitfähigen und reaktiven Roboter zu gestalten. Durch die Zentralisierung der Sensorauswertung im EV3-Brick ist es nicht möglich, mit leJOS ein echtzeitfähiges System aufzubauen. Das ist für den Rahmen unserer Anwendungen aber auch nicht nötig. leJOS bietet deswegen dennoch die Möglichkeit, Roboter nach dem Vorbild der Subsumption-Architektur zu programmieren. Dazu wird im Package » lejos.robotics.subsumption« das Interface »Behavior« und die Klasse »Arbitrator« zur Verfügung gestellt. »Behavior« wird benutzt, um verschiedene Verhaltensweisen des Roboters zu beschreiben. »Arbitrator« ist dafür zuständig die Hierarchie zu realisieren, und Verhalten zu aktivieren sowie zu deaktivieren.
Mit Hilfe des Interfaces »Behavior« können eigene Klassen definiert werden (vgl. Schnittstellen: Abschnitt 8.1). Jede dieser Klassen entspricht dann einem bestimmten Verhalten (engl.: behavior) des Roboters. Denkbar wären zum Beispiel die Verhalten (bzw. Klassen) »Vorwärtsfahren«, »Ausweichen bei erkanntem Hindernis durch Ultraschall«, »Ausweichen bei erkanntem Hindernis durch Taster« und »Notstopp« für einen Roberta-Roboter, der sich in einem Raum bewegen soll. Das Interface »Behavior« gibt dabei nur den Namen und die Art der Methoden vor. Eine abgeleitete Klasse muss dann jede dieser vorgegebenen Methoden implementieren. Beim hier beschriebenen Interface »Behavior« sind das die folgenden drei Methoden:
action()
beschreibt die Aktionen, die der Roboter ausführt, wenn dieses
Verhalten aktiv wird. Dies kann beispielsweise nur das Abspielen
eines Tons sein, oder etwas Komplexeres, wie die Navigation in
einem Raum. Es muss berücksichtigt werden, dass die Methode
abgebrochen werden muss, sobald die im Folgenden beschriebene Methode
suppress()
aufgerufen wird. Dazu bietet sich beispielsweise die
Verwendung einer booleschen Variablen an. Diese wird gesetzt,
sobald suppress()
aufgerufen wird, und in action()
abgefragt.
suppress()
Der Aufruf dieser Methode muss das aktuelle Verhalten schnellstmöglich
beenden. Eventuell gestartete Threads müssen beendet und die Methode
action()
abgebrochen werden.
takeControl()
prüft, ob das beschriebene Verhalten die Kontrolle über den Roboter
übernehmen und ein entsprechender boolean-Wert zurückgegeben werden
soll. Eine einfache Implementierung könnte beispielsweise die Abfrage
eines Touch-Sensors sein, der prüft ob ein Objekt berührt wurde und ein
entsprechendes Ausweich-Verhalten aktiviert.
boolean | – | Boolesche Variable gibt an, ob das dargestellte Verhalten die Kontrolle über den Roboter übernehmen soll |
Die Klasse »Arbitrator« (engl. für Vermittler) ist für die Steuerung und Hierarchisierung von Verhalten zuständig, die mit dem Interface »Behavior« erstellt wurden. Sie hat drei Hauptaufgaben:
()
-Methode true
zurückgibt
()
) des aktiven Verhaltens, falls es ein
anderes mit höherer Priorität und takeControl()
= true
gibt
()
-Methode des Verhaltens mit höchster
Priorität, wenn eine action()
-Methode ausläuftEin »Arbitrator« nimmt an, dass ein Verhalten nicht länger aktiv ist,
wenn seine action()
-Methode ausgelaufen ist. Deswegen wird er die
suppress()
-Methode nur bei denjenigen Verhalten aufrufen, deren
action()
-Methoden laufen. Zum Instanziieren eines » Arbitrators« ist
folgender Konstruktor zu benutzen:
Arbitrator(Behavior[] behaviorList, boolean returnWhenInactive)
instanziiert ein Arbitrator-Objekt mit einer Liste von Behavior-Objekten,
die die verschiedenen Verhalten des Roboters darstellen. Der Index des
Behavior-Objekts in der Liste entspricht der Priorität des Verhaltens. Das
Verhalten mit dem größten Index hat also auch die höchste Priorität. Ist
der Arbitrator einmal instanziiert, können die ihm übergebenenen
Verhalten nicht mehr verändert werden. Nach der Instanziierung muss
die Vermittlung des Arbitrators noch mittels start()
gestartet
werden.
behaviorList | – | Liste der Verhalten zwischen denen der Arbitrator vermittelt |
returnWhenInactive | – | Optionaler Parameter;
standardmäßig
|
Wir haben im letzten Kapitel häufig einen Roberta-Roboter verwendet, um grundlegende Funktionen der Komponenten des EV3-Systems, wie Motoren und Sensoren, darzustellen. Mit der Subsumption-Architektur haben wir nun die Möglichkeit einen Roboter mit etwas komplexeren Verhaltensmustern auszustatten. Das Roberta- Grundmodell verfügt über zwei große Motoren zur Fortbewegung, zwei Touch-Sensoren mit Stoßfühlern (die sogenannten »Bumper«) und einen Ultraschallsensor. Damit ist sie perfekt ausgerüstet, um sich autonom in einem Raum umherzubewegen, ihre Bahn blockierende Hindernisse zu entdecken und diesen auszuweichen. Mit diesem Verhalten ist sie vielleicht nicht direkt der hellste Stern am Roboterhimmel, jedoch reagiert sie – ähnlich einem kleinen Insekt – direkt auf die Reize ihrer Umgebung.
Um das eben beschriebene Verhalten mit Hilfe der Subsumption-Architektur
von leJOS zu programmieren, werden im Folgenden fünf Klassen
verwendet. MoveForward
, UltrasonicCollision
, BumperCollision
sowie EmergencyStop
implementieren jeweils das Interface Behavior
und
beschreiben die Unterverhaltensweisen der autonomen Roberta. Die
Klasse Roberta
stellt mit ihrer main
-Funktion das Hauptprogramm dar
und beinhaltet den Arbitrator, der für die Hierarchisierung sowie
Aktivierung und Deaktivierung der Unterverhaltensweisen zuständig
ist.
Es wird in jeder Behavior-Klasse eine boolesche Variable isActive
verwendet, die anzeigt, wenn ein Verhalten unterdrückt werden soll, also
suppress()
aufgerufen wurde. Die action()
-Methoden sind daher so
zu implementieren, dass sie direkt auslaufen, sobald isActive
== false
gilt. Es dürfen dort also keine Funktionen verwendet
werden, die blockieren, also eine gewisse Zeit brauchen, bis sie
auslaufen.
Wir beginnen die Beschreibung mit dem Verhalten niedrigster Priorität:
MoveForward
. Die Implementierung der Klasse ist unter Programm 11.3
angegeben. Sie ist für die reine Vorwärtsbewegung des Roboters zuständig
und immer dann aktiv, wenn sie nicht von einer anderen Klasse
unterdrückt wird, also kein Hindernis in der Nähe ist und der Not-Stop
(ENTER-Taste) nicht betätigt wurde. Ihre takeControl()-Methode
liefert daher immer true
zurück. In action()
wird eine while
-Schleife
verwendet, um auf das Umspringen von isActive
auf false
zu
warten, woraufhin die Motoren direkt gestoppt werden und action()
ausläuft.
Das Verhalten nächsthöherer Priorität ist UltrasonicCollision
; siehe
Programm 11.4. Detektiert die autonome Roberta ein Hindernis mit
ihrem Ultraschallsensor in einer Entfernung von mindestens 30cm, dreht
sie sich zufällig nach rechts oder links, bis sie wieder mindestens 1m freie
Strecke vor sich hat (ebenfalls mit dem Ultraschallsensor gemessen).
Danach läuft ihre action()
-Methode aus und das nächste zu aktivierende
Verhalten wird vom Arbitrator ermittelt.
Stößt Roberta mit einem der Bumper gegen ein Hindernis, wird
BumperCollision
aktiviert. Da diese Situation dringender Handlung
bedarf, hat nur der Not-Stop noch höhere Priorität. Roberta setzt in
diesem Fall ein Stück zurück und dreht sich vom Hindernis weg. Es
werden zwei boolesche Variablen verwendet, um zu bestimmen welcher
Bumper gegen das Hindernis gestoßen ist und in welche Richtung Roberta
sich drehen muss. Die Implementierung dieses Verhaltens ist in folgendem
Programm 11.5 aufgeführt.
Höchste Priorität hat – wie es bei allen autonomen Robotern der Fall sein
sollte – EmergencyStop
, also der Not-Stop. Er wird ausgelöst, sobald die
ENTER-Taste gedrückt wird. Daraufhin stoppt Roberta sofort
jegliche Motorbewegung und zeigt ein kleines Menü an. Durch
Drücken der RIGHT-Taste kann das autonome Umherfahren und
Ausweichen fortgesetzt werden. Drücken der ESCAPE-Taste beendet die
Programmausführung gänzlich. Die entsprechende Klasse ist nachfolgend
in Programm 11.6 implementiert.
Die Klasse Roberta
beinhaltet neben dem Hauptprogramm und
dem Arbitrator auch Variablen für die Sensoren und Motoren des
Roberta-Roboters. Diese werden den anderen Behavior-Klassen, die
ebenfalls darauf zugreifen müssen, als Konstruktorparameter übergeben.
Bei der Speicherung der Behavior- Klassenobjekte in das Array
behaviorList[]
ist die Hierarchie in Form des Arrayindexes gut zu
erkennen. Jeder Eintrag des Arrays (also jedes Objekt) bildet damit
genau ein Layer der Subsumption-Architektur.
Da so gut wie die gesamte Funktion in die Verhaltensklassen ausgelagert
wurde, ist das Hauptprogramm sehr kurz. Es muss lediglich ein Objekt
der Klasse Roberta
instanziiert und der dazugehörige Arbitrator gestartet
werden.
Die Klasse »Stopwatch« aus dem Package »lejos.utility« gehört zwar
nicht zu den leJOS-Robotikklassen, soll aufgrund ihrer Praktikabilität
hier aber trotzdem kurz erläutert werden. Dabei handelt es sich um eine
einfach zu benutzende Stoppuhr, die für verschiedene Zeitanwendungen
genutzt werden kann. Ein » Stopwatch«-Objekt kann mit dem
parameterlosen Konstruktor Stopwatch()
instanziiert werden. Die
Zeitmessung startet unmittelbar. Um auf die gemessene Zeit zuzugreifen
gibt es folgende zwei Methoden:
elapsed()
gibt die Anzahl der seit dem Start der Stopwatch verstrichenen
Millisekunden als Integer zurück.
int | – | Anzahl der verstrichenen Millisekunden |
reset()
setzt die Stopwatch auf null zurück. Die Messung der verstrichenen
Millisekunden wird unmittelbar nach dem Aufruf dieser Methode
fortgesetzt.
Die Verwendung der Klasse Stopwatch
wird im Beispielprogramm 11.8
demonstriert. Mit ihrer Hilfe wird eine klassische Stoppuhr programmiert.
Beim ersten Drücken der ENTER-Taste wird die Stoppuhr gestartet und
dies auf dem Display ausgegeben. Die wiederholte Betätigung der
ENTER-Taste stoppt die Uhr und gibt die verstrichene Zeit auf dem
Display aus. Das Programm kann daraufhin durch Drücken der
ESCAPE-Taste verlassen werden.
Die Ausgabe des Programms auf dem EV3-Display ist in Abbildung 11.4 zu sehen.