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.

@@ -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()