Kapitel 4. Dienstleistungen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Dienste sind eine weitere Möglichkeit, Daten zwischen Knoten in ROS zu übertragen. Dienste sind einfach synchrone Remote-Prozeduraufrufe; sie ermöglichen es einem Knoten, eine Funktion aufzurufen, die in einem anderen Knoten ausgeführt wird. Wir definieren die Ein- und Ausgänge dieser Funktion ähnlich wie bei der Definition neuer Nachrichtentypen. Der Server (der den Dienst bereitstellt) legt einen Callback fest, um die Dienstanfrage zu bearbeiten, und kündigt den Dienst an. Der Client (der den Dienst aufruft) greift dann über einen lokalen Proxy auf diesen Dienst zu.

Serviceaufrufe eignen sich gut für Dinge, die du nur gelegentlich erledigen musst und die nur eine begrenzte Zeit in Anspruch nehmen. Ein gutes Beispiel dafür sind allgemeine Berechnungen, die du vielleicht an andere Computer verteilen möchtest. Auch diskrete Aktionen des Roboters, wie das Einschalten eines Sensors oder das Aufnehmen eines hochauflösenden Bildes mit einer Kamera, sind gute Kandidaten für die Implementierung von Serviceaufrufen.

Obwohl es in ROS bereits mehrere Dienste gibt, die von Paketen definiert wurden, schauen wir uns zunächst an, wie wir unseren eigenen Dienst definieren und implementieren können, da dies einen Einblick in die zugrunde liegenden Mechanismen von Dienstaufrufen gibt. Als konkretes Beispiel werden wir in diesem Kapitel zeigen, wie wir einen Dienst erstellen, der die Anzahl der Wörter in einer Zeichenkette zählt.

Einen Dienst definieren

Der erste Schritt bei der Erstellung eines neuen Dienstes besteht darin, die Ein- und Ausgänge des Dienstaufrufs zu definieren. Dies geschieht in einer Service-Definitionsdatei, die ähnlich aufgebaut ist wie die Nachrichten-Definitionsdateien, die wir bereits kennengelernt haben. Da ein Dienstaufruf jedoch sowohl Eingänge als auch Ausgänge hat, ist er etwas komplizierter als eine Nachricht.

Unser Beispieldienst zählt die Anzahl der Wörter in einer Zeichenkette. Das bedeutet, dass die Eingabe für den Dienstaufruf eine string und die Ausgabe eine Ganzzahl sein sollte. Obwohl wir hier Nachrichten vonstd_msgverwenden, kannst du jede beliebige ROS-Nachricht verwenden, auch solche, die du selbst definiert hast. Beispiel 4-1 zeigt eine Dienstdefinition für diesen Fall.

Beispiel 4-1. WordCount.srv
string words
---
uint32 count
Hinweis

Wie Nachrichten-Definitionsdateien sind auch Service-Definitionsdateien nur Listen von Nachrichtentypen. Diese können eingebaut sein, wie z. B. die, die im Paket std_msgsdefiniert sind, oder du kannst sie selbst definieren.

Die Eingaben für den Dienstaufruf kommen zuerst. In diesem Fall verwenden wir einfach den in ROS eingebauten Typ string. Drei Bindestriche (---) markieren das Ende der Eingaben und den Beginn der Ausgabedefinition. Für die Ausgabe verwenden wir eine 32-Bit-Ganzzahl ohne Vorzeichen (uint32). Die Datei, die diese Definition enthält, heißt WordCount.srvund befindet sich traditionell im Verzeichnis srv im Hauptverzeichnis des Pakets (obwohl dies nicht unbedingt erforderlich ist).

Sobald wir die Definitionsdatei an der richtigen Stelle haben, müssen wircatkin_make ausführen, um den Code und die Klassendefinitionen zu erstellen, die wir bei der Interaktion mit dem Dienst tatsächlich verwenden werden, genau wie bei neuen Nachrichten. Damit catkin_make diesen Code erzeugt, müssen wir sicherstellen, dass der Aufruf find_package() in CMakeLists.txtmessage_generation enthält, genau wie bei den neuen Nachrichten:

find_package(catkin REQUIRED COMPONENTS
   roscpp
   rospy
   message_generation   # Add message_generation here, after the other packages
)

Außerdem müssen wir die Datei package.xml ergänzen, um die Abhängigkeiten von rospy und dem Nachrichtensystem zu berücksichtigen. Das bedeutet, dass wir eine Build-Abhängigkeit von message_generation und eine Laufzeit-Abhängigkeit von message_runtime benötigen:

<build_depend>rospy</build_depend>
<run_depend>rospy</run_depend>

<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>

Dann müssen wir catkin mitteilen, welche Service-Definitionsdateien wir kompilieren wollen. Dazu verwenden wir den Aufruf add_service_files() in CMakeLists.txt:

add_service_files(
  FILES
  WordCount.srv
)

Schließlich müssen wir sicherstellen, dass die Abhängigkeiten für die Service-Definitionsdatei deklariert sind (wieder in CMakeLists.txt), indem wir den Aufruf generate_messages()verwenden:

generate_messages(
  DEPENDENCIES
  std_msgs
)

Wenn du catkin_make aufrufst, werden drei Klassen erzeugt: WordCount, WordCountRequest und WordCountResponse. Diese Klassen werden für die Interaktion mit dem Dienst verwendet, wie wir noch sehen werden. Genau wie bei den Nachrichten wirst du dir die Details der erzeugten Klassen wahrscheinlich nie ansehen müssen. Falls es dich dennoch interessiert, kannst du dir in Beispiel 4-2(einen Teil) der Klassen ansehen, die das Beispiel WordCounterzeugt.

Beispiel 4-2. Die von catkin_make generierten Python-Klassen für das WordCount-Beispiel (Code in den Funktionen wurde aus Gründen der Übersichtlichkeit entfernt)
"""autogenerated by genpy from basics/WordCountRequest.msg. Do not edit."""
import sys
python3 = True if sys.hexversion > 0x03000000 else False
import genpy
import struct


class WordCountRequest(genpy.Message):
  _md5sum = "6f897d3845272d18053a750c1cfb862a"
  _type = "basics/WordCountRequest"
  _has_header = False #flag to mark the presence of a Header object
  _full_text = """string words

"""
  __slots__ = ['words']
  _slot_types = ['string']

  def __init__(self, *args, **kwds):
    """
    Constructor. Any message fields that are implicitly/explicitly
    set to None will be assigned a default value. The recommend
    use is keyword arguments as this is more robust to future message
    changes.  You cannot mix in-order arguments and keyword arguments.

    The available fields are:
       words

    :param args: complete set of field values, in .msg order
    :param kwds: use keyword arguments corresponding to message field names
    to set specific fields.
    """
    if args or kwds:
      super(WordCountRequest, self).__init__(*args, **kwds)
      #message fields cannot be None, assign default values for those that are
      if self.words is None:
        self.words = ''
    else:
      self.words = ''

  def _get_types(self):
    ...    """

  def serialize(self, buff):
    ...

  def deserialize(self, str):
    ...

  def serialize_numpy(self, buff, numpy):
    ...

  def deserialize_numpy(self, str, numpy):
    ...

class WordCountResponse(genpy.Message):
  ...

class WordCount(genpy.Message):
  ...

Die Details der Definitionen für WordCountResponse und WordCountsind ähnlich wie die für WordCountRequest. All dies sind nur ROS-Meldungen.

Wir können mit dem Befehl rossrv überprüfen, ob die Definition des Dienstaufrufs den Erwartungen entspricht:

user@hostname$ rossrv show WordCount
[basics/WordCount]:
string words
---
uint32 count

Mit rossrv list kannst du alle verfügbaren Dienste sehen, mit rossrv packages alle Pakete, die Dienste anbieten, und mit rossrv package alle Dienste, die von einem bestimmten Paket angeboten werden.

Einen Dienst implementieren

Jetzt, da wir die Ein- und Ausgänge für den Dienstaufruf definiert haben, können wir den Code für die Implementierung des Dienstes schreiben. Wie Themen sind Dienste ein Callback-basierter Mechanismus. Der Dienstanbieter legt einen Callback fest, der ausgeführt wird, wenn der Dienstaufruf erfolgt, und wartet dann auf eingehende Anfragen.Beispiel 4-3 zeigt einen einfachen Server, der unseren Dienstaufruf für die Wortzählung implementiert.

Beispiel 4-3. service_server.py
#!/usr/bin/env python

import rospy

from basics.srv import WordCount,WordCountResponse


def count_words(request):
    return WordCountResponse(len(request.words.split()))


rospy.init_node('service_server')

service = rospy.Service('word_count', WordCount, count_words)

rospy.spin()

Zuerst müssen wir den von catkin generierten Code importieren:

from basics.srv import WordCount,WordCountResponse

Beachte, dass wir sowohl WordCount als auchWordCountResponse importieren müssen. Beide werden in einem Python-Modul mit demselben Namen wie das Paket und der Erweiterung .srv(in unserem Fallbasics.srv) erstellt.

Die Callback-Funktion nimmt ein einzelnes Argument vom Typ WordCountRequestentgegen und gibt ein einzelnes Argument vom Typ WordCountResponse zurück:

def count_words(request):
    return WordCountResponse(len(request.words.split()))

Der Konstruktor für WordCountResponse benötigt Parameter, die mit denen in der Service-Definitionsdatei übereinstimmen. Für uns bedeutet das eine Ganzzahl ohne Vorzeichen. Konventionell sollten Dienste, die aus irgendeinem Grund fehlschlagen, None zurückgeben.

Nach der Initialisierung des Knotens kündigen wir den Dienst an, indem wir ihm einen Namen (word_count) und einen Typ (WordCount) geben und den Callback angeben, der ihn implementieren wird:

service = rospy.Service('word_count', WordCount, count_words)

Zum Schluss machen wir einen Aufruf an rospy.spin(), der die Kontrolle über den Knoten an ROS übergibt und beendet wird, wenn der Knoten bereit ist, herunterzufahren. Anders als in der C++-API musst du die Kontrolle mit dem Aufruf von rospy.spin()nicht wirklich übergeben, da Callbacks in ihren eigenen Threads laufen. Du könntest eine eigene Schleife einrichten und dich daran erinnern, dass der Knoten beendet wird, wenn du etwas anderes zu tun hast. Die Verwendung von rospy.spin() ist jedoch eine bequeme Möglichkeit, den Knoten am Leben zu erhalten, bis er bereit ist, beendet zu werden.

Prüfen, ob alles wie erwartet funktioniert

Jetzt wir den Dienst definiert und implementiert haben, können wir mit dem Befehl rosservice überprüfen, ob alles wie erwartet funktioniert. Starten Sie einen roscore und führen Sie den Dienstknoten aus:

user@hostname$ rosrun basics service_server.py

Prüfen wir zunächst, ob der Dienst vorhanden ist:

user@hostname$ rosservice list
/rosout/get_loggers
/rosout/set_logger_level
/service_server/get_loggers
/service_server/set_logger_level
/word_count

Zusätzlich zu den Logging-Diensten, die ROS anbietet, scheint es unseren Dienst zu geben. Mitrosservice info können wir mehr Informationen darüber erhalten:

user@hostname$ rosservice info word_count
Node: /service_server
URI: rosrpc://hostname:60085
Type: basics/WordCount
Args: words

So erfahren wir, welcher Knoten den Dienst bereitstellt, wo er ausgeführt wird, welchen Typ er verwendet und wie die Argumente für den Dienstaufruf lauten. Einige dieser Informationen können wir auch über rosservice type word_count und roservice args word_count abrufen.

Andere Möglichkeiten der Rückgabe von Werten aus einem Dienst

Im vorherigen Beispiel haben wir explizit ein WordCountResponseObjekt erstellt und es vom Service-Callback zurückgegeben. Es gibt noch eine Reihe anderer Möglichkeiten, Werte aus einem Service-Callback zurückzugeben, die du nutzen kannst. Wenn es nur ein einziges Rückgabeargument für den Dienst gibt, kannst du einfach diesen Wert zurückgeben:

def count_words(request):
    return len(request.words.split())

Wenn es mehrere Rückgabeargumente gibt, kannst du ein Tupel oder eine Liste zurückgeben. Die Werte in der Liste werden der Reihe nach den Werten in der Dienstdefinition zugewiesen. Das funktioniert auch, wenn es nur einen Rückgabewert gibt:

def count_words(request):
    return [len(request.words.split())]

Du kannst auch ein Wörterbuch zurückgeben, in dem die Schlüssel die Namen der Argumente sind (die als Strings angegeben werden):

def count_words(request):
    return {'count': len(request.words.split())}

In beiden Fällen wird der zugrundeliegende Service-Aufrufcode in ROS diese Rückgabetypen in ein WordCountResponse Objekt übersetzen und es an den aufrufenden Knoten zurückgeben, genau wie im ursprünglichen Beispielcode.

Einen Dienst nutzen

Der einfachste Weg, einen Dienst zu nutzen, ist, ihn mit dem Befehl rosserviceaufzurufen. Für unseren Wortzählungsdienst sieht der Aufruf wie folgt aus:

user@hostname$ rosservice call word_count 'one two three'
count: 3

Der Befehl nimmt den Unterbefehl call, den Namen des Dienstes und die Argumente entgegen. Damit können wir den Dienst zwar aufrufen und sicherstellen, dass er wie erwartet funktioniert, aber es ist nicht so nützlich wie der Aufruf von einem anderen laufenden Knoten. Beispiel 4-4 zeigt, wie wir unseren Dienst programmatisch aufrufen können.

Beispiel 4-4. service_client.py
#!/usr/bin/env python

import rospy

from basics.srv import WordCount

import sys


rospy.init_node('service_client')

rospy.wait_for_service('word_count')

word_counter = rospy.ServiceProxy('word_count', WordCount)

words = ' '.join(sys.argv[1:])

word_count = word_counter(words)

print words, '->', word_count.count

Zuerst warten wir darauf, dass der Dienst vom Server bekannt gegeben wird:

rospy.wait_for_service('word_count')

Wenn wir versuchen, den Dienst zu nutzen, bevor er angekündigt wurde, schlägt der Aufruf mit einer Ausnahme fehl. Dies ist ein wesentlicher Unterschied zwischen Themen und Diensten. Wir können Themen abonnieren, die noch nicht ausgeschrieben sind, aber wir können nur ausgeschriebene Dienste nutzen. Sobald der Dienst bekannt gemacht wurde, können wir einen lokalen Proxy für ihn einrichten:

word_counter = rospy.ServiceProxy('word_count', WordCount)

Wir müssen den Namen des Dienstes (word_count) und den Typ (WordCount) angeben. So können wir word_counter wie eine lokale Funktion verwenden, die, wenn sie aufgerufen wird, den Dienst tatsächlich für uns aufruft:

word_count = word_counter(words)

Prüfen, ob alles wie erwartet funktioniert

Jetzt, da wir den Dienst definiert, den Support-Code mitcatkin erstellt und sowohl einen Server als auch einen Client implementiert haben, ist es an der Zeit zu überprüfen, ob alles funktioniert. Überprüfe, ob dein Server noch läuft, und führe den Client-Knoten aus (vergewissere dich, dass du deine Workspace-Setup-Datei in der Shell gespeichert hast, in der du den Client-Knoten ausführst, sonst wird er nicht funktionieren):

user@hostname$ rosrun basics service_client.py these are some words
these are some words -> 4

Stoppe jetzt den Server und starte den Client-Knoten erneut. Er sollte anhalten und darauf warten, dass der Dienst bekannt gegeben wird. Wenn du den Server-Knoten startest, sollte der Client normal weiterarbeiten, sobald der Dienst verfügbar ist. Dies zeigt eine der Einschränkungen von ROS-Diensten: Der Client kann möglicherweise ewig warten, wenn der Dienst aus irgendeinem Grund nicht verfügbar ist. Vielleicht ist der Dienstserver unerwartet gestorben oder der Name des Dienstes wurde im Client-Aufruf falsch geschrieben. In jedem Fall bleibt der Dienstclient stecken.

Andere Möglichkeiten, Dienste anzurufen

In unserem Client-Knoten rufen wir den Dienst über den Proxy auf, als wäre er eine lokale Funktion. Die Argumente dieser Funktion werden verwendet, um die Elemente der Dienstanforderung in der richtigen Reihenfolge auszufüllen. In unserem Beispiel haben wir nur ein Argument (words), also dürfen wir der Proxy-Funktion nur ein Argument geben. Da der Dienstaufruf nur eine Ausgabe hat, gibt die Proxy-Funktion auch nur einen einzigen Wert zurück. Wenn unsere Dienstdefinition hingegen so aussehen würde:

string words
int min_word_length
---
uint32 count
uint32 ignored

dann würde die Proxy-Funktion zwei Argumente annehmen und zwei Werte zurückgeben:

c,i = word_count(words, 3)

Die Argumente werden in der Reihenfolge übergeben, in der sie in der Dienstdefinition definiert sind. Es ist auch möglich, explizit ein Dienstanforderungsobjekt zu erstellen und dieses zum Aufrufen des Dienstes zu verwenden:

request = WordCountRequest('one two three', 3)
count,ignored = word_counter(request)

Wenn du dich für diesen Mechanismus entscheidest, musst du auch die Definition für WordCountRequest in den Client-Code importieren, und zwar wie folgt:

from basics.srv import WordCountRequest

Wenn du nur einige der Argumente setzen willst, kannst du den Dienstaufruf mit Schlüsselwortargumenten durchführen:

count,ignored = word_counter(words='one two three')

Dieser Mechanismus kann zwar nützlich sein, du solltest ihn aber mit Vorsicht verwenden, da alle Argumente, die du nicht explizit angibst, undefiniert bleiben. Wenn du Argumente weglässt, die der Dienst zur Ausführung benötigt, kann es zu seltsamen Rückgabewerten kommen. Du solltest diese Art des Aufrufs vermeiden, es sei denn, du musst sie tatsächlich verwenden.

Zusammenfassung

Jetzt weißt du alles über Dienste, den zweiten wichtigen Kommunikationsmechanismus in ROS. Dienste sind eigentlich nur synchrone Remote Procedure Calls und ermöglichen eine explizite Zwei-Wege-Kommunikation zwischen Knoten. Du solltest jetzt in der Lage sein, Dienste zu nutzen, die von anderen Paketen in ROS bereitgestellt werden, und auch deine eigenen Dienste zu implementieren.

Auch hier haben wir nicht alle Details von Diensten behandelt. Wenn du mehr Informationen über anspruchsvollere Verwendungen von Diensten haben möchtest, solltest du dirdieAPI-Dokumentation derDienste ansehen.

Du solltest Dienste für Dinge verwenden, die du nur gelegentlich brauchst, oder wenn du eine synchrone Antwort brauchst. Die Berechnungen in einem Dienst-Callback sollten eine kurze, begrenzte Zeitspanne in Anspruch nehmen. Wenn sie sehr lange dauern oder die Zeit sehr variabel ist, solltest du über eine Aktion nachdenken, die wir im nächsten Kapitel beschreiben.

Get Programmierung von Robotern mit ROS now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.