hand-tracking-teleop 0.1.0__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 hand-tracking-teleop might be problematic. Click here for more details.
- hand_tracking/__init__.py +17 -0
- hand_tracking/cli.py +279 -0
- hand_tracking/hand_detector.py +475 -0
- hand_tracking/visualizer_3d.py +214 -0
- hand_tracking/visualizer_combined.py +221 -0
- hand_tracking/visualizer_rerun.py +255 -0
- hand_tracking_teleop-0.1.0.dist-info/METADATA +436 -0
- hand_tracking_teleop-0.1.0.dist-info/RECORD +14 -0
- hand_tracking_teleop-0.1.0.dist-info/WHEEL +5 -0
- hand_tracking_teleop-0.1.0.dist-info/entry_points.txt +6 -0
- hand_tracking_teleop-0.1.0.dist-info/top_level.txt +3 -0
- mapping/__init__.py +12 -0
- mapping/kinematic_mapper.py +405 -0
- robot_interface/dual_interface.py +780 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Robot Interface for Hand Tracking Data Collection
|
|
3
|
+
|
|
4
|
+
This module provides a simplified interface to control the dual-arm robot
|
|
5
|
+
using hand tracking data. The robot model now handles MoveIt IK internally.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import numpy as np
|
|
10
|
+
from typing import Dict, List, Optional, Tuple, Any
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
# Import the existing robot model
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
sys.path.append(os.path.join(os.path.dirname(__file__), '../../../../src'))
|
|
17
|
+
from lerobot.robots.dual_arm.robot_dual_arm import DualArmRobot
|
|
18
|
+
from lerobot.robots.dual_arm.configuration_dual_arm import DualArmConfig
|
|
19
|
+
|
|
20
|
+
# Import the mapping module
|
|
21
|
+
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
|
22
|
+
from mapping.kinematic_mapper import RobotJointState
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class RobotState:
|
|
27
|
+
"""Current state of the robot."""
|
|
28
|
+
joint_positions: np.ndarray
|
|
29
|
+
joint_velocities: np.ndarray
|
|
30
|
+
joint_names: List[str]
|
|
31
|
+
timestamp: float
|
|
32
|
+
is_moving: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class HandPose:
|
|
37
|
+
"""Hand pose for IK solving."""
|
|
38
|
+
position: np.ndarray # [x, y, z]
|
|
39
|
+
orientation: np.ndarray # [x, y, z, w] quaternion
|
|
40
|
+
hand_side: str # "left" or "right"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RobotInterface:
|
|
44
|
+
"""Simplified interface to control robot using the robot model's built-in MoveIt IK."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, use_action_client: bool = False):
|
|
47
|
+
"""
|
|
48
|
+
Initialize the robot interface.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
use_action_client: Whether to use action client (True) or direct control (False)
|
|
52
|
+
"""
|
|
53
|
+
self.is_connected = False
|
|
54
|
+
self.current_state = None
|
|
55
|
+
|
|
56
|
+
# Cache for current hand poses to reduce FK service calls
|
|
57
|
+
self._cached_hand_poses = None
|
|
58
|
+
self._last_pose_update = 0
|
|
59
|
+
self._pose_cache_duration = 2.0 # Cache poses for 2 seconds (was 500ms)
|
|
60
|
+
self._disable_fk_calls = False # Option to completely disable FK calls
|
|
61
|
+
|
|
62
|
+
# Create robot configuration
|
|
63
|
+
self.config = DualArmConfig(mock=False)
|
|
64
|
+
|
|
65
|
+
# Initialize the robot with use_action_client parameter
|
|
66
|
+
self.robot = DualArmRobot(self.config, use_action_client=use_action_client)
|
|
67
|
+
|
|
68
|
+
# Connect to robot
|
|
69
|
+
self.robot.connect()
|
|
70
|
+
self.is_connected = True
|
|
71
|
+
|
|
72
|
+
print(f"โ
Connected to robot with built-in MoveIt IK (action_client: {use_action_client})")
|
|
73
|
+
|
|
74
|
+
def send_joint_command(self, joint_state: RobotJointState, execution_time: float = 1.0,
|
|
75
|
+
fingers_only: bool = True):
|
|
76
|
+
"""
|
|
77
|
+
Send joint command to robot using direct control for fingers.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
joint_state: Target joint positions
|
|
81
|
+
execution_time: Time to execute the movement
|
|
82
|
+
fingers_only: If True, only send finger commands (safer for testing)
|
|
83
|
+
"""
|
|
84
|
+
if not self.is_connected:
|
|
85
|
+
print("โ Robot not connected")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
# Always handle fingers with direct control
|
|
90
|
+
success = self._send_finger_commands(joint_state)
|
|
91
|
+
|
|
92
|
+
# Handle arms with direct control if requested (IK is separate method)
|
|
93
|
+
if not fingers_only:
|
|
94
|
+
success &= self._send_arm_commands_direct(joint_state)
|
|
95
|
+
|
|
96
|
+
return success
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
print(f"โ Failed to send joint commands to robot: {e}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def _send_finger_commands(self, joint_state: RobotJointState) -> bool:
|
|
103
|
+
"""Send finger commands using direct control."""
|
|
104
|
+
try:
|
|
105
|
+
left_finger_commands = {}
|
|
106
|
+
right_finger_commands = {}
|
|
107
|
+
|
|
108
|
+
# Group finger joint commands
|
|
109
|
+
for i, joint_name in enumerate(joint_state.joint_names):
|
|
110
|
+
if i < len(joint_state.joint_positions):
|
|
111
|
+
position = float(joint_state.joint_positions[i])
|
|
112
|
+
|
|
113
|
+
if joint_name in self.config.left_finger_joints:
|
|
114
|
+
left_finger_commands[joint_name] = position
|
|
115
|
+
elif joint_name in self.config.right_finger_joints:
|
|
116
|
+
right_finger_commands[joint_name] = position
|
|
117
|
+
|
|
118
|
+
# Send finger commands
|
|
119
|
+
if left_finger_commands:
|
|
120
|
+
print(f"๐ค Sending {len(left_finger_commands)} left finger commands")
|
|
121
|
+
self.robot._send_direct_joint_commands(
|
|
122
|
+
self.config.left_hand_controller,
|
|
123
|
+
left_finger_commands
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if right_finger_commands:
|
|
127
|
+
print(f"๐ค Sending {len(right_finger_commands)} right finger commands")
|
|
128
|
+
self.robot._send_direct_joint_commands(
|
|
129
|
+
self.config.right_hand_controller,
|
|
130
|
+
right_finger_commands
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"โ Failed to send finger commands: {e}")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def _send_arm_commands_direct(self, joint_state: RobotJointState) -> bool:
|
|
140
|
+
"""Send arm commands using direct control (fallback)."""
|
|
141
|
+
try:
|
|
142
|
+
arm_commands = {}
|
|
143
|
+
|
|
144
|
+
# Group arm/torso joint commands
|
|
145
|
+
for i, joint_name in enumerate(joint_state.joint_names):
|
|
146
|
+
if i < len(joint_state.joint_positions):
|
|
147
|
+
position = float(joint_state.joint_positions[i])
|
|
148
|
+
|
|
149
|
+
if joint_name in (self.config.left_arm_joints +
|
|
150
|
+
self.config.right_arm_joints +
|
|
151
|
+
self.config.torso_joints):
|
|
152
|
+
arm_commands[joint_name] = position
|
|
153
|
+
|
|
154
|
+
if arm_commands:
|
|
155
|
+
print(f"๐ฆพ Sending {len(arm_commands)} direct arm commands")
|
|
156
|
+
self.robot._send_direct_joint_commands(
|
|
157
|
+
self.config.torso_controller,
|
|
158
|
+
arm_commands
|
|
159
|
+
)
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"โ Failed to send direct arm commands: {e}")
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def _get_cached_hand_poses(self) -> Tuple[Optional[Dict], Optional[Dict]]:
|
|
169
|
+
"""Get current hand poses with caching to reduce FK service calls."""
|
|
170
|
+
# If FK calls are disabled, always return fallback poses
|
|
171
|
+
if self._disable_fk_calls:
|
|
172
|
+
fallback_left = {
|
|
173
|
+
'position': [0.3, 0.3, 0.6],
|
|
174
|
+
'orientation': [0.0, 0.0, 0.0, 1.0]
|
|
175
|
+
}
|
|
176
|
+
fallback_right = {
|
|
177
|
+
'position': [0.3, -0.3, 0.6],
|
|
178
|
+
'orientation': [0.0, 0.0, 0.0, 1.0]
|
|
179
|
+
}
|
|
180
|
+
return fallback_left, fallback_right
|
|
181
|
+
|
|
182
|
+
current_time = time.time()
|
|
183
|
+
|
|
184
|
+
# Check if cache is still valid
|
|
185
|
+
if (self._cached_hand_poses is not None and
|
|
186
|
+
current_time - self._last_pose_update < self._pose_cache_duration):
|
|
187
|
+
return self._cached_hand_poses
|
|
188
|
+
|
|
189
|
+
# Cache expired or doesn't exist, get new poses
|
|
190
|
+
try:
|
|
191
|
+
current_left, current_right = self.robot.get_current_hand_poses()
|
|
192
|
+
self._cached_hand_poses = (current_left, current_right)
|
|
193
|
+
self._last_pose_update = current_time
|
|
194
|
+
return self._cached_hand_poses
|
|
195
|
+
except Exception as e:
|
|
196
|
+
# If FK service fails, use fallback poses or cached poses
|
|
197
|
+
print(f"โ ๏ธ FK service call failed, using fallback: {e}")
|
|
198
|
+
# Disable FK calls after repeated failures
|
|
199
|
+
self._disable_fk_calls = True
|
|
200
|
+
print("๐ซ FK calls disabled due to repeated failures")
|
|
201
|
+
|
|
202
|
+
if self._cached_hand_poses is not None:
|
|
203
|
+
return self._cached_hand_poses
|
|
204
|
+
else:
|
|
205
|
+
# Fallback to reasonable default poses
|
|
206
|
+
fallback_left = {
|
|
207
|
+
'position': [0.3, 0.3, 0.6],
|
|
208
|
+
'orientation': [0.0, 0.0, 0.0, 1.0]
|
|
209
|
+
}
|
|
210
|
+
fallback_right = {
|
|
211
|
+
'position': [0.3, -0.3, 0.6],
|
|
212
|
+
'orientation': [0.0, 0.0, 0.0, 1.0]
|
|
213
|
+
}
|
|
214
|
+
return fallback_left, fallback_right
|
|
215
|
+
|
|
216
|
+
def update_hand_pose_cache(self):
|
|
217
|
+
"""Force update of hand pose cache."""
|
|
218
|
+
self._cached_hand_poses = None
|
|
219
|
+
self._last_pose_update = 0
|
|
220
|
+
# Next call to _get_cached_hand_poses will fetch fresh data
|
|
221
|
+
|
|
222
|
+
def disable_fk_calls(self):
|
|
223
|
+
"""Disable FK service calls completely to eliminate timeouts."""
|
|
224
|
+
self._disable_fk_calls = True
|
|
225
|
+
print("๐ซ FK service calls disabled - using fallback poses")
|
|
226
|
+
|
|
227
|
+
def enable_fk_calls(self):
|
|
228
|
+
"""Re-enable FK service calls."""
|
|
229
|
+
self._disable_fk_calls = False
|
|
230
|
+
self._cached_hand_poses = None # Clear cache to force fresh data
|
|
231
|
+
print("โ
FK service calls enabled")
|
|
232
|
+
|
|
233
|
+
def convert_hand_landmarks_to_poses(self, hands_data, control_mode: str = "full_body") -> Tuple[Optional[Dict], Optional[Dict]]:
|
|
234
|
+
"""
|
|
235
|
+
Convert hand tracking landmarks to robot hand poses.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
hands_data: List of HandLandmarks from hand tracking
|
|
239
|
+
control_mode: "full_body" or "fingers_only" - if fingers_only, skip pose conversion
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Tuple of (left_pose_dict, right_pose_dict) for robot IK
|
|
243
|
+
"""
|
|
244
|
+
# Skip pose conversion entirely in fingers_only mode to avoid FK calls
|
|
245
|
+
if control_mode == "fingers_only":
|
|
246
|
+
return None, None
|
|
247
|
+
|
|
248
|
+
left_pose = None
|
|
249
|
+
right_pose = None
|
|
250
|
+
|
|
251
|
+
# Get current hand poses as reference (with caching to reduce FK calls)
|
|
252
|
+
current_left, current_right = self._get_cached_hand_poses()
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
for hand in hands_data:
|
|
256
|
+
# Get wrist position (landmark 0)
|
|
257
|
+
wrist = hand.landmarks[0]
|
|
258
|
+
|
|
259
|
+
# Debug: Print raw hand tracking data
|
|
260
|
+
# print(f"Hand {hand.handedness}: wrist at ({wrist[0]:.3f}, {wrist[1]:.3f}, {wrist[2]:.3f})")
|
|
261
|
+
|
|
262
|
+
# Convert to relative movement from current position
|
|
263
|
+
# Map camera movement to robot movement (TOP-VIEW CAMERA)
|
|
264
|
+
# Camera X (left-right) โ Robot Y (left-right, but inverted: right in image = -Y in robot)
|
|
265
|
+
# Camera Y (top-bottom) โ Robot X (forward-back: down in image = -X in robot)
|
|
266
|
+
# Camera Z (depth) โ Robot -Z (up-down, inverted)
|
|
267
|
+
dy = -(wrist[0] / 640.0 - 0.5) * 0.8 # ยฑ40cm movement in Y (left/right, inverted)
|
|
268
|
+
dx = -(wrist[1] / 480.0 - 0.5) * 0.8 # ยฑ40cm movement in X (forward/back)
|
|
269
|
+
dz = -wrist[2] * 0.8 # 0-20cm movement in Z (up/down)
|
|
270
|
+
|
|
271
|
+
# Apply movement relative to current hand position
|
|
272
|
+
if hand.handedness == "Left" and current_left:
|
|
273
|
+
x = current_left['position'][0] + dx
|
|
274
|
+
y = current_left['position'][1] + dy
|
|
275
|
+
z = current_left['position'][2] + dz
|
|
276
|
+
base_orientation = current_left['orientation']
|
|
277
|
+
elif hand.handedness == "Right" and current_right:
|
|
278
|
+
x = current_right['position'][0] + dx
|
|
279
|
+
y = current_right['position'][1] + dy
|
|
280
|
+
z = current_right['position'][2] + dz
|
|
281
|
+
base_orientation = current_right['orientation']
|
|
282
|
+
else:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
# print(f"Movement: dx={dx:.3f}, dy={dy:.3f}, dz={dz:.3f}")
|
|
286
|
+
print(f"Target position {hand.handedness}: ({x:.3f}, {y:.3f}, {z:.3f})")
|
|
287
|
+
|
|
288
|
+
# Calculate hand orientation from landmarks for realistic training data
|
|
289
|
+
# Use wrist, middle finger MCP, and index finger MCP for orientation
|
|
290
|
+
middle_mcp = hand.landmarks[9] # Middle finger MCP
|
|
291
|
+
index_mcp = hand.landmarks[5] # Index finger MCP
|
|
292
|
+
|
|
293
|
+
# Calculate hand vectors
|
|
294
|
+
palm_vec = middle_mcp - wrist # Palm direction
|
|
295
|
+
finger_vec = index_mcp - wrist # Finger direction
|
|
296
|
+
|
|
297
|
+
# Calculate hand normal (cross product)
|
|
298
|
+
palm_normal = np.cross(palm_vec, finger_vec)
|
|
299
|
+
palm_normal = palm_normal / (np.linalg.norm(palm_normal) + 1e-6)
|
|
300
|
+
|
|
301
|
+
# Simple orientation mapping - blend with current orientation for stability
|
|
302
|
+
blend_factor = 0.7 # 70% current, 30% tracked
|
|
303
|
+
|
|
304
|
+
# Convert to simple rotation around Z-axis (yaw)
|
|
305
|
+
yaw = np.arctan2(palm_vec[1], palm_vec[0]) * 0.3 # Scale down rotation
|
|
306
|
+
|
|
307
|
+
# Create quaternion from yaw rotation
|
|
308
|
+
qw_new = np.cos(yaw/2)
|
|
309
|
+
qx_new = 0.0
|
|
310
|
+
qy_new = 0.0
|
|
311
|
+
qz_new = np.sin(yaw/2)
|
|
312
|
+
|
|
313
|
+
# Blend with current orientation for stability
|
|
314
|
+
qx = blend_factor * base_orientation[0] + (1-blend_factor) * qx_new
|
|
315
|
+
qy = blend_factor * base_orientation[1] + (1-blend_factor) * qy_new
|
|
316
|
+
qz = blend_factor * base_orientation[2] + (1-blend_factor) * qz_new
|
|
317
|
+
qw = blend_factor * base_orientation[3] + (1-blend_factor) * qw_new
|
|
318
|
+
|
|
319
|
+
# Normalize quaternion
|
|
320
|
+
quat_norm = np.sqrt(qx*qx + qy*qy + qz*qz + qw*qw)
|
|
321
|
+
qx, qy, qz, qw = qx/quat_norm, qy/quat_norm, qz/quat_norm, qw/quat_norm
|
|
322
|
+
|
|
323
|
+
# Create pose dictionary for robot
|
|
324
|
+
pose_dict = {
|
|
325
|
+
'position': [x, y, z],
|
|
326
|
+
'orientation': [qx, qy, qz, qw]
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if hand.handedness == "Left":
|
|
330
|
+
left_pose = pose_dict
|
|
331
|
+
elif hand.handedness == "Right":
|
|
332
|
+
right_pose = pose_dict
|
|
333
|
+
|
|
334
|
+
except Exception as e:
|
|
335
|
+
print(f"Warning: Failed to convert hand landmarks to poses: {e}")
|
|
336
|
+
|
|
337
|
+
return left_pose, right_pose
|
|
338
|
+
|
|
339
|
+
def convert_hand_landmarks_to_poses_fast(self, hands_data, control_mode: str = "full_body") -> Tuple[Optional[Dict], Optional[Dict]]:
|
|
340
|
+
"""
|
|
341
|
+
Convert hand tracking landmarks to robot hand poses (fast version without FK calls).
|
|
342
|
+
|
|
343
|
+
This version uses fixed reference poses to avoid FK service calls entirely,
|
|
344
|
+
making it suitable for continuous real-time control.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
hands_data: List of HandLandmarks from hand tracking
|
|
348
|
+
control_mode: "full_body" or "fingers_only" - if fingers_only, skip pose conversion
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Tuple of (left_pose_dict, right_pose_dict) for robot IK
|
|
352
|
+
"""
|
|
353
|
+
# Skip pose conversion entirely in fingers_only mode
|
|
354
|
+
if control_mode == "fingers_only":
|
|
355
|
+
return None, None
|
|
356
|
+
|
|
357
|
+
left_pose = None
|
|
358
|
+
right_pose = None
|
|
359
|
+
|
|
360
|
+
# Get current hand poses as reference (with caching to reduce FK calls)
|
|
361
|
+
current_left, current_right = self._get_cached_hand_poses()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
for hand in hands_data:
|
|
367
|
+
# Get wrist position (landmark 0)
|
|
368
|
+
wrist = hand.landmarks[0]
|
|
369
|
+
|
|
370
|
+
# Use much smaller, more conservative movement ranges
|
|
371
|
+
# Map camera movement to robot movement (TOP-VIEW CAMERA)
|
|
372
|
+
dy = -(wrist[0] / 640.0 - 0.5) * 0.15 # ยฑ7.5cm movement in Y (reduced from 15cm)
|
|
373
|
+
dx = -(wrist[1] / 480.0 - 0.5) * 0.10 # ยฑ5cm movement in X (reduced from 10cm)
|
|
374
|
+
dz = -wrist[2] * 0.10 # 0-10cm movement in Z (reduced from 20cm)
|
|
375
|
+
|
|
376
|
+
# Apply movement relative to reference position
|
|
377
|
+
if hand.handedness == "Left":
|
|
378
|
+
x = current_left['position'][0] + dx
|
|
379
|
+
y = current_left['position'][1] + dy
|
|
380
|
+
z = current_left['position'][2] + dz
|
|
381
|
+
base_orientation = current_left['orientation']
|
|
382
|
+
elif hand.handedness == "Right":
|
|
383
|
+
x = current_right['position'][0] + dx
|
|
384
|
+
y = current_right['position'][1] + dy
|
|
385
|
+
z = current_right['position'][2] + dz
|
|
386
|
+
base_orientation = current_right['orientation']
|
|
387
|
+
else:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
# # Clamp to safe workspace bounds to avoid IK failures
|
|
391
|
+
# x = max(0.2, min(0.6, x)) # 20-60cm forward from base
|
|
392
|
+
# y = max(-0.4, min(0.4, y)) # ยฑ40cm left/right from base
|
|
393
|
+
# z = max(0.5, min(1.0, z)) # 50-100cm height from base
|
|
394
|
+
|
|
395
|
+
print(f"Target position {hand.handedness}: ({x:.3f}, {y:.3f}, {z:.3f}) [clamped to workspace]")
|
|
396
|
+
|
|
397
|
+
# Use fixed, simple orientation to avoid orientation IK failures
|
|
398
|
+
qx, qy, qz, qw = base_orientation
|
|
399
|
+
|
|
400
|
+
# Create pose dictionary for robot
|
|
401
|
+
pose_dict = {
|
|
402
|
+
'position': [x, y, z],
|
|
403
|
+
'orientation': [qx, qy, qz, qw]
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if hand.handedness == "Left":
|
|
407
|
+
left_pose = pose_dict
|
|
408
|
+
elif hand.handedness == "Right":
|
|
409
|
+
right_pose = pose_dict
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
print(f"Warning: Failed to convert hand landmarks to poses: {e}")
|
|
413
|
+
|
|
414
|
+
return left_pose, right_pose
|
|
415
|
+
|
|
416
|
+
def send_hand_poses(self, left_pose: Optional[Dict], right_pose: Optional[Dict],
|
|
417
|
+
execution_time: float = 1.0, use_motion_planning: bool = True,
|
|
418
|
+
use_individual_groups: bool = False) -> bool:
|
|
419
|
+
"""
|
|
420
|
+
Send target poses for hands using MoveIt planning groups.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
left_pose: Target pose for left hand (None to ignore)
|
|
424
|
+
right_pose: Target pose for right hand (None to ignore)
|
|
425
|
+
execution_time: Time to execute the movement
|
|
426
|
+
use_motion_planning: If True, use full MoveIt pipeline; if False, use direct IK
|
|
427
|
+
use_individual_groups: If True, use left_arm+right_arm groups; if False, use dual_arm group
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
True if successful (dual_arm) or at least one arm succeeded (individual)
|
|
431
|
+
"""
|
|
432
|
+
if not self.is_connected:
|
|
433
|
+
print("โ Robot not connected")
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
# Handle async call
|
|
437
|
+
import asyncio
|
|
438
|
+
try:
|
|
439
|
+
loop = asyncio.get_event_loop()
|
|
440
|
+
except RuntimeError:
|
|
441
|
+
loop = asyncio.new_event_loop()
|
|
442
|
+
asyncio.set_event_loop(loop)
|
|
443
|
+
|
|
444
|
+
if use_individual_groups:
|
|
445
|
+
# Use individual planning groups (left_arm + right_arm)
|
|
446
|
+
if not (left_pose or right_pose):
|
|
447
|
+
print("โ ๏ธ Need at least one hand pose for individual groups")
|
|
448
|
+
return False
|
|
449
|
+
|
|
450
|
+
print("๐ฏ Using individual planning groups (left_arm + right_arm)...")
|
|
451
|
+
success_count = 0
|
|
452
|
+
total_attempts = 0
|
|
453
|
+
|
|
454
|
+
# Send left arm command if left pose available
|
|
455
|
+
if left_pose:
|
|
456
|
+
print(" ๐ฏ Sending left_arm command...")
|
|
457
|
+
total_attempts += 1
|
|
458
|
+
try:
|
|
459
|
+
left_success = loop.run_until_complete(
|
|
460
|
+
self.send_single_arm_pose("left_arm", "left_tcp", left_pose, execution_time, use_motion_planning)
|
|
461
|
+
)
|
|
462
|
+
if left_success:
|
|
463
|
+
success_count += 1
|
|
464
|
+
print(" โ
Left arm command succeeded")
|
|
465
|
+
else:
|
|
466
|
+
print(" โ Left arm command failed")
|
|
467
|
+
except Exception as e:
|
|
468
|
+
print(f" โ Left arm command error: {e}")
|
|
469
|
+
|
|
470
|
+
# Send right arm command if right pose available
|
|
471
|
+
if right_pose:
|
|
472
|
+
print(" ๐ฏ Sending right_arm command...")
|
|
473
|
+
total_attempts += 1
|
|
474
|
+
try:
|
|
475
|
+
right_success = loop.run_until_complete(
|
|
476
|
+
self.send_single_arm_pose("right_arm", "right_tcp", right_pose, execution_time, use_motion_planning)
|
|
477
|
+
)
|
|
478
|
+
if right_success:
|
|
479
|
+
success_count += 1
|
|
480
|
+
print(" โ
Right arm command succeeded")
|
|
481
|
+
else:
|
|
482
|
+
print(" โ Right arm command failed")
|
|
483
|
+
except Exception as e:
|
|
484
|
+
print(f" โ Right arm command error: {e}")
|
|
485
|
+
|
|
486
|
+
# Return success if at least one arm succeeded
|
|
487
|
+
overall_success = success_count > 0
|
|
488
|
+
print(f"๐ Individual groups result: {success_count}/{total_attempts} succeeded")
|
|
489
|
+
return overall_success
|
|
490
|
+
|
|
491
|
+
else:
|
|
492
|
+
# Use original dual_arm planning group
|
|
493
|
+
if not (left_pose and right_pose):
|
|
494
|
+
print("โ ๏ธ Dual arm planning group requires both hand poses")
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
print("๐ฏ Using dual_arm planning group...")
|
|
498
|
+
try:
|
|
499
|
+
return loop.run_until_complete(
|
|
500
|
+
self.robot.send_dual_arm_pose(left_pose, right_pose, execution_time, use_motion_planning)
|
|
501
|
+
)
|
|
502
|
+
except Exception as e:
|
|
503
|
+
print(f"โ Dual arm planning failed: {e}")
|
|
504
|
+
return False
|
|
505
|
+
|
|
506
|
+
async def send_single_arm_pose(self, group_name: str, link_name: str, pose: Dict,
|
|
507
|
+
execution_time: float = 1.0, use_motion_planning: bool = True) -> bool:
|
|
508
|
+
"""
|
|
509
|
+
Send target pose for a single arm using MoveIt.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
group_name: MoveIt planning group name ("left_arm" or "right_arm")
|
|
513
|
+
link_name: End effector link name ("left_tcp" or "right_tcp")
|
|
514
|
+
pose: Target pose dictionary with 'position' and 'orientation'
|
|
515
|
+
execution_time: Time to execute the movement
|
|
516
|
+
use_motion_planning: If True, use full MoveIt pipeline; if False, use direct IK
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
True if successful
|
|
520
|
+
"""
|
|
521
|
+
try:
|
|
522
|
+
if use_motion_planning:
|
|
523
|
+
# Skip IK solving and send pose directly to MoveIt for non-blocking execution
|
|
524
|
+
success = await self.robot._execute_single_arm_moveit_planning(group_name, link_name, pose, execution_time)
|
|
525
|
+
return success
|
|
526
|
+
else:
|
|
527
|
+
# For direct execution, we still need IK but make it faster
|
|
528
|
+
current_state = await self.robot.get_current_robot_state()
|
|
529
|
+
ik_solution = await self.robot.solve_single_arm_ik(group_name, link_name, pose, current_state)
|
|
530
|
+
|
|
531
|
+
if ik_solution is None:
|
|
532
|
+
print(f"โ No IK solution found for {group_name}")
|
|
533
|
+
return False
|
|
534
|
+
|
|
535
|
+
# Extract joint commands from IK solution for direct execution
|
|
536
|
+
arm_commands = {}
|
|
537
|
+
for i, joint_name in enumerate(ik_solution.joint_state.name):
|
|
538
|
+
# Filter for arm and torso joints only
|
|
539
|
+
if joint_name in (self.robot.config.left_arm_joints +
|
|
540
|
+
self.robot.config.right_arm_joints +
|
|
541
|
+
self.robot.config.torso_joints):
|
|
542
|
+
arm_commands[joint_name] = ik_solution.joint_state.position[i]
|
|
543
|
+
|
|
544
|
+
# Send joint commands directly
|
|
545
|
+
if arm_commands:
|
|
546
|
+
print(f"๐ง Sending {len(arm_commands)} joint commands for {group_name}")
|
|
547
|
+
self.robot._send_direct_joint_commands(self.robot.config.torso_controller, arm_commands)
|
|
548
|
+
return True
|
|
549
|
+
else:
|
|
550
|
+
print(f"โ No valid joint commands extracted for {group_name}")
|
|
551
|
+
return False
|
|
552
|
+
|
|
553
|
+
except Exception as e:
|
|
554
|
+
print(f"โ Single arm pose failed for {group_name}: {e}")
|
|
555
|
+
return False
|
|
556
|
+
|
|
557
|
+
def get_current_state(self) -> Optional[RobotState]:
|
|
558
|
+
"""Get current robot state."""
|
|
559
|
+
try:
|
|
560
|
+
# Get all joint names
|
|
561
|
+
all_joints = (
|
|
562
|
+
self.config.left_arm_joints +
|
|
563
|
+
self.config.right_arm_joints +
|
|
564
|
+
self.config.torso_joints +
|
|
565
|
+
self.config.left_finger_joints +
|
|
566
|
+
self.config.right_finger_joints
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Get current joint positions from robot's internal state
|
|
570
|
+
current_positions = []
|
|
571
|
+
for joint in all_joints:
|
|
572
|
+
# Access the robot's internal joint position tracking
|
|
573
|
+
pos = self.robot._joint_positions.get(joint, 0.0)
|
|
574
|
+
current_positions.append(pos)
|
|
575
|
+
|
|
576
|
+
return RobotState(
|
|
577
|
+
joint_positions=np.array(current_positions),
|
|
578
|
+
joint_velocities=np.zeros(len(all_joints)), # Could get from _joint_velocities
|
|
579
|
+
joint_names=all_joints,
|
|
580
|
+
timestamp=time.time(),
|
|
581
|
+
is_moving=False # Could check if any velocities > threshold
|
|
582
|
+
)
|
|
583
|
+
except Exception as e:
|
|
584
|
+
print(f"Warning: Could not get robot state: {e}")
|
|
585
|
+
return None
|
|
586
|
+
|
|
587
|
+
def get_joint_names(self) -> List[str]:
|
|
588
|
+
"""Get ordered list of joint names."""
|
|
589
|
+
return (
|
|
590
|
+
self.config.left_arm_joints +
|
|
591
|
+
self.config.right_arm_joints +
|
|
592
|
+
self.config.torso_joints +
|
|
593
|
+
self.config.left_finger_joints +
|
|
594
|
+
self.config.right_finger_joints
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
def shutdown(self):
|
|
598
|
+
"""Shutdown the robot interface."""
|
|
599
|
+
try:
|
|
600
|
+
# Disconnect robot (robot handles its own ROS cleanup)
|
|
601
|
+
self.robot.disconnect()
|
|
602
|
+
except Exception as e:
|
|
603
|
+
print(f"Warning during robot disconnect: {e}")
|
|
604
|
+
|
|
605
|
+
print("๐ Robot interface shutdown")
|
|
606
|
+
|
|
607
|
+
def send_combined_command(self, left_pose: Optional[Dict], right_pose: Optional[Dict],
|
|
608
|
+
finger_commands: Optional[Any], execution_time: float = 1.0,
|
|
609
|
+
use_motion_planning: bool = False, use_individual_groups: bool = False) -> bool:
|
|
610
|
+
"""
|
|
611
|
+
Send both arm motion and finger commands together.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
left_pose: Target pose for left hand (None to ignore)
|
|
615
|
+
right_pose: Target pose for right hand (None to ignore)
|
|
616
|
+
finger_commands: RobotJointState with finger positions
|
|
617
|
+
execution_time: Time to execute the movement
|
|
618
|
+
use_motion_planning: If True, use full MoveIt pipeline; if False, use direct IK
|
|
619
|
+
use_individual_groups: If True, use left_arm+right_arm; if False, use dual_arm
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
True if at least one command succeeded
|
|
623
|
+
"""
|
|
624
|
+
if not self.is_connected:
|
|
625
|
+
print("โ Robot not connected")
|
|
626
|
+
return False
|
|
627
|
+
|
|
628
|
+
group_type = "individual groups" if use_individual_groups else "dual_arm group"
|
|
629
|
+
print(f"๐ Sending combined command ({group_type} + fingers)...")
|
|
630
|
+
success_count = 0
|
|
631
|
+
total_attempts = 0
|
|
632
|
+
|
|
633
|
+
# Send arm motion using selected planning approach
|
|
634
|
+
if left_pose or right_pose:
|
|
635
|
+
if use_motion_planning:
|
|
636
|
+
print(f" ๐ฏ Sending arm motion with MoveIt planning ({group_type})...")
|
|
637
|
+
else:
|
|
638
|
+
print(f" ๐ง Sending arm motion with direct IK ({group_type})...")
|
|
639
|
+
total_attempts += 1
|
|
640
|
+
if self.send_hand_poses(left_pose, right_pose, execution_time, use_motion_planning, use_individual_groups):
|
|
641
|
+
success_count += 1
|
|
642
|
+
print(" โ
Arm motion command sent")
|
|
643
|
+
else:
|
|
644
|
+
print(" โ Arm motion command failed")
|
|
645
|
+
else:
|
|
646
|
+
print(" โ ๏ธ Skipping arm motion (no hand poses provided)")
|
|
647
|
+
|
|
648
|
+
# Send finger commands if available
|
|
649
|
+
if finger_commands:
|
|
650
|
+
print(" ๐ค Sending finger commands...")
|
|
651
|
+
total_attempts += 1
|
|
652
|
+
if self.send_joint_command(finger_commands, execution_time, fingers_only=True):
|
|
653
|
+
success_count += 1
|
|
654
|
+
print(" โ
Finger commands sent")
|
|
655
|
+
else:
|
|
656
|
+
print(" โ Finger commands failed")
|
|
657
|
+
else:
|
|
658
|
+
print(" โ ๏ธ No finger commands to send")
|
|
659
|
+
|
|
660
|
+
result = success_count > 0
|
|
661
|
+
print(f"๐ Combined command result: {success_count}/{total_attempts} succeeded")
|
|
662
|
+
return result
|
|
663
|
+
|
|
664
|
+
def send_combined_command_async(self, left_pose: Optional[Dict], right_pose: Optional[Dict],
|
|
665
|
+
finger_commands: Optional[Any], execution_time: float = 1.0,
|
|
666
|
+
use_motion_planning: bool = True, use_individual_groups: bool = False) -> bool:
|
|
667
|
+
"""
|
|
668
|
+
Send both arm motion and finger commands together (non-blocking version for real-time control).
|
|
669
|
+
|
|
670
|
+
This version is optimized for continuous data collection and won't block the main thread.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
left_pose: Target pose for left hand (None to ignore)
|
|
674
|
+
right_pose: Target pose for right hand (None to ignore)
|
|
675
|
+
finger_commands: RobotJointState with finger positions
|
|
676
|
+
execution_time: Time to execute the movement
|
|
677
|
+
use_motion_planning: If True, use full MoveIt pipeline; if False, use direct IK
|
|
678
|
+
use_individual_groups: If True, use left_arm+right_arm; if False, use dual_arm
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
True if commands were queued successfully (not necessarily executed)
|
|
682
|
+
"""
|
|
683
|
+
if not self.is_connected:
|
|
684
|
+
return False
|
|
685
|
+
|
|
686
|
+
success_count = 0
|
|
687
|
+
total_attempts = 0
|
|
688
|
+
|
|
689
|
+
# Send arm motion using direct action client calls (non-blocking)
|
|
690
|
+
if left_pose or right_pose and use_motion_planning:
|
|
691
|
+
total_attempts += 1
|
|
692
|
+
try:
|
|
693
|
+
if use_individual_groups:
|
|
694
|
+
# Send individual arm commands directly to action clients
|
|
695
|
+
if left_pose:
|
|
696
|
+
success = self.robot._send_single_arm_goal_async("left_arm", "left_tcp", left_pose, execution_time)
|
|
697
|
+
if success:
|
|
698
|
+
print("๐ Left arm goal queued")
|
|
699
|
+
|
|
700
|
+
if right_pose:
|
|
701
|
+
success = self.robot._send_single_arm_goal_async("right_arm", "right_tcp", right_pose, execution_time)
|
|
702
|
+
if success:
|
|
703
|
+
print("๐ Right arm goal queued")
|
|
704
|
+
|
|
705
|
+
success_count += 1
|
|
706
|
+
else:
|
|
707
|
+
# For dual arm, just send one pose for now (simplified)
|
|
708
|
+
if left_pose:
|
|
709
|
+
success = self.robot._send_single_arm_goal_async("left_arm", "left_tcp", left_pose, execution_time)
|
|
710
|
+
if success:
|
|
711
|
+
success_count += 1
|
|
712
|
+
|
|
713
|
+
except Exception as e:
|
|
714
|
+
print(f"โ ๏ธ Arm command queuing failed: {e}")
|
|
715
|
+
|
|
716
|
+
# Send finger commands (these are fast, so keep synchronous)
|
|
717
|
+
if finger_commands:
|
|
718
|
+
total_attempts += 1
|
|
719
|
+
if self.send_joint_command(finger_commands, execution_time, fingers_only=True):
|
|
720
|
+
success_count += 1
|
|
721
|
+
|
|
722
|
+
return success_count > 0
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
if __name__ == "__main__":
|
|
726
|
+
# Test the robot interface with MoveIt
|
|
727
|
+
print("Testing Robot Interface with built-in MoveIt dual_arm IK...")
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
robot = RobotInterface(use_action_client=False)
|
|
731
|
+
|
|
732
|
+
print("โ
Robot interface initialized successfully")
|
|
733
|
+
|
|
734
|
+
# Get joint names
|
|
735
|
+
joint_names = robot.get_joint_names()
|
|
736
|
+
print(f"Robot has {len(joint_names)} joints")
|
|
737
|
+
|
|
738
|
+
# Test finger-only control
|
|
739
|
+
test_state = RobotJointState(
|
|
740
|
+
joint_names=joint_names,
|
|
741
|
+
joint_positions=np.random.rand(len(joint_names)) * 0.3, # Random small angles
|
|
742
|
+
timestamp=time.time()
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Send finger-only command
|
|
746
|
+
print("\n๐งช Testing finger-only control...")
|
|
747
|
+
robot.send_joint_command(test_state, execution_time=2.0, fingers_only=True)
|
|
748
|
+
|
|
749
|
+
# Test dual arm IK with example poses
|
|
750
|
+
print("\n๐งช Testing dual arm IK with example poses...")
|
|
751
|
+
left_pose = {
|
|
752
|
+
'position': [0.3, 0.3, 0.60],
|
|
753
|
+
'orientation': [0.0, 0.0, 0.0, 1.0]
|
|
754
|
+
}
|
|
755
|
+
right_pose = {
|
|
756
|
+
'position': [-0.3, 0.3, 0.60],
|
|
757
|
+
'orientation': [0.0, 0.0, 0.0, 1.0]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
ik_success = robot.send_hand_poses(left_pose, right_pose, execution_time=3.0)
|
|
761
|
+
if ik_success:
|
|
762
|
+
print("โ
Dual arm IK test successful!")
|
|
763
|
+
else:
|
|
764
|
+
print("โ ๏ธ Dual arm IK test failed (expected if MoveIt not fully configured)")
|
|
765
|
+
|
|
766
|
+
# Get current state
|
|
767
|
+
current = robot.get_current_state()
|
|
768
|
+
if current:
|
|
769
|
+
print(f"\nCurrent robot state:")
|
|
770
|
+
print(f" Joints: {len(current.joint_names)}")
|
|
771
|
+
print(f" Timestamp: {current.timestamp}")
|
|
772
|
+
|
|
773
|
+
time.sleep(2)
|
|
774
|
+
robot.shutdown()
|
|
775
|
+
|
|
776
|
+
except Exception as e:
|
|
777
|
+
print(f"โ Failed to initialize robot interface: {e}")
|
|
778
|
+
print("Make sure Gazebo and MoveIt are running with the robot!")
|
|
779
|
+
import traceback
|
|
780
|
+
traceback.print_exc()
|