Benutzer-Werkzeuge

Webseiten-Werkzeuge


Seitenleiste

projektewise1617:remixpublic:start

Projektdokumentation

Einleitung

Ein Fußballspiel geht mindestens neunzig Minuten, dazu 15 Minuten Halbzeit, Verlängerung und im schlimmsten Fall noch ein Elfmeterschießen. In neunzig Minuten schafft der geneigte Sportfan mindestens fünf Bier, in der nervenanspannenden Verlängerung drei Whiskey-Shots, das Elfmeterschießen steht er ohne vier mal 2cl Wodka nicht durch.

Für einen solchen Abend ist der Roboter ReMix perfekt geeignet, da er den Trinkern die Aufgabe des Einschenkens abnimmt und Ihnen die volle Konzentration auf das Spiel ermöglicht (der normale Deutsche ist schließlich längst noch nicht betrunken genug, um sich nicht mehr konzentrieren zu können). Denn Remix ist ein Roboter, der selbstständig über den Tisch fährt, leere Shotgläser sucht und diese wieder befüllt.

Dies wird mithilfe eines Sonars (ein Abstandssensor, der in diesem Fall auf einen Servo montiert wurde) erreicht, der zu Beginn den Tisch nach Gläsern absucht. Hat das Licht-Sonar etwas gefunden, richtet sich der Roboter danach aus, steuert das Glas an, füllt es bis zum Anschlag und begibt sich danach in eine Parkposition, bereit für einen erneuten Start. Findet er nichts, bewegt er sich seinem Muster entsprechend über den Tisch und sucht weiter.

Es wurde entschieden diesen Roboter zu bauen, da jeder weiß wie anstrengend die Arbeit in einer Bar an vollen Tagen ist (zum Beispiel natürlich wenn die Championsleague läuft und nur Sky Übertragungsrechte hat) und eine Erleichterung im Barbetrieb geschaffen werden soll.

Im der folgenden Projektdokumentation soll genau beschrieben und erklärt werden, wie der Roboter funktioniert, welche Probleme bei der Durchführung des Projekts auftraten und wie diese gelöst wurden.

Dabei werden zunächst die auftretenden Teilprobleme und deren Lösung erläutert, anschließend die Zusammenführung dieser Teillösungen dargestellt und schließlich eine Ergebnisbetrachtung und -kritik durchgeführt.

Teilprobleme und deren Lösung

Dieser Abschnitt ist den Teilproblemen des Projekts gewidmet. In der Projektplanung wurden einzelne Arbeitspakete identifiziert, die nun als Teilprobleme intepretiert und gelöst werden. Ziel ist es, aus den gelösten Teilproblemen schließlich mit wenig Aufwand das fertige Projekt zusammenzubauen. Der Vorteil, der sich aus diesem Ansatz ergibt ist, dass einzelne Gruppenmitglieder gut gesondert an den Teillösungen arbeiten und die Ergebnisse anschließend den restlichen Teammitgliedern präsentieren können. Als Teilprobleme wurden im Rahmen der Projektplanung die Bewegung des Roboters, die Glaserkennung, ein Fallschutz für den Roboter, die Ausgabe des Getränks und die Positionierung des Auslasses identifiziert.

Bewegung des Roboters

Eines der größten Probleme stellt die Bewegung des Roboters dar. Da der Roboter sich auf dem Tisch bewegen soll und auf die entdeckten Gläser zufahren soll, lässt sich ohne eine gute Lösung für die Bewegung das gesamte Projekt nicht bewerkstelligen. Bei der Überlegung zur Bewegung ergaben sich weitere Unterprobleme. Zunächst müssen die Motoren überhaupt zum Laufen gebracht werden, wozu Verkabelung und Ansteuerung der Motoren gehört, bevor anschließend das Problem einzelner Bewegungsmuster gelöst werden muss.

Ansteuerung der Motoren

Verschaltung

In ReMix verbauter stepper-Motor Zur Fortbewegung werden stepper-Motoren benutzt. Anders als Getriebemotoren sind diese nicht auf konstante Drehung ausgelegt, sondern eher auf wiederholgenaue Bewegungen. Entsprechend drehen sich die Motoren bei anliegender Spannung nicht einfach, sondern warten auf treiberseitige Signale. Bei jedem detektierten Signal dreht der Motor um eine feste, wiederholgenaue Schrittweite weiter. Ein solcher stepper-Motor, verbaut in ReMix, ist in der Abbildung rechts dargestellt. Die Verschaltung der Motoren läuft über einen Motortreiber, der die Stromversorgung regelt und die Befehle, die über den Arduino gesendet werden in Motorbewegung wandelt. Da die Motoren mit einer höheren Spannung betrieben werden, als der Arduino zur Verfügung stellt, muss mit einer externen Stromquelle, in unserem Fall einem Akku, gearbeitet werden. Der Motortreiber wird sowohl mit der höheren Betriebsspannung versorgt, um die Motoren zu steuern als auch mit der Betriebsspannung des Arduino, die die Logikelemente des Treibers versorgt. Die vollständige Schaltung ist in folgender Abbildung dargestellt.

Verschaltung eines Steppermotors

Über den „DIR“-Pin (direction) kann die Drehrichtung des Motors eingestellt werden. Wird dieser Pin über einen Digitalpin des Arduino mit 5 Volt versorgt (Zustand HIGH), ist die eine Richtung aktiviert, liegt keine Spannung an (Zustand LOW) ist die andere Richtung aktiviert. Über den STEP-Pin, der ebenfalls über einen Digitalpin des Arduino angesprochen wird, kann ein kurzer HIGH-Puls von etwa einer Mikrosekunde gesendet werden. Die Motorsteuerung führt dann für jeden detektierten Puls einen Schritt mit dem Motor aus.

Weiterhin kann mithilfe der Pins MS1 bis MS3 microstepping aktiviert werden. Beim microstepping wird für jeden gesendeten Puls dann nicht mehr ein ganzer Schritt, sondern nur ein Teil eines Schrittes ausgeführt. Das microstepping kann durch setzen von jumpern zwischen den Pins reguliert werden. Über die 3 verfügbaren Pins lassen sich durch 5 verschiedene jumper-Kombinationen microsteps von 1/2 bis 1/16 eingestellt werden. Da die die vibrationsärmste Bewegung mit den kleinsten Schrittweite gewährleistet ist, wurden für dieses Projekt 16tel microsteps eingestellt. Da der stepper mit vollen Schritten 200 Schritte für eine Umdrehung braucht, also 1,8° pro step dreht, ergeben sich für 16tel steps 200*16=3200 steps pro Umdrehung bzw. 0,1125° pro step.

Codeseitige Ansteuerung

Um den Motor in Bewegung zu versetzen muss auf den STEP-Pin (im Code als MOTOR_PIN hinterlegt) ein Puls gesendet werden. Hat der Digitalpin den Zustand LOW und wird für einen kurzen Moment auf HIGH gestellt, so entspricht dies einem Puls und der Motor bewegt sich um einen step. Der einfachste Fall der Motoransteuerung ergibt sich damit in einem Programm, das die Drehgeschwindigkeit des Motors über ein delay regelt, also einen Puls sendet und dann eine definierte Zeit wartet, bevor ein weiterer Puls gesendet wird. Diese einfache Art könnte im Loop-Teil des Arduino-Quelltextes folgendermaßen aussehen:

void loop()
{
  digitalWrite(MOTOR_PIN, HIGH);  //Starte Puls
  delayMicroseconds(1);           //Sende Puls für eine Mikrosekunde
  digitalWrite(MOTOR_PIN, LOW);   //Beende Puls
  delay(30);                      //Warte 30 Millisekunden vor dem nächsten Puls
}

Die Grenzen dieser Ansteuerung werden deutlich, wenn mehrere Motoren gleichzeitig mit verschiedenen Geschwindigkeiten (also verschiedenen delays) angesprochen werden sollen. Da der Befehl delay die Programmausführung für eine bestimmte Zeit unterbricht, werden während der Pause keine weiteren Befehle ausgeführt. Soll nun der eine Motor alle 10 Millisekunden einen Puls gesendet bekommen, der andere Motor aber nur alle 20 Millisekunden, so stoppt das Programm für den zweiten Motor 20 Millisekunden, so dass der erste Motor sich niemals schneller bewegen kann als der zweite. Um dieses Problem zu umgehen, wird nicht mehr die delay-Funktion verwendet, sondern die millis-Funktion. Diese Funktion gibt die Zeit seit dem Programmstart in Millisekunden zurück und eignet sich damit zur Definition von Wiederholzeitpunkten.

Statt also den Programmablauf zu unterbrechen, wird nun definiert wann erneut ein Puls gesendet werden soll. Es wird also eine globale Variable „repeatTime“ deklariert, auf der der Zeitpunkt in Millisekunden gespeichert wird, zu dem ein Puls gesendet werden soll. Über einen Vergleich dieser Variablen mit der millis-Funktion kann ermittelt werden, ob ein Puls gesendet werden soll oder nicht. Wurde ein Puls gesendet, so wird auf die Variable „repeatTime“ direkt der neue Wiederholzeitpunkt gespeichert, indem die aktuelle Programmlaufzeit (millis()) mit der vordefinierte Pause zwischen den Pulsen (pulseDel) addiert wird. Der Code könnte für den Loop dann folgendermaßen aussehen:

unsigned long repeatTime = 0;  //Wiederholzeitpunkt für einen Puls
 
void loop()
{
  int pulseDel = 10;  //Pause zwischen den Pulsen in Millisekunden
 
  if(repeatTime <= millis())
  {
    digitalWrite(MOTOR_PIN, HIGH);    //Starte Puls
    delayMicroseconds(1);             //Sende Puls für eine Mikrosekunde
    digitalWrite(MOTOR_PIN, LOW);     //Beende Puls
    repeatTime = millis() + pulseDel; //Berechne Wiederholzeitpunkt für den nächsten Puls
  }
}

Dieser Code wird dann mit der üblichen Geschwindigkeit des Arduino stets wiederholt, ein Puls wird aber nur gesendet, wenn der Wiederholzeitpunkt erreicht wurde. Mit diesem Code lassen sich problemlos mehrere Motoren gleichzeitig ansteuern, sofern jeder Motor seinen eigenen Wiederholzeitpunkt in einer Variablen gespeichert hat.

Motorklasse

Um den Code strukturiert und übersichtlich zu halten und nicht mit globalen Variablen zu spicken, wurde eine Klasse für die Motoren erstellt. Diese Klasse enthält alle wichtigen Informationen und Funktionen zu den Stepper-Motoren. Der Prototyp der Klasse sieht wie folgt aus:

class StepperMotor
{
  private:
    int  ena, stp, dir;        //Die drei Pins für Stepper: enable, step, direction
    unsigned long repeatTime;  //Wiederholzeitpunkt für die Stepper-Pulse
    long steps;                //step-Zähler, der jeden Step der vom Motor ausgeführt werden soll zählt
    long t_beschl;             //Zeitvariable, um die Beschleunigung zu berechnen
    int  dir_curr;             //Aktuelle Drehrichtung des Motors
 
  public:
    float v, v_p;  //Die aktuelle Geschwindigkeit und die vergangene dieses Motors, public, damit sie immer eingesehen werden kann.
    int   fullturn;  //steps, die für eine volle Raddrehung benötigt werden (normal 200, 16tel microstepping -> 3200)
    void  setup(int _Ena, int _Stp, int _Dir, int _Fullturn);  //setup-Funktion zur Verknüpfung der Hardware
    int   setV(float geschw, int richtung);  //setV-Funktion zum Einstellen der Motorgeschwindigkeit
    int   outputSteps();  //Gibt aus, wie viele steps der Motor gemacht hat
};

Jedes Objekt dieser Klasse beinhaltet damit Variablen mit Informationen über den Stepper und Funktionen, mit denen der Stepper kontrolliert werden kann. Die setV-Funktion ist dafür zuständig, den Motor mit einer bestimmten Geschwindigkeit fahren zu lassen und ist dabei wie im vorherigen Abschnitt beschrieben aufgebaut; Anders als vorher nimmt sie nun aber nicht mehr Bezug auf eine globale Variable, sondern auf eine Klassenvariable innerhalb der Stepper-Klasse.

Bewegungsmuster

Im nächsten Schritt werden die Bewegungsmuster die der Roboter ausführen soll vorgestellt und deren Implementierung erläutert.

Linear fahren

Schematische Darstellung des geradeaus Fahrens Die einfachste Form der Bewegung ist eine lineare Bewegung, also das geradeaus fahren. Aus der Abbildung rechts wird schnell klar, dass sich dafür beide angetriebenen Räder des Roboters mit derselben Geschwindigkeit in dieselbe Richtung drehen müssen. Um also den Roboter geradeaus fahren zu lassen, müssen lediglich beide Motoren mit derselben Geschwindigkeit angesteuert werden. Mit zwei Motorobjekten (leftMotor und rightMotor) der zuvor gezeigten StepperMotor-Klasse, könnte eine Funktion zum geradeaus fahren wie folgt aussehen:

void bewegeGeradeaus(int v, int dir)  //Parameter: v - delay zwischen den steps, dir - Drehrichtung
{
  leftMotor.setV(v, dir);  //10 Millisekunden delay zwischen den steps, Drehrichtung 1
  rightMotor.setV(v, dir);
}

Zusammenhang zwischen Raddrehung und zurückgelegter Strecke Soll nun der Roboter eine bestimmte Strecke geradeaus-fahrend zurücklegen, so muss die Geometrie des Roboters in Betracht gezogen werden. Die Abbildung rechts zeigt ein Rad des Roboters, mit der zurückzulegenden Strecke $s$. Die zurückgelegte Strecke entspricht der abgerollten Strecke auf dem Rad. Der Zusammenhang zwischen dem Drehwinkel des Motors ($\varphi$) und der zurückgelegten Strecke ist damit über die Formel des Kreissektors im Bogenmaß gegeben: $$s = r \cdot \varphi \Leftrightarrow \varphi = \frac{s}{r}$$ Mit dieser Formel lässt sich berechnen, um welchen Winkel der Motor gedreht werden muss, damit die Strecke $s$ zurückgelegt wird. Mit der Kenntnis über die den Winkel, der mit einem Step zurückgelegt wird ($\varphi_{step}$), lässt sich weiterhin die benötigte Anzahl steps für die zurückzulegende Strecke berechnen: $$steps = \frac{\varphi}{\varphi_{step}} $$ Die Funktion zum Fahren einer bestimmten Strecke sieht dann wie folgt aus:

void bewegeGeradeStrecke(float v, int s) //Parameter: v - Geschwindigkeit, s - Strecke in mm
{
  float winkel = (float)s / r; //zurückzulegender Winkel des Rades
  int steps = round(winkel / stepwinkel); //Steps, um den winkel zu erreichen
  int count = 0; //counter für die zurückgelegten steps
 
  while (count < steps) //Wiederhole, bis die benötigte Anzahl steps zurückgelegt wurde
  {
    count += leftMotor.setV(v, 1); //Zähle zurückgelegte Schritte über return-Wert der setV-Funktion
    rightMotor.setV(v, 1);
  }
}

Die setV-Funktion wurde dabei so angepasst, dass sie als return-Wert 1 zurückgibt, wenn ein step ausgeführt wurde und sonst 0.

Drehen

Schematische Darstellung einer Drehung Das zweite wichtige Bewegungsmuster ist die Drehung, die in der Abbildung rechts schematisch dargestellt ist. Aus der Abbildung wird ersichtlich, dass die beiden Räder sich mit derselben Geschwindigkeit in verschiedene Richtungen bewegen müssen, um eine einfache Drehung auszuführen. Soll der Roboter sich also schlicht drehen, so kann der Code für das einfache geradeaus-fahren angepasst werden, indem die Drehrichtung des einen Rades umgedreht wird.

Soll sich der Roboter um einen bestimmten Winkel $\Theta$ drehen, so müssen wiederum die Beziehungen aus dem vorangegangenen Abschnitt angewendet werden. Sowohl für die Strecke die das Rad beim Abrollen zurücklegt $s$, als auch für die Strecke des Roboters auf der Kreisbahn $b$ gilt der bereits genutzte Zusammenhang: $$s = r \cdot \varphi, \quad b = R \cdot \Theta $$ Da die Strecke, die das Rad zurücklegt gleich der Strecke sein muss, die der Roboter auf der Kreisbahn zurücklegt, lassen sich die beiden Formeln gleichsetzen. Umstellen nach $\varphi$ ergibt eine Funktionsvorschrift für den Winkel der Räder in Abhängigkeit von dem angestrebten Drehwinkel des Roboters: $$\varphi(\Theta) = \frac{\Theta \cdot R}{r}$$ Über denselben Zusammenhang wie vorher lassen sich nun die benötigten Steps errechnen, die jeder Motor zurücklegen muss. Eine Funktion, die eine solche Drehung ausführt könnte wie folgt aussehen:

void bewegeDreh(float theta, float v)  //Parameter: theta- angestrebter Drehwinkel in Grad, v - Geschwindigkeit
{
  theta= (winkel * 3.14159265) / 180;  //Umrechnung Grad in Bogenmass
  int phi = (theta * R)/r;  //Berechne den Drehwinkel der Räder
  int steps = abs(phi / stepwinkel);  //Berechne benötigte Steps
 
  int count = 0; //Schrittzähler
 
  while (count < steps)  //Bis Anzahl Steps erreicht wurde
  {
    if (winkel > 0) //Drehung in die eine Richtung
    {
      count += leftMotor.setV(v, 1);
      rightMotor.setV(v, 0);
    }
    else if (winkel < 0) //Drehung in die andere Richtung
    {
      count += leftMotor.setV(v, 0);
      rightMotor.setV(v, 1);
    }
  }
}
Kurve fahren

Schematische Darstellung einer Kurvenfahrt Das dritte wichtige Bewegungsmuster ist die Kurvenfahrt. Diese ergibt sich, wenn beide Motoren mit unterschiedlicher Geschwindigkeit in dieselbe Richtung drehen. Das Schema der Kurvenfahrt ist rechts dargestellt. Bei gegebenem Kurvenradius $r_{Kurve}$ und gegebener Tangentialgeschwindigkeit $v$ lassen sich die beiden Radgeschwindigkeiten $v_l$ und $v_r$ direkt über trigonometrische Zusammenhänge berechnen: \begin{align*} v_l &= tan(\phi) \cdot (r_{Kurve} - R) \\[2mm] v_r &= tan(\phi) \cdot (r_{Kurve} + R) \\[2mm] &\text{mit } tan(\phi) = \frac{v}{r_{Kurve}} \end{align*} Die Implementierung einer Funktion zur Kurvenfahrt verläuft analog zu den beiden bereits gezeigten Bewegungsfunktionen und wird daher an dieser Stelle nicht gezeigt.

Robot-Klasse

Um den Code weiterhin strukturiert zu halten und benötigte Geometrie-Variablen, wie z.B. Radradius und Achsenradius, nicht jeder Bewegungsfunktion separat zukommen lassen zu müssen wird eine Klasse namens „Robot“ erstellt. Diese Klasse vereint Variablen und Funktionen, die den Roboter als Gesamtes betreffen. Dazu gehören die eben genannten Geometrievariablen, aber auch die beiden Stepper-Objekte, die für die Motorsteuerung verantwortlich sind. Durch diesen Schritt wird im Hauptcode nur noch ein Robot-Objekt erstellt, die Initialisierung der Stepper-Motoren findet innerhalb des Robot-Objektes statt.

Um zu zeigen, welche Funktionen und Variablen die Robot-Klasse beinhaltet, wird an dieser Stelle der Prototyp der Klasse gezeigt:

class Robot
{
  private:
    int r;            //Radradius in mm
    int R;            //Roboter-Drehradius in mm
    int l;            //Roboter-Länge, von der Achse bis zum Richtungssensor in mm
    float stepwinkel; //Winkel pro step des Motors
    StepperMotor leftMotor;  //Deklaration der beiden Steppermotoren
    StepperMotor rightMotor;
 
  public:
    void setup(int _r, int _R, int _l, int _fullturn);  //setup-Funktion zur Initialisierung der Geometrie und der Motoren
    void bewegeGerade(char dir, float v);               //Funktion zum unbegrenzten geradeaus-fahren
    void bewegeGeradeStrecke(char dir, float v, int s); //Funktion zum streckenbegrenzten geradeaus-fahren
    void bewegeStop();                                  //Funktion zum stoppen beider Motoren
    void bewegeDreh(float winkel, float v);             //Funktion zum Drehen um einen bestimmten Winkel
    void bewegeKurve(char dir, int r_Kurve, float v);   //Funktion zum Fahren einer Kurve
    void ausgGeschw();                                  //Gibt die aktuelle Geschwindigkeit aus
    int  returnGeo(char geo);                           //Gibt einen Geometriewert des Roboters zurück
    int  returnStepCount(char Motor);                   //Gibt den step-count eines Motors zurück
};

Glaserkennung und Fallsicherung

Die Glaserkennung und der Fallschutz erfolgen über jeweils einen Sharp-Lichtdistanzsensor. Für den Fallschutz wird ein Sensor vorne mit Ausrichtung zur Tischplatte am Roboter befestigt, der meldet, sobald ein Abgrund entdeckt wurde. Der zweite Sensor, der für die Glaserkennung zuständig ist, wird an einem Servo montiert, wodurch er sich um 180° drehen und die Umgebung scannen kann. Dieser Sensor ist dafür zuständig, zu melden ob ein Glas im Scanbereich ist. Die folgende Abbildung zeigt den Aufbau der beiden Sensoren und des Servo. Die beiden vorderen Sensoren des Roboters auf dem Testaufbau Da mehrere Lichtsensoren in diesem Projekt verwendet werden, wird eine Klasse „LichtSensor“ geschrieben, die alle wichtigen Variablen und Funktionen für die Benutzung der Lichtsensoren beinhaltet. Teil der Klasse ist z.B. die Funktion messen(), die einen Messwert des Lichtsensors zurückgibt.

Fallsicherung

Der Algorithmus zur Fallsicherung basiert auf einer Kalibrierung auf den üblichen Tischabstand. Da de Abstand zwischen Sensor und Tischplatte sich im normalen Betrieb nicht ändert, kann bei Starten des Roboters diese Entfernung einmal eingelesen werden und anschließend als Vergleichspunkt genutzt werden. Ändert sich der Messwert relativ zu dem normalen Tischabstand zu stark, so wurde eine Kante detektiert und der Roboter kann entsprechend reagieren.

Für die Messung des normalen Tischabstandes wird über eine Messreihe aus fünf Werten gemittelt, um dem vorhandenen Sensorrauschen entgegenzuwirken. Die Funktion für die Ermittlung des normalen Tischabstandes als Teil der Klasse „LichtSensor“ folgt:

void LichtSensor::SchwelleErmitteln()
{
  int messungen = 5; //Anzahl der Mesungen, über die gemittelt werden soll
 
  for (int k = 0; k < messungen; k++)  //for-Schleife zur Summenbildung
  {
    Schwelle += messen();  //Aufsummierung der Messwerte, messen() returnt einen Messwert
  }
  Schwelle /= messungen;  //Ermittlung des arithmetischen Mittels
}

Mit dem bekannten normalen Tischabstand kann im Folgenden eine Funktion geschrieben werden, die den aktuellen Messwert des Sensor mit der Schwelle vergleicht. Diese Funktion gibt eine boolsche Variable zurück, in Abhängigkeit davon, ob ein Abgrund detektiert wurde oder nicht:

boolean TestSensorVorne()
{
  int delta = 10; //Erlaubte Abweichung in Prozent
  int Messwert;   //Variable für den aktuellen Messwert
 
  Messwert = SensorVorne.messen();  //Ermittle aktuellen Messwert
 
  if (Messwert - SensorVorne.Schwelle > delta)  //Ist die Abweichung von Messwert zu Schwelle größer als die erlaubte Abweichung...
    {return true;}   //...gib true zurück, da ein Abgrund detektiert wurde
  else               //Sonst...
    {return false;}  //...gib false zurück, da kein Abgrund detektiert wurde
}

Diese Funktion kann nun jederzeit aufgerufen werden, um zu überprüfen, ob ein Abgrund angefahren wird oder nicht. Idealerweise wird sie beim Fahren stets überprüft, um das Fallen zu vermeiden.

Glaspositionsbestimmung

Die Glaspositionsbestimmung wird mittels eines auf einem Servo montierten Abstandssensors umgesetzt. Während des Scannvorganges wird der Sharp-Sensor mit Hilfe des Servos in einem bestimmten Bereich hin- und herbewegt. Falls sich ein Gegenstand, hier ein Glas, in einer definierten Reichweite befindet, wird dieses von dem Sensor erfasst und der Roboter kann auf das Glas ausgerichtet werden. Das Prinzip der Glassuche ist in der Abbildung unten dargestellt. Schema der Glaspositionsbestimmung Wird mit dem Sensor ein Glas erfasst, so kann über geometrische Beziehungen abgeleitet werden, um welchen Winkel sich der Roboter drehen muss, um auf das Glas ausgerichtet zu sein. Bei bekanntem Servo-Winkel $\Theta_s$ und bekanntem Abstand zum Glas $d$ können die Komponenten von $d$ in x- und y-Richtung folgendermaßen berechnet werden: \begin{align*} d_x &= d \cdot cos(\Theta_s) \\[2mm] d_y &= d \cdot sin(\Theta_s) \end{align*} Der Achsenwinkel $\Theta_a$, um den sich der Roboter drehen muss lässt sich nun über folgenden Zusammenhang berechnen: $$\Theta_a = tan^{-1} \left( \frac{d_y}{l + d_x} \right)$$

Da die Abstandssensoren als Messwert nicht den Abstand in mm zurückgeben, sondern einen analogen Messwert zwischen 0 und 1023, muss der Sensor zur Glassuche kalibriert werden. Ziel der Kalibrierung ist es, eine Rechenvorschrift zu ermitteln, mit der sich die Messwerte in Abstände in mm umrechnen lassen.

Zur Kalibrierung des Sensors wird der Sensor auf einen Testroboter montiert und mit konstanter Geschwindigkeit auf eine Wand zubewegt. Zu jedem step, den der Roboter ausführt wird ein Messwert mit dem Abstandssensor aufgenommen. Start- und Zielabstand werden per Maßband vermessen. Da der Roboter sich mit konstanter Geschwindigkeit bewegt, kann der Abstand zwischen Start und Ziel linear interpoliert werden. Auf diese Art und Weise kann zu jeder Messung eine korrelierende Entfernung berechnet werden. Aus dem Datenblatt des Sensors kann entnommen werden, dass die Messwerte linear mit dem inversen Abstand korrelieren. In der unten gezeigten Abbildung sind die Messwerte über den inversen Abstand aufgetragen. Eine lineare Interpolation samt Rechenvorschrift ist ebenfalls eingezeichnet. Mit dieser Rechenvorschrift kann eine Funktion geschrieben werden, die den Messwert umrechnet in einen Abstand in mm. Mithilfe einer solchen Funktion kann anschließend der oben beschreibene Glasfinde-Algorithmus implementiert werden.

Für die Glassuche durchfährt der Sensor mit Hilfe des Servos einen bestimmten Bereich (z.B. 120°) zwei mal, einmal hin und einmal zurück, und nimmt für jeden Winkel Messwerte auf. Für beide Messreihen, also Hinweg und Rückweg, wird in dem Algorithmus der Winkel und der Wert des geringsten Abstandes gespeichert. Um Fehler durch Sensorrauschen oder Spannungsspitzen auszuschließen werden hierbei nur Messwerte als gültig angenommen, die sich in einem definierten Bereich befinden. Negative und zu große Abstände werden so ignoriert. Anschließend werden der Abstand zum Glas und der zugehörige Servowinkel als Mittelwert der beiden Minima berechnet. Der Winkel um den sich der Roboter drehen muss, der Achsenwinkel, wird mit der oben genannten Vorschrift berechnet. Schließlich wird geprüft, ob die Messung erfolgreich war und ein Glas gefunden wurde. Dafür werden zwei Kriterien überprüft:

  • Der ermittelte Abstand des Glases ist kleiner als die definierte maximal messbare Entfernung
  • Die beiden Servowinkel für die Minimal liegen weniger als 10° auseinander

Treffen beide Kriterien zu, so gibt die Funktion „true“ zurück, es wurde also ein Glas gefunden. sonst gibt sie „false“ zurück - Es wurde kein Glas gefunden. Da die Funktion neben der Erfolgsmeldung auch den Achsenwinkel und den Abstand zum Glas zurückgeben soll, werden diese beiden Variablen als Zeiger zurückgegeben.Im Quelltext sieht die Funktion für die Suche folgendermaßen aus:

boolean SucheGlas(int range_min, int range_max, float *winkel, float *abstand)
{
  float Mess = SensorGlas.messen_mm();      //Messvariable mit initialem Vergleichswert
  int dist_max = 450;                       //maximale Distanz, zu der zuverlässig gemessen werden kann (experimentell ermittelt, 800 lt. Datenblatt)
  float min_mess[] = {dist_max, dist_max};  //Speichert die minimalen Messwerte, initial-Werte sind dist_max
  int min_pos[] = {0, 0};                   //Speichert für hin und zurück die Position des min. Messwertes
  int del = 30;                             //delay, das zwischen Servobewegungen und Messungen liegt
  int pos;                                  //Position des Servos in Grad (90 ist mittig)
 
  servo.write(range_min);
  delay(1000);
 
  //Durchfahre die range in beide Richtungen, während zu jedem Grad ein Messwert aufgenommen wird
  for (pos = range_min; pos <= range_max; pos++)  //Durchfahre den Bereich in der einen Richtung
  {
    Mess = SensorGlas.messen_mm();  //Messwert aufnehmen
    if (Mess < dist_max && Mess > 50 && Mess < min_mess[0])  //Suche das Minimum aus gültigen Werten
    {
      min_mess[0] = Mess;  //Speichere das Minimum
      min_pos[0] = pos;    //Und dessen Position
    }
    servo.write(pos);
    delay(del);
  }
  for (pos = range_max; pos > range_min; pos--)  //Durchfahre den Bereich in die andere Richtung
  {
    Mess = SensorGlas.messen_mm();
    if (Mess < dist_max && Mess > 50 && Mess < min_mess[0])
    {
      min_mess[1] = Mess;
      min_pos[1] = pos;
    }
    servo.write(pos);
    delay(del);
  }
 
  //Berechnung der Servoposition und des min. Abstandes als Mittelwert aus den beiden gemessenen Minima
  int servo_pos_min = round((min_pos[0] + min_pos[1]) / 2.0); //Servoposition bei min. Abstand
  *abstand = (min_mess[0] + min_mess[1]) / 2.0;     //gemessener Abstand zum Glas auf entsprechenden Zeiger zuweisen
 
  //Berechnung des Achsenwinkel, um den sich die Roboterachse drehen muss über atan
  int winkel_servo = 90 - servo_pos_min;
  *winkel = (atan((abstand_glas * sin(winkel_servo / 180.0 * 3.1415)) / (Shotti.returnGeo('l') + abstand_glas * cos(winkel_servo / 180.0 * 3.1415))))/ 3.1415 * 180;
 
  //Überprüfung, ob die Messung erfolgreich war
  //Kriterien: Sinniger Abstand, Winkeldifferenz < 10 Grad zwischen den gemessenen Minima
  if (abstand_glas < dist_max && abs(min_pos[0] - min_pos[1]) < 10)
  {
    return true;
  }
  else
  {
    return false;
  }
}

Magnetventil

Mit dem Magnetventil soll das korrekt positionierte Glas wieder befüllt werden. Das Magnetventil hat die beiden Zustände „offen“ und „geschlossen“, abhängig davon, ob eine Spannung anliegt, oder nicht. Um das Magnetventil durch den Arduino steuerbar zu machen, wird ein Mosfet benutzt. Mit diesem lässt sich über eine Steuerspannung ein Stromfluss regeln. Wird die Steuerspannung über einen Digitalpin des Arduino gespeist, so lässt sich das Ventil über ein Steuersignal öffnen oder schließen. Die Ansteuerung des Magnetventils wird damit sehr simpel: Über den Zustand des Digitalpins (LOW oder HIGH) wird das Ventil entweder geöffnet oder geschlossen. Die Auslassmenge wird über die Zeit reguliert, die das Ventil geöffnet ist.

Um zu garantieren, dass trotz sich leerender Flasche stets die richtige Menge an Flüssigkeit ausgegeben wird, wird eine Kalibrierungsmessung des Durchflusses ausgeführt. Dafür wird eine volle Flasche durch das Magnetventil in Schritten von 20 ml entleert und die Zeit für jeden der Durchläufe gemessen. Anschließend werden die Messdaten in Excel übertragen und eine lineare Approximation für den Zusammenhang von Zeit und Auslassmenge bestimmt. Die Darstellung der Messdaten ist in der Abbildung unten dargestellt.

Aus der Abbildung lässt sich entnehmen, dass eine lineare Approximation angemessen erscheint. Als Funktionsvorschrift für die Öffnungszeit des Magnetventils in $ms$ in Abhängigkeit von dem Füllstand der Flasche in $ml$ ergibt sich somit folgendes: $$t_{offen}(V_{Flasche}) = (-0,0102 \cdot V_{Flasche} + 13,299) \cdot 1000$$

Die Implementierung einer Funktion zur Öffnung des Magnetventils für einen entsprechenden Zeitraum könnte folgendermaßen aussehen:

int oeffneVentil(int fuellmenge)
{
  int oeffnezeit_ms = (-0.0102*fuellmenge + 13.299)*1000; //Berechne die Zeit, die das Ventil geöffnet sein muss, um 2cl durchlaufen zu lassen
 
  digitalWrite(ventilPin, HIGH); //Öffne Magnetventil
  delay(oeffnezeit_ms);
  digitalWrite(ventilPin, LOW); //Schließe Magnetventil
 
  return fuellmenge - 20;
}

Positionierung des Auslasses

Ein weiteres zu lösendes Problem ist die Glaspositionierung unter dem Auslass. Hierfür wird ein teils mechanischer, teils elektronischer Ansatz gewählt: Um unabhängig von kleinen Messfehlern das Glas sicher zu positionieren, wird ein kleiner V-förmiger Unterbau unter den Roboter gebaut, der das Glas „einfängt“ und unter dem Auslass positioniert.

Um zu garantieren, dass sich das Glas am Ende des Unterbaus, also direkt unter dem Auslass des Magnetventils, befindet wird ein Endschalter verwendet. Ein kleiner Schalter aus einem Drucker hat sich hierbei als passend ergeben, da er einen sehr geringen Druckwiderstand hat; Er löst damit aus, ohne das leere Glas auf dem Tisch zu verschieben. Parallel über einen Widerstand an einen Digitalpin des Arduino angeschlossen kann so gemessen werden, ob der Schalter offen oder geschlossen ist. Ist er offen, so ist der Stromkreis unterbrochen und es wird keine Spannung gemessen (Zustand LOW). Ist der Schalter geschlossen, so wird am Digitalpin des Arduino der Zustand HIGH ausgelesen. Das Schema der Auslasspositionierung ist in der Abbildung rechts dargestellt.

Eine einfache Funktion, die den Zustand des Schalters überprüft und zurückgibt, ob der Schalter offen oder geschlossen ist, eignet sich, um die Positionierung des Glases zu bestätigen und mit dem Einschenken des Getränks zu beginnen. Eine solche Funktion könnte folgendermaßen aussehen:

boolean Endschalter()
{
  if(digitalRead(endschalterPin)==HIGH) //Wird HIGH gemessen, ist der Schalter geschlossen...
  {
    return true;                        //...es wird true zurückgegeben
  }
  else             //Wird LOW gemessen, so ist der Schalter offen...
  {
    return false;  //...es wird false zurückgegeben
  }
}

Zusammenführung der Teillösungen

Die Zusammenführung der oben genannten Teillösungen erfolgt in der Form eines Zustandsautomaten. Der Roboter befindet sich dabei zu jedem Zeitpunkt in einem definierten Zustand und dieser Zustand entscheidet darüber, welcher Zustand folgt. Ist ein Zustand z.B. „Glassuche“, so könnte die Suche erfolgreich sein, so dass als nächster Zustand z.B. „Zum Glas bewegen“ folgen könnte. Wäre die Suche nicht erfolgreich, so könnte z.B. ein Zustand „Standby“ folgen, der auf eine Aktivierung wartet.

Die einzelnen Zustandsfunktionen bedienen sich dabei aller in den Teillösungen vorgestellten Funktionen und kontrollieren somit das gesamte Verhalten des Roboters. Die diskreten Zustände und die ReMix haben soll, und wie diese miteinander verknüpft sind, zeigt folgende Abbildung:

ReMix als Zustandsautomat

Die einzelnen Zustände des Roboter werden im folgenden etwas genauer erklärt.

Fahre auf Tisch

Der Zustand „Fahre auf Tisch“ ist der Normalzustand, wenn kein Glas gefunden wurde. In diesem Zustand fährt der Roboter geradeaus, während mit dem Fallsensor stets überprüft wird, ob der Roboter auf einen Abgrund trifft. Trifft der Roboter auf einen Abgrund, so dreht er sich, bis der Sensor keinen Abgrund mehr meldet und wechselt anschließend in den Zustand „Glas suchen“. Weiterhin wechselt der Roboter auch in den Zustand „Glas suchen“, wenn eine definierte Strecke zurückgelegt wurde, ohne dass ein Abgrund erfasst wurde. Die hat den Hintergrund, dass der Roboter Gläser nur in einem begrenzten Bereich entdecken kann. Sind die Gläser zu weit weg, so stechen sie in den Messdaten des Sensors nicht klar aus der Umgebung hervor.

Glas suchen

Im Zustand Glas suchen bleibt der Roboter stehen und führt eine systematische Suche nach einem Glas durch. Dafür wird mit der oben beschriebenen Glassuche-Funktion ein Bereich von 120° durchsucht. War die Suche erfolgreich, so dreht sich der Roboter zu dem Glas und fährt ein Stück in die Richtung. Ein weiterer Suchlauf in einem kleineren Bereich von 60° wird ausgeführt. ist die zweite Messung ebenfalls erfolgreich und der gefundene Winkel kleiner als 5°, so wird angenommen, dass ein Glas gefunden wurde und es wird in den Zustand „Fahre zu Glas“ gewechselt. War eine der beiden Messungen nicht erfolgreich, so wechselt der Roboter wieder in den Zustand „Fahre auf Tisch“ und setzt seine Fahrt wie oben beschrieben fort.

Fahre zu Glas

Dieser Zustand wird eingeleitet, wenn ein Glas gefunden wurde und der Roboter darauf ausgerichtet ist. Der Roboter fährt langsam geradeaus, während mit den Funktionen TestSensorVorne() und Endschalter() geprüft wird, ob ein Abgrund bevorsteht, oder ob der Endschalter geschlossen wurde. Sobald eine der beiden Funktionen anschlägt, wird die Fahrt unterbrochen. Es wird nun überprüft, ob die Fahrt durch den Endschalter oder durch die Fallsicherung gestoppt wurde. In dem Fall der Fallsicherung scheint die Glassuche fehlgeschlagen zu sein und der Roboter steht vor einem Abgrund. Er wechselt in den Zustand „Fahre auf Tisch“, wodurch er sich von dem Abgrund wegdreht. Wurde der Endschalter gedrückt, so wurde das Glas erreicht und der Roboter wechselt in den Zustand „Ausgabe Getränk“.

Ausgabe Getränk

In diesem Zustand wird mit der oben vorgestellten Funktion zum Magnetventil die Ausgabe des Getränkes geregelt. Anschließend nimmt der Roboter Abstand von dem befüllten Glas und wechselt in den Zustand „Standby“.

Standby

Der „Standby“-Zustand ist der Standard-Zustand, wenn der Roboter gestartet wird. In diesem Zustand bewegt sich der Roboter nicht und wartet auf ein „wakeup Event“. Wird der Roboter durch dieses Event aktiviert, so wechselt er in den Zustand „Glas suchen“ und bleibt so lange aktiv, bis er erfolgreich ein Glas befüllt hat. Ursprünglich sollte zur Aktivierung ein Klatschsignal dienen, wobei sich der verwendete Sound-Sensor jedoch als dafür nicht nutzbar erwies. Nun wird das Aktivierungssignal mit einem Schalter, der auf dem Roboter sitzt gesendet.

Ergebnis und Auswertung

Der Planungsphase folgte die Umsetzung des Projekts. Dieser Abschnitt beschreibt die Ergebnisse des Projekts und die Probleme, die sich im Laufe der Arbeit ergaben.

Ergebnis

Das Ergebnis dieses Projekts ist in der folgenden Abbildung in Aktion zu sehen.

In der zweiwöchigen Konstruktionsphase gegen Ende des Semesters wurde das Grundgerüst des Roboters entworfen und das hier vorgestellte Gesamtkonzept implementiert. Um dem Kontakt von Flüssigkeit und Elektronik vorzubeugen wurden die Flasche, das Ventil und die Schläuche oberhalb der Basisplatte befestigt, während die gesamte Elektronik unter dem Roboter sitzt. Das Vorderrad, eine reibungsarme Plastikkugel, wurde recht weit nach hinten gesetzt, was durch den ebenfalls weit hinten sitzenden Schwerpunkt des Roboters aber die Stabilität nicht beeinträchtigt. Diese Entscheidung wurde getroffen, um vorne ausreichend Platz für die Sensorik zu haben. Dieser vordere Bereich wurde genutzt, um den Fallsensor, den Servo mit dem Abstandssensor zur Glasbestimmung und die Glasfänger unterzubringen. Seitlich davon wurde zudem das Magnetventil an der Grundplatte verspannt. Im hinteren Teil des Roboters befinden sich das Breadboard mit der Steuerelektronik, sowie die Motoren und der Akku. Eine Unteransicht des Roboters ist in der Abbildung rechts gezeigt. Softwareseitig wurde der zuvor vorgestellte Zustandsautomat umgesetzt.

Insgesamt wurde so ein funktionierender Roboter gebaut, der jedoch an einigen Stellen verbessert werden könnte. Sehr gut funktionieren das Fahren auf dem Tisch und die Fallvermeidung. Durch die Initialisierung auf den normalen Tischabstand erkennt der Fallsensor sehr sicher Kanten und lässt den Roboter durch Rückwärtsfahren und drehen darauf reagieren. Ebenfalls sehr gut funktioniert die implementierte Glassuche, die beim zufälligen Fahren auf Hindernisse reagiert. Der auf dem Servo montierte Abstandssensor sucht dabei einen bestimmten Winkelbereich konstant ab und meldet Hindernisse, die dem Roboter näher als 15 cm. kommen. In diesem Fall wird eine normale Glassuche eingeleitet um den Fund zu verifizieren. Die Glassuche selbst funktioniert ab einem gewissen Abstand zum Glas einigermaßen zuverlässig. Wurde ein Glas gefunden, funktionieren das Anfahren und Ausgeben des Getränkes absolut reibungslos. Der Roboter funktioniert damit gut, allerdings nicht fehlerfrei. Im folgenden Abschnitt werden demnach die Probleme und möglichen Lösungen diskutiert.

Probleme

Ein Problem, das sich bei der Bewegung des Roboters offenbart sind flache Anfahrwinkel auf eine Tischkante. Durch die Geometrie des Roboters bedingt übertritt bei sehr flachen Winkeln ein Rad des Roboters die Tischkante, bevor der vorne montierte Sensor diese Kante detektieren konnte. Dies kann zum Absturz führen. Lösen ließe sich dieses Problem entweder durch das Versetzen des vorderen Sensors auf einen Ausleger, so dass Kanten früher erkannt werden, oder durch die Ergänzung von seitlichen Fallsensoren, die das außenliegende Rad beschleunigen, so dass der Roboter sich in einer Kurve von der Kante wegbewegt.

Weiterhin kann es vorkommen, dass Gläser, die nahe an Kanten platziert sind und an denen der Roboter knapp vorbeifährt, ohne sie zu detektieren, bei der Drehung des Roboters zur Fallvermeidung seitlich erfasst werden. In diesem Fall wird das Glas seitlich vom Roboter mitgeführt und kann nicht mehr detektiert werden. Dieser Fall trat beim Testen äußerst selten auf. Durch eine Optimierung der Suchalgorithmen könnte er vermutlich ganz beseitigt werden.

Als schwerwiegendstes Problem ergab sich die Ungenauigkeit des verwendeten Abstandssensors. Dieser misst im angegebenen Abstandsberich (10 – 80 cm) ziemlich genau, fängt aber außerhalb dieses Bereichs sehr stark an im Messspektrum zu rauschen. Damit lässt sich schwer definieren, bei welchen Werten reguläre Messwerte vorliegen und welche Werte eigentlich außerhalb des Messbereichs liegen. Trotz Mittelwertbildung und Ignorieren von Messungen bei denen die zur Mittelwertbildung genutzten Werte zu weit auseinander liegen lässt sich nicht vermeiden, dass teils falsche Werte ermittelt werden. Dies sorgt dafür, dass zum Teil vermeintlich Gläser identifiziert werden, wo eigentlich nur freier Raum vor dem Sensor liegt. Dieses Verhalten wird noch dadurch verstärkt, dass der Sensor durch Sonnenlicht andere Lichtquellen teils zu falschen Messwerten gebracht wird, also nicht komplett immun gegen Umgebungslicht ist. Dies sorgt auch dafür, dass der effektive Messbereich des Roboters auf etwa 40 cm eingeschränkt ist, was die Glassuche langwieriger gestaltet als nötig. In abgedunkelten Räumen funktioniert der Sensor entsprechend deutlich besser. Behandeln ließe sich dieses Problem durch Verwendung besserer Sensorik oder durch effektivere Filteralgorithmen, die die Messgrenzen des Sensors zuverlässiger erkennen.

Fazit und Ausblick

Zusammenfassend ist festzuhalten, dass der Roboter mit Einschränkungen gut funktioniert. Bis auf das geometrische Problem, das zum Absturz führen kann, schränkt nichts die Funktionstüchtigkeit des Roboters ein. Falsch identifizierte Objekte werden zwar wie Gläser angefahren, die aber der Fallschutz auch in diesem Zustand aktiv ist, fährt der Roboter so lediglich bis zur nächsten Tischkante und wechselt dort wieder in den Such- bzw. Fahrmodus. Trotz der Probleme sind wir mit dem Ergebnis der Projektarbeit sehr zufrieden und denken, dass wir unsere selbst definierten Ziele erreicht haben. Der Roboter ist eine gute Basis, von der auch zukünftige Projekte gut profitieren können. Wir haben durch die Projektarbeit sehr viel und auch sehr domänenübergreifend gelernt. Wir hoffen dieses Projekt auch in unserer Freizeit noch ein wenig weiter gestalten zu können und bestehende Probleme zu lösen. Abschließend ein Bild von Shotti, so der Name des Roboters der Familie ReMix, beim Einschenken.

projektewise1617/remixpublic/start.txt · Zuletzt geändert: 2017/05/24 16:15 von c.jaedicke