kuavo-humanoid-sdk 1.1.6b1125__20250610230424-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.

Potentially problematic release.


This version of kuavo-humanoid-sdk might be problematic. Click here for more details.

Files changed (132) hide show
  1. kuavo_humanoid_sdk/__init__.py +6 -0
  2. kuavo_humanoid_sdk/common/logger.py +42 -0
  3. kuavo_humanoid_sdk/interfaces/__init__.py +4 -0
  4. kuavo_humanoid_sdk/interfaces/data_types.py +276 -0
  5. kuavo_humanoid_sdk/interfaces/end_effector.py +62 -0
  6. kuavo_humanoid_sdk/interfaces/robot.py +22 -0
  7. kuavo_humanoid_sdk/interfaces/robot_info.py +56 -0
  8. kuavo_humanoid_sdk/kuavo/__init__.py +11 -0
  9. kuavo_humanoid_sdk/kuavo/core/audio.py +32 -0
  10. kuavo_humanoid_sdk/kuavo/core/core.py +617 -0
  11. kuavo_humanoid_sdk/kuavo/core/dex_hand_control.py +114 -0
  12. kuavo_humanoid_sdk/kuavo/core/leju_claw_control.py +67 -0
  13. kuavo_humanoid_sdk/kuavo/core/ros/audio.py +92 -0
  14. kuavo_humanoid_sdk/kuavo/core/ros/control.py +1309 -0
  15. kuavo_humanoid_sdk/kuavo/core/ros/observation.py +94 -0
  16. kuavo_humanoid_sdk/kuavo/core/ros/param.py +183 -0
  17. kuavo_humanoid_sdk/kuavo/core/ros/sat_utils.py +103 -0
  18. kuavo_humanoid_sdk/kuavo/core/ros/state.py +606 -0
  19. kuavo_humanoid_sdk/kuavo/core/ros/tools.py +219 -0
  20. kuavo_humanoid_sdk/kuavo/core/ros/vision.py +234 -0
  21. kuavo_humanoid_sdk/kuavo/core/ros_env.py +460 -0
  22. kuavo_humanoid_sdk/kuavo/dexterous_hand.py +201 -0
  23. kuavo_humanoid_sdk/kuavo/leju_claw.py +235 -0
  24. kuavo_humanoid_sdk/kuavo/robot.py +464 -0
  25. kuavo_humanoid_sdk/kuavo/robot_arm.py +210 -0
  26. kuavo_humanoid_sdk/kuavo/robot_audio.py +39 -0
  27. kuavo_humanoid_sdk/kuavo/robot_head.py +50 -0
  28. kuavo_humanoid_sdk/kuavo/robot_info.py +113 -0
  29. kuavo_humanoid_sdk/kuavo/robot_observation.py +64 -0
  30. kuavo_humanoid_sdk/kuavo/robot_state.py +299 -0
  31. kuavo_humanoid_sdk/kuavo/robot_tool.py +82 -0
  32. kuavo_humanoid_sdk/kuavo/robot_vision.py +83 -0
  33. kuavo_humanoid_sdk/kuavo_strategy/__init__.py +2 -0
  34. kuavo_humanoid_sdk/kuavo_strategy/grasp_box/grasp_box_strategy.py +1126 -0
  35. kuavo_humanoid_sdk/kuavo_strategy/kuavo_strategy.py +104 -0
  36. kuavo_humanoid_sdk/msg/__init__.py +4 -0
  37. kuavo_humanoid_sdk/msg/kuavo_msgs/__init__.py +7 -0
  38. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_AprilTagDetection.py +306 -0
  39. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_AprilTagDetectionArray.py +437 -0
  40. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_Metadata.py +199 -0
  41. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_RobotActionState.py +112 -0
  42. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_TFArray.py +323 -0
  43. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/__init__.py +44 -0
  44. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_armHandPose.py +160 -0
  45. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_armPoseWithTimeStamp.py +168 -0
  46. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_armTargetPoses.py +151 -0
  47. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_bezierCurveCubicPoint.py +178 -0
  48. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_dexhandCommand.py +229 -0
  49. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_dexhandTouchState.py +256 -0
  50. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_endEffectorData.py +227 -0
  51. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_footPose.py +123 -0
  52. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_footPoseTargetTrajectories.py +301 -0
  53. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_footPoses.py +149 -0
  54. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_fullBodyTargetTrajectories.py +258 -0
  55. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_gaitTimeName.py +147 -0
  56. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_gestureInfo.py +218 -0
  57. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_gestureTask.py +149 -0
  58. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_handPose.py +136 -0
  59. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_headBodyPose.py +145 -0
  60. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_ikSolveError.py +171 -0
  61. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_ikSolveParam.py +140 -0
  62. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_imuData.py +165 -0
  63. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_jointBezierTrajectory.py +201 -0
  64. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_jointCmd.py +390 -0
  65. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_jointData.py +205 -0
  66. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_lejuClawCommand.py +320 -0
  67. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_lejuClawState.py +341 -0
  68. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_motorParam.py +122 -0
  69. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_planArmState.py +120 -0
  70. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_questJoySticks.py +191 -0
  71. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_qv.py +121 -0
  72. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_robotArmQVVD.py +177 -0
  73. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_robotHandPosition.py +225 -0
  74. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_robotHeadMotionData.py +128 -0
  75. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_robotState.py +222 -0
  76. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_sensorsData.py +495 -0
  77. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_switchGaitByName.py +200 -0
  78. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_touchSensorStatus.py +162 -0
  79. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_twoArmHandPose.py +272 -0
  80. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_twoArmHandPoseCmd.py +315 -0
  81. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_yoloDetection.py +251 -0
  82. kuavo_humanoid_sdk/msg/kuavo_msgs/msg/_yoloOutputData.py +168 -0
  83. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_ExecuteArmAction.py +281 -0
  84. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_RepublishTFs.py +373 -0
  85. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_SetJoyTopic.py +282 -0
  86. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_SpeechSynthesis.py +270 -0
  87. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/__init__.py +28 -0
  88. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_changeArmCtrlMode.py +275 -0
  89. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_changeArmCtrlModeKuavo.py +236 -0
  90. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_changeMotorParam.py +299 -0
  91. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_changeTorsoCtrlMode.py +274 -0
  92. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_controlLejuClaw.py +408 -0
  93. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_enableHandTouchSensor.py +304 -0
  94. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_fkSrv.py +394 -0
  95. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_footPoseTargetTrajectoriesSrv.py +409 -0
  96. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_gestureExecute.py +339 -0
  97. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_gestureExecuteState.py +257 -0
  98. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_gestureList.py +418 -0
  99. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_getCurrentGaitName.py +253 -0
  100. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_getMotorParam.py +299 -0
  101. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_handForceLevel.py +330 -0
  102. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_jointMoveTo.py +302 -0
  103. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_planArmTrajectoryBezierCurve.py +421 -0
  104. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_planArmTrajectoryCubicSpline.py +490 -0
  105. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_playmusic.py +268 -0
  106. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_setHwIntialState.py +304 -0
  107. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_setMmCtrlFrame.py +273 -0
  108. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_setMotorEncoderRoundService.py +283 -0
  109. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_setTagId.py +275 -0
  110. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_singleStepControl.py +444 -0
  111. kuavo_humanoid_sdk/msg/kuavo_msgs/srv/_twoArmHandPoseCmdSrv.py +662 -0
  112. kuavo_humanoid_sdk/msg/motion_capture_ik/__init__.py +7 -0
  113. kuavo_humanoid_sdk/msg/ocs2_msgs/__init__.py +7 -0
  114. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/__init__.py +12 -0
  115. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_constraint.py +142 -0
  116. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_controller_data.py +121 -0
  117. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_lagrangian_metrics.py +148 -0
  118. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mode_schedule.py +150 -0
  119. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_flattened_controller.py +666 -0
  120. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_input.py +122 -0
  121. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_observation.py +209 -0
  122. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_performance_indices.py +140 -0
  123. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_solver_data.py +886 -0
  124. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_state.py +122 -0
  125. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_mpc_target_trajectories.py +239 -0
  126. kuavo_humanoid_sdk/msg/ocs2_msgs/msg/_multiplier.py +148 -0
  127. kuavo_humanoid_sdk/msg/ocs2_msgs/srv/__init__.py +1 -0
  128. kuavo_humanoid_sdk/msg/ocs2_msgs/srv/_reset.py +376 -0
  129. kuavo_humanoid_sdk-1.1.6b1125.dist-info/METADATA +291 -0
  130. kuavo_humanoid_sdk-1.1.6b1125.dist-info/RECORD +132 -0
  131. kuavo_humanoid_sdk-1.1.6b1125.dist-info/WHEEL +6 -0
  132. kuavo_humanoid_sdk-1.1.6b1125.dist-info/top_level.txt +1 -0
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+
4
+ import os
5
+ try:
6
+ import rospy
7
+ except ImportError:
8
+ pass
9
+ import subprocess
10
+ import atexit
11
+ from kuavo_humanoid_sdk.common.logger import SDKLogger
12
+
13
+ class KuavoROSEnv:
14
+ _instance = None
15
+ _processes = [] # Store all subprocess instances
16
+
17
+ def __new__(cls):
18
+ if cls._instance is None:
19
+ cls._instance = super(KuavoROSEnv, cls).__new__(cls)
20
+ cls._instance._initialized = False
21
+ cls._instance._init_success = False # Add flag to track successful Init() call
22
+ # Register cleanup handler
23
+ atexit.register(cls._cleanup_processes)
24
+ return cls._instance
25
+
26
+ def __init__(self):
27
+ if not self._initialized:
28
+ self._initialized = True
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
+ if not rospy.has_param('/modelPath'):
44
+ raise Exception("modelPath parameter not found")
45
+ model_path = rospy.get_param('/modelPath')
46
+
47
+ if not os.path.exists(model_path):
48
+ raise Exception(f"modelPath f{model_path} not found")
49
+ # ws
50
+ return model_path.replace('/src/kuavo_assets/models', '')
51
+
52
+ def Init(self) -> bool:
53
+ """
54
+ Initialize the ROS environment.
55
+ Raises:
56
+ Exception: If the modelPath parameter is not found or the modelPath parameter is not a valid path.
57
+ """
58
+ # if generate docs, skip init.
59
+ if 'GEN_KUAVO_HUMANOID_SDK_DOCS' in os.environ:
60
+ return True
61
+
62
+ # Return directly if already initialized successfully
63
+ if self._init_success:
64
+ return True
65
+
66
+ # Check if ROS master is running
67
+ try:
68
+ rospy.get_master().getPid()
69
+ except Exception as e:
70
+ print(f"\033[31m\nError:Can't connect to ros master, Please start roscore first or check ROS_MASTER_URI.\nException:{e}\033[0m"
71
+ "\nMaybe manually launch the app first?"
72
+ "\n - for example(sim): roslaunch humanoid_controller load_kuavo_mujoco_sim.launch, "
73
+ "\n - for example(real): roslaunch humanoid_controller load_kuavo_real.launch\n")
74
+ exit(1)
75
+
76
+ """
77
+ # NOTE: We add kuavo_msgs/motion_capture_ik package in SDK integration.
78
+ kuavo_ws_root = self._get_kuavo_ws_root()
79
+ # Add kuavo_msgs package to Python path
80
+ dist_path = os.path.join(kuavo_ws_root, 'devel/lib/python3/dist-packages')
81
+ if not os.path.exists(dist_path):
82
+ dist_path = os.path.join(kuavo_ws_root, 'install/lib/python3/dist-packages')
83
+ if not os.path.exists(dist_path):
84
+ raise Exception(f"{dist_path} package not found in Python path")
85
+ if dist_path not in os.sys.path:
86
+ os.sys.path.append(dist_path)
87
+ """
88
+
89
+ # Initialize the ROS node
90
+ if not rospy.get_node_uri():
91
+ rospy.init_node(f'kuavo_sdk_node', anonymous=True, disable_signals=True)
92
+
93
+ # Only check nodes exist when Init SDK, if not, tips user manually launch nodes.
94
+ # self.launch_ik_node()
95
+ # self.launch_gait_switch_node()
96
+ deps_nodes = ['/humanoid_gait_switch_by_name']
97
+ for node in deps_nodes:
98
+ if not KuavoROSEnv.check_rosnode_exists(node):
99
+ print(f"\033[31m\nError:Node {node} not found. Please launch it manuallly.\033[0m")
100
+ exit(1)
101
+
102
+ self._init_success = True # Set flag after successful initialization
103
+
104
+ return True
105
+
106
+ def _launch_ros_node(self, node_name, launch_cmd, log_name):
107
+ """Launch a ROS node with the given command and log the output.
108
+
109
+ Args:
110
+ node_name (str): Name of the node to launch
111
+ launch_cmd (str): Full launch command including source and roslaunch
112
+ log_name (str): Name for the log file
113
+
114
+ Raises:
115
+ Exception: If node launch fails
116
+ """
117
+ # Launch in background and check if successful
118
+ try:
119
+ os.makedirs('/var/log/kuavo_humanoid_sdk', exist_ok=True)
120
+ log_path = f'/var/log/kuavo_humanoid_sdk/{log_name}.log'
121
+ except PermissionError:
122
+ os.makedirs('log/kuavo_humanoid_sdk', exist_ok=True)
123
+ log_path = f'log/kuavo_humanoid_sdk/{log_name}.log'
124
+
125
+ with open(log_path, 'w') as log_file:
126
+ process = subprocess.Popen(launch_cmd, shell=True, executable='/bin/bash', stdout=log_file, stderr=log_file)
127
+ self._processes.append(process) # Add process to tracking list
128
+
129
+ if process.returncode is not None and process.returncode != 0:
130
+ raise Exception(f"Failed to launch {node_name}, return code: {process.returncode}")
131
+
132
+ SDKLogger.info(f"{node_name} launched successfully")
133
+
134
+ def _get_setup_file(self, ws_root=None):
135
+ """Get the appropriate ROS setup file path based on shell type.
136
+
137
+ Args:
138
+ ws_root (str, optional): ROS workspace root path. If None, uses ROS_WORKSPACE.
139
+
140
+ Returns:
141
+ str: Path to the setup file
142
+
143
+ Raises:
144
+ Exception: If setup file not found
145
+ """
146
+ is_zsh = 'zsh' in os.environ.get('SHELL', '')
147
+
148
+ if ws_root is None:
149
+ ws_root = os.environ['ROS_WORKSPACE']
150
+
151
+ setup_files = {
152
+ 'zsh': os.path.join(ws_root, 'devel/setup.zsh'),
153
+ 'bash': os.path.join(ws_root, 'devel/setup.bash')
154
+ }
155
+
156
+ setup_file = setup_files['zsh'] if is_zsh else setup_files['bash']
157
+ if not os.path.exists(setup_file):
158
+ setup_file = setup_file.replace('devel', 'install')
159
+ if not os.path.exists(setup_file):
160
+ raise Exception(f"Setup file not found in either devel or install: {setup_file}")
161
+
162
+ return setup_file
163
+
164
+ def launch_ik_node(self):
165
+ # nodes: /arms_ik_node
166
+ # services: /ik/two_arm_hand_pose_cmd_srv, /ik/fk_srv
167
+ try:
168
+ rospy.get_master().getPid()
169
+ except Exception as e:
170
+ raise Exception(f"ROS master is not running. Please start roscore first or check ROS_MASTER_URI, {e}")
171
+
172
+ # Check if IK node and services exist
173
+ try:
174
+ # Check if arms_ik_node is running
175
+ nodes = subprocess.check_output(['rosnode', 'list']).decode('utf-8').split('\n')
176
+ if '/arms_ik_node' not in nodes:
177
+ # Launch IK node if not running
178
+ kuavo_ws_root = self._get_kuavo_ws_root()
179
+ setup_file = self._get_setup_file(kuavo_ws_root)
180
+ source_cmd = f"source {setup_file}"
181
+
182
+ # Get robot version from rosparam
183
+ robot_version = rospy.get_param('/robot_version', None)
184
+ if robot_version is None:
185
+ raise Exception("Failed to get robot_version from rosparam")
186
+
187
+ # Launch IK node
188
+ launch_cmd = f"roslaunch motion_capture_ik ik_node.launch robot_version:={robot_version}"
189
+ full_cmd = f"{source_cmd} && {launch_cmd}"
190
+
191
+ self._launch_ros_node('IK node', full_cmd, 'launch_ik')
192
+
193
+ return True
194
+
195
+ except Exception as e:
196
+ raise Exception(f"Failed to verify IK node and services: {e}")
197
+
198
+
199
+ def launch_gait_switch_node(self)-> bool:
200
+ """Verify that the gait switch node is running, launch if not."""
201
+ try:
202
+ # Check if node exists
203
+ nodes = subprocess.check_output(['rosnode', 'list']).decode('utf-8').split('\n')
204
+ if '/humanoid_gait_switch_by_name' not in nodes:
205
+ kuavo_ws_root = self._get_kuavo_ws_root()
206
+ setup_file = self._get_setup_file(kuavo_ws_root)
207
+ source_cmd = f"source {setup_file}"
208
+
209
+ # Launch gait switch node
210
+ launch_cmd = "roslaunch humanoid_interface_ros humanoid_switch_gait.launch"
211
+ full_cmd = f"{source_cmd} && {launch_cmd}"
212
+
213
+ self._launch_ros_node('Gait switch node', full_cmd, 'launch_gait_switch')
214
+
215
+ except Exception as e:
216
+ raise Exception(f"Failed to launch gait_switch node: {e}")
217
+
218
+
219
+ @staticmethod
220
+ def check_rosnode_exists(node_name):
221
+ """Check if a ROS node is running.
222
+
223
+ Args:
224
+ node_name (str): Name of the node to check
225
+
226
+ Returns:
227
+ bool: True if node is running, False otherwise
228
+ """
229
+ try:
230
+ nodes = subprocess.check_output(['rosnode', 'list']).decode('utf-8').split('\n')
231
+ return node_name in nodes
232
+ except subprocess.CalledProcessError as e:
233
+ SDKLogger.error(f"Error checking if node {node_name} exists: {e}")
234
+ return False
235
+ except Exception as e:
236
+ SDKLogger.error(f"Error checking if node {node_name} exists: {e}")
237
+ return False
238
+
239
+ class KuavoROSEnvWebsocket:
240
+ _instance = None
241
+ _processes = [] # Store all subprocess instances
242
+
243
+ def __new__(cls):
244
+ if cls._instance is None:
245
+ cls._instance = super(KuavoROSEnvWebsocket, cls).__new__(cls)
246
+ cls._instance._initialized = False
247
+ cls._instance._init_success = False # Add flag to track successful Init() call
248
+ # Register cleanup handler
249
+ atexit.register(cls._cleanup_processes)
250
+ return cls._instance
251
+
252
+ def __init__(self):
253
+ if not self._initialized:
254
+ self._initialized = True
255
+ self.websocket = None
256
+
257
+ @classmethod
258
+ def _cleanup_processes(cls):
259
+ """Cleanup all registered processes on exit"""
260
+ for process in cls._processes:
261
+ if process.poll() is None: # Process is still running
262
+ process.terminate()
263
+ try:
264
+ process.wait(timeout=3) # Wait for process to terminate
265
+ except subprocess.TimeoutExpired:
266
+ process.kill() # Force kill if not terminated
267
+ cls._processes.clear()
268
+
269
+ def _get_kuavo_ws_root(self) -> str:
270
+ # For WebSocket version, we'll use environment variable instead of ROS param
271
+ model_path = os.environ.get('KUAVO_MODEL_PATH')
272
+ if not model_path:
273
+ raise Exception("KUAVO_MODEL_PATH environment variable not found")
274
+
275
+ if not os.path.exists(model_path):
276
+ raise Exception(f"Model path {model_path} not found")
277
+
278
+ # ws
279
+ return model_path.replace('/src/kuavo_assets/models', '')
280
+
281
+ def Init(self) -> bool:
282
+ """
283
+ Initialize the WebSocket environment.
284
+ Raises:
285
+ Exception: If the KUAVO_MODEL_PATH environment variable is not found or the path is not valid.
286
+ """
287
+ # if generate docs, skip init.
288
+ if 'GEN_KUAVO_HUMANOID_SDK_DOCS' in os.environ:
289
+ return True
290
+
291
+ # Return directly if already initialized successfully
292
+ if self._init_success:
293
+ return True
294
+
295
+ # Check if WebSocket server is running
296
+ try:
297
+
298
+ self.websocket = WebSocketKuavoSDK()
299
+ if not self.websocket.client.is_connected:
300
+ print(f"\033[31m\nError: Can't connect to WebSocket server. Please ensure the server is running.\033[0m"
301
+ "\nMaybe manually launch the app first?"
302
+ "\n - for example(sim): roslaunch humanoid_controller load_kuavo_mujoco_sim.launch, "
303
+ "\n - for example(real): roslaunch humanoid_controller load_kuavo_real.launch\n")
304
+ exit(1)
305
+ except Exception as e:
306
+ print(f"\033[31m\nError: Failed to connect to WebSocket server: {e}\033[0m")
307
+ exit(1)
308
+
309
+ # Only check nodes exist when Init SDK, if not, tips user manually launch nodes.
310
+ deps_nodes = ['/humanoid_gait_switch_by_name']
311
+ for node in deps_nodes:
312
+ if not self.check_rosnode_exists(node):
313
+ print(f"\033[31m\nError: Node {node} not found. Please launch it manually.\033[0m")
314
+ exit(1)
315
+
316
+ self._init_success = True # Set flag after successful initialization
317
+
318
+ return True
319
+
320
+ def _launch_ros_node(self, node_name, launch_cmd, log_name):
321
+ """Launch a ROS node with the given command and log the output.
322
+
323
+ Args:
324
+ node_name (str): Name of the node to launch
325
+ launch_cmd (str): Full launch command including source and roslaunch
326
+ log_name (str): Name for the log file
327
+
328
+ Raises:
329
+ Exception: If node launch fails
330
+ """
331
+ # Launch in background and check if successful
332
+ try:
333
+ os.makedirs('/var/log/kuavo_humanoid_sdk', exist_ok=True)
334
+ log_path = f'/var/log/kuavo_humanoid_sdk/{log_name}.log'
335
+ except PermissionError:
336
+ os.makedirs('log/kuavo_humanoid_sdk', exist_ok=True)
337
+ log_path = f'log/kuavo_humanoid_sdk/{log_name}.log'
338
+
339
+ with open(log_path, 'w') as log_file:
340
+ process = subprocess.Popen(launch_cmd, shell=True, executable='/bin/bash', stdout=log_file, stderr=log_file)
341
+ self._processes.append(process) # Add process to tracking list
342
+
343
+ if process.returncode is not None and process.returncode != 0:
344
+ raise Exception(f"Failed to launch {node_name}, return code: {process.returncode}")
345
+
346
+ SDKLogger.info(f"{node_name} launched successfully")
347
+
348
+ def _get_setup_file(self, ws_root=None):
349
+ """Get the appropriate ROS setup file path based on shell type.
350
+
351
+ Args:
352
+ ws_root (str, optional): ROS workspace root path. If None, uses ROS_WORKSPACE.
353
+
354
+ Returns:
355
+ str: Path to the setup file
356
+
357
+ Raises:
358
+ Exception: If setup file not found
359
+ """
360
+ is_zsh = 'zsh' in os.environ.get('SHELL', '')
361
+
362
+ if ws_root is None:
363
+ ws_root = os.environ['ROS_WORKSPACE']
364
+
365
+ setup_files = {
366
+ 'zsh': os.path.join(ws_root, 'devel/setup.zsh'),
367
+ 'bash': os.path.join(ws_root, 'devel/setup.bash')
368
+ }
369
+
370
+ setup_file = setup_files['zsh'] if is_zsh else setup_files['bash']
371
+ if not os.path.exists(setup_file):
372
+ setup_file = setup_file.replace('devel', 'install')
373
+ if not os.path.exists(setup_file):
374
+ raise Exception(f"Setup file not found in either devel or install: {setup_file}")
375
+
376
+ return setup_file
377
+
378
+ def launch_ik_node(self):
379
+ # nodes: /arms_ik_node
380
+ # services: /ik/two_arm_hand_pose_cmd_srv, /ik/fk_srv
381
+ try:
382
+ if not self.websocket or not self.websocket.client.is_connected:
383
+ raise Exception("WebSocket server is not running")
384
+ except Exception as e:
385
+ raise Exception(f"WebSocket server is not running: {e}")
386
+
387
+ # Check if IK node and services exist
388
+ try:
389
+ # Check if arms_ik_node is running using roslibpy
390
+ service = roslibpy.Service(self.websocket.client, '/rosnode/list', 'rosapi/Nodes')
391
+ response = service.call({})
392
+ nodes = response.get('nodes', [])
393
+
394
+ if '/arms_ik_node' not in nodes:
395
+ # Launch IK node if not running
396
+ kuavo_ws_root = self._get_kuavo_ws_root()
397
+ setup_file = self._get_setup_file(kuavo_ws_root)
398
+ source_cmd = f"source {setup_file}"
399
+
400
+ # Get robot version from environment variable
401
+ robot_version = os.environ.get('ROBOT_VERSION')
402
+ if robot_version is None:
403
+ raise Exception("Failed to get ROBOT_VERSION from environment variables")
404
+
405
+ # Launch IK node
406
+ launch_cmd = f"roslaunch motion_capture_ik ik_node.launch robot_version:={robot_version}"
407
+ full_cmd = f"{source_cmd} && {launch_cmd}"
408
+
409
+ self._launch_ros_node('IK node', full_cmd, 'launch_ik')
410
+
411
+ return True
412
+
413
+ except Exception as e:
414
+ raise Exception(f"Failed to verify IK node and services: {e}")
415
+
416
+
417
+ def launch_gait_switch_node(self)-> bool:
418
+ """Verify that the gait switch node is running, launch if not."""
419
+ try:
420
+ # Check if node exists using roslibpy
421
+ service = roslibpy.Service(self.websocket.client, '/rosnode/list', 'rosapi/Nodes')
422
+ response = service.call({})
423
+ nodes = response.get('nodes', [])
424
+
425
+ if '/humanoid_gait_switch_by_name' not in nodes:
426
+ kuavo_ws_root = self._get_kuavo_ws_root()
427
+ setup_file = self._get_setup_file(kuavo_ws_root)
428
+ source_cmd = f"source {setup_file}"
429
+
430
+ # Launch gait switch node
431
+ launch_cmd = "roslaunch humanoid_interface_ros humanoid_switch_gait.launch"
432
+ full_cmd = f"{source_cmd} && {launch_cmd}"
433
+
434
+ self._launch_ros_node('Gait switch node', full_cmd, 'launch_gait_switch')
435
+
436
+ except Exception as e:
437
+ raise Exception(f"Failed to launch gait_switch node: {e}")
438
+
439
+ @staticmethod
440
+ def check_rosnode_exists(node_name):
441
+ """Check if a ROS node is running using roslibpy.
442
+
443
+ Args:
444
+ node_name (str): Name of the node to check
445
+
446
+ Returns:
447
+ bool: True if node is running, False otherwise
448
+ """
449
+ websocket = WebSocketKuavoSDK()
450
+ try:
451
+ if not websocket or not websocket.client.is_connected:
452
+ return False
453
+
454
+ service = roslibpy.Service(websocket.client, '/rosapi/nodes', 'rosapi/Nodes')
455
+ response = service.call({})
456
+ nodes = response.get('nodes', [])
457
+ return node_name in nodes
458
+ except Exception as e:
459
+ SDKLogger.error(f"Error checking if node {node_name} exists: {e}")
460
+ return False
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env python3
2
+ # coding: utf-8
3
+ from typing import Tuple
4
+ from kuavo_humanoid_sdk.interfaces.end_effector import EndEffector
5
+ from kuavo_humanoid_sdk.interfaces.data_types import EndEffectorSide, EndEffectorState, KuavoDexHandTouchState
6
+ from kuavo_humanoid_sdk.kuavo.core.dex_hand_control import DexHandControl
7
+ from kuavo_humanoid_sdk.kuavo.core.ros.state import KuavoRobotStateCore
8
+
9
+ class DexterousHand(EndEffector):
10
+ """普通灵巧手控制类"""
11
+ def __init__(self):
12
+ joint_names = ['l_thumb', 'l_thumb_aux', 'l_index', 'l_middle', 'l_ring', 'l_pinky',
13
+ 'r_thumb', 'r_thumb_aux', 'r_index', 'r_middle', 'r_ring', 'r_pinky',]
14
+ super().__init__(joint_names=joint_names)
15
+ self.dex_hand_control = DexHandControl()
16
+ self._rb_state = KuavoRobotStateCore()
17
+
18
+ def control(self, target_positions:list, target_velocities:list=None, target_torques:list=None)->bool:
19
+ """控制灵巧手的位置。
20
+
21
+ Args:
22
+ target_positions (list): 所有手指关节的目标位置列表,长度必须为12(每只手6个手指关节),范围 => [0.0 ~ 100.0]
23
+ target_velocities (list, optional): 不支持。默认为None。
24
+ target_torques (list, optional): 不支持。默认为None。
25
+
26
+ Returns:
27
+ bool: 如果控制成功返回 True,否则返回 False。
28
+
29
+ Note:
30
+ target_velocities 和 target_torques 参数暂不支持。
31
+ """
32
+ if len(target_positions) != self.joint_count():
33
+ raise ValueError(f"Target positions must have the same length as joint names {len(target_positions)} != {self.joint_count()}")
34
+
35
+ q = [max(0, min(100, pos if isinstance(pos, int) else int(pos))) for pos in target_positions]
36
+
37
+ # control the hand
38
+ return self.dex_hand_control.control(target_positions=q, side=EndEffectorSide.BOTH)
39
+
40
+ def control_right(self, target_positions:list, target_velocities:list=None, target_torques:list=None)->bool:
41
+ """控制右手灵巧手。
42
+
43
+ Args:
44
+ target_positions (list): 右手关节的目标位置 [0 ~ 100],长度必须为6
45
+ target_velocities (list, optional): 不支持。默认为None。
46
+ target_torques (list, optional): 不支持。默认为None。
47
+
48
+ Returns:
49
+ bool: 如果控制成功返回True,否则返回False。
50
+
51
+ Raises:
52
+ ValueError: 如果目标位置长度与关节数不匹配或值超出[0,100]范围
53
+
54
+ Note:
55
+ target_velocities 和 target_torques 参数暂不支持。
56
+ """
57
+ if len(target_positions) != (self.joint_count()/2):
58
+ raise ValueError(f"Target positions must have the same length as joint names {len(target_positions)} != {self.joint_count()/2}.")
59
+
60
+ q = [max(0, min(100, pos if isinstance(pos, int) else int(pos))) for pos in target_positions]
61
+
62
+ return self.dex_hand_control.control(target_positions=q, side=EndEffectorSide.RIGHT)
63
+
64
+ def control_left(self, target_positions:list, target_velocities:list=None, target_torques:list=None)->bool:
65
+ """控制左手灵巧手。
66
+
67
+ Args:
68
+ target_positions (list): 左手关节的目标位置 [0 ~ 100],长度必须为6
69
+ target_velocities (list, optional): 不支持。默认为None。
70
+ target_torques (list, optional): 不支持。默认为None。
71
+
72
+ Returns:
73
+ bool: 如果控制成功返回True,否则返回False。
74
+
75
+ Raises:
76
+ ValueError: 如果目标位置长度与关节数不匹配或值超出[0,100]范围
77
+
78
+ Note:
79
+ target_velocities 和 target_torques 参数不支持。
80
+ """
81
+ if len(target_positions) != (self.joint_count()/2):
82
+ raise ValueError(f"Target positions must have the same length as joint names {len(target_positions)} != {self.joint_count()/2}.")
83
+
84
+ q = [max(0, min(100, pos if isinstance(pos, int) else int(pos))) for pos in target_positions]
85
+
86
+ return self.dex_hand_control.control(target_positions=q, side=EndEffectorSide.LEFT)
87
+
88
+ def open(self, side: EndEffectorSide=EndEffectorSide.BOTH)->bool:
89
+ """通过将所有关节位置设置为 0 来张开灵巧手。
90
+
91
+ Args:
92
+ side (EndEffectorSide, optional): 要打开的手。默认为 :attr:`EndEffectorSide.BOTH`。 \n
93
+ 可以是 :attr:`EndEffectorSide.LEFT`、:attr:`EndEffectorSide.RIGHT` 或 :attr:`EndEffectorSide.BOTH`。
94
+
95
+ Returns:
96
+ bool: 如果打开命令发送成功返回True,否则返回False。
97
+ """
98
+ zero_pos = [0]*self.joint_count()
99
+ if side == EndEffectorSide.LEFT:
100
+ return self.dex_hand_control.control(target_positions=zero_pos[:len(zero_pos)//2], side=EndEffectorSide.LEFT)
101
+ elif side == EndEffectorSide.RIGHT:
102
+ return self.dex_hand_control.control(target_positions=zero_pos[len(zero_pos)//2:], side=EndEffectorSide.RIGHT)
103
+ else:
104
+ return self.dex_hand_control.control(target_positions=zero_pos, side=EndEffectorSide.BOTH)
105
+
106
+ def make_gesture(self, l_gesture_name: str, r_gesture_name: str)->bool:
107
+ """为双手做预定义的手势。
108
+
109
+ Args:
110
+ l_gesture_name (str): 左手手势的名称。None表示跳过左手。
111
+ r_gesture_name (str): 右手手势的名称。None表示跳过右手。
112
+
113
+ Returns:
114
+ bool: 如果手势命令发送成功返回True,否则返回False。
115
+
116
+ Note:
117
+ 手势示例:'fist'、'ok'、'thumbs_up'、'666'等...
118
+ """
119
+ gesture = []
120
+ if l_gesture_name is not None:
121
+ gesture.append({'gesture_name':l_gesture_name, 'hand_side':EndEffectorSide.LEFT})
122
+ self.dex_hand_control.make_gestures(gesture)
123
+ if r_gesture_name is not None:
124
+ gesture.append({'gesture_name':r_gesture_name, 'hand_side':EndEffectorSide.RIGHT})
125
+ self.dex_hand_control.make_gestures(gesture)
126
+ return True
127
+ def get_gesture_names(self)->list:
128
+ """获取所有手势的名称。
129
+
130
+ Returns:
131
+ list: 手势名称列表。
132
+ 例如:['fist', 'ok', 'thumbs_up', '666', 'number_1', 'number_2', 'number_3', ... ], 如果没有手势则返回 None。
133
+ """
134
+ return self.dex_hand_control.get_gesture_names()
135
+
136
+ def get_state(self)->Tuple[EndEffectorState, EndEffectorState]:
137
+ """获取灵巧手的状态。
138
+
139
+ Returns:
140
+ Tuple[EndEffectorState, EndEffectorState]: 灵巧手的状态。
141
+ """
142
+ return self._rb_state.eef_state
143
+
144
+ def get_position(self)->Tuple[list, list]:
145
+ """获取灵巧手的位置。
146
+
147
+ Returns:
148
+ Tuple[list, list]: 灵巧手的位置。
149
+ """
150
+ state = self._rb_state.eef_state
151
+ return (state[0].position, state[1].position)
152
+
153
+ def get_velocity(self)->Tuple[list, list]:
154
+ """获取灵巧手的速度。
155
+
156
+ Returns:
157
+ Tuple[list, list]: 灵巧手的速度。
158
+ """
159
+ state = self._rb_state.eef_state
160
+ return (state[0].velocity, state[1].velocity)
161
+
162
+ def get_effort(self)->Tuple[list, list]:
163
+ """获取灵巧手的力。
164
+
165
+ Returns:
166
+ Tuple[list, list]: 灵巧手的力。
167
+
168
+ Note:
169
+ 每个手指的范围为0 ~ 100。表示最大电机电流的分数,绝对数值。
170
+ 最大电机电流为600mA,换句话说,100。
171
+ """
172
+ state = self._rb_state.eef_state
173
+ return (state[0].effort, state[1].effort)
174
+
175
+ def get_grasping_state(self)->Tuple[EndEffectorState.GraspingState, EndEffectorState.GraspingState]:
176
+ """获取灵巧手的抓取状态。
177
+
178
+ Note:
179
+ 该功能尚未实现。
180
+
181
+ Returns:
182
+ Tuple[EndEffectorState.GraspingState, EndEffectorState.GraspingState]: 灵巧手的抓取状态。
183
+ """
184
+ raise NotImplementedError("This function is not implemented yet")
185
+
186
+
187
+ class TouchDexterousHand(DexterousHand):
188
+ """触觉灵巧手控制类,继承自普通灵巧手控制类,可调用普通灵巧手控制类中的所有方法"""
189
+ def __init__(self):
190
+ super().__init__()
191
+
192
+ def get_touch_state(self)-> Tuple[KuavoDexHandTouchState, KuavoDexHandTouchState]:
193
+ """获取灵巧手的触觉状态。
194
+
195
+ Warning:
196
+ 该功能仅在触觉灵巧手上可用。
197
+
198
+ Returns:
199
+ Tuple[KuavoDexHandTouchState, KuavoDexHandTouchState]
200
+ """
201
+ return self._rb_state.dexhand_touch_state