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_msg
verwenden, 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_msgs
definiert 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 WordCount
erzeugt.
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 WordCount
sind ä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 WordCountRequest
entgegen 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 WordCountResponse
Objekt 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 rosservice
aufzurufen. 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
)
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.