diff --git a/rclpy/rclpy/node.py b/rclpy/rclpy/node.py index 240835baa..697f13ac5 100644 --- a/rclpy/rclpy/node.py +++ b/rclpy/rclpy/node.py @@ -1448,18 +1448,36 @@ def get_service_names_and_types_by_node( node_namespace: str ) -> List[Tuple[str, List[str]]]: """ - Get a list of discovered service topics for a remote node. + Get a list of discovered service server topics for a remote node. :param node_name: Name of a remote node to get services for. :param node_namespace: Namespace of the remote node. :return: List of tuples. - The first element of each tuple is the service name and the second element is a list of - service types. + The first element of each tuple is the service server name + and the second element is a list of service types. """ with self.handle as capsule: return _rclpy.rclpy_get_service_names_and_types_by_node( capsule, node_name, node_namespace) + def get_client_names_and_types_by_node( + self, + node_name: str, + node_namespace: str + ) -> List[Tuple[str, List[str]]]: + """ + Get a list of discovered service client topics for a remote node. + + :param node_name: Name of a remote node to get service clients for. + :param node_namespace: Namespace of the remote node. + :return: List of tuples. + The fist element of each tuple is the service client name + and the second element is a list of service client types. + """ + with self.handle as capsule: + return _rclpy.rclpy_get_client_names_and_types_by_node( + capsule, node_name, node_namespace) + def get_topic_names_and_types(self, no_demangle: bool = False) -> List[Tuple[str, List[str]]]: """ Get a list topic names and types for the node. diff --git a/rclpy/src/rclpy/_rclpy.c b/rclpy/src/rclpy/_rclpy.c index 9a881c7e1..b5c578753 100644 --- a/rclpy/src/rclpy/_rclpy.c +++ b/rclpy/src/rclpy/_rclpy.c @@ -3141,6 +3141,7 @@ rclpy_get_service_names_and_types_by_node(PyObject * Py_UNUSED(self), PyObject * PyErr_Format(PyExc_RuntimeError, "Failed to get_service_names_and_types: %s", rcl_get_error_string().str); rcl_reset_error(); + rclpy_names_and_types_fini(&service_names_and_types); return NULL; } @@ -3153,6 +3154,55 @@ rclpy_get_service_names_and_types_by_node(PyObject * Py_UNUSED(self), PyObject * return pyservice_names_and_types; } +/// Get a list of service client names and types associated with the given node name. +/** + * Raises ValueError if pynode is not a node capsule + * Raises RuntimeError if there is an rcl error + * + * \param[in] pynode Capsule pointing to the node + * \param[in] node_name of a remote node to get publishers for + * \return Python list of tuples. + * The first element of each tuple is the service name (string) and the second element + * is a list of service types (list of strings). +*/ +static PyObject * +rclpy_get_client_names_and_types_by_node(PyObject * Py_UNUSED(self), PyObject * args) +{ + PyObject * pynode; + char * node_name; + char * node_namespace; + + if (!PyArg_ParseTuple(args, "Oss", &pynode, &node_name, &node_namespace)) { + return NULL; + } + + rcl_node_t * node = (rcl_node_t *)PyCapsule_GetPointer(pynode, "rcl_node_t"); + if (!node) { + return NULL; + } + + rcl_names_and_types_t client_names_and_types = rcl_get_zero_initialized_names_and_types(); + rcl_allocator_t allocator = rcl_get_default_allocator(); + rcl_ret_t ret = + rcl_get_client_names_and_types_by_node(node, &allocator, node_name, node_namespace, + &client_names_and_types); + if (ret != RCL_RET_OK) { + PyErr_Format(PyExc_RuntimeError, + "Failed to get_client_names_and_types: %s", rcl_get_error_string().str); + rcl_reset_error(); + rclpy_names_and_types_fini(&client_names_and_types); + return NULL; + } + + PyObject * pyclient_names_and_types = rclpy_convert_to_py_names_and_types( + &client_names_and_types); + if (!rclpy_names_and_types_fini(&client_names_and_types)) { + Py_XDECREF(pyclient_names_and_types); + return NULL; + } + return pyclient_names_and_types; +} + /// Get a list of topic names and types having at least one subscription from the given node name. /** * Raises ValueError if pynode is not a node capsule @@ -4810,7 +4860,12 @@ static PyMethodDef rclpy_methods[] = { { "rclpy_get_service_names_and_types_by_node", rclpy_get_service_names_and_types_by_node, METH_VARARGS, - "Get service list of specified node from graph API." + "Get service server list of specified node from graph API." + }, + { + "rclpy_get_client_names_and_types_by_node", rclpy_get_client_names_and_types_by_node, + METH_VARARGS, + "Get a service client list of a specified node from graph API." }, { "rclpy_get_topic_names_and_types", rclpy_get_topic_names_and_types, METH_VARARGS, diff --git a/rclpy/test/test_node.py b/rclpy/test/test_node.py index 22229d7cd..252f8e2e1 100644 --- a/rclpy/test/test_node.py +++ b/rclpy/test/test_node.py @@ -176,6 +176,14 @@ def test_service_names_and_types(self): # test that it doesn't raise self.node.get_service_names_and_types() + def test_service_names_and_types_by_node(self): + # test that it doesnt raise + self.node.get_service_names_and_types_by_node(TEST_NODE, TEST_NAMESPACE) + + def test_client_names_and_types_by_node(self): + # test that it doesnt raise + self.node.get_client_names_and_types_by_node(TEST_NODE, TEST_NAMESPACE) + def test_topic_names_and_types(self): # test that it doesn't raise self.node.get_topic_names_and_types(no_demangle=True)