kuavo-humanoid-sdk-ws 1.3.2__20260122221834-py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. kuavo_humanoid_sdk/__init__.py +6 -0
  2. kuavo_humanoid_sdk/common/launch_robot_tool.py +170 -0
  3. kuavo_humanoid_sdk/common/logger.py +45 -0
  4. kuavo_humanoid_sdk/common/websocket_kuavo_sdk.py +26 -0
  5. kuavo_humanoid_sdk/interfaces/__init__.py +4 -0
  6. kuavo_humanoid_sdk/interfaces/data_types.py +293 -0
  7. kuavo_humanoid_sdk/interfaces/end_effector.py +62 -0
  8. kuavo_humanoid_sdk/interfaces/robot.py +22 -0
  9. kuavo_humanoid_sdk/interfaces/robot_info.py +56 -0
  10. kuavo_humanoid_sdk/kuavo/__init__.py +12 -0
  11. kuavo_humanoid_sdk/kuavo/core/audio.py +32 -0
  12. kuavo_humanoid_sdk/kuavo/core/core.py +755 -0
  13. kuavo_humanoid_sdk/kuavo/core/dex_hand_control.py +114 -0
  14. kuavo_humanoid_sdk/kuavo/core/leju_claw_control.py +67 -0
  15. kuavo_humanoid_sdk/kuavo/core/ros/audio.py +86 -0
  16. kuavo_humanoid_sdk/kuavo/core/ros/control.py +1325 -0
  17. kuavo_humanoid_sdk/kuavo/core/ros/observation.py +125 -0
  18. kuavo_humanoid_sdk/kuavo/core/ros/param.py +335 -0
  19. kuavo_humanoid_sdk/kuavo/core/ros/sat_utils.py +103 -0
  20. kuavo_humanoid_sdk/kuavo/core/ros/state.py +426 -0
  21. kuavo_humanoid_sdk/kuavo/core/ros/tools.py +197 -0
  22. kuavo_humanoid_sdk/kuavo/core/ros/vision.py +280 -0
  23. kuavo_humanoid_sdk/kuavo/core/ros_env.py +236 -0
  24. kuavo_humanoid_sdk/kuavo/dexterous_hand.py +198 -0
  25. kuavo_humanoid_sdk/kuavo/leju_claw.py +232 -0
  26. kuavo_humanoid_sdk/kuavo/robot.py +550 -0
  27. kuavo_humanoid_sdk/kuavo/robot_arm.py +235 -0
  28. kuavo_humanoid_sdk/kuavo/robot_audio.py +39 -0
  29. kuavo_humanoid_sdk/kuavo/robot_head.py +39 -0
  30. kuavo_humanoid_sdk/kuavo/robot_info.py +235 -0
  31. kuavo_humanoid_sdk/kuavo/robot_observation.py +69 -0
  32. kuavo_humanoid_sdk/kuavo/robot_state.py +346 -0
  33. kuavo_humanoid_sdk/kuavo/robot_tool.py +58 -0
  34. kuavo_humanoid_sdk/kuavo/robot_vision.py +82 -0
  35. kuavo_humanoid_sdk/kuavo/robot_waist.py +53 -0
  36. kuavo_humanoid_sdk/kuavo_strategy/__init__.py +2 -0
  37. kuavo_humanoid_sdk/kuavo_strategy/grasp_box/grasp_box_strategy.py +418 -0
  38. kuavo_humanoid_sdk/kuavo_strategy/kuavo_strategy.py +63 -0
  39. kuavo_humanoid_sdk/msg/__init__.py +4 -0
  40. kuavo_humanoid_sdk_ws-1.3.2.dist-info/METADATA +276 -0
  41. kuavo_humanoid_sdk_ws-1.3.2.dist-info/RECORD +43 -0
  42. kuavo_humanoid_sdk_ws-1.3.2.dist-info/WHEEL +6 -0
  43. kuavo_humanoid_sdk_ws-1.3.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,280 @@
1
+ #! /usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ import copy
5
+ import time
6
+ import numpy as np
7
+ from collections import deque
8
+ from typing import Tuple, Optional
9
+ from transforms3d import quaternions, euler
10
+
11
+ from kuavo_humanoid_sdk.kuavo.core.ros.param import make_robot_param, EndEffectorType
12
+ from kuavo_humanoid_sdk.common.logger import SDKLogger
13
+ from kuavo_humanoid_sdk.interfaces.data_types import (AprilTagData, AprilTagDetection)
14
+ import roslibpy
15
+ from kuavo_humanoid_sdk.common.websocket_kuavo_sdk import WebSocketKuavoSDK
16
+
17
+ class KuavoRobotVisionCoreWebsocket:
18
+ """Handles vision-related data processing for Kuavo humanoid robot.
19
+
20
+ Attributes:
21
+ tf_client (roslibpy.Topic): TF client for coordinate transformations
22
+ tf_publisher (roslibpy.Topic): TF publisher for broadcasting transforms
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initializes vision system components including TF and AprilTag subscribers."""
27
+ if not hasattr(self, '_initialized'):
28
+ # Initialize WebSocket connection
29
+ self.websocket = WebSocketKuavoSDK()
30
+
31
+ # Initialize AprilTag subscribers
32
+ self._apriltag_data_camera_sub = roslibpy.Topic(
33
+ self.websocket.client,
34
+ '/tag_detections',
35
+ 'apriltag_ros/AprilTagDetectionArray'
36
+ )
37
+ self._apriltag_data_camera_sub.subscribe(self._apriltag_data_callback_camera)
38
+
39
+ self._apriltag_data_base_sub = roslibpy.Topic(
40
+ self.websocket.client,
41
+ '/robot_tag_info',
42
+ 'apriltag_ros/AprilTagDetectionArray'
43
+ )
44
+ self._apriltag_data_base_sub.subscribe(self._apriltag_data_callback_base)
45
+
46
+ self._apriltag_data_odom_sub = roslibpy.Topic(
47
+ self.websocket.client,
48
+ '/robot_tag_info_odom',
49
+ 'apriltag_ros/AprilTagDetectionArray'
50
+ )
51
+ self._apriltag_data_odom_sub.subscribe(self._apriltag_data_callback_odom)
52
+
53
+ # Initialize data structures
54
+ self._apriltag_data_from_camera = AprilTagData(
55
+ id = [],
56
+ size = [],
57
+ pose = []
58
+ )
59
+
60
+ self._apriltag_data_from_base = AprilTagData(
61
+ id = [],
62
+ size = [],
63
+ pose = []
64
+ )
65
+
66
+ self._apriltag_data_from_odom = AprilTagData(
67
+ id = [],
68
+ size = [],
69
+ pose = []
70
+ )
71
+
72
+ self._initialized = True
73
+
74
+ def _tf_callback(self, msg):
75
+ """Callback for TF messages.
76
+
77
+ Args:
78
+ msg: TF message containing transforms
79
+ """
80
+ for transform in msg['transforms']:
81
+ key = (transform['header']['frame_id'], transform['child_frame_id'])
82
+ self._transforms[key] = transform
83
+
84
+ def _apriltag_data_callback_camera(self, data):
85
+ """Callback for processing AprilTag detections from camera.
86
+
87
+ Args:
88
+ data (dict): Raw detection data from camera
89
+ """
90
+ # 清空之前的数据
91
+ self._apriltag_data_from_camera.id = []
92
+ self._apriltag_data_from_camera.size = []
93
+ self._apriltag_data_from_camera.pose = []
94
+
95
+ # 处理每个标签检测
96
+ for detection in data['detections']:
97
+ # 添加ID
98
+ for id in detection['id']:
99
+ self._apriltag_data_from_camera.id.append(id)
100
+
101
+ # 添加尺寸
102
+ if detection.get('size') and len(detection['size']) > 0:
103
+ self._apriltag_data_from_camera.size.append(detection['size'][0])
104
+ else:
105
+ self._apriltag_data_from_camera.size.append(0.0)
106
+
107
+
108
+ # Convert pose dict to AprilTagDetection
109
+ pose_dict = detection['pose']['pose']['pose']
110
+
111
+ tag_detection = AprilTagDetection(
112
+ position=AprilTagDetection.Point(
113
+ x=float(pose_dict['position']['x']),
114
+ y=float(pose_dict['position']['y']),
115
+ z=float(pose_dict['position']['z'])
116
+ ),
117
+ orientation=AprilTagDetection.Quaternion(
118
+ x=float(pose_dict['orientation']['x']),
119
+ y=float(pose_dict['orientation']['y']),
120
+ z=float(pose_dict['orientation']['z']),
121
+ w=float(pose_dict['orientation']['w'])
122
+ ))
123
+ # 添加姿态
124
+ self._apriltag_data_from_camera.pose.append(tag_detection)
125
+
126
+ def _apriltag_data_callback_base(self, data):
127
+ """Callback for processing AprilTag detections from base link.
128
+
129
+ Args:
130
+ data (dict): Raw detection data from base frame
131
+ """
132
+ # 清空之前的数据
133
+ self._apriltag_data_from_base.id = []
134
+ self._apriltag_data_from_base.size = []
135
+ self._apriltag_data_from_base.pose = []
136
+
137
+ # 处理每个标签检测
138
+ for detection in data['detections']:
139
+ # 添加ID
140
+ for id in detection['id']:
141
+ self._apriltag_data_from_base.id.append(id)
142
+
143
+ # 添加尺寸
144
+ if detection.get('size') and len(detection['size']) > 0:
145
+ self._apriltag_data_from_base.size.append(detection['size'][0])
146
+ else:
147
+ self._apriltag_data_from_base.size.append(0.0)
148
+
149
+ # Convert pose dict to AprilTagDetection
150
+ pose_dict = detection['pose']['pose']['pose']
151
+
152
+ tag_detection = AprilTagDetection(
153
+ position=AprilTagDetection.Point(
154
+ x=float(pose_dict['position']['x']),
155
+ y=float(pose_dict['position']['y']),
156
+ z=float(pose_dict['position']['z'])
157
+ ),
158
+ orientation=AprilTagDetection.Quaternion(
159
+ x=float(pose_dict['orientation']['x']),
160
+ y=float(pose_dict['orientation']['y']),
161
+ z=float(pose_dict['orientation']['z']),
162
+ w=float(pose_dict['orientation']['w'])
163
+ ))
164
+
165
+ # 添加姿态
166
+ self._apriltag_data_from_base.pose.append(tag_detection)
167
+
168
+ def _apriltag_data_callback_odom(self, data):
169
+ """Callback for processing AprilTag detections from odom frame.
170
+
171
+ Args:
172
+ data (dict): Raw detection data from odom frame
173
+ """
174
+ # 清空之前的数据
175
+ self._apriltag_data_from_odom.id = []
176
+ self._apriltag_data_from_odom.size = []
177
+ self._apriltag_data_from_odom.pose = []
178
+
179
+ # 处理每个标签检测
180
+ for detection in data['detections']:
181
+ # 添加ID
182
+ for id in detection['id']:
183
+ self._apriltag_data_from_odom.id.append(id)
184
+
185
+ # 添加尺寸
186
+ if detection.get('size') and len(detection['size']) > 0:
187
+ self._apriltag_data_from_odom.size.append(detection['size'][0])
188
+ else:
189
+ self._apriltag_data_from_odom.size.append(0.0)
190
+
191
+ # Convert pose dict to AprilTagDetection
192
+ pose_dict = detection['pose']['pose']['pose']
193
+
194
+ tag_detection = AprilTagDetection(
195
+ position=AprilTagDetection.Point(
196
+ x=float(pose_dict['position']['x']),
197
+ y=float(pose_dict['position']['y']),
198
+ z=float(pose_dict['position']['z'])
199
+ ),
200
+ orientation=AprilTagDetection.Quaternion(
201
+ x=float(pose_dict['orientation']['x']),
202
+ y=float(pose_dict['orientation']['y']),
203
+ z=float(pose_dict['orientation']['z']),
204
+ w=float(pose_dict['orientation']['w'])
205
+ ))
206
+
207
+ # 添加姿态
208
+ self._apriltag_data_from_odom.pose.append(tag_detection)
209
+
210
+ @property
211
+ def apriltag_data_from_camera(self) -> AprilTagData:
212
+ """AprilTag detection data in camera coordinate frame.
213
+
214
+ Returns:
215
+ AprilTagData: Contains lists of tag IDs, sizes and poses
216
+ """
217
+ return self._apriltag_data_from_camera
218
+
219
+ @property
220
+ def apriltag_data_from_base(self) -> AprilTagData:
221
+ """AprilTag detection data in base_link coordinate frame.
222
+
223
+ Returns:
224
+ AprilTagData: Contains lists of tag IDs, sizes and poses
225
+ """
226
+ return self._apriltag_data_from_base
227
+
228
+ @property
229
+ def apriltag_data_from_odom(self) -> AprilTagData:
230
+ """AprilTag detection data in odom coordinate frame.
231
+
232
+ Returns:
233
+ AprilTagData: Contains lists of tag IDs, sizes and transformed poses
234
+ """
235
+ return self._apriltag_data_from_odom
236
+
237
+ def _get_data_by_id(self, target_id: int, data_source: str = "base") -> Optional[dict]:
238
+ """Retrieves AprilTag data by specific ID from selected source.
239
+
240
+ Args:
241
+ target_id (int): AprilTag ID to search for
242
+ data_source (str): Data source selector, valid options:
243
+ "camera", "base", "odom"
244
+
245
+ Returns:
246
+ Optional[dict]: Dictionary containing 'sizes' and 'poses' lists if found,
247
+ None if no matching data
248
+ """
249
+ data_map = {
250
+ "camera": self._apriltag_data_from_camera,
251
+ "base": self._apriltag_data_from_base,
252
+ "odom": self._apriltag_data_from_odom
253
+ }
254
+
255
+ if data_source not in data_map:
256
+ raise ValueError(f"Invalid data source: {data_source}")
257
+
258
+ data = data_map[data_source]
259
+
260
+ # 查找匹配的ID
261
+ try:
262
+ idx = data.id.index(target_id)
263
+ return {
264
+ 'sizes': [data.size[idx]],
265
+ 'poses': [data.pose[idx]]
266
+ }
267
+ except ValueError:
268
+ return None
269
+
270
+ # if __name__ == "__main__":
271
+
272
+ # kuavo_robot_vision_core = KuavoRobotVisionCoreWebsocket()
273
+ # time.sleep(5)
274
+ # print("apriltag_data_from_camera:")
275
+ # print(kuavo_robot_vision_core.apriltag_data_from_camera)
276
+ # print("apriltag_data_from_base:")
277
+ # print(kuavo_robot_vision_core.apriltag_data_from_base)
278
+ # print("apriltag_data_from_odom:")
279
+ # print(kuavo_robot_vision_core.apriltag_data_from_odom)
280
+ # rospy.spin()
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ import os
5
+ import subprocess
6
+ import atexit
7
+ from kuavo_humanoid_sdk.common.logger import SDKLogger
8
+ from kuavo_humanoid_sdk.common.websocket_kuavo_sdk import WebSocketKuavoSDK
9
+ import roslibpy
10
+
11
+
12
+ class KuavoROSEnvWebsocket:
13
+ _instance = None
14
+ _processes = [] # Store all subprocess instances
15
+
16
+ def __new__(cls):
17
+ if cls._instance is None:
18
+ cls._instance = super(KuavoROSEnvWebsocket, cls).__new__(cls)
19
+ cls._instance._initialized = False
20
+ cls._instance._init_success = False # Add flag to track successful Init() call
21
+ # Register cleanup handler
22
+ atexit.register(cls._cleanup_processes)
23
+ return cls._instance
24
+
25
+ def __init__(self):
26
+ if not self._initialized:
27
+ self._initialized = True
28
+ self.websocket = None
29
+
30
+ @classmethod
31
+ def _cleanup_processes(cls):
32
+ """Cleanup all registered processes on exit"""
33
+ for process in cls._processes:
34
+ if process.poll() is None: # Process is still running
35
+ process.terminate()
36
+ try:
37
+ process.wait(timeout=3) # Wait for process to terminate
38
+ except subprocess.TimeoutExpired:
39
+ process.kill() # Force kill if not terminated
40
+ cls._processes.clear()
41
+
42
+ def _get_kuavo_ws_root(self) -> str:
43
+ # For WebSocket version, we'll use environment variable instead of ROS param
44
+ model_path = os.environ.get('KUAVO_MODEL_PATH')
45
+ if not model_path:
46
+ raise Exception("KUAVO_MODEL_PATH environment variable not found")
47
+
48
+ if not os.path.exists(model_path):
49
+ raise Exception(f"Model path {model_path} not found")
50
+
51
+ # ws
52
+ return model_path.replace('/src/kuavo_assets/models', '')
53
+
54
+ def Init(self) -> bool:
55
+ """
56
+ Initialize the WebSocket environment.
57
+ Raises:
58
+ Exception: If the KUAVO_MODEL_PATH environment variable is not found or the path is not valid.
59
+ """
60
+ # if generate docs, skip init.
61
+ if 'GEN_KUAVO_HUMANOID_SDK_DOCS' in os.environ:
62
+ return True
63
+
64
+ # Return directly if already initialized successfully
65
+ if self._init_success:
66
+ return True
67
+
68
+ # Check if WebSocket server is running
69
+ try:
70
+
71
+ self.websocket = WebSocketKuavoSDK()
72
+ if not self.websocket.client.is_connected:
73
+ print(f"\033[31m\nError: Can't connect to WebSocket server. Please ensure the server is running.\033[0m"
74
+ "\nMaybe manually launch the app first?"
75
+ "\n - for example(sim): roslaunch humanoid_controller load_kuavo_mujoco_sim.launch, "
76
+ "\n - for example(real): roslaunch humanoid_controller load_kuavo_real.launch\n")
77
+ exit(1)
78
+ except Exception as e:
79
+ print(f"\033[31m\nError: Failed to connect to WebSocket server: {e}\033[0m")
80
+ exit(1)
81
+
82
+ # Only check nodes exist when Init SDK, if not, tips user manually launch nodes.
83
+ #
84
+ # NOTE: 轮臂(robot_type==1)不依赖双足步态切换节点 humanoid_gait_switch_by_name
85
+ robot_type = int(os.environ.get('ROBOT_TYPE', '0'))
86
+ deps_nodes = [] if robot_type == 1 else ['/humanoid_gait_switch_by_name']
87
+ for node in deps_nodes:
88
+ if not self.check_rosnode_exists(node):
89
+ print(f"\033[31m\nError: Node {node} not found. Please launch it manually.\033[0m")
90
+ exit(1)
91
+
92
+ self._init_success = True # Set flag after successful initialization
93
+
94
+ return True
95
+
96
+ def _launch_ros_node(self, node_name, launch_cmd, log_name):
97
+ """Launch a ROS node with the given command and log the output.
98
+
99
+ Args:
100
+ node_name (str): Name of the node to launch
101
+ launch_cmd (str): Full launch command including source and roslaunch
102
+ log_name (str): Name for the log file
103
+
104
+ Raises:
105
+ Exception: If node launch fails
106
+ """
107
+ # Launch in background and check if successful
108
+ try:
109
+ os.makedirs('/var/log/kuavo_humanoid_sdk', exist_ok=True)
110
+ log_path = f'/var/log/kuavo_humanoid_sdk/{log_name}.log'
111
+ except PermissionError:
112
+ os.makedirs('log/kuavo_humanoid_sdk', exist_ok=True)
113
+ log_path = f'log/kuavo_humanoid_sdk/{log_name}.log'
114
+
115
+ with open(log_path, 'w') as log_file:
116
+ process = subprocess.Popen(launch_cmd, shell=True, executable='/bin/bash', stdout=log_file, stderr=log_file)
117
+ self._processes.append(process) # Add process to tracking list
118
+
119
+ if process.returncode is not None and process.returncode != 0:
120
+ raise Exception(f"Failed to launch {node_name}, return code: {process.returncode}")
121
+
122
+ SDKLogger.info(f"{node_name} launched successfully")
123
+
124
+ def _get_setup_file(self, ws_root=None):
125
+ """Get the appropriate ROS setup file path based on shell type.
126
+
127
+ Args:
128
+ ws_root (str, optional): ROS workspace root path. If None, uses ROS_WORKSPACE.
129
+
130
+ Returns:
131
+ str: Path to the setup file
132
+
133
+ Raises:
134
+ Exception: If setup file not found
135
+ """
136
+ is_zsh = 'zsh' in os.environ.get('SHELL', '')
137
+
138
+ if ws_root is None:
139
+ ws_root = os.environ['ROS_WORKSPACE']
140
+
141
+ setup_files = {
142
+ 'zsh': os.path.join(ws_root, 'devel/setup.zsh'),
143
+ 'bash': os.path.join(ws_root, 'devel/setup.bash')
144
+ }
145
+
146
+ setup_file = setup_files['zsh'] if is_zsh else setup_files['bash']
147
+ if not os.path.exists(setup_file):
148
+ setup_file = setup_file.replace('devel', 'install')
149
+ if not os.path.exists(setup_file):
150
+ raise Exception(f"Setup file not found in either devel or install: {setup_file}")
151
+
152
+ return setup_file
153
+
154
+ def launch_ik_node(self):
155
+ # nodes: /arms_ik_node
156
+ # services: /ik/two_arm_hand_pose_cmd_srv, /ik/fk_srv
157
+ try:
158
+ if not self.websocket or not self.websocket.client.is_connected:
159
+ raise Exception("WebSocket server is not running")
160
+ except Exception as e:
161
+ raise Exception(f"WebSocket server is not running: {e}")
162
+
163
+ # Check if IK node and services exist
164
+ try:
165
+ # Check if arms_ik_node is running using roslibpy
166
+ service = roslibpy.Service(self.websocket.client, '/rosnode/list', 'rosapi/Nodes')
167
+ response = service.call({})
168
+ nodes = response.get('nodes', [])
169
+
170
+ if '/arms_ik_node' not in nodes:
171
+ # Launch IK node if not running
172
+ kuavo_ws_root = self._get_kuavo_ws_root()
173
+ setup_file = self._get_setup_file(kuavo_ws_root)
174
+ source_cmd = f"source {setup_file}"
175
+
176
+ # Get robot version from environment variable
177
+ robot_version = os.environ.get('ROBOT_VERSION')
178
+ if robot_version is None:
179
+ raise Exception("Failed to get ROBOT_VERSION from environment variables")
180
+
181
+ # Launch IK node
182
+ launch_cmd = f"roslaunch motion_capture_ik ik_node.launch robot_version:={robot_version}"
183
+ full_cmd = f"{source_cmd} && {launch_cmd}"
184
+
185
+ self._launch_ros_node('IK node', full_cmd, 'launch_ik')
186
+
187
+ return True
188
+
189
+ except Exception as e:
190
+ raise Exception(f"Failed to verify IK node and services: {e}")
191
+
192
+
193
+ def launch_gait_switch_node(self)-> bool:
194
+ """Verify that the gait switch node is running, launch if not."""
195
+ try:
196
+ # Check if node exists using roslibpy
197
+ service = roslibpy.Service(self.websocket.client, '/rosnode/list', 'rosapi/Nodes')
198
+ response = service.call({})
199
+ nodes = response.get('nodes', [])
200
+
201
+ if '/humanoid_gait_switch_by_name' not in nodes:
202
+ kuavo_ws_root = self._get_kuavo_ws_root()
203
+ setup_file = self._get_setup_file(kuavo_ws_root)
204
+ source_cmd = f"source {setup_file}"
205
+
206
+ # Launch gait switch node
207
+ launch_cmd = "roslaunch humanoid_interface_ros humanoid_switch_gait.launch"
208
+ full_cmd = f"{source_cmd} && {launch_cmd}"
209
+
210
+ self._launch_ros_node('Gait switch node', full_cmd, 'launch_gait_switch')
211
+
212
+ except Exception as e:
213
+ raise Exception(f"Failed to launch gait_switch node: {e}")
214
+
215
+ @staticmethod
216
+ def check_rosnode_exists(node_name):
217
+ """Check if a ROS node is running using roslibpy.
218
+
219
+ Args:
220
+ node_name (str): Name of the node to check
221
+
222
+ Returns:
223
+ bool: True if node is running, False otherwise
224
+ """
225
+ websocket = WebSocketKuavoSDK()
226
+ try:
227
+ if not websocket or not websocket.client.is_connected:
228
+ return False
229
+
230
+ service = roslibpy.Service(websocket.client, '/rosapi/nodes', 'rosapi/Nodes')
231
+ response = service.call({})
232
+ nodes = response.get('nodes', [])
233
+ return node_name in nodes
234
+ except Exception as e:
235
+ SDKLogger.error(f"Error checking if node {node_name} exists: {e}")
236
+ return False