Ein einführendes Tutorial zur Roboterprogrammierung

Veröffentlicht: 2022-03-11
Anmerkung der Redaktion: Am 16. Oktober 2018 wurde dieser Artikel überarbeitet, um mit den neuesten Technologien zu arbeiten.

Seien wir ehrlich, Roboter sind cool. Sie werden auch eines Tages die Welt regieren, und hoffentlich werden sie zu dieser Zeit Mitleid mit ihren armen, fleischigen Schöpfern (alias Roboterentwickler) haben und uns helfen, eine Weltraumutopie voller Fülle zu bauen. Ich scherze natürlich, aber nur irgendwie.

In meinem Ehrgeiz, einen kleinen Einfluss auf die Angelegenheit zu nehmen, habe ich letztes Jahr einen Kurs in autonomer Robotersteuerungstheorie belegt, der darin gipfelte, dass ich einen Python-basierten Robotersimulator baute, der es mir ermöglichte, die Steuerungstheorie an einem einfachen, mobilen, programmierbaren Roboter zu üben .

In diesem Artikel zeige ich, wie man ein Python-Roboter-Framework verwendet, um Steuerungssoftware zu entwickeln, beschreibe das Steuerungsschema, das ich für meinen simulierten Roboter entwickelt habe, veranschauliche, wie er mit seiner Umgebung interagiert und seine Ziele erreicht, und bespreche einige der grundlegenden Herausforderungen der Robotikprogrammierung, denen ich dabei begegnet bin.

Um diesem Tutorial zur Robotikprogrammierung für Anfänger folgen zu können, sollten Sie über zwei Grundkenntnisse verfügen:

  • Mathematik – wir werden einige trigonometrische Funktionen und Vektoren verwenden
  • Python – da Python zu den beliebtesten grundlegenden Programmiersprachen für Roboter gehört – werden wir grundlegende Python-Bibliotheken und -Funktionen verwenden

Die hier gezeigten Codeschnipsel sind nur ein Teil des gesamten Simulators, der auf Klassen und Schnittstellen angewiesen ist. Um den Code also direkt lesen zu können, benötigen Sie möglicherweise etwas Erfahrung in Python und objektorientierter Programmierung.

Schließlich sind optionale Themen, die Ihnen helfen, diesem Tutorial besser zu folgen, zu wissen, was eine Zustandsmaschine ist und wie Bereichssensoren und Encoder funktionieren.

Die Herausforderung des programmierbaren Roboters: Wahrnehmung vs. Realität und die Fragilität der Kontrolle

Die grundlegende Herausforderung aller Robotik ist diese: Es ist unmöglich, jemals den wahren Zustand der Umwelt zu kennen. Die Robotersteuerungssoftware kann den Zustand der realen Welt nur auf der Grundlage der von ihren Sensoren zurückgegebenen Messwerte erraten. Es kann nur versuchen, den Zustand der realen Welt durch die Erzeugung von Steuersignalen zu verändern.

Diese Grafik zeigt die Interaktion zwischen einem physischen Roboter und Computersteuerungen beim Üben der Python-Roboterprogrammierung.

Die Robotersteuerungssoftware kann den Zustand der realen Welt nur auf der Grundlage der von ihren Sensoren zurückgegebenen Messwerte erraten.

Daher besteht einer der ersten Schritte beim Steuerungsdesign darin, eine Abstraktion der realen Welt zu entwickeln, die als Modell bekannt ist, mit der unsere Sensormesswerte interpretiert und Entscheidungen getroffen werden können. Solange sich die reale Welt gemäß den Annahmen des Modells verhält, können wir gute Vermutungen anstellen und die Kontrolle ausüben. Sobald die reale Welt jedoch von diesen Annahmen abweicht, können wir nicht mehr gut raten, und die Kontrolle geht verloren. Oft kann die einmal verlorene Kontrolle nie wieder zurückgewonnen werden. (Es sei denn, eine wohlwollende äußere Kraft stellt es wieder her.)

Dies ist einer der Hauptgründe, warum die Roboterprogrammierung so schwierig ist. Wir sehen oft Videos des neuesten Forschungsroboters im Labor, der fantastische Leistungen in Bezug auf Geschicklichkeit, Navigation oder Teamarbeit vollbringt, und wir sind versucht zu fragen: „Warum wird das nicht in der realen Welt verwendet?“ Nun, wenn Sie das nächste Mal ein solches Video sehen, werfen Sie einen Blick darauf, wie streng kontrolliert die Laborumgebung ist. Diese beeindruckenden Aufgaben können diese Roboter meist nur erfüllen, solange sich die Umgebungsbedingungen in den engen Grenzen ihres inneren Modells bewegen. Ein Schlüssel zum Fortschritt der Robotik ist daher die Entwicklung komplexerer, flexiblerer und robusterer Modelle – und dieser Fortschritt unterliegt den Grenzen der verfügbaren Rechenressourcen.

Ein Schlüssel zum Fortschritt der Robotik ist die Entwicklung komplexerer, flexiblerer und robusterer Modelle.

[Nebenbemerkung: Philosophen und Psychologen würden gleichermaßen anmerken, dass Lebewesen auch unter der Abhängigkeit von ihrer eigenen inneren Wahrnehmung dessen leiden, was ihre Sinne ihnen sagen. Viele Fortschritte in der Robotik resultieren aus der Beobachtung von Lebewesen und der Beobachtung, wie sie auf unerwartete Reize reagieren. Denk darüber nach. Was ist Ihr inneres Modell der Welt? Es unterscheidet sich von dem einer Ameise und dem eines Fisches? (Hoffentlich.) Allerdings wird es, wie die Ameise und der Fisch, wahrscheinlich einige Realitäten der Welt zu stark vereinfachen. Wenn Ihre Annahmen über die Welt nicht korrekt sind, besteht die Gefahr, dass Sie die Kontrolle über die Dinge verlieren. Manchmal nennen wir das „Gefahr“. Genauso wie unser kleiner Roboter ums Überleben gegen das unbekannte Universum kämpft, tun wir das alle. Dies ist eine starke Erkenntnis für Robotiker.]

Der programmierbare Robotersimulator

Der Simulator, den ich gebaut habe, ist in Python geschrieben und sehr geschickt Sobot Rimulator genannt . Sie finden v1.0.0 auf GitHub. Es hat nicht viel Schnickschnack, aber es wurde entwickelt, um eines sehr gut zu machen: eine genaue Simulation eines mobilen Roboters bereitzustellen und einem aufstrebenden Robotiker einen einfachen Rahmen zum Üben der Robotersoftwareprogrammierung zu geben. Während es immer besser ist, einen echten Roboter zum Spielen zu haben, ist ein guter Python-Robotersimulator viel zugänglicher und ein großartiger Ausgangspunkt.

In realen Robotern muss die Software, die die Steuersignale generiert (der „Controller“), mit sehr hoher Geschwindigkeit laufen und komplexe Berechnungen durchführen. Dies wirkt sich auf die Wahl der am besten geeigneten Roboterprogrammiersprachen aus: Normalerweise wird C++ für solche Szenarien verwendet, aber in einfacheren Robotikanwendungen ist Python ein sehr guter Kompromiss zwischen Ausführungsgeschwindigkeit und einfacher Entwicklung und Test.

Die von mir geschriebene Software simuliert einen echten Forschungsroboter namens Khepera, kann aber an eine Reihe mobiler Roboter mit unterschiedlichen Abmessungen und Sensoren angepasst werden. Da ich versucht habe, den Simulator so ähnlich wie möglich zu den Fähigkeiten des echten Roboters zu programmieren, kann die Steuerlogik mit minimalem Refactoring in einen echten Khepera-Roboter geladen werden, und er verhält sich genauso wie der simulierte Roboter. Die implementierten Besonderheiten beziehen sich auf den Khepera III, können aber leicht an den neuen Khepera IV angepasst werden.

Mit anderen Worten, das Programmieren eines simulierten Roboters ist analog zum Programmieren eines echten Roboters. Dies ist von entscheidender Bedeutung, wenn der Simulator für die Entwicklung und Bewertung unterschiedlicher Steuerungssoftwareansätze von Nutzen sein soll.

In diesem Tutorial werde ich die Architektur der Robotersteuerungssoftware beschreiben, die mit v1.0.0 von Sobot Rimulator geliefert wird , und Ausschnitte aus der Python-Quelle bereitstellen (mit leichten Modifikationen zur Verdeutlichung). Ich ermutige Sie jedoch, in die Quelle einzutauchen und herumzuspielen. Der Simulator wurde gegabelt und zur Steuerung verschiedener mobiler Roboter verwendet, darunter ein Roomba2 von iRobot. Ebenso können Sie das Projekt gerne forken und verbessern.

Die Steuerlogik des Roboters ist auf diese Python-Klassen/Dateien beschränkt:

  • models/supervisor.py – diese Klasse ist für die Interaktion zwischen der simulierten Welt um den Roboter und dem Roboter selbst verantwortlich. Es entwickelt unsere Roboter-Zustandsmaschine und löst die Controller aus, um das gewünschte Verhalten zu berechnen.
  • models/supervisor_state_machine.py – diese Klasse stellt die verschiedenen Zustände dar, in denen sich der Roboter befinden kann, abhängig von seiner Interpretation der Sensoren.
  • Die Dateien im Verzeichnis „ models/controllers “ – diese Klassen implementieren unterschiedliche Verhaltensweisen des Roboters bei einem bekannten Zustand der Umgebung. Insbesondere wird abhängig von der Zustandsmaschine ein bestimmter Controller ausgewählt.

Das Ziel

Roboter brauchen wie Menschen einen Sinn im Leben. Das Ziel unserer Software, die diesen Roboter steuert, wird sehr einfach sein: Er wird versuchen, seinen Weg zu einem vorbestimmten Zielpunkt zu finden. Dies ist normalerweise die grundlegende Funktion, die jeder mobile Roboter haben sollte, von autonomen Autos bis hin zu Roboterstaubsaugern. Die Koordinaten des Ziels werden in die Steuerungssoftware einprogrammiert, bevor der Roboter aktiviert wird, könnten aber von einer zusätzlichen Python-Anwendung generiert werden, die die Roboterbewegungen überwacht. Stellen Sie sich zum Beispiel vor, Sie fahren durch mehrere Wegpunkte.

Erschwerend kommt jedoch hinzu, dass die Umgebung des Roboters mit Hindernissen übersät sein kann. Der Roboter DARF auf seinem Weg zum Ziel NICHT mit einem Hindernis kollidieren. Trifft der Roboter also auf ein Hindernis, muss er sich zurechtfinden, damit er seinen Weg zum Ziel fortsetzen kann.

Der programmierbare Roboter

Jeder Roboter hat unterschiedliche Fähigkeiten und Steuerungsanforderungen. Machen wir uns mit unserem simulierten programmierbaren Roboter vertraut.

Das erste, was zu beachten ist, ist, dass unser Roboter in diesem Handbuch ein autonomer mobiler Roboter sein wird . Das bedeutet, dass es sich frei und unter eigener Kontrolle im Raum bewegen wird. Dies steht im Gegensatz zu beispielsweise einem ferngesteuerten Roboter (der nicht autonom ist) oder einem Fabrikroboterarm (der nicht mobil ist). Unser Roboter muss selbst herausfinden, wie er seine Ziele erreicht und in seiner Umgebung überlebt. Dies erweist sich als überraschend schwierige Herausforderung für unerfahrene Roboterprogrammierer.

Steuereingänge: Sensoren

Es gibt viele verschiedene Möglichkeiten, wie ein Roboter ausgestattet sein kann, um seine Umgebung zu überwachen. Dazu kann alles gehören, von Näherungssensoren, Lichtsensoren, Stoßstangen, Kameras und so weiter. Darüber hinaus können Roboter mit externen Sensoren kommunizieren, die ihnen Informationen liefern, die sie selbst nicht direkt beobachten können.

Unser Referenzroboter ist mit neun Infrarotsensoren ausgestattet – das neuere Modell hat acht Infrarot- und fünf Ultraschall-Näherungssensoren – die in jeder Richtung in einem „Rock“ angeordnet sind. Auf der Vorderseite des Roboters befinden sich mehr Sensoren als auf der Rückseite, da es für den Roboter normalerweise wichtiger ist zu wissen, was sich vor ihm befindet, als was sich dahinter befindet.

Zusätzlich zu den Näherungssensoren verfügt der Roboter über ein Paar Radticker , die die Radbewegung verfolgen. Mit diesen können Sie verfolgen, wie viele Umdrehungen jedes Rad macht, wobei eine volle Vorwärtsdrehung eines Rads 2.765 Ticks entspricht. Drehungen in die entgegengesetzte Richtung zählen rückwärts, wodurch die Tickzahl verringert wird, anstatt sie zu erhöhen. Sie müssen sich in diesem Tutorial nicht um bestimmte Zahlen kümmern, da die Software, die wir schreiben, die zurückgelegte Entfernung in Metern verwendet. Später werde ich Ihnen zeigen, wie Sie es mit einer einfachen Python-Funktion aus Ticks berechnen können.

Steuerausgänge: Mobilität

Manche Roboter bewegen sich auf Beinen. Manche rollen wie ein Ball. Manche schlängeln sich sogar wie eine Schlange.

Unser Roboter ist ein Differentialantriebsroboter, das heißt, er rollt auf zwei Rädern herum. Wenn sich beide Räder gleich schnell drehen, bewegt sich der Roboter auf einer geraden Linie. Wenn sich die Räder mit unterschiedlichen Geschwindigkeiten bewegen, dreht sich der Roboter. Die Steuerung der Bewegung dieses Roboters läuft also darauf hinaus, die Geschwindigkeiten, mit denen sich jedes dieser beiden Räder dreht, richtig zu steuern.

API

In Sobot Rimulator wird die Trennung zwischen dem „Computer“ des Roboters und der (simulierten) physischen Welt durch die Datei robot_supervisor_interface.py verkörpert, die die gesamte API für die Interaktion mit den Sensoren und Motoren des „echten Roboters“ definiert:

  • read_proximity_sensors() gibt ein Array von neun Werten im nativen Format der Sensoren zurück
  • read_wheel_encoders() gibt ein Array aus zwei Werten zurück, die die Gesamtzahl der Ticks seit dem Start angeben
  • set_wheel_drive_rates( v_l, v_r ) nimmt zwei Werte (in Radianten pro Sekunde) und setzt die linke und rechte Geschwindigkeit der Räder auf diese beiden Werte

Diese Schnittstelle verwendet intern ein Roboterobjekt, das die Daten von Sensoren bereitstellt und die Möglichkeit bietet, Motoren oder Räder zu bewegen. Wenn Sie einen anderen Roboter erstellen möchten, müssen Sie einfach eine andere Python-Roboterklasse bereitstellen, die von derselben Schnittstelle verwendet werden kann, und der Rest des Codes (Controller, Supervisor und Simulator) funktioniert sofort!

Der Simulator

Da Sie einen echten Roboter in der realen Welt verwenden würden, ohne den Gesetzen der Physik zu viel Beachtung zu schenken, können Sie ignorieren, wie der Roboter simuliert wird, und direkt zur Programmierung der Steuerungssoftware übergehen, da sie fast gleich sein wird zwischen der realen Welt und einer Simulation. Aber wenn Sie neugierig sind, stelle ich es hier kurz vor.

Die Datei world.py ist eine Python-Klasse, die die simulierte Welt darstellt, mit Robotern und Hindernissen darin. Die Step-Funktion in dieser Klasse kümmert sich um die Entwicklung unserer einfachen Welt, indem sie:

  • Anwenden physikalischer Regeln auf die Bewegungen des Roboters
  • Berücksichtigung von Kollisionen mit Hindernissen
  • Bereitstellung neuer Werte für die Robotersensoren

Am Ende ruft es die Roboter-Supervisoren an, die für die Ausführung der Roboter-Gehirn-Software verantwortlich sind.

Die Schrittfunktion wird in einer Schleife ausgeführt, sodass robot.step_motion() den Roboter mit der vom Supervisor im vorherigen Simulationsschritt berechneten Radgeschwindigkeit bewegt.

 # step the simulation through one time interval def step( self ): dt = self.dt # step all the robots for robot in self.robots: # step robot motion robot.step_motion( dt ) # apply physics interactions self.physics.apply_physics() # NOTE: The supervisors must run last to ensure they are observing the "current" world # step all of the supervisors for supervisor in self.supervisors: supervisor.step( dt ) # increment world time self.world_time += dt

Die Funktion apply_physics() aktualisiert intern die Werte der Näherungssensoren des Roboters, sodass der Supervisor die Umgebung beim aktuellen Simulationsschritt abschätzen kann. Die gleichen Konzepte gelten für die Encoder.

Ein einfaches Modell

Zuerst wird unser Roboter ein sehr einfaches Modell haben. Es wird viele Annahmen über die Welt treffen. Einige der wichtigsten sind:

  • Das Gelände ist immer flach und eben
  • Hindernisse sind nie rund
  • Die Räder rutschen nie
  • Nichts wird den Roboter jemals herumschubsen
  • Die Sensoren fallen niemals aus oder geben falsche Messwerte
  • Die Räder drehen sich immer, wenn man es ihnen sagt

Obwohl die meisten dieser Annahmen in einer hausähnlichen Umgebung sinnvoll sind, könnten runde Hindernisse vorhanden sein. Unsere Hindernisvermeidungssoftware hat eine einfache Implementierung und folgt der Grenze von Hindernissen, um sie zu umgehen. Wir werden die Leser darauf hinweisen, wie sie den Steuerungsrahmen unseres Roboters mit einer zusätzlichen Prüfung verbessern können, um kreisförmige Hindernisse zu vermeiden.

Der Regelkreis

Wir werden jetzt in den Kern unserer Steuerungssoftware einsteigen und die Verhaltensweisen erklären, die wir im Inneren des Roboters programmieren wollen. Zusätzliche Verhaltensweisen können zu diesem Framework hinzugefügt werden, und Sie sollten Ihre eigenen Ideen ausprobieren, nachdem Sie mit dem Lesen fertig sind! Verhaltensbasierte Robotik-Software wurde vor mehr als 20 Jahren vorgeschlagen und ist immer noch ein leistungsstarkes Werkzeug für die mobile Robotik. Beispielsweise wurde 2007 eine Reihe von Verhaltensweisen bei der DARPA Urban Challenge verwendet – dem ersten Wettbewerb für autonom fahrende Autos!

Ein Roboter ist ein dynamisches System. Der Zustand des Roboters, die Messwerte seiner Sensoren und die Auswirkungen seiner Steuersignale sind in ständigem Fluss. Die Steuerung der Art und Weise, wie Ereignisse ablaufen, umfasst die folgenden drei Schritte:

  1. Steuersignale anlegen.
  2. Messen Sie die Ergebnisse.
  3. Generieren Sie neue Steuersignale, die berechnet werden, um uns unserem Ziel näher zu bringen.

Diese Schritte werden immer wieder wiederholt, bis wir unser Ziel erreicht haben. Je öfter wir dies pro Sekunde tun können, desto genauer haben wir die Kontrolle über das System. Der Sobot Rimulator-Roboter wiederholt diese Schritte 20 Mal pro Sekunde (20 Hz), aber viele Roboter müssen dies Tausende oder Millionen Mal pro Sekunde tun, um eine angemessene Kontrolle zu haben. Erinnern Sie sich an unsere vorherige Einführung zu verschiedenen Roboterprogrammiersprachen für verschiedene Robotersysteme und Geschwindigkeitsanforderungen.

Im Allgemeinen verwendet unser Roboter jedes Mal, wenn er Messungen mit seinen Sensoren durchführt, diese Messungen, um seine interne Schätzung des Zustands der Welt zu aktualisieren – zum Beispiel die Entfernung von seinem Ziel. Es vergleicht diesen Zustand mit einem Referenzwert dessen, was es für den Zustand haben möchte (für den Abstand soll es Null sein) und berechnet den Fehler zwischen dem gewünschten Zustand und dem tatsächlichen Zustand. Sobald diese Informationen bekannt sind, kann das Erzeugen neuer Steuersignale auf ein Problem der Minimierung des Fehlers reduziert werden, der den Roboter schließlich zum Ziel bewegen wird.

Ein raffinierter Trick: Vereinfachung des Modells

Um den zu programmierenden Roboter zu steuern, müssen wir ein Signal an das linke Rad senden, das ihm sagt, wie schnell es sich drehen soll, und ein separates Signal an das rechte Rad, das ihm sagt , wie schnell es sich drehen soll. Nennen wir diese Signale v L und v R . Es ist jedoch sehr umständlich, ständig in v L und v R zu denken. Anstatt zu fragen: „Wie schnell soll sich das linke Rad drehen und wie schnell soll sich das rechte Rad drehen?“ Es ist natürlicher zu fragen: „Wie schnell soll sich der Roboter vorwärts bewegen, und wie schnell soll er drehen oder seine Richtung ändern?“ Nennen wir diese Parameter Geschwindigkeit v und Winkel-(Rotations-)Geschwindigkeit ω (sprich „Omega“). Es stellt sich heraus, dass wir unser gesamtes Modell auf v und ω statt auf v L und v R stützen können, und erst wenn wir bestimmt haben, wie sich unser programmierter Roboter bewegen soll, transformieren wir diese beiden Werte mathematisch in die benötigten v L und v R um die Roboterräder tatsächlich zu steuern. Dies ist als Einradmodell der Steuerung bekannt.

Bei der Robotikprogrammierung ist es wichtig, den Unterschied zwischen Einrad- und Differentialantriebsmodellen zu verstehen.

Hier ist der Python-Code, der die endgültige Transformation in supervisor.py implementiert. Beachten Sie, dass sich beide Räder mit der gleichen Geschwindigkeit drehen, wenn ω 0 ist:

 # generate and send the correct commands to the robot def _send_robot_commands( self ): # ... v_l, v_r = self._uni_to_diff( v, omega ) self.robot.set_wheel_drive_rates( v_l, v_r ) def _uni_to_diff( self, v, omega ): # v = translational velocity (m/s) # omega = angular velocity (rad/s) R = self.robot_wheel_radius L = self.robot_wheel_base_length v_l = ( (2.0 * v) - (omega*L) ) / (2.0 * R) v_r = ( (2.0 * v) + (omega*L) ) / (2.0 * R) return v_l, v_r

Schätzzustand: Roboter, erkenne dich selbst

Mithilfe seiner Sensoren muss der Roboter versuchen, den Zustand der Umgebung sowie seinen eigenen Zustand einzuschätzen. Diese Schätzungen werden niemals perfekt sein, aber sie müssen ziemlich gut sein, da der Roboter alle seine Entscheidungen auf diesen Schätzungen basieren wird. Allein mit seinen Näherungssensoren und Radtickern muss er versuchen, Folgendes zu erraten:

  • Die Richtung zu Hindernissen
  • Der Abstand zu Hindernissen
  • Die Position des Roboters
  • Die Richtung des Roboters

Die ersten beiden Eigenschaften werden durch die Messwerte des Näherungssensors bestimmt und sind ziemlich einfach. Die API-Funktion read_proximity_sensors() gibt ein Array von neun Werten zurück, einen für jeden Sensor. Wir wissen im Voraus, dass beispielsweise der siebte Messwert dem Sensor entspricht, der um 75 Grad nach rechts vom Roboter zeigt.

Wenn dieser Wert also einen Wert anzeigt, der einem Abstand von 0,1 Metern entspricht, wissen wir, dass sich in 0,1 Metern Entfernung und 75 Grad nach links ein Hindernis befindet. Wenn kein Hindernis vorhanden ist, gibt der Sensor einen Messwert seiner maximalen Reichweite von 0,2 Metern zurück. Wenn wir also auf Sensor sieben 0,2 Meter ablesen, gehen wir davon aus, dass es in dieser Richtung tatsächlich kein Hindernis gibt.

Aufgrund der Funktionsweise der Infrarotsensoren (Messung der Infrarotreflexion) sind die von ihnen zurückgegebenen Zahlen eine nichtlineare Transformation der tatsächlich erfassten Entfernung. Die Python-Funktion zur Bestimmung der angezeigten Entfernung muss diese Messwerte also in Meter umrechnen. Dies geschieht in supervisor.py wie folgt:

 # update the distances indicated by the proximity sensors def _update_proximity_sensor_distances( self ): self.proximity_sensor_distances = [ 0.02-( log(readval/3960.0) )/30.0 for readval in self.robot.read_proximity_sensors() ]

Auch hier haben wir ein spezifisches Sensormodell in diesem Python-Roboter-Framework, während Sensoren in der realen Welt mit begleitender Software geliefert werden, die ähnliche Konvertierungsfunktionen von nichtlinearen Werten in Meter bieten sollte.

Die Bestimmung der Position und Richtung des Roboters (in der Robotikprogrammierung zusammen als Pose bezeichnet) ist etwas anspruchsvoller. Unser Roboter verwendet Odometrie , um seine Pose zu schätzen. Hier kommen die Radticker ins Spiel. Indem gemessen wird, um wie viel sich jedes Rad seit der letzten Iteration der Regelschleife gedreht hat, ist es möglich, eine gute Schätzung darüber zu erhalten, wie sich die Pose des Roboters geändert hat – aber nur, wenn die Änderung gering ist .

Dies ist einer der Gründe, warum es wichtig ist, den Regelkreis in einem realen Roboter sehr häufig zu iterieren, wo die Motoren, die die Räder bewegen, möglicherweise nicht perfekt sind. Wenn wir zu lange gewartet haben, um die Radticker zu messen, hätten beide Räder ziemlich viel tun können, und es wird unmöglich sein, abzuschätzen, wo wir gelandet sind.

Angesichts unseres aktuellen Softwaresimulators können wir es uns leisten, die Odometrieberechnung mit 20 Hz auszuführen – der gleichen Frequenz wie die Controller. Aber es könnte eine gute Idee sein, einen separaten Python-Thread schneller laufen zu lassen, um kleinere Bewegungen der Ticker abzufangen.

Unten ist die vollständige Odometrie-Funktion in supervisor.py , die die Roboter-Pose-Schätzung aktualisiert. Beachten Sie, dass die Pose des Roboters aus den Koordinaten x und y und der Richtung theta besteht, die im Bogenmaß von der positiven X-Achse gemessen wird. Positives x liegt im Osten und positives y im Norden. Somit zeigt eine Richtung von 0 an, dass der Roboter direkt nach Osten ausgerichtet ist. Der Roboter nimmt immer an, dass seine anfängliche Pose (0, 0), 0 .

 # update the estimated position of the robot using it's wheel encoder readings def _update_odometry( self ): R = self.robot_wheel_radius N = float( self.wheel_encoder_ticks_per_revolution ) # read the wheel encoder values ticks_left, ticks_right = self.robot.read_wheel_encoders() # get the difference in ticks since the last iteration d_ticks_left = ticks_left - self.prev_ticks_left d_ticks_right = ticks_right - self.prev_ticks_right # estimate the wheel movements d_left_wheel = 2*pi*R*( d_ticks_left / N ) d_right_wheel = 2*pi*R*( d_ticks_right / N ) d_center = 0.5 * ( d_left_wheel + d_right_wheel ) # calculate the new pose prev_x, prev_y, prev_theta = self.estimated_pose.scalar_unpack() new_x = prev_x + ( d_center * cos( prev_theta ) ) new_y = prev_y + ( d_center * sin( prev_theta ) ) new_theta = prev_theta + ( ( d_right_wheel - d_left_wheel ) / self.robot_wheel_base_length ) # update the pose estimate with the new values self.estimated_pose.scalar_update( new_x, new_y, new_theta ) # save the current tick count for the next iteration self.prev_ticks_left = ticks_left self.prev_ticks_right = ticks_right

Jetzt, da unser Roboter in der Lage ist, eine gute Schätzung der realen Welt zu erstellen, nutzen wir diese Informationen, um unsere Ziele zu erreichen.

Siehe auch: Physik-Tutorial für Videospiele – Kollisionserkennung für feste Objekte

Programmiermethoden für Python-Roboter: Go-to-Goal-Verhalten

Der oberste Zweck in der Existenz unseres kleinen Roboters in diesem Programmier-Tutorial ist es, zum Zielpunkt zu gelangen. Wie bringen wir also die Räder dazu, sich zu drehen, um es dorthin zu bringen? Beginnen wir damit, unser Weltbild etwas zu vereinfachen und gehen davon aus, dass uns keine Hindernisse im Weg stehen.

Dies wird dann zu einer einfachen Aufgabe und kann einfach in Python programmiert werden. Wenn wir vorwärts gehen, während wir auf das Ziel blicken, werden wir es erreichen. Dank unserer Odometrie kennen wir unsere aktuellen Koordinaten und Kurs. Wir kennen auch die Koordinaten des Ziels, weil sie vorprogrammiert waren. Daher können wir mit ein wenig linearer Algebra den Vektor von unserem Standort zum Ziel bestimmen, wie in go_to_goal_controller.py :

 # return a go-to-goal heading vector in the robot's reference frame def calculate_gtg_heading_vector( self ): # get the inverse of the robot's pose robot_inv_pos, robot_inv_theta = self.supervisor.estimated_pose().inverse().vector_unpack() # calculate the goal vector in the robot's reference frame goal = self.supervisor.goal() goal = linalg.rotate_and_translate_vector( goal, robot_inv_theta, robot_inv_pos ) return goal

Beachten Sie, dass wir den Vektor zum Ziel im Referenzrahmen des Roboters erhalten und NICHT in Weltkoordinaten. Befindet sich das Ziel auf der X-Achse im Bezugssystem des Roboters, befindet es sich direkt vor dem Roboter. Somit ist der Winkel dieses Vektors von der X-Achse die Differenz zwischen unserem Kurs und dem Kurs, auf dem wir uns befinden wollen. Mit anderen Worten, es ist der Fehler zwischen unserem aktuellen Zustand und dem, was wir uns für unseren aktuellen Zustand wünschen. Wir wollen daher unsere Drehrate ω so anpassen, dass sich der Winkel zwischen unserem Kurs und dem Ziel in Richtung 0 ändert. Wir wollen den Fehler minimieren:

 # calculate the error terms theta_d = atan2( self.gtg_heading_vector[1], self.gtg_heading_vector[0] ) # calculate angular velocity omega = self.kP * theta_d

self.kP im obigen Ausschnitt der Controller-Python-Implementierung ist eine Steuerungsverstärkung. Es ist ein Koeffizient, der bestimmt, wie schnell wir uns drehen, je nachdem , wie weit wir uns vom Ziel entfernen. Wenn der Fehler in unserem Kurs 0 ist, dann ist die Wenderate auch 0 . In der echten Python-Funktion in der Datei go_to_goal_controller.py sehen Sie ähnlichere Verstärkungen, da wir einen PID-Regler anstelle eines einfachen Proportionalkoeffizienten verwendet haben.

Nun, da wir unsere Winkelgeschwindigkeit ω haben, wie bestimmen wir unsere Vorwärtsgeschwindigkeit v ? Eine gute allgemeine Faustregel ist eine, die Sie wahrscheinlich instinktiv kennen: Wenn wir nicht abbiegen, können wir mit voller Geschwindigkeit vorwärts fahren, und je schneller wir abbiegen, desto langsamer sollten wir sein. Dies hilft uns im Allgemeinen, unser System stabil zu halten und innerhalb der Grenzen unseres Modells zu agieren. Somit ist v eine Funktion von ω . In go_to_goal_controller.py die Gleichung:

 # calculate translational velocity # velocity is v_max when omega is 0, # drops rapidly to zero as |omega| rises v = self.supervisor.v_max() / ( abs( omega ) + 1 )**0.5

Ein Vorschlag zur Ausarbeitung dieser Formel ist, zu berücksichtigen, dass wir normalerweise langsamer werden, wenn wir uns dem Ziel nähern, um es mit Nullgeschwindigkeit zu erreichen. Wie würde sich diese Formel ändern? Es muss irgendwie ein Ersatz von v_max() durch etwas Proportionales zum Abstand enthalten sein. OK, wir haben einen einzigen Regelkreis fast abgeschlossen. Das Einzige, was noch zu tun ist, ist, diese beiden Parameter des Einradmodells in Differenzradgeschwindigkeiten umzuwandeln und die Signale an die Räder zu senden. Hier ist ein Beispiel für die Flugbahn des Roboters unter dem Go-to-Goal-Controller ohne Hindernisse:

Dies ist ein Beispiel für die Flugbahn des programmierten Roboters.

Wie wir sehen können, ist der Vektor zum Ziel eine effektive Referenz, auf der wir unsere Steuerberechnungen aufbauen können. Es ist eine interne Darstellung dessen, „wo wir hinwollen“. Wie wir sehen werden, besteht der einzige große Unterschied zwischen Go-to-Goal und anderen Verhaltensweisen darin, dass es manchmal eine schlechte Idee ist, auf das Ziel zuzugehen, sodass wir einen anderen Referenzvektor berechnen müssen.

Programmiermethoden für Python-Roboter: Verhalten bei Hindernissen vermeiden

Auf das Ziel zuzugehen, wenn es ein Hindernis in dieser Richtung gibt, ist ein typisches Beispiel. Anstatt uns kopfüber in die Dinge zu stürzen, versuchen wir, ein Kontrollgesetz zu programmieren, das den Roboter dazu bringt, ihnen auszuweichen.

Um das Szenario zu vereinfachen, vergessen wir jetzt den Zielpunkt komplett und machen uns einfach folgendes zum Ziel: Wenn vor uns keine Hindernisse sind, geht es weiter. Wenn Sie auf ein Hindernis stoßen, wenden Sie sich davon ab, bis es nicht mehr vor uns ist.

Dementsprechend möchten wir, dass unser Referenzvektor einfach nach vorne zeigt, wenn sich kein Hindernis vor uns befindet. Dann ist ω Null und v die maximale Geschwindigkeit. Sobald wir jedoch mit unseren Näherungssensoren ein Hindernis erkennen, möchten wir, dass der Referenzvektor in eine beliebige Richtung zeigt, die vom Hindernis wegführt. Dies führt dazu, dass ω nach oben schießt, um uns vom Hindernis abzuwenden, und dass v sinkt, um sicherzustellen, dass wir dabei nicht versehentlich auf das Hindernis stoßen.

Eine nette Möglichkeit, unseren gewünschten Referenzvektor zu erzeugen, besteht darin, unsere neun Näherungswerte in Vektoren umzuwandeln und eine gewichtete Summe zu nehmen. Wenn keine Hindernisse erkannt werden, summieren sich die Vektoren symmetrisch, was zu einem Referenzvektor führt, der wie gewünscht geradeaus zeigt. Aber wenn ein Sensor auf der rechten Seite ein Hindernis erfasst, trägt er einen kleineren Vektor zur Summe bei und das Ergebnis ist ein nach links verschobener Referenzvektor.

Für einen allgemeinen Roboter mit einer anderen Platzierung von Sensoren kann die gleiche Idee angewendet werden, kann jedoch Änderungen in den Gewichten und/oder zusätzliche Sorgfalt erfordern, wenn Sensoren symmetrisch vor und hinter dem Roboter sind, da die gewichtete Summe Null werden könnte .

Bei richtiger Programmierung kann der Roboter diesen komplexen Hindernissen ausweichen.

Hier ist der Code, der dies in avoid_obstacles_controller.py :

 # sensor gains (weights) self.sensor_gains = [ 1.0+( (0.4*abs(p.theta)) / pi ) for p in supervisor.proximity_sensor_placements() ] # ... # return an obstacle avoidance vector in the robot's reference frame # also returns vectors to detected obstacles in the robot's reference frame def calculate_ao_heading_vector( self ): # initialize vector obstacle_vectors = [ [ 0.0, 0.0 ] ] * len( self.proximity_sensor_placements ) ao_heading_vector = [ 0.0, 0.0 ] # get the distances indicated by the robot's sensor readings sensor_distances = self.supervisor.proximity_sensor_distances() # calculate the position of detected obstacles and find an avoidance vector robot_pos, robot_theta = self.supervisor.estimated_pose().vector_unpack() for i in range( len( sensor_distances ) ): # calculate the position of the obstacle sensor_pos, sensor_theta = self.proximity_sensor_placements[i].vector_unpack() vector = [ sensor_distances[i], 0.0 ] vector = linalg.rotate_and_translate_vector( vector, sensor_theta, sensor_pos ) obstacle_vectors[i] = vector # store the obstacle vectors in the robot's reference frame # accumulate the heading vector within the robot's reference frame ao_heading_vector = linalg.add( ao_heading_vector, linalg.scale( vector, self.sensor_gains[i] ) ) return ao_heading_vector, obstacle_vectors

Unter Verwendung des resultierenden ao_heading_vector als unsere Referenz für den zu versuchenden Roboter, sind hier die Ergebnisse der Ausführung der Robotersoftware in der Simulation nur mit dem Vermeidungs-Hindernis-Controller, wobei der Zielpunkt vollständig ignoriert wird. Der Roboter hüpft ziellos herum, kollidiert aber nie mit einem Hindernis und schafft es sogar, einige sehr enge Räume zu navigieren:

Dieser Roboter vermeidet erfolgreich Hindernisse innerhalb des Python-Robotersimulators.

Programmiermethoden für Python-Roboter: Hybride Automaten (Behavior State Machine)

Bisher haben wir zwei Verhaltensweisen – zum Ziel gehen und Hindernissen ausweichen – isoliert beschrieben. Beide erfüllen ihre Funktion bewundernswert, aber um das Ziel in einem Umfeld voller Hindernisse erfolgreich zu erreichen, müssen wir sie kombinieren.

Die Lösung, die wir entwickeln werden, liegt in einer Klasse von Maschinen, die die äußerst cool klingende Bezeichnung Hybridautomaten trägt . Ein hybrider Automat ist mit mehreren unterschiedlichen Verhaltensweisen oder Modi sowie einer überwachenden Zustandsmaschine programmiert. Die überwachende Zustandsmaschine wechselt in diskreten Zeiten (wenn Ziele erreicht sind oder sich die Umgebung plötzlich zu stark verändert) von einem Modus in einen anderen, während jedes Verhalten Sensoren und Räder verwendet, um kontinuierlich auf Umgebungsänderungen zu reagieren. Die Lösung wurde Hybrid genannt, weil sie sich sowohl diskret als auch kontinuierlich weiterentwickelt.

Unser Python-Roboter-Framework implementiert die Zustandsmaschine in der Datei supervisor_state_machine.py .

Ausgestattet mit unseren beiden praktischen Verhaltensweisen bietet sich eine einfache Logik an: Wenn kein Hindernis erkannt wird, verwenden Sie das Go-to-Goal-Verhalten. Wenn ein Hindernis erkannt wird, wechseln Sie zum Verhalten „Hindernisse vermeiden“, bis das Hindernis nicht mehr erkannt wird.

Wie sich jedoch herausstellt, wird diese Logik viele Probleme hervorrufen. Wenn dieses System auf ein Hindernis trifft, tendiert es dazu, sich von ihm abzuwenden und, sobald es sich davon entfernt hat, sofort wieder umzudrehen und erneut darauf zu stoßen. Das Ergebnis ist eine endlose Schleife aus schnellen Umschaltungen, die den Roboter nutzlos macht. In the worst case, the robot may switch between behaviors with every iteration of the control loop—a state known as a Zeno condition .

There are multiple solutions to this problem, and readers that are looking for deeper knowledge should check, for example, the DAMN software architecture.

What we need for our simple simulated robot is an easier solution: One more behavior specialized with the task of getting around an obstacle and reaching the other side.

Python Robot Programming Methods: Follow-Wall Behavior

Here's the idea: When we encounter an obstacle, take the two sensor readings that are closest to the obstacle and use them to estimate the surface of the obstacle. Then, simply set our reference vector to be parallel to this surface. Keep following this wall until A) the obstacle is no longer between us and the goal, and B) we are closer to the goal than we were when we started. Then we can be certain we have navigated the obstacle properly.

With our limited information, we can't say for certain whether it will be faster to go around the obstacle to the left or to the right. To make up our minds, we select the direction that will move us closer to the goal immediately. To figure out which way that is, we need to know the reference vectors of the go-to-goal behavior and the avoid-obstacle behavior, as well as both of the possible follow-wall reference vectors. Here is an illustration of how the final decision is made (in this case, the robot will choose to go left):

Utilizing a few types of behaviors, the programmed robot avoids obstacles and continues onward.

Determining the follow-wall reference vectors turns out to be a bit more involved than either the avoid-obstacle or go-to-goal reference vectors. Take a look at the Python code in follow_wall_controller.py to see how it's done.

Final Control Design

The final control design uses the follow-wall behavior for almost all encounters with obstacles. However, if the robot finds itself in a tight spot, dangerously close to a collision, it will switch to pure avoid-obstacles mode until it is a safer distance away, and then return to follow-wall. Once obstacles have been successfully negotiated, the robot switches to go-to-goal. Here is the final state diagram, which is programmed inside the supervisor_state_machine.py :

This diagram illustrates the switching between robotics programming behaviors to achieve a goal and avoid obstacles.

Here is the robot successfully navigating a crowded environment using this control scheme:

The robot simulator has successfully allowed the robot software to avoid obstacles and achieve its original purpose.

An additional feature of the state machine that you can try to implement is a way to avoid circular obstacles by switching to go-to-goal as soon as possible instead of following the obstacle border until the end (which does not exist for circular objects!)

Tweak, Tweak, Tweak: Trial and Error

The control scheme that comes with Sobot Rimulator is very finely tuned. It took many hours of tweaking one little variable here, and another equation there, to get it to work in a way I was satisfied with. Robotics programming often involves a great deal of plain old trial-and-error. Robots are very complex and there are few shortcuts to getting them to behave optimally in a robot simulator environment…at least, not much short of outright machine learning, but that's a whole other can of worms.

Robotics often involves a great deal of plain old trial-and-error.

I encourage you to play with the control variables in Sobot Rimulator and observe and attempt to interpret the results. Changes to the following all have profound effects on the simulated robot's behavior:

  • The error gain kP in each controller
  • The sensor gains used by the avoid-obstacles controller
  • The calculation of v as a function of ω in each controller
  • The obstacle standoff distance used by the follow-wall controller
  • The switching conditions used by supervisor_state_machine.py
  • Pretty much anything else

When Programmable Robots Fail

We've done a lot of work to get to this point, and this robot seems pretty clever. Yet, if you run Sobot Rimulator through several randomized maps, it won't be long before you find one that this robot can't deal with. Sometimes it drives itself directly into tight corners and collides. Sometimes it just oscillates back and forth endlessly on the wrong side of an obstacle. Occasionally it is legitimately imprisoned with no possible path to the goal. After all of our testing and tweaking, sometimes we must come to the conclusion that the model we are working with just isn't up to the job, and we have to change the design or add functionality.

In the mobile robot universe, our little robot's “brain” is on the simpler end of the spectrum. Many of the failure cases it encounters could be overcome by adding some more advanced software to the mix. More advanced robots make use of techniques such as mapping , to remember where it's been and avoid trying the same things over and over; heuristics , to generate acceptable decisions when there is no perfect decision to be found; and machine learning , to more perfectly tune the various control parameters governing the robot's behavior.

A Sample of What's to Come

Robots are already doing so much for us, and they are only going to be doing more in the future. While even basic robotics programming is a tough field of study requiring great patience, it is also a fascinating and immensely rewarding one.

In this tutorial, we learned how to develop reactive control software for a robot using the high-level programming language Python. But there are many more advanced concepts that can be learned and tested quickly with a Python robot framework similar to the one we prototyped here. I hope you will consider getting involved in the shaping of things to come!


Acknowledgement: I would like to thank Dr. Magnus Egerstedt and Jean-Pierre de la Croix of the Georgia Institute of Technology for teaching me all this stuff, and for their enthusiasm for my work on Sobot Rimulator.

Related: OpenCV Tutorial: Real-time Object Detection Using MSER in iOS