Skip to content

Commit 3294bf1

Browse files
committed
add : get clients, servers info
Signed-off-by: Minju, Lee <[email protected]>
1 parent ee22148 commit 3294bf1

File tree

5 files changed

+204
-0
lines changed

5 files changed

+204
-0
lines changed

rclpy/rclpy/node.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2280,6 +2280,66 @@ def get_subscriptions_info_by_topic(
22802280
no_mangle,
22812281
_rclpy.rclpy_get_subscriptions_info_by_topic)
22822282

2283+
def get_clients_info_by_service(
2284+
self,
2285+
service_name: str,
2286+
no_mangle: bool = False
2287+
) -> List[TopicEndpointInfo]:
2288+
"""
2289+
Return a list of clients on a given service.
2290+
2291+
The returned parameter is a list of TopicEndpointInfo objects, where each will contain
2292+
the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
2293+
2294+
When the ``no_mangle`` parameter is ``True``, the provided ``service_name`` should be a
2295+
valid service name for the middleware (useful when combining ROS with native middleware
2296+
(e.g. DDS) apps). When the ``no_mangle`` parameter is ``False``,the provided
2297+
``service_name`` should follow ROS service name conventions.
2298+
2299+
``service_name`` may be a relative, private, or fully qualified service name.
2300+
A relative or private service will be expanded using this node's namespace and name.
2301+
The queried ``service_name`` is not remapped.
2302+
2303+
:param service_name: The service_name on which to find the clients.
2304+
:param no_mangle: If ``True``, `service_name` needs to be a valid middleware service
2305+
name, otherwise it should be a valid ROS service name. Defaults to ``False``.
2306+
:return: A list of TopicEndpointInfo for all the clients on this service.
2307+
"""
2308+
return self._get_info_by_topic(
2309+
service_name,
2310+
no_mangle,
2311+
_rclpy.rclpy_get_clients_info_by_service)
2312+
2313+
def get_servers_info_by_service(
2314+
self,
2315+
service_name: str,
2316+
no_mangle: bool = False
2317+
) -> List[TopicEndpointInfo]:
2318+
"""
2319+
Return a list of servers on a given service.
2320+
2321+
The returned parameter is a list of TopicEndpointInfo objects, where each will contain
2322+
the node name, node namespace, service type, service endpoint's GID, and its QoS profile.
2323+
2324+
When the ``no_mangle`` parameter is ``True``, the provided ``service_name`` should be a
2325+
valid service name for the middleware (useful when combining ROS with native middleware
2326+
(e.g. DDS) apps). When the ``no_mangle`` parameter is ``False``,the provided
2327+
``service_name`` should follow ROS service name conventions.
2328+
2329+
``service_name`` may be a relative, private, or fully qualified service name.
2330+
A relative or private service will be expanded using this node's namespace and name.
2331+
The queried ``service_name`` is not remapped.
2332+
2333+
:param service_name: The service_name on which to find the servers.
2334+
:param no_mangle: If ``True``, `service_name` needs to be a valid middleware service
2335+
name, otherwise it should be a valid ROS service name. Defaults to ``False``.
2336+
:return: A list of TopicEndpointInfo for all the servers on this service.
2337+
"""
2338+
return self._get_info_by_topic(
2339+
service_name,
2340+
no_mangle,
2341+
_rclpy.rclpy_get_servers_info_by_service)
2342+
22832343
def wait_for_node(
22842344
self,
22852345
fully_qualified_node_name: str,

rclpy/src/rclpy/_rclpy_pybind11.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ PYBIND11_MODULE(_rclpy_pybind11, m) {
198198
"rclpy_get_subscriptions_info_by_topic",
199199
&rclpy::graph_get_subscriptions_info_by_topic,
200200
"Get subscriptions info for a topic.");
201+
m.def(
202+
"rclpy_get_clients_info_by_service",
203+
&rclpy::graph_get_clients_info_by_service,
204+
"Get clients info for a service.");
205+
m.def(
206+
"rclpy_get_servers_info_by_service",
207+
&rclpy::graph_get_servers_info_by_service,
208+
"Get servers info for a service.");
201209
m.def(
202210
"rclpy_get_service_names_and_types",
203211
&rclpy::graph_get_service_names_and_types,

rclpy/src/rclpy/graph.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,22 @@ graph_get_subscriptions_info_by_topic(
277277
rcl_get_subscriptions_info_by_topic);
278278
}
279279

280+
py::list
281+
graph_get_clients_info_by_service(
282+
Node & node, const char * service_name, bool no_mangle)
283+
{
284+
return _get_info_by_topic(
285+
node, service_name, no_mangle, "clients",
286+
rcl_get_clients_info_by_service);
287+
}
288+
289+
py::list
290+
graph_get_servers_info_by_service(
291+
Node & node, const char * service_name, bool no_mangle)
292+
{
293+
return _get_info_by_topic(
294+
node, service_name, no_mangle, "servers",
295+
rcl_get_servers_info_by_service);
296+
}
297+
280298
} // namespace rclpy

rclpy/src/rclpy/graph.hpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,42 @@ py::list
164164
graph_get_subscriptions_info_by_topic(
165165
Node & node, const char * topic_name, bool no_mangle);
166166

167+
/// Return a list of clients on a given service.
168+
/**
169+
* The returned clients information includes node name, node namespace, service type, gid,
170+
* and qos profile.
171+
*
172+
* Raises NotImplementedError if the call is not supported by RMW
173+
* Raises RCLError if there is an rcl error
174+
*
175+
* \param[in] node node to get service clients info
176+
* \param[in] service_name the service name to get the clients for.
177+
* \param[in] no_mangle if `true`, `service_name` needs to be a valid middleware service name,
178+
* otherwise it should be a valid ROS service name.
179+
* \return list of clients.
180+
*/
181+
py::list
182+
graph_get_clients_info_by_service(
183+
Node & node, const char * service_name, bool no_mangle);
184+
185+
/// Return a list of servers on a given service.
186+
/**
187+
* The returned servers information includes node name, node namespace, service type, gid,
188+
* and qos profile.
189+
*
190+
* Raises NotImplementedError if the call is not supported by RMW
191+
* Raises RCLError if there is an rcl error
192+
*
193+
* \param[in] node node to get service servers info
194+
* \param[in] service_name the service name to get the servers for.
195+
* \param[in] no_mangle if `true`, `service_name` needs to be a valid middleware service name,
196+
* otherwise it should be a valid ROS service name.
197+
* \return list of servers.
198+
*/
199+
py::list
200+
graph_get_servers_info_by_service(
201+
Node & node, const char * service_name, bool no_mangle);
202+
167203
} // namespace rclpy
168204

169205
#endif // RCLPY__GRAPH_HPP_

rclpy/test/test_node.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@
5151
from rclpy.qos import QoSProfile
5252
from rclpy.qos import QoSReliabilityPolicy
5353
from rclpy.time_source import USE_SIM_TIME_NAME
54+
from rclpy.topic_endpoint_info import TopicEndpointTypeEnum
5455
from rclpy.type_description_service import START_TYPE_DESCRIPTION_SERVICE_PARAM
5556
from rclpy.utilities import get_rmw_implementation_identifier
5657
from test_msgs.msg import BasicTypes
58+
from test_msgs.srv import Empty
5759

5860
TEST_NODE = 'my_node'
5961
TEST_NAMESPACE = '/my_ns'
@@ -311,6 +313,86 @@ def test_get_publishers_subscriptions_info_by_topic(self):
311313
self.node.get_subscriptions_info_by_topic('13')
312314
self.node.get_publishers_info_by_topic('13')
313315

316+
def test_get_clients_servers_info_by_service(self):
317+
service_name = 'test_service_endpoint_info'
318+
fq_service_name = '{namespace}/{name}'.format(namespace=TEST_NAMESPACE, name=service_name)
319+
# Lists should be empty
320+
self.assertFalse(self.node.get_clients_info_by_service(fq_service_name))
321+
self.assertFalse(self.node.get_servers_info_by_service(fq_service_name))
322+
323+
# Add a client
324+
qos_profile = QoSProfile(
325+
depth=10,
326+
history=QoSHistoryPolicy.KEEP_ALL,
327+
deadline=Duration(seconds=1, nanoseconds=12345),
328+
lifespan=Duration(seconds=20, nanoseconds=9887665),
329+
reliability=QoSReliabilityPolicy.BEST_EFFORT,
330+
durability=QoSDurabilityPolicy.TRANSIENT_LOCAL,
331+
liveliness_lease_duration=Duration(seconds=5, nanoseconds=23456),
332+
liveliness=QoSLivelinessPolicy.MANUAL_BY_TOPIC)
333+
self.node.create_client(Empty, service_name, qos_profile=qos_profile)
334+
# List should have at least one item
335+
client_list = self.node.get_clients_info_by_service(fq_service_name)
336+
self.assertGreaterEqual(len(client_list), 1)
337+
# Server list should be empty
338+
self.assertFalse(self.node.get_servers_info_by_service(fq_service_name))
339+
# Verify client list has the right data
340+
for client in client_list:
341+
self.assertEqual(self.node.get_name(), client.node_name)
342+
self.assertEqual(self.node.get_namespace(), client.node_namespace)
343+
assert 'test_msgs/srv/Empty' in client.topic_type
344+
if 'test_msgs/srv/Empty_Request' == client.topic_type:
345+
actual_qos_profile = client.qos_profile
346+
assert client.endpoint_type == TopicEndpointTypeEnum.PUBLISHER
347+
self.assert_qos_equal(qos_profile, actual_qos_profile, is_publisher=True)
348+
elif 'test_msgs/srv/Empty_Response' == client.topic_type:
349+
actual_qos_profile = client.qos_profile
350+
assert client.endpoint_type == TopicEndpointTypeEnum.SUBSCRIPTION
351+
self.assert_qos_equal(qos_profile, actual_qos_profile, is_publisher=False)
352+
353+
# Add a server
354+
qos_profile2 = QoSProfile(
355+
depth=1,
356+
history=QoSHistoryPolicy.KEEP_LAST,
357+
deadline=Duration(seconds=15, nanoseconds=1678),
358+
lifespan=Duration(seconds=29, nanoseconds=2345),
359+
reliability=QoSReliabilityPolicy.RELIABLE,
360+
durability=QoSDurabilityPolicy.VOLATILE,
361+
liveliness_lease_duration=Duration(seconds=5, nanoseconds=23456),
362+
liveliness=QoSLivelinessPolicy.AUTOMATIC)
363+
self.node.create_service(
364+
Empty,
365+
service_name,
366+
lambda msg: print(msg),
367+
qos_profile=qos_profile2
368+
)
369+
# Both lists should have at least one item
370+
client_list = self.node.get_clients_info_by_service(fq_service_name)
371+
server_list = self.node.get_servers_info_by_service(fq_service_name)
372+
self.assertGreaterEqual(len(client_list), 1)
373+
self.assertGreaterEqual(len(server_list), 1)
374+
# Verify server list has the right data
375+
for server in server_list:
376+
self.assertEqual(self.node.get_name(), server.node_name)
377+
self.assertEqual(self.node.get_namespace(), server.node_namespace)
378+
assert 'test_msgs/srv/Empty' in server.topic_type
379+
if 'test_msgs/srv/Empty_Request' == server.topic_type:
380+
actual_qos_profile = server.qos_profile
381+
assert server.endpoint_type == TopicEndpointTypeEnum.SUBSCRIPTION
382+
self.assert_qos_equal(qos_profile2, actual_qos_profile, is_publisher=False)
383+
elif 'test_msgs/srv/Empty_Response' == server.topic_type:
384+
actual_qos_profile = server.qos_profile
385+
assert server.endpoint_type == TopicEndpointTypeEnum.PUBLISHER
386+
self.assert_qos_equal(qos_profile2, actual_qos_profile, is_publisher=True)
387+
388+
# Error cases
389+
with self.assertRaises(TypeError):
390+
self.node.get_clients_info_by_service(1)
391+
self.node.get_servers_info_by_service(1)
392+
with self.assertRaisesRegex(ValueError, 'is invalid'):
393+
self.node.get_clients_info_by_service('13')
394+
self.node.get_servers_info_by_service('13')
395+
314396
def test_count_publishers_subscribers(self):
315397
short_topic_name = 'chatter'
316398
fq_topic_name = '%s/%s' % (TEST_NAMESPACE, short_topic_name)

0 commit comments

Comments
 (0)