5.2 INTELLIGENTE ROBOTER

 

 

EINFÜHRUNG

 

  Roboter, die den Weg in einer veränderlichen Umgebung finden, haben ein grosses Einsatzgebiet, beispielsweise als fliegende Objekte, in der Unterwassererforschung und bei der Untersuchung von Kanalrohrsystemen. Hier lernst du schrittweise, wie man grundsätzlich einen fahrenden Roboter bauen kann, der sich in einer veränderlichen Umgebung zurecht findet.

  PROGRAMMIERKONZEPTE: Fremdgesteuerter, autonomer, selbstlernender Roboter,
Teach-Mode, Execution-Mode, Ereignisschleife

 

 

DER ROBOTER KENNT DEN WEG

 

Im einfachsten Fall soll ein Roboter den Durchgang in einem sehr speziellen Kanal finden, der aus gleich langen, rechtwinklig angeordneten Kanalelementen besteht.

Die Information über die konstante Länge der Kanalelemente und ob es sich um Rechts- oder Linkskurven handelt, wird im Programm fest einprogrammiert ("verdrahtet").

 

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

RobotContext.useObstacle("sprites/bg.gif", 250, 250)
RobotContext.setStartPosition(310, 470)

moveTime = 3200
turnTime = 545

robot = LegoRobot()
gear = Gear()
robot.addPart(gear)
gear.forward(moveTime)
gear.left(turnTime)
gear.forward(moveTime)
gear.right(turnTime)
gear.forward(moveTime)
gear.right(turnTime)
gear.forward(moveTime)
gear.left(turnTime)
gear.forward(moveTime)
robot.exit()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Die Zeiten moveTime und turnTime musst du durch eigene Versuche herausfinden und entsprechend anpassen. Sie hängen natürlich mit der Geschwindigkeit des Roboters zusammen. In Wirklichkeit würde man wohl eher an Stelle von Zeiten die zu durchlaufene Strecke und den Drehwinkel angeben.

 

 

VOM MENSCH GESTEUERTER ROBOTER

 

Die konstante Länge der Kanalelemente kennt der Roboter selbst, aber für die Kurvenbewegung wird er durch einen Menschen gesteuert. Der Roboter ist aber nicht lernfähig, er kann sich den Pfad nicht merken, er bleibt "dumm".

Für die Steuerung des Roboters verwendest du im Simulations- und fremdgesteuerten Modus die Cursor-Links- und Cursor-Rechts-Tasten der Tastatur und im autonomen Modus die entsprechenden LEFT- und RIGHT-Buttons. Mit den Methoden isLeftHit() und isRightHit() kannst du abfragen, ob die Tasten bzw. die Buttons gedrückt und wieder losgelassen wurden. Um das Programm zu beenden, verwendest du die Escape-Taste bzw. den ESCAPE-Button.

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

RobotContext.useObstacle("sprites/bg.gif", 250, 250)
RobotContext.setStartPosition(310, 470)

moveTime = 3200
turnTime = 545

robot = LegoRobot()
gear = Gear()
robot.addPart(gear)
gear.forward(moveTime)

while not robot.isEscapeHit():
    if robot.isLeftHit():
        gear.left(turnTime)
        gear.forward(moveTime)
    if robot.isRightHit():
        gear.right(turnTime)
        gear.forward(moveTime)
robot.exit()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Der autonome Modus macht hier weniger Sinn, da man ja den Roboter eigentlich fernsteueren möchte. Du kannst für den EV3 dazu statt der Tastatur auch die Infrarot-Fernsteuerung verwenden (siehe Anhang).

 

 

DER ROBOTER LERNT IM TEACH-MODE

 

Computergestützte Systeme, deren Verhalten nicht fest einprogrammiert ist, die also später ihr Verhalten an eine Umgebung anpassen können, nennt man adaptive Systeme. Diese sind daher in einem gewissen Sinn lernfähig. Industrierobotern werden in einem "Teach Mode" durch einen Spezialisten "angelernt", beispielsweise welche Armbewegungen durchzuführen sind. Der Operator verwendet dabei meist ein Eingabesystem ähnlich einer Fernsteuerung. Dabei wird der Roboter nacheinander in die gewünschten Positionen gefahren und der jeweilige Zustand abgespeichert.

Im "Execution Mode" fährt dann der Roboter die gespeicherten Zustände selbständig (und mit hoher Geschwindigkeit) ab.

Wie vorher kennt dein Kanal-Roboter die konstante Länge der Kanalelemente, wird aber für die Kurvenbewegung durch einen Menschen gesteuert. Der Roboter ist aber nun lernfähig, er kann sich den Pfad einprägen und ihn nachher beliebig oft selbständig durchlaufen.

Es ist oft zweckmässig sich vorzustellen, dass sich ein Roboter in jedem Moment in einem bestimmten Zustand befindet. Die Zustände werden mit aussagekräftigen Wörtern bezeichnet und als String gespeichert. Du gehst von folgenden Zuständen aus: Der Roboter ist gestoppt, vorwärtsfahrend, link- oder rechtsdrehend und nennst sie: STOPPED, FORWARD, LEFT, RIGHT. [mehr... Besser wäre es für die Zustände einen Aufzählungstyp (Enumeration) zu verwenden. In der Python-Version auf dem EV3 fehlen diese aber]

Statt die Tasten- bzw. Button ständig abzufragen, verwendest du hier eleganter das Event-Programmiermodell mit registrierten Callback-Funktionen, die unabhängig vom gerade laufenden Programm immer dann automatisch aufgerufen werden, wenn ein Ereignis eintritt.

Das Hauptprogramm ist in einer Endlos-Schleife damit beschäftigt, in jedem Zustand die entsprechenden Aktionen durchzuführen. Der Zustandswechsel erfolgt im Callback onButtonHit().

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

RobotContext.useObstacle("sprites/bg.gif", 250, 250)
RobotContext.setStartPosition(310, 470)
RobotContext.showStatusBar(30)

def onButtonHit(buttonID):
    global state
    if buttonID == BrickButton.ID_LEFT:
        state = "LEFT"
    elif buttonID == BrickButton.ID_RIGHT:
        state = "RIGHT"
    elif buttonID == BrickButton.ID_ENTER:
        state = "RUN"

moveTime = 3200
turnTime = 545
memory = []        
robot = LegoRobot(buttonHit = onButtonHit)
gear = Gear()
robot.addPart(gear)
state = "FORWARD"

while not robot.isEscapeHit():
    if state == "FORWARD":
        robot.drawString("Moving forward", 0, 3)
        gear.forward(moveTime)
        state = "STOPPED"
        robot.drawString("Teach me!", 0, 3)
    elif state == "LEFT":
        memory.append(0)
        robot.drawString("Saved: LEFT-TURN", 0, 3)
        gear.left(turnTime)
        state = "FORWARD"
    elif state == "RIGHT":
        memory.append(1)
        robot.drawString("Saved: RIGHT-TURN", 0, 3)
        gear.right(turnTime)
        state = "FORWARD"
    elif state == "RUN":
        robot.drawString("Executing memory", 0, 1)
        robot.drawString(str(memory), 0, 2)
        robot.reset()
        robot.drawString("Moving forward", 0, 3)
        gear.forward(moveTime)
        for k in memory:
            if k == 0:
                robot.drawString("Turning left", 0, 3)
                gear.left(turnTime)
            else:          
                robot.drawString("Turning right", 0, 3)
                gear.right(turnTime)
            robot.drawString("Moving forward", 0, 3)
            gear.forward(moveTime)
        gear.stop()  
        robot.drawString("All done", 0, 3)
        state = "STOPPED"
robot.exit()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Die Bearbeitung der Zustände erfolgt in einer while-Schleife im Hauptteil des Programms, also nicht in den Callback-Funktionen. Eine solche Schleife nennt man in der Informatik üblicherweise eine Ereignisschleife (event loop). Im Callback wird lediglich der entsprechende Zustand ausgewählt (state switch). Mit dieser Programmiertechnik erhält man eine übersichtliche zeitliche Synchronisation zwischen den länger dauernden Aktionen und dem ereignisgesteuerten Callback-Aufruf, der zu jeder beliebigen Zeit auftreten kann.

Das "Gedächtnis" besteht aus einer Liste, in der du die Zahlen 0 oder 1 abspeicherst, je nachdem ob der Weg nach links oder rechts abzweigt.

 

 

DER SELBSTLERNENDE ROBOTER

 

Bei bestimmten Anwendungen ist es nicht möglich, dass der Roboter durch einen Operator angelernt wird. Der Roboter kann sich beispielsweise in einem Gebiet aufhalten, dass sich ausserhalb unmittelbarer Kommunikation befindet (z.B. auf dem Mars).

Um den Weg zu finden, muss der Roboter nun mittels eingebauten Sensoren die Umgebung erfassen und entsprechend "handeln" können. Der Mensch erkennt die Umgebung meist mit seinen Augen. Für Roboter ist zwar die Bilderfassung mit einer Kamera einfach, die Analyse des Bildes aber äusserst kompliziert [mehr... Die Auswertung von Bildern gehört zur Wissenschaft der Mustererkennung].

Um sich im Kanal zu orientieren, verwendet dein Roboter lediglich einen Berührungssensor (Touchsensor), der ein Ereignis auslöst, wenn er gedrückt wird. Der Kanal soll immer noch aus gleich langen Kanalelementen bestehen. Wenn der Roboter nach dem Durchfahren eines Kanalelements einen Touchevent erhält, so weiss er, dass er sich an einer Weggabelung befindet. Er fährt dann etwas zurück und versucht ein Vorankommen mit einer Linkskurve.

Stösst er innerhalb kurzer Zeit wieder auf eine Wand, so weiss er, dass der eingeschlagene Weg falsch war. Er kehrt zurück und fährt nach rechts. Dabei merkt er sich, ob er für den richtigen Weg nach links oder rechts abbiegen muss und kann später den Kanal beliebig oft selbständig und ohne anzustossen durchlaufen.

Du lässt den Roboter im Teach-Mode durch den Kanal laufen und drückst nachher die Enter-Taste bzw. den ENTER-Button, um ihn vom Teach-Mode in den Execute-Mode zu versetzen.

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

import time

RobotContext.useObstacle("sprites/bg.gif", 250, 250)  
RobotContext.setStartPosition(310, 470)
RobotContext.showStatusBar(30)

def onPressed(port):
    global startTime
    global backTime
    robot.drawString("Press event!", 0, 1)
    dt = time.clock() - startTime # time since last hit in s
    gear.backward(backTime)
    if dt > 2:
        memory.append(0) 
        gear.left(turnTime) # turning left   
    else:
        memory.pop()
        memory.append(1) 
        gear.right(2 * turnTime) # turning right
    robot.drawString("Mem: " + str(memory), 0, 1)
    gear.forward()
    startTime = time.clock()      

def run():
    for k in memory:
        robot.drawString("Moving forward", 0, 1)
        gear.forward(moveTime)
        if k == 0:
            robot.drawString("Turning left", 0, 1)
            gear.left(turnTime)            
        elif k == 1:
            robot.drawString("Turning right", 0, 1)
            gear.right(turnTime)            
    gear.forward(moveTime) 
    robot.drawString("All done", 0, 1)          

    
moveTime = 3200
turnTime = 545
backTime = 700
memory = []      
robot = LegoRobot()
gear = Gear()

robot.addPart(gear)
ts = TouchSensor(SensorPort.S3, pressed = onPressed)      
robot.addPart(ts)
startTime = time.clock()
gear.forward()
robot.drawString("Moving forward", 0, 1)

while not robot.isEscapeHit():
    if robot.isEnterHit():
        robot.reset()    
        run()      
robot.exit()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Der Touchsensor wird am Port S3 angeschlossen (im Simulationsmodus entspricht dies einer Montageposition vorne in der Mitte). Der Sensor meldet die Touchevents über den Callback onPressed(), der mit dem benannten Parameter pressed registriert wird. Der Touchsensor ist ein Roboter-Bauteil, der wie üblich mit addPart() zum Roboter hinzugefügt wird.

Um herauszufinden, ob sich der Roboter in einem Kanalelement oder in einer Sackgasse bewegt hat, bestimmst du mit der in Python eingebauten Clock die Zeit seit dem letzten Touchevent. Ist sie grösser als 2 Sekunden, bewegt er sich im Kanalelement, sonst war es eine Sackgasse. Da der Roboter immer zuerst nach links dreht und dabei eine 0 in sein Gehirn schreibt, muss er im Fall der Sackgasse die fehlerhafte 0 durch 1 ersetzen.

 

 

DIE UMGEBUNG WIRD KOMPLEXER

 

Du hast sicher gemerkt, dass der Roboter durchaus in der Lage ist, selbst zu merken, wie weit er vorwärts fahren muss, bis er an die nächste Abzweigung kommt, da er ja die Zeit messen kann, die er benötigt, bis er am Ende des Kanalelements anstösst. Dadurch ist er in der Lage,  sich auch in einem Kanal mit  verschieden langen Kanalelementen richtig zu bewegen. Allerdings muss er jetzt nicht nur die Links-Rechts-Information, sondern auch die Laufzeit im Kopf behalten. Du packst am besten beide zusammen gehörenden Informationen in eine Liste node = [moveTime, k], wo moveTime die Laufzeit (in ms) und k = 0 für eine Links-, k = 1 für eine Rechtskurve stehen.

Die Laufzeit moveTime kriegst du nach dem Durchlaufen des Kanalelements, musst sie aber noch um die Zeit korrigieren, um die der Roboter zu weit gefahren ist. Du speicherst sie in einer globalen Variable, da du sie nochmals verwenden musst, falls der Roboter in die Sackgasse gefahren bist.

 


# Robo2e.py

from simrobot import *
#from nxtrobot import *
#from ev3robot import *

import time

RobotContext.useObstacle("sprites/bg2.gif", 250, 250)  
RobotContext.setStartPosition(410, 460)
RobotContext.showStatusBar(30)

def pressCallback(port):
    global startTime
    global backTime
    global turnTime
    global moveTime
    dt = time.clock() - startTime # time since last hit in s
    gear.backward(backTime)
    if dt > 2: 
        moveTime = int(dt * 1000) - backTime  # save long-track time
        node = [moveTime, 0] 
        memory.append(node) # save long-track time 
        gear.left(turnTime) # turning left   
    else:
        memory.pop() # discard node
        node = [moveTime, 1] 
        memory.append(node) 
        gear.right(2 * turnTime) # turning right
    robot.drawString("Memory: " + str(memory), 0, 1)
    gear.forward()
    startTime = time.clock()      

def run():
    for node in memory:
        moveTime = node[0]
        k = node[1]
        robot.drawString("Moving forward",0, 1)
        gear.forward(moveTime)
        if k == 0:
            robot.drawString("Turning left",0, 1)
            gear.left(turnTime)            
        elif k == 1:
            robot.drawString("Turning right",0, 1)
            gear.right(turnTime)            
    gear.forward() # must stop manually
    robot.drawString("All done, press DOWN to stop", 0, 1)          
    isExecuting = False
    
turnTime = 545
backTime = 700
     
robot = LegoRobot()
gear = Gear()

robot.addPart(gear)
ts = TouchSensor(SensorPort.S3, pressed = pressCallback)      
robot.addPart(ts)
startTime = time.clock()
moveTime = 0
memory = [] 
gear.forward()

while not robot.isEscapeHit():
    if robot.isDownHit():  
        gear.stop()
    elif robot.isEnterHit():
        robot.reset()    
        run()      
robot.exit()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

MEMO

 

Im ersten Anlauf erscheint dir die Datenstruktur von memory als eine Liste von node-Listen kompliziert. Zusammengehörende Daten (hier die Laufzeit für die nächste Strecke und die darauf folgende Links-Rechts-Info) sollten aber immer in einer gemeinsamen Datenstruktur abgelegt werden.

Erfreue dich daran, dass der Roboter sich auch mit dem gleichen Programm in einem ganz anderen Kanal (z.B. bg3.gif) zurecht findet.

 

 

AUFGABEN

 

1.

Der Roboter mit einem Touchsensor soll den nebenstehenden Kanal selbständig durchlaufen, aber nicht lernfähig sein. Verwende für den Simulationsmodus folgende RobotContext-Optionen:

RobotContext.useObstacle("sprites/bg2gif", 250, 250)
RobotContext.setStartPosition(400, 470)
 

 

2.

Schreib ein Programm für einen Rasenmäher-Roboter mit einem Touchsensor, der streifenweise einen Rasen mäht. Oben und unten stösst er an eine Rasenbegrenzung.

Verwende für den Simulationsmode folgende RobotContext-Optionen:

RobotContext.useBackground("sprites/fieldbg.gif")
RobotContext.useObstacle("sprites/field1.gif", 250, 250)
RobotContext.setStartPosition(350, 300)
 

3.

Die Rasenbegrenzung hat leider ein Loch, sodass der Roboter den Rasen verlässt. Erstell ein Programm mit einem lernenden Roboter, der nach dem Abfahren der ersten Streifen weiss, wie lange die Streifen sind und den Touchsensor nicht mehr benötigt.

Verwende für den Simulationsmode folgende RobotContext-Optionen:

RobotContext.useBackground("sprites/fieldbg.gif")
RobotContext.useObstacle("sprites/field2.gif", 250, 250)
RobotContext.setStartPosition(350, 300)
 
 

 

   

ZUSATZSTOFF


 

TEACH-MODE MIT DER INFRAROT-FERSTEUERUNG

 

(nur EV3 autonomer Modus)

Du hast bereits im letzen Kapitel gelernt, wie man mit der EV3 Infrarot-Fernsteuerung umgeht. Hier setzt du sie dazu ein, den Roboter im Teach-Mode durch den Kanal zu führen. Sowohl im Teach- wie im Execute-Mode läuft das Programm autonom auf dem EV3, du benötigst also nach dem Download keinen externen Computer mehr.

Dieses Vorgehen kommt der Realität sehr nahe, da es bei Industrierobotern üblich ist, sie mit einer Fernsteuerung "anzulernen" und dann das "gelernte" Programm abzuspielen.

Du verwendest im Teach-Mode die beiden oberen Buttons der Fernsteuerung, um den Roboter nach links oder rechts fahren zu lassen. Drückst du den linken unteren Button, so startet der Execute-Mode. Auch hier ist es elegant, mit Zuständen zu arbeiten.


from ev3robot import *

def onActionPerformed(port, command):
    global state
    if command == 1:
        state = "LEFT"
    elif command == 3:
        state = "RIGHT"
    elif command == 2:
        state = "RUN"

moveTime = 3200
turnTime = 545
memory = []       
robot = LegoRobot()
gear = Gear()
gear.setSpeed(50)
robot.addPart(gear)
irs = IRRemoteSensor(SensorPort.S1, actionPerformed = onActionPerformed)
robot.addPart(irs)
state = "FORWARD"
robot.drawString("Learning...", 0, 3)

while not robot.isEscapeHit():
    if state == "FORWARD":
        gear.forward(moveTime)
        state = "STOPPED"
    elif state == "LEFT":
        memory.append(0)
        gear.left(turnTime)
        gear.forward(moveTime)
        state = "STOPPED"
    elif state == "RIGHT":
        memory.append(1)
        gear.right(turnTime)
        gear.forward(moveTime)
        state = "STOPPED"
    elif state == "RUN":
        robot.drawString("Executing...", 0, 3)
        robot.reset()
        gear.forward(moveTime)
        for k in memory:
            if k == 0:
                gear.left(turnTime)
            else:         
                gear.right(turnTime)
            gear.forward(moveTime)
        gear.stop() 
        robot.drawString("All done", 0, 3)
        state = "STOPPED"
robot.exit()
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)