Schrittketten und Ihre Verwendung

 

In diesem Tutorial will ich die mal erklären wozu diese Schrittketten denn eigentlich dienen und wie einem Enumeratoren helfen können Programme übersichtlich zu schreiben. Beispielquellcode zum Tutorial findet sich hier.

 

Als Beispiel nehmen wir eine simple Pick and Place Anwendung. Wir haben drei Zuführbänder Z1, Z2 und Z3. Von diesen soll ein Teil abgeholt werden und auf einer Ablageposition abgelegt werden.

Z1 soll allerdings vor Z2 und Z2 vor Z3 behandelt werden. Die Signale, die uns anzeigen ob etwas auf Z1, Z2 oder Z3 liegt heissen Z1Ready, Z2Ready und Z3Ready. Die Greifersignale sind GripperOpen wenn der Greifer geöffnet ist und GripperClosed, wenn er geschlossen ist, sowie GripperHasPart wenn er ein Teil korrekt gegriffen hat. Ein zusätzliches Signal DONE zeigt uns, ob wir die SPS uns sagt, ob wir fertig sind oder nicht. Solange DONE nicht gesetzt ist, arbeiten wir weiter.

 

Als Zentrale Position haben wir eine Warteposition (Wait) zu der nach der Ablage und nach der Aufnahme von Teilen gefahren wird. Die Unterprogramme um sich zu Bewegen nennen wir

 

MoveToWait()

MoveToZ1()

MoveToZ2()

MoveToZ3()

MoveToPlace()

 

und zusätzlich haben wir noch zwei Unterprogramme für den Greifer:

 

OpenGripper()

CloseGripper()

 

Das schlechte Beispiel für den Quellcode hierzu, nämlich das ohne Enum sieht in den meisten Fällen etwa so aus.

 

WHILE (NOT DONE)

  IF(Z1Ready AND GripperOpen) THEN

    MoveToZ1()

    CloseGripper()

    MoveToWait()

  ELSE IF(Z2Ready AND GripperOpen) THEN

    MoveToZ2()

    CloseGripper()

    MoveToWait()

  ELSE IF(Z3Ready AND GripperOpen) THEN

    MoveToZ3()

    CloseGripper()

    MoveToWait()

  ELSE IF(GripperClosed AND GripperHasPart) THEN

    MoveToPlace()

    OpenGripperGripper()

    MoveToWait()

  ENDIF

ENDWHILE

 

Da wird man mir jetzt aber gleich sagen, jaaaaa, so unübersichtlich sieht das doch gar nicht aus und überhaupt, das ist ja ganz einfach, warum sollte ich das denn nun anders machen?

Ganz einfach: Wer Quellcode in dieser Form produziert schreibtfehleranfälligen Quellcode. Bedenkt bitte, das wir es hier mit einem Stark vereinfachten Beispiel zu tun haben. In einer realen Anlage müssen wahrscheinlich nicht nur zwei Signale abgefragt werden, sondern möglicherweise fünf oder sechs.

 

Das führt dann zu sehr langen IF-Verknüpfungen, die dann nicht mehr so leicht zu lesen sind und bei deren Programmierung man auch aus versehen eine bestimmte Kombination aus Signalen vergessen kann. Habe ich wie in unserem Beispiel nur zwei Signale um zu entscheiden wo ich mein Teil hole, gibt es vier verschiedene Varianten wie die Signale zusammengesetzt werden können. Habe ich 6 Signale sind das schonmal 64 Variationen. Das heißt, mein IF-ELSE Baum wird auch um weitere Varianten erweitert werden müssen. Und was passiert wenn dann noch ein bestimmter Sonderfall mit behandelt werden soll? Dann hab ich ein siebtes Signal und plötzlich 128 Möglichkeiten. Ja klar, ich kann das natürlich auch so schreiben, das nicht alle 128 Möglichkeiten einzeln behandelt werden. Dann besteht aber die große Gefahr, daß ich als Mensch eine bestimmte Kombination vergesse und der Roboter dann nicht mehr weiß, was er tun muss, wenn er mal in diese Situation kommt.

 

Noch ein Punkt ist, das ich nicht sehe, welche Aktion denn auf das Ablegen folgt. Ich sehe nur, auf welche Signale der Roboter wie reagiert, aber nicht, wohin er fahren soll, wenn er z.B. etwas gegriffen hat.

 

Für unser Beispiel nehme ich mal an, wir haben einen etwas konfusen Anlagenplaner dem in letzter Minute einfällt, das ja an Z3 ab und an ein Stück Plutonium ankommt, das dann da auch ganz schnell weg muss. Nun hat Z3 in einem Sonderfall plötzlich Priorität vor allen anderen. Also was macht man meistens, man passt den bestehenden Quellcode an und das sieht dann in etwa so aus.

 

Wir haben ein neues Signal hinzubekmmen: Z3HasPlutonium


WHILE (NOT DONE)

  IF(Z1Ready AND GripperOpen AND NOT Z3HasPlutonium) THEN

    MoveToZ1()

    CloseGripper()

    MoveToWait()

  ELSE IF(Z2Ready AND GripperOpen AND NOT Z3HasPlutonium) THEN

    MoveToZ2()

    CloseGripper()

    MoveToWait()

  ELSE IF(Z3Ready AND GripperOpen) THEN

    MoveToZ3()

    CloseGripper()

    MoveToWait()

  ELSE IF(GripperClosed AND GripperHasPart) THEN

    MoveToPlace()

    OpenGripperGripper()

    MoveToWait()

  ENDIF

ENDWHILE

 

Das Spielchen kann ich aber noch weiter treiben. Das Plutonium soll ja nun besonders behandelt werden, also der Roboter muss langsam fahren wenn er es hat. Das erfordert nun zusätzliche Abfragen und einen weiteren Else-If Zweig. Und so weiter und so fort. Ich glaube jeder kennt die Situation, wenn eine Anlage schlecht beschrieben ist und dem Anlagenplaner immer neue lustige Sachen einfallen, die noch bedacht werden müssen.

 

Um auf solche Änderungen schnell und flexibel reagieren zu können ohne dabei Fehler oder unbekannte Zustände zu produzieren programmiert man sowas von vornherein als Schrittkette oder Zustandsmaschine. Schrittketten haben einen linearen ablauf, Zustandsmaschinen können von jedem Ihrer Zustände in jeden anderen Zustand wechseln.

 

Wie baue ich nun so etwas auf? Indem ich Enumeratoren verwende. Diese Dinger von denen jeder mal gehört hat, das es sie gibt aber nie gewusst hat wofür man sie braucht. Weil es geht ja auch ohne.

 

Als erstes überlege ich mir, welche Zustände mein Roboter haben kann.

 

  • Roboter ist in Warteposition - IstInWarten
  • Roboter soll sich in Warteposition begeben - FahreWarten
  • Roboter soll zum Ablageplatz fahren - FahreAblegen
  • Roboter soll zum Aufnahmeplatz fahren - FahreAufnehmen
  • Der Greifer soll geöffnet werden - OeffneGreifer
  • Der Greifer soll geschlossen werden - SchliesseGreifer

 

Dann definiere ich mir ein Enum mit eben diesen Zuständen. Je nach verwendeter Robotersprache sieht das dann so aus.

 

Enum EnumRoboterZustaende = {IstInWarten, FahreWarten, FahreAblegen, FahreAufnehmen, OeffneGreifer, SchliesseGreifer}

 

Damit habe ich mir jetzt einen neuen Datentypen kreiert. Dieser ist ähnlich einem Boolean, nur das er halt mehr als zwei Zustände hat die er annehmen kann. Da ich bisher nur einen Datentypen erzeugt habe muss ich natürlich noch eine Variable dazu deklarieren.

 

EnumRoboterZustaende RobZustaende = IstInWarten

 

Nun habe ich eine Variable mit dem Namen RobZustaende und dem Wert IstInWarten.

 

Der Quellcode meines Hauptprogrammes sieht nun etwa so aus.

 

WHILE(NOT DONE)

  SWITCH(RobZustaende)

 

    CASE IstInWarten

      IF (GripperHasPart) THEN

        RobZustaende = FahreAblegen

      ELSE

        RobZustaende = FahreAufnehmen

      ENDIF

 

    CASE FahreWarten

      MoveToWait()

      RobZustaende = IstInWarten

 

    CASE FahreAblegen

      MoveToPlace()

      RobZustaende = OeffneGreifer

 

    CASE FahreAufnehmen

      IF(Z1Ready)

        MoveToZ1()

      ELSE IF(Z2Ready)

        MoveToZ2()

      ELSE IF(Z3Ready)

        MoveToZ3()

      ENDIF

      RobZustaende = SchliesseGreifer

 

    CASE OeffneGreifer

      OpenGripper()

      RobZustaende = FahreWarten

 

    CASE SchliesseGreifer

      CloseGripper()

      RobZustaende = FahreWarten

 

    CASE DEFAULT

      WRITE_MESSAGE(„Unbehandelter Zustand erreicht:“ + RobZustand)

 

   ENDSWITCH

ENDWHILE

 

Ja, das ist ja mehr Quellcode als vorher wird nun mancher sagen. Ja klar, ist so. Ist aber auch gut so.

Erstens sehe ich nun welcher Programmteil auf welchen folgt. Da immer am Ende eines Zustandes definiert wird welcher als nächstes kommt. Das macht mir die Fehlersuche einfach. Was ich hier ohne Probleme einbauen könnte wären Signale, die Abfragen ob der Greifer geschlossen ist oder offen ist und evtl ne Fehlermeldung dazu rausgeben. Oder aber wenn der Roboter ein Teil verloren hat nen neuen Zustand definieren, das er selber wieder dahinfindet wo er hin soll etc etc.

Der zweite Pluspunkt ist, das ich das ganze einfach und lesbar erweitern kann ohne das sich Fehler einschleichen.

 

Nehmen wir an, es kommt nun wieder unser tapsiger Anlagenplaner mit seinem Plutonium und will das berücksichtigt haben. Und zwar vollständig. Also langsame Fahrt und anderer Ablageort. Dann wird extra für diese Sonderbahndlung noch ein Zustand eingeführt. Wir erweitern also unser Enum um FahrePlutoniumAblegen.

 

WHILE(NOT DONE)

  SWITCH(RobZustaende)

 

    CASE IstInWarten

      IF (GripperHasPart) THEN

        RobZustaende = FahreAblegen

      ELSE

        RobZustaende = FahreAufnehmen

      ENDIF

 

    CASE FahreWarten

       MoveToWait()

       RobZustaende = IstInWarten

 

    CASE FahreAblegen

      MoveToPlace()

      RobZustaende = OeffneGreifer

 

    CASE FahreAufnehmen

      IF(Z3HasPlutonium)

        MoveToZ3()

      ELSE IF(Z1Ready)

        MoveToZ1()

      ELSE IF(Z2Ready)

        MoveToZ2()

      ELSE IF(Z3Ready)

        MoveToZ3()

      ENDIF

      RobZustaende = SchliesseGreifer

 

    CASE OeffneGreifer

      OpenGripper()

      RobZustaende = FahreWarten

 

    CASE SchliesseGreifer

      CloseGripper()

      IF (Z3HasPlutonium)

        RobZustaende = FahrePlutoniumAblegen

      ELSE

        RobZustaende = FahreWarten

      ENDIF

 

    CASE FahrePlutoniumAblegen

      RobSpeed = 10%

      MoveToPlacePlutonium()

      RobZustaende = OeffneGreifer

 

    CASE DEFAULT

      WRITE_MESSAGE(„Unbehandelter Zustand erreicht:“ + RobZustand)

 

  ENDSWITCH

ENDWHILE

 

Was macht unser Roboter nun? Kommt er in die Warteposition und sieht das da Plutonium liegt nimmt er das auch auf. Im Zustand für Greifen wird nun entschieden ob ich Plutonium gegriffen habe oder nicht. Habe ich welches, Ist mein nächster Zustand unser neu eingefügter, nämlich die Sonderbehandlung für das Plutonium. Dieses bewegt den roboter nun mit 10% Geschwindigkeit auf seine neue Ablageposition für das Plutonium und dann gehe ich wieder in einen normalen Zustand, nämlich das Greifer öffnen. Nach dem Greifer öffnen wird automatisch wieder unser Standardverhalten ausgeführt, der Roboter fährt zur Warteposition.

 

Fragen, Kritik oder Anregungen zu diesem Tutorial?

Adrian AT Roboterszene Punkt De

Zurück