11  leJOS-Robotikklassen

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:

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.

11.1  DifferentialPilot

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.

11.1.1  Konstruktoren

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.

Parameter
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.

Parameter
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; true falls sich der Roboter durch Rotation der Motoren in negative Richtung vorwärts bewegen soll, false wenn nicht

11.1.2  Methoden

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.

Parameter
radius  – 

Radius des Kreises

angle   – 

Winkel, den der Roboter auf dem Kreis zurücklegt

immediateReturn – 

Optionaler Parameter; wenn immediateReturn true ist, wird sofort mit der Programmausführung fortgefahren, die Bewegung wird im Hintergrund zu Ende geführt


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).

Parameter
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).

Parameter
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.

Rückgabewert
boolean – 

true, falls sich der Roboter zum Zeitpunkt des Methodenaufrufs bewegt, false wenn nicht


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.

Rückgabewert
boolean – 

true, falls die Räder blockiert sind, false wenn nicht


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.

Parameter
angle  – 

Winkel um den sich der Roboter drehen soll

immediateReturn – 

Optionaler Parameter; wenn immediateReturn true ist, wird sofort mit der Programmausführung fortgefahren, die Bewegung wird im Hintergrund zu Ende geführt


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.

Parameter
rotateSpeed – 

Rotationsgeschwindigkeit in Grad pro Sekunde


setTravelSpeed(double travelSpeed)
setzt die Bewegungsgeschwindigkeit des Roboters. Einheit ist: Raddurchmessers pro Sekunde.

Parameter
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).

Parameter
distance  – 

Zurückzulegende Distanz

immediateReturn – 

Optionaler Parameter; wenn immediateReturn true ist, wird sofort mit der Programmausführung fortgefahren, die Bewegung wird im Hintergrund zu Ende geführt


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.

Parameter
radius  – 

Radius des Kreises

distance   – 

Auf dem Kreis zurückzulegende Distanz

immediateReturn – 

Optionaler Parameter; wenn immediateReturn true ist, wird sofort mit der Programmausführung fortgefahren, die Bewegung wird im Hintergrund zu Ende geführt


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.


Programm 11.1: Beispielprogramm zum DifferentialPilot

1import lejos.hardware.motor.EV3LargeRegulatedMotor;
2import lejos.hardware.port.MotorPort;
3import lejos.robotics.navigation.DifferentialPilot;
4
5
6/∗∗
7 This class describes a Robertarobot that moves along a squared path.
8 It uses the "DifferentialPilot" class to do so.
9 /
10public class DifferentialRoberta {
11 private EV3LargeRegulatedMotor engineR = new EV3LargeRegulatedMotor(MotorPort.B);
12 private EV3LargeRegulatedMotor engineL = new EV3LargeRegulatedMotor(MotorPort.C);
13 private DifferentialPilot pilot = new DifferentialPilot(5.5, 12.5, engineL, engineR);
14
15 private void driveSquare() {
16 for (int i = 0; i < 4; i++) {
17 pilot.travel(30);
18 pilot.rotate(-90);
19 }
20 }
21
22 public static void main(String[] args) {
23 DifferentialRoberta roberta = new DifferentialRoberta();
24 roberta.driveSquare();
25 }
26}

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.

11.2  Filter

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 Samples

Die 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)

Parameter
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

11.2.1  IntegrationFilter

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)

Parameter
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.


Programm 11.2: Beispielprogramm zum MaximumFilter

1import lejos.hardware.Button;
2import lejos.hardware.lcd.LCD;
3import lejos.hardware.port.SensorPort;
4import lejos.hardware.sensor.EV3ColorSensor;
5import lejos.robotics.SampleProvider;
6import lejos.robotics.filter.MaximumFilter;
7
8/∗∗
9 This class implements a simple ambientlightprobe.
10 It takes a sample on ENTER and prints the maximum of
11 the last five measurements on the display.
12 /
13public class FilterTest {
14
15 public static void main(String[] args) {
16 EV3ColorSensor colorSensor = new EV3ColorSensor(SensorPort.S1);
17 SampleProvider spAmbient = colorSensor.getAmbientMode();
18 SampleProvider max = new MaximumFilter(spAmbient, 5);
19
20 float[] sample = new float[spAmbient.sampleSize()];
21
22 while(Button.ESCAPE.isUp()) {
23 Button.ENTER.waitForPress();
24 max.fetchSample(sample, 0);
25 LCD.drawString("Max. of last 5:", 0, 0);
26 LCD.drawString(Float.toString(sample[0]), 0, 1);
27 }
28 }
29}

11.3  Subsumption-Architektur

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.

pict

Abbildung 11.1.: Schichten der Subsumption-Architektur [2]

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.

pict

Abbildung 11.2.: Verhaltensmodul der Subsumption-Architektur [2]

11.3.1  Subsumption in leJOS

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.

11.3.2  Interface Behavior

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.

Rückgabewert
boolean – 

Boolesche Variable gibt an, ob das dargestellte Verhalten die Kontrolle über den Roboter übernehmen soll

11.3.3  Klasse Arbitrator

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:

Ein »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.

Parameter
behaviorList  – 

Liste der Verhalten zwischen denen der Arbitrator vermittelt

returnWhenInactive – 

Optionaler Parameter; standardmäßig false: start()-Methode läuft nie aus; falls true , läuft die start()-Methode aus, sobald kein Verhalten mehr aktiv ist

11.4  Subsumption-Beispiel: AutonomousRoberta

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.

pict

Abbildung 11.3.: Schichten des Subsumption-Beispiels

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.


Programm 11.3: MoveForward.java

1import lejos.hardware.motor.EV3LargeRegulatedMotor;
2import lejos.robotics.subsumption.Behavior;
3
4/∗∗
5 This class implements the robot moving forward.
6 This is the lowest priority behavior.
7 /
8public class MoveForward implements Behavior {
9 private final EV3LargeRegulatedMotor engineR;
10 private final EV3LargeRegulatedMotor engineL;
11 private boolean isActive = false;
12
13 MoveForward(EV3LargeRegulatedMotor motorR, EV3LargeRegulatedMotor motorL) {
14 engineR = motorR;
15 engineL = motorL;
16 }
17
18 public void action() {
19 isActive = true;
20 engineL.setSpeed(Roberta.SPEED);
21 engineR.setSpeed(Roberta.SPEED);
22 engineR.forward();
23 engineL.forward();
24 while (isActive) {}
25 engineR.stop(true);
26 engineL.stop(true);
27 }
28
29 public void suppress() {
30 isActive = false;
31 }
32
33 public boolean takeControl() {
34 return true;
35 }
36}

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.


Programm 11.4: UltrasonicCollision.java

1import lejos.hardware.motor.EV3LargeRegulatedMotor;
2import lejos.hardware.sensor.EV3UltrasonicSensor;
3import lejos.robotics.subsumption.Behavior;
4
5/∗∗
6 This class implements the robots behavior after an obstacle is detected by ultrasonicsensor.
7 The robot turns randomly to the left or to the right until theres at least 1m of free path in front of it.
8 /
9public class UltrasonicCollision implements Behavior {
10 protected final EV3LargeRegulatedMotor engineR;
11 protected final EV3LargeRegulatedMotor engineL;
12 private final EV3UltrasonicSensor uSensor;
13 private float[] distanceSample;
14
15 private boolean isActive = false;
16
17 UltrasonicCollision(EV3UltrasonicSensor sensor, EV3LargeRegulatedMotor motorR, EV3LargeRegulatedMotor motorL) {
18 engineR = motorR;
19 engineL = motorL;
20 uSensor = sensor;
21 uSensor.setCurrentMode("Distance");
22 distanceSample = new float[uSensor.sampleSize()];
23 }
24
25 public void action() {
26 isActive = true;
27 //turn to left or right randomly with half the motorspeed
28 double rand = Math.random();
29 engineR.setSpeed(Roberta.SPEED * 0.5F);
30 engineL.setSpeed(Roberta.SPEED * 0.5F);
31 if (rand < 0.5) {
32 engineR.forward();
33 engineL.backward();
34 }
35 else {
36 engineL.forward();
37 engineR.backward();
38 }
39 //wait until 1m of path is free or behavior is suppressed
40 while (isActive && (distanceSample[0] < 1.0)) {
41 uSensor.fetchSample(distanceSample, 0);
42 }
43 engineL.stop(true);
44 engineR.stop(true);
45 }
46
47 public void suppress() {
48 isActive = false;
49 }
50
51 public boolean takeControl() {
52 uSensor.fetchSample(distanceSample, 0);
53 return (distanceSample[0] < 0.30) ;
54 }
55}

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.


Programm 11.5: BumperCollision.java

1import lejos.hardware.motor.EV3LargeRegulatedMotor;
2import lejos.hardware.sensor.EV3TouchSensor;
3import lejos.robotics.subsumption.Behavior;
4
5/∗∗
6 This class implements the robots behavior after one of its bumpers touches an obstacle.
7 The robot moves backwards with different motorspeeds (on an arc) until the slower motor has turned 180 degrees.
8 Depending on which bumper detected the obstacle, either the left or the right motor is turning slower.
9 After that the robot faces away from the obstacle and can proceed to move forward.
10 /
11public class BumperCollision implements Behavior {
12 protected final EV3LargeRegulatedMotor engineR;
13 protected final EV3LargeRegulatedMotor engineL;
14 private final EV3TouchSensor tSensorR;
15 private final EV3TouchSensor tSensorL;
16
17 private float[] rBumperSample;
18 private float[] lBumperSample;
19
20 private boolean bLeftBumper = false;
21 private boolean bRightBumper = false;
22 private boolean isActive = false;
23 private final float SPEEDFACTOR = 0.5F;
24 private final int LIMITANGLE = 180;
25
26 BumperCollision(EV3TouchSensor touchR, EV3TouchSensor touchL, EV3LargeRegulatedMotor motorR, EV3LargeRegulatedMotor motorL) {
27 engineR = motorR;
28 engineL = motorL;
29 tSensorR = touchR;
30 tSensorL = touchL;
31 tSensorR.setCurrentMode("Touch");
32 tSensorL.setCurrentMode("Touch");
33 rBumperSample = new float[tSensorR.sampleSize()];
34 lBumperSample = new float[tSensorL.sampleSize()];
35 }
36
37 private void resetBoolean() {
38 bLeftBumper = false;
39 bRightBumper = false;
40 }
41
42 public void action() {
43 isActive = true;
44 if (bLeftBumper) {
45 engineL.setSpeed(Roberta.SPEED * SPEEDFACTOR);
46 }
47 else if (bRightBumper) {
48 engineR.setSpeed(Roberta.SPEED * SPEEDFACTOR);
49 }
50 engineR.resetTachoCount();
51 engineL.resetTachoCount();
52 engineR.backward();
53 engineL.backward();
54 while (isActive && ((engineR.getTachoCount() > -LIMITANGLE) || (engineL.getTachoCount() > -LIMITANGLE))) {}
55 engineL.stop(true);
56 engineR.stop(true);
57 }
58
59 public void suppress() {
60 isActive = false;
61 }
62
63 public boolean takeControl() {
64 resetBoolean();
65 tSensorR.fetchSample(rBumperSample, 0);
66 tSensorL.fetchSample(lBumperSample, 0);
67 bRightBumper = (rBumperSample[0] == 1.0);
68 bLeftBumper = (lBumperSample[0] == 1.0);
69 return (bRightBumper || bLeftBumper);
70 }
71}

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.


Programm 11.6: EmergencyStop.java

1import lejos.hardware.Button;
2import lejos.hardware.lcd.LCD;
3import lejos.hardware.motor.EV3LargeRegulatedMotor;
4import lejos.robotics.subsumption.Behavior;
5
6/∗∗
7 This class implements the robots emergency stopfunction.
8 When ENTER is pressed, the robot stops its movement immediately and waits for further buttoninput.
9 RIGHT > The robot continues its movement; ESCAPE > The robots control program exits.
10 /
11public class EmergencyStop implements Behavior {
12 protected final EV3LargeRegulatedMotor engineR;
13 protected final EV3LargeRegulatedMotor engineL;
14
15 EmergencyStop(EV3LargeRegulatedMotor motorR, EV3LargeRegulatedMotor motorL) {
16 engineR = motorR;
17 engineL = motorL;
18 }
19
20 public void action() {
21 engineR.stop(true);
22 engineL.stop(true);
23 int buttonID;
24 LCD.clear();
25 LCD.drawString("Arbitration halted", 0, 0);
26 LCD.drawString("RIGHT to Continue", 0, 1);
27 LCD.drawString("ESCAPE to Exit", 0, 2);
28 do {
29 buttonID = Button.waitForAnyPress();
30 }
31 while (buttonID != Button.ID_RIGHT && buttonID != Button.ID_ESCAPE);
32 if (buttonID == Button.ID_ESCAPE) {System.exit(1);}
33 else {
34 LCD.clear();
35 LCD.drawString("Arbitrating", 0, 0);
36 }
37 }
38
39 public void suppress() {
40 }
41
42 public boolean takeControl() {
43 return (Button.ENTER.isDown());
44 }
45}

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.


Programm 11.7: Roberta.java

1import lejos.hardware.lcd.LCD;
2import lejos.hardware.motor.EV3LargeRegulatedMotor;
3import lejos.hardware.port.MotorPort;
4import lejos.hardware.port.SensorPort;
5import lejos.hardware.sensor.EV3TouchSensor;
6import lejos.hardware.sensor.EV3UltrasonicSensor;
7import lejos.robotics.subsumption.Arbitrator;
8import lejos.robotics.subsumption.Behavior;
9
10/∗∗
11 This class describes a Robertarobot that can move around in a room while avoiding obstacles.
12 It uses four Behaviorclasses as seen in its constructor. MoveForward has lowest priority, EmergencyStop has highest priority.
13 The robot uses either its ultrasonicsensor or its bumpers to detect an obstacle.
14 /
15public class Roberta {
16 EV3LargeRegulatedMotor engineR = new EV3LargeRegulatedMotor(MotorPort.B);
17 EV3LargeRegulatedMotor engineL = new EV3LargeRegulatedMotor(MotorPort.C);
18 EV3UltrasonicSensor uSensor = new EV3UltrasonicSensor(SensorPort.S4);
19 EV3TouchSensor bumperR = new EV3TouchSensor(SensorPort.S1);
20 EV3TouchSensor bumperL = new EV3TouchSensor(SensorPort.S2);
21
22 Arbitrator arby;
23 Behavior[] behaviorList = new Behavior[4];
24
25 public final static int SPEED = 360;
26
27 Roberta() {
28 behaviorList[0] = new MoveForward(engineR, engineL);
29 behaviorList[1] = new UltrasonicCollision(uSensor, engineR, engineL);
30 behaviorList[2] = new BumperCollision(bumperR, bumperL, engineR, engineL);
31 behaviorList[3] = new EmergencyStop(engineR, engineL);
32 arby = new Arbitrator(behaviorList);
33 }
34
35 public static void main(String[] args) {
36 Roberta autonomousRoberta = new Roberta();
37 LCD.drawString("Arbitrating", 0, 0);
38 autonomousRoberta.arby.start();
39 }
40}

11.5  leJOS-Utility: Stopwatch

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.

Rückgabewert
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.


Programm 11.8: StopwatchTester.java

1import lejos.hardware.Button;
2import lejos.hardware.lcd.LCD;
3import lejos.utility.Stopwatch;
4
5/∗∗
6 This class describes a simple stopwatchprogram.
7 The ENTERButton starts and stops the measurement.
8 The passed time is the printed on the EV3display.
9 /
10public class StopwatchTester {
11 private Stopwatch timer = new Stopwatch();
12
13 private void startMeasuring() {
14 int time;
15 timer.reset();
16 LCD.drawString("Timer started", 0, 1);
17
18 Button.ENTER.waitForPress();
19 time = timer.elapsed();
20 LCD.drawString("Timer stopped", 0, 2);
21
22 LCD.drawString("", 0, 3);
23 LCD.drawString("Time passed:", 0, 4);
24 LCD.drawString(time + " milliseconds", 0, 5);
25 LCD.drawString((time / 1000.0) + " seconds", 0, 6);
26 }
27
28 public static void main(String[] args) {
29 StopwatchTester watch = new StopwatchTester();
30 LCD.drawString("ENTER to start", 0, 0);
31 Button.ENTER.waitForPress();
32
33 watch.startMeasuring();
34
35 LCD.drawString("ESCAPE to exit", 0, 7);
36 Button.ESCAPE.waitForPress();
37 }
38}

Die Ausgabe des Programms auf dem EV3-Display ist in Abbildung 11.4 zu sehen.

pict

Abbildung 11.4.: Displayausgabe des Stopwatch-Programms