|
14 | 14 |
|
15 | 15 | import os
|
16 | 16 | import uuid
|
| 17 | +from threading import Lock |
17 | 18 | from typing import (
|
18 | 19 | Any,
|
19 | 20 | Callable,
|
|
30 | 31 | import rclpy.qos
|
31 | 32 | import rclpy.subscription
|
32 | 33 | import rclpy.task
|
| 34 | +from rclpy.client import Client |
33 | 35 | from rclpy.service import Service
|
34 | 36 |
|
35 | 37 | from rai.communication.ros2.api.base import (
|
|
39 | 41 |
|
40 | 42 |
|
41 | 43 | class ROS2ServiceAPI(BaseROS2API):
|
42 |
| - """Handles ROS2 service operations including calling services.""" |
| 44 | + """Handles ROS 2 service operations including calling services.""" |
43 | 45 |
|
44 | 46 | def __init__(self, node: rclpy.node.Node) -> None:
|
45 | 47 | self.node = node
|
46 | 48 | self._logger = node.get_logger()
|
47 | 49 | self._services: Dict[str, Service] = {}
|
| 50 | + self._persistent_clients: Dict[str, Client] = {} |
| 51 | + self._persistent_clients_lock = Lock() |
| 52 | + |
| 53 | + def release_client(self, service_name: str) -> bool: |
| 54 | + with self._persistent_clients_lock: |
| 55 | + return self._persistent_clients.pop(service_name, None) is not None |
48 | 56 |
|
49 | 57 | def call_service(
|
50 | 58 | self,
|
51 | 59 | service_name: str,
|
52 | 60 | service_type: str,
|
53 | 61 | request: Any,
|
54 | 62 | timeout_sec: float = 5.0,
|
| 63 | + *, |
| 64 | + reuse_client: bool = True, |
55 | 65 | ) -> Any:
|
56 | 66 | """
|
57 |
| - Call a ROS2 service. |
| 67 | + Call a ROS 2 service. |
58 | 68 |
|
59 | 69 | Args:
|
60 |
| - service_name: Name of the service to call |
61 |
| - service_type: ROS2 service type as string |
62 |
| - request: Request message content |
| 70 | + service_name: Fully-qualified service name. |
| 71 | + service_type: ROS 2 service type string (e.g., 'std_srvs/srv/SetBool'). |
| 72 | + request: Request payload dict. |
| 73 | + timeout_sec: Seconds to wait for availability/response. |
| 74 | + reuse_client: Reuse a cached client. Client creation is synchronized; set |
| 75 | + False to create a new client per call. |
63 | 76 |
|
64 | 77 | Returns:
|
65 |
| - The response message |
| 78 | + Response message instance. |
| 79 | +
|
| 80 | + Raises: |
| 81 | + ValueError: Service not available within the timeout. |
| 82 | + AttributeError: Service type or request cannot be constructed. |
| 83 | +
|
| 84 | + Note: |
| 85 | + With reuse_client=True, access to the cached client (including the |
| 86 | + service call) is serialized by a lock, preventing concurrent calls |
| 87 | + through the same client. Use reuse_client=False for per-call clients |
| 88 | + when concurrent service calls are required. |
66 | 89 | """
|
67 | 90 | srv_msg, srv_cls = self.build_ros2_service_request(service_type, request)
|
68 |
| - service_client = self.node.create_client(srv_cls, service_name) # type: ignore |
69 |
| - client_ready = service_client.wait_for_service(timeout_sec=timeout_sec) |
70 |
| - if not client_ready: |
71 |
| - raise ValueError( |
72 |
| - f"Service {service_name} not ready within {timeout_sec} seconds. " |
73 |
| - "Try increasing the timeout or check if the service is running." |
74 |
| - ) |
75 |
| - if os.getenv("ROS_DISTRO") == "humble": |
76 |
| - return service_client.call(srv_msg) |
| 91 | + |
| 92 | + def _call_service(client: Client, timeout_sec: float) -> Any: |
| 93 | + is_service_available = client.wait_for_service(timeout_sec=timeout_sec) |
| 94 | + if not is_service_available: |
| 95 | + raise ValueError( |
| 96 | + f"Service {service_name} not ready within {timeout_sec} seconds. " |
| 97 | + "Try increasing the timeout or check if the service is running." |
| 98 | + ) |
| 99 | + if os.getenv("ROS_DISTRO") == "humble": |
| 100 | + return client.call(srv_msg) |
| 101 | + else: |
| 102 | + return client.call(srv_msg, timeout_sec=timeout_sec) |
| 103 | + |
| 104 | + if reuse_client: |
| 105 | + with self._persistent_clients_lock: |
| 106 | + client = self._persistent_clients.get(service_name, None) |
| 107 | + if client is None: |
| 108 | + client = self.node.create_client(srv_cls, service_name) # type: ignore |
| 109 | + self._persistent_clients[service_name] = client |
| 110 | + return _call_service(client, timeout_sec) |
77 | 111 | else:
|
78 |
| - return service_client.call(srv_msg, timeout_sec=timeout_sec) |
| 112 | + client = self.node.create_client(srv_cls, service_name) # type: ignore |
| 113 | + return _call_service(client, timeout_sec) |
79 | 114 |
|
80 | 115 | def get_service_names_and_types(self) -> List[Tuple[str, List[str]]]:
|
81 | 116 | return self.node.get_service_names_and_types()
|
|
0 commit comments