| @@ -1,198 +1,171 @@ | |||
| import torch | |||
| import numpy as np | |||
| import time | |||
| import pyarrow as pa | |||
| from dora import Node | |||
| import pytorch_kinematics as pk | |||
| from scipy.spatial.transform import Rotation | |||
| class FrankaController: | |||
| def __init__(self): | |||
| """ | |||
| Initialize the Franka controller with differential IK nullspace control | |||
| Initialize the Franka controller | |||
| """ | |||
| # Load the robot model for IK and FK | |||
| urdf_path = "../franka_panda/panda.urdf" | |||
| with open(urdf_path, 'rb') as f: | |||
| urdf_content = f.read() | |||
| # Build serial chain for kinematics | |||
| self.chain = pk.build_serial_chain_from_urdf(urdf_content, "panda_hand") | |||
| # Move to GPU if available | |||
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |||
| self.chain = self.chain.to(device=self.device) | |||
| # Control parameters | |||
| self.integration_dt = 0.1 | |||
| self.damping = 1e-4 | |||
| self.Kpos = 0.95 # Position gain | |||
| self.Kori = 0.95 # Orientation gain | |||
| self.Kn = np.array([10.0, 10.0, 10.0, 10.0, 5.0, 5.0, 5.0]) # Nullspace gains | |||
| self.max_angvel = 0.785 # Max joint velocity (rad/s) | |||
| self.max_angvel = 3.14 # Max joint velocity (rad/s) | |||
| # State variables | |||
| self.current_joint_pos = np.array([0.0, -0.785, 0.0, -2.356, 0.0, 1.571, 0.785]) | |||
| self.current_joint_vel = np.zeros(7) | |||
| self.dof = None | |||
| self.current_joint_pos = None # Full robot state | |||
| self.home_pos = None # Home position for arm joints only | |||
| self.target_pos = np.array([0.4, 0.0, 0.3]) # Conservative default target | |||
| self.target_rpy = [180.0, 0.0, 90.0] # Downward orientation | |||
| self.home_pos = np.array([0, 0, 0, -1.57079, 0, 1.57079, -0.7853]) # joint angles for home position | |||
| # Initialize timestamps | |||
| self.last_time = time.time() | |||
| print("FrankaController initialized with PyTorch Kinematics Differential IK") | |||
| # print(f"Chain DOF: {len(self.chain.get_joint_parameter_names())}") | |||
| self.current_ee_pose = None # End-effector pose | |||
| self.current_jacobian = None # Jacobian matrix | |||
| print("FrankaController initialized") | |||
| def _initialize_robot(self, positions, jacobian_dof=None): | |||
| """Initialize controller state with appropriate dimensions""" | |||
| self.full_joint_count = len(positions) | |||
| # Set DOF from Jacobian if available | |||
| self.dof = jacobian_dof if jacobian_dof is not None else self.full_joint_count | |||
| self.current_joint_pos = positions.copy() | |||
| self.home_pos = np.zeros(self.dof) | |||
| def get_target_rotation_matrix(self): | |||
| """Convert RPY to rotation matrix.""" | |||
| roll_rad, pitch_rad, yaw_rad = np.radians(self.target_rpy) | |||
| desired_rot = Rotation.from_euler('ZYX', [yaw_rad, pitch_rad, roll_rad]) | |||
| return desired_rot.as_matrix() | |||
| def compute_jacobian_pytorch(self, joint_angles): | |||
| """Compute Jacobian using PyTorch Kinematics.""" | |||
| # Convert to torch tensor | |||
| q = torch.tensor(joint_angles, device=self.device, dtype=torch.float32).unsqueeze(0) | |||
| # Compute Jacobian (returns 6x7 matrix: [linear_vel; angular_vel]) | |||
| J = self.chain.jacobian(q) # Shape: (1, 6, 7) | |||
| return J.squeeze(0).cpu().numpy() # Convert back to numpy (6, 7) | |||
| def get_current_ee_pose(self, joint_angles): | |||
| """Get current end-effector pose using forward kinematics.""" | |||
| # Convert to torch tensor | |||
| q = torch.tensor(joint_angles, device=self.device, dtype=torch.float32).unsqueeze(0) | |||
| # Forward kinematics | |||
| tf = self.chain.forward_kinematics(q) # Returns Transform3d | |||
| # Extract position and rotation | |||
| transform_matrix = tf.get_matrix().squeeze(0).cpu().numpy() # (4, 4) | |||
| position = transform_matrix[:3, 3] | |||
| rotation_matrix = transform_matrix[:3, :3] | |||
| return position, rotation_matrix | |||
| def update_joint_state(self, positions, velocities): | |||
| """Update the current joint state.""" | |||
| # Filter to only use first 7 joints | |||
| self.current_joint_pos = positions[:7] | |||
| self.current_joint_vel = velocities[:7] | |||
| def set_target_pose(self, pose_array): | |||
| """Set target pose from input array.""" | |||
| if len(pose_array) >= 3: | |||
| self.target_pos = np.array(pose_array[:3]) | |||
| # print(f"Updated target position: {self.target_pos}") | |||
| if len(pose_array) >= 6: | |||
| self.target_pos = np.array(pose_array[:3]) | |||
| if len(pose_array) == 6: | |||
| self.target_rpy = list(pose_array[3:6]) | |||
| # print(f"Updated target orientation (RPY): {self.target_rpy}") | |||
| else: | |||
| self.target_rpy = [180.0, 0.0, 90.0] # Default orientation if not provided | |||
| def update_jacobian(self, jacobian_flat, shape): | |||
| """Update current jacobian and initialize DOF if needed.""" | |||
| jacobian_dof = shape[1] | |||
| self.current_jacobian = jacobian_flat.reshape(shape) | |||
| if self.dof is None: | |||
| if self.current_joint_pos is not None: | |||
| self._initialize_robot(self.current_joint_pos, jacobian_dof) | |||
| else: | |||
| self.dof = jacobian_dof | |||
| elif self.dof != jacobian_dof: | |||
| self.dof = jacobian_dof | |||
| self.home_pos = np.zeros(self.dof) | |||
| def apply_differential_ik_control(self): | |||
| """Apply differential IK control with nullspace projection.""" | |||
| current_ee_pos, current_ee_rot = self.get_current_ee_pose(self.current_joint_pos) | |||
| if self.current_ee_pose is None or self.current_jacobian is None: | |||
| return self.current_joint_pos | |||
| current_ee_pos = self.current_ee_pose['position'] | |||
| current_ee_rpy = self.current_ee_pose['rpy'] | |||
| # Calculate position and orientation error | |||
| pos_error = self.target_pos - current_ee_pos | |||
| twist = np.zeros(6) | |||
| twist[:3] = self.Kpos * pos_error / self.integration_dt | |||
| current_rot = Rotation.from_matrix(current_ee_rot) | |||
| # Convert current RPY to rotation matrix | |||
| current_rot = Rotation.from_euler('XYZ', current_ee_rpy) | |||
| desired_rot = Rotation.from_matrix(self.get_target_rotation_matrix()) | |||
| rot_error = (desired_rot * current_rot.inv()).as_rotvec() | |||
| twist[3:] = self.Kori * rot_error / self.integration_dt | |||
| # Get Jacobian using PyTorch Kinematics | |||
| jac = self.compute_jacobian_pytorch(self.current_joint_pos) # (6, 7) | |||
| jac = self.current_jacobian | |||
| # Damped least squares inverse kinematics | |||
| diag = self.damping * np.eye(6) | |||
| dq = jac.T @ np.linalg.solve(jac @ jac.T + diag, twist) | |||
| # Nullspace control | |||
| jac_pinv = np.linalg.pinv(jac) | |||
| N = np.eye(7) - jac_pinv @ jac # Nullspace projection matrix | |||
| dq_null = self.Kn * (self.home_pos - self.current_joint_pos) # Nullspace velocity | |||
| dq += N @ dq_null # nullspace movement | |||
| # # Apply nullspace control | |||
| # # Calculate nullspace projection # uncomment to enable nullspace control | |||
| # current_arm = self.current_joint_pos[:self.dof] | |||
| # jac_pinv = np.linalg.pinv(jac) | |||
| # N = np.eye(self.dof) - jac_pinv @ jac | |||
| # # Apply gains to pull towards home position | |||
| # k_null = np.ones(self.dof) * 5.0 | |||
| # dq_null = k_null * (self.home_pos - current_arm) # Nullspace velocity | |||
| # dq += N @ dq_null # Nullspace movement | |||
| # Limit joint velocities | |||
| dq_abs_max = np.abs(dq).max() | |||
| if dq_abs_max > self.max_angvel: | |||
| dq *= self.max_angvel / dq_abs_max | |||
| # Integrate to get new joint positions | |||
| new_joints = self.current_joint_pos + dq * self.integration_dt | |||
| # Apply joint limits (simple clipping - you could load from URDF) | |||
| joint_limits = np.array([ | |||
| [-2.8973, 2.8973], | |||
| [-1.7628, 1.7628], | |||
| [-2.8973, 2.8973], | |||
| [-3.0718, -0.0698], | |||
| [-2.8973, 2.8973], | |||
| [-0.0175, 3.7525], | |||
| [-2.8973, 2.8973] | |||
| ]) | |||
| for i in range(7): | |||
| new_joints[i] = np.clip(new_joints[i], joint_limits[i][0], joint_limits[i][1]) | |||
| # Debug output | |||
| # pos_error_mag = np.linalg.norm(pos_error) | |||
| # rot_error_mag = np.linalg.norm(rot_error) | |||
| # print(f"Position error: {pos_error_mag:.4f}, Rotation error: {rot_error_mag:.4f}") | |||
| # print(f"Joint velocity magnitude: {np.linalg.norm(dq):.4f}") | |||
| # Create full joint command (apply IK result to arm joints, keep other joints unchanged) | |||
| new_joints = self.current_joint_pos.copy() | |||
| new_joints[:self.dof] += dq * self.integration_dt | |||
| return new_joints | |||
| def main(): | |||
| node = Node() | |||
| controller = FrankaController() | |||
| for event in node: | |||
| if event["type"] == "INPUT": | |||
| if event["id"] == "joint_positions": | |||
| joint_pos = event["value"].to_numpy() | |||
| controller.update_joint_state(joint_pos, controller.current_joint_vel) | |||
| # Apply differential IK control | |||
| joint_commands = controller.apply_differential_ik_control() | |||
| # Pad control to match full robot DOF if needed | |||
| if len(joint_commands) == 7: # Arm only | |||
| # Add zero control for gripper joints | |||
| full_control = np.zeros(9) | |||
| full_control[:7] = joint_commands | |||
| control_to_send = full_control | |||
| if controller.current_joint_pos is None: | |||
| controller._initialize_robot(joint_pos) | |||
| else: | |||
| control_to_send = joint_commands | |||
| controller.current_joint_pos = joint_pos | |||
| # Send control commands to the robot | |||
| # Request FK computation | |||
| node.send_output( | |||
| "joint_commands", | |||
| pa.array(control_to_send, type=pa.float32()), | |||
| metadata={"timestamp": time.time()} | |||
| "fk_request", | |||
| pa.array(controller.current_joint_pos, type=pa.float32()), | |||
| metadata={"encoding": "jointstate", "timestamp": time.time()} | |||
| ) | |||
| # Send current end-effector position | |||
| current_ee_pos, _ = controller.get_current_ee_pose(controller.current_joint_pos) | |||
| # Request Jacobian computation | |||
| node.send_output( | |||
| "ee_position", | |||
| pa.array(current_ee_pos, type=pa.float32()), | |||
| "jacobian_request", | |||
| pa.array(controller.current_joint_pos, type=pa.float32()), | |||
| metadata={"encoding": "jacobian", "timestamp": time.time()} | |||
| ) | |||
| joint_commands = controller.apply_differential_ik_control() | |||
| # Send control commands to the robot | |||
| node.send_output( | |||
| "joint_commands", | |||
| pa.array(joint_commands, type=pa.float32()), | |||
| metadata={"timestamp": time.time()} | |||
| ) | |||
| if event["id"] == "joint_velocities": | |||
| joint_vel = event["value"].to_numpy() | |||
| controller.update_joint_state(controller.current_joint_pos, joint_vel) | |||
| # Handle target pose updates | |||
| if event["id"] == "target_pose": | |||
| target_pose = event["value"].to_numpy() | |||
| # print(f"Received target pose from publisher: {target_pose}") | |||
| controller.set_target_pose(target_pose) | |||
| # Handle FK results | |||
| if event["id"] == "fk_result": | |||
| if event["metadata"]["encoding"] == "xyzrpy": | |||
| ee_pose = event["value"].to_numpy() | |||
| controller.current_ee_pose = {'position': ee_pose[:3],'rpy': ee_pose[3:6]} | |||
| # Handle Jacobian results | |||
| if event["id"] == "jacobian_result": | |||
| if event["metadata"]["encoding"] == "jacobian_result": | |||
| jacobian_flat = event["value"].to_numpy() | |||
| jacobian_shape = event["metadata"]["jacobian_shape"] | |||
| controller.update_jacobian(jacobian_flat, jacobian_shape) | |||
| if __name__ == "__main__": | |||
| main() | |||
| @@ -12,8 +12,8 @@ class PosePublisher: | |||
| [0.5, 0.5, 0.3, 180.0, 0.0, 90.0], | |||
| [0.6, 0.2, 0.5, 180.0, 0.0, 45.0], | |||
| [0.7, 0.1, 0.4, 90.0, 90.0, 90.0], | |||
| [0.5, -0.3, 0.6, 180.0, 0.0, 135.0], | |||
| [-0.3, -0.7, 0.2, 180.0, 0.0, 90.0], | |||
| [0.4, -0.3, 0.5, 180.0, 0.0, 135.0], | |||
| [-0.3, -0.6, 0.3, 180.0, 0.0, 90.0], | |||
| ] | |||
| self.current_pose_index = 0 | |||
| @@ -4,7 +4,7 @@ nodes: | |||
| path: dora-mujoco | |||
| inputs: | |||
| tick: dora/timer/millis/2 | |||
| control_input: franka_controller_pytorch/joint_commands | |||
| control_input: kuka_controller_pytorch/joint_commands | |||
| outputs: | |||
| - joint_positions | |||
| - joint_velocities | |||
| @@ -13,32 +13,36 @@ nodes: | |||
| env: | |||
| MODEL_NAME: "panda" | |||
| - id: franka_controller_pytorch | |||
| - id: kuka_controller_pytorch | |||
| path: nodes/franka_controller_pytorch.py | |||
| inputs: | |||
| joint_positions: mujoco_sim/joint_positions | |||
| joint_velocities: mujoco_sim/joint_velocities | |||
| target_pose: pose_publisher/target_pose | |||
| # fk_result: pytorch_kinematics/fk_result # Make sure this matches the ACTUAL output name | |||
| fk_result: pytorch_kinematics/fk_request | |||
| jacobian_result: pytorch_kinematics/jacobian_request | |||
| outputs: | |||
| - joint_commands | |||
| - fk_request | |||
| - ee_position | |||
| - jacobian_request | |||
| # - id: pytorch_kinematics | |||
| # build: pip install -e ../../../node-hub/dora-pytorch-kinematics | |||
| # path: dora-pytorch-kinematics | |||
| # inputs: | |||
| # fk_request: franka_controller_pytorch/fk_request | |||
| # outputs: | |||
| # - fk_result # Check this output name matches what this node produces | |||
| # env: | |||
| # URDF_PATH: "/home/shashwatgpatil/Downloads/panda.urdf" | |||
| # END_EFFECTOR_LINK: "panda_hand" | |||
| - id: pytorch_kinematics | |||
| build: pip install -e ../../../node-hub/dora-pytorch-kinematics | |||
| path: dora-pytorch-kinematics | |||
| inputs: | |||
| fk_request: kuka_controller_pytorch/fk_request | |||
| jacobian_request: kuka_controller_pytorch/jacobian_request | |||
| outputs: | |||
| - fk_request | |||
| - jacobian_request | |||
| env: | |||
| URDF_PATH: "/home/shashwatgpatil/Dora-rs/dora/examples/franka-mujoco-control/franka_panda/panda.urdf" | |||
| END_EFFECTOR_LINK: "panda_hand" | |||
| TRANSFORM: "0. 0. 0. 1. 0. 0. 0." # Pytorch kinematics uses wxyz format for quaternion | |||
| - id: pose_publisher | |||
| path: nodes/pose_publisher.py | |||
| inputs: | |||
| tick: dora/timer/millis/10000 | |||
| tick: dora/timer/millis/5000 | |||
| outputs: | |||
| - target_pose | |||
| @@ -20,22 +20,32 @@ nodes: | |||
| - actuator_controls | |||
| - sensor_data | |||
| env: | |||
| MODEL_NAME: "panda" | |||
| MODEL_NAME: "kuka_iiwa14" | |||
| - id: franka_gamepad_controller | |||
| path: nodes/franka_gamepad_controller.py | |||
| inputs: | |||
| joint_positions: mujoco_sim/joint_positions | |||
| joint_velocities: mujoco_sim/joint_velocities | |||
| raw_control: gamepad/raw_control | |||
| # target_pose: pose_publisher/target_pose # Optional | |||
| cmd_vel: gamepad/cmd_vel | |||
| fk_result: pytorch_kinematics/fk_request | |||
| jacobian_result: pytorch_kinematics/jacobian_request | |||
| outputs: | |||
| - joint_commands | |||
| - ee_position | |||
| - fk_request | |||
| - jacobian_request | |||
| # Optional: uncomment to also have programmatic control | |||
| # - id: pose_publisher | |||
| # path: ../02_target_pose_control/nodes/pose_publisher.py | |||
| # inputs: | |||
| # tick: dora/timer/millis/20000 # New pose every 20 seconds | |||
| # outputs: | |||
| # - target_pose | |||
| - id: pytorch_kinematics | |||
| build: pip install -e ../../../node-hub/dora-pytorch-kinematics | |||
| path: dora-pytorch-kinematics | |||
| inputs: | |||
| fk_request: franka_gamepad_controller/fk_request | |||
| jacobian_request: franka_gamepad_controller/jacobian_request | |||
| outputs: | |||
| - fk_request | |||
| - jacobian_request | |||
| env: | |||
| URDF_PATH: "/home/shashwatgpatil/Dora-rs/dora/examples/franka-mujoco-control/kuka_iiwa/model.urdf" | |||
| END_EFFECTOR_LINK: "lbr_iiwa_link_7" | |||
| TRANSFORM: "0. 0. 0. 1. 0. 0. 0." | |||
| @@ -1,104 +1,60 @@ | |||
| """Enhanced Franka controller with gamepad support using PyTorch Kinematics. | |||
| """ | |||
| import json | |||
| import time | |||
| import numpy as np | |||
| import pyarrow as pa | |||
| import torch | |||
| from dora import Node | |||
| import pytorch_kinematics as pk | |||
| from scipy.spatial.transform import Rotation | |||
| class EnhancedFrankaController: | |||
| """Franka controller with gamepad and target pose support using PyTorch Kinematics.""" | |||
| class GamepadFrankaController: | |||
| def __init__(self): | |||
| """Initialize controller with PyTorch Kinematics.""" | |||
| # Load the robot model for IK and FK | |||
| urdf_path = "../franka_panda/panda.urdf" | |||
| with open(urdf_path, 'rb') as f: | |||
| urdf_content = f.read() | |||
| # Build serial chain for kinematics | |||
| self.chain = pk.build_serial_chain_from_urdf(urdf_content, "panda_hand") | |||
| # Move to GPU if available | |||
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |||
| self.chain = self.chain.to(device=self.device) | |||
| # Control parameters | |||
| self.integration_dt = 0.1 | |||
| self.damping = 1e-4 | |||
| self.Kpos = 0.95 # Position gain | |||
| self.Kori = 0.95 # Orientation gain | |||
| self.Kn = np.array([10.0, 10.0, 10.0, 10.0, 5.0, 5.0, 5.0]) # Nullspace gains | |||
| self.max_angvel = 0.785 # Max joint velocity (rad/s) | |||
| self.max_angvel = 1.57 # Max joint velocity (rad/s) | |||
| # Gamepad control parameters | |||
| self.speed_scale = 0.5 | |||
| self.deadzone = 0.05 | |||
| self.last_input_source = None | |||
| # Gripper control parameters | |||
| self.gripper_range = [0, 255] | |||
| self.gripper_state = 0.0 # (0=closed, 1=open) | |||
| # Robot state variables | |||
| self.dof = None | |||
| self.current_joint_pos = None # Full robot state | |||
| self.home_pos = None # Home position for arm joints only | |||
| # Robot state | |||
| self.current_joint_pos = None | |||
| self.target_pos = np.array([0.55, 0.0, 0.6]) # Conservative default target | |||
| # Target pose (independent of DOF) | |||
| self.target_pos = np.array([0.4, 0.0, 0.3]) # Conservative default target | |||
| self.target_rpy = [180.0, 0.0, 90.0] # Downward orientation | |||
| self.home_pos = np.array([0, 0, 0, -1.67079, 0, 1.64079, -0.7853]) | |||
| print("Enhanced Franka Controller initialized with PyTorch Kinematics") | |||
| # print(f"Chain DOF: {len(self.chain.get_joint_parameter_names())}") | |||
| def apply_deadzone(self, value, deadzone=None): | |||
| """Apply deadzone to joystick input.""" | |||
| deadzone = deadzone or self.deadzone | |||
| return 0.0 if abs(value) < deadzone else value | |||
| def get_current_ee_pose(self, joint_angles): | |||
| """Get current end-effector pose using PyTorch Kinematics forward kinematics.""" | |||
| q = torch.tensor(joint_angles, device=self.device, dtype=torch.float32).unsqueeze(0) | |||
| # Forward kinematics | |||
| tf = self.chain.forward_kinematics(q) # Returns Transform3d | |||
| # Extract position and rotation | |||
| transform_matrix = tf.get_matrix().squeeze(0).cpu().numpy() # (4, 4) | |||
| position = transform_matrix[:3, 3] | |||
| rotation_matrix = transform_matrix[:3, :3] | |||
| # Cache for external computations | |||
| self.current_ee_pose = None # End-effector pose | |||
| self.current_jacobian = None # Jacobian matrix | |||
| return position, rotation_matrix | |||
| print("Gamepad Franka Controller initialized") | |||
| def compute_jacobian_pytorch(self, joint_angles): | |||
| """Compute Jacobian using PyTorch Kinematics.""" | |||
| q = torch.tensor(joint_angles, device=self.device, dtype=torch.float32).unsqueeze(0) | |||
| # Compute Jacobian (returns 6x7 matrix: [linear_vel; angular_vel]) | |||
| J = self.chain.jacobian(q) # Shape: (1, 6, 7) | |||
| def _initialize_robot(self, positions, jacobian_dof=None): | |||
| self.full_joint_count = len(positions) | |||
| self.dof = jacobian_dof if jacobian_dof is not None else self.full_joint_count | |||
| self.current_joint_pos = positions.copy() | |||
| self.home_pos = np.zeros(self.dof) | |||
| def process_cmd_vel(self, cmd_vel): | |||
| print(f"Processing cmd_vel: {cmd_vel}") | |||
| dx = cmd_vel[0] * self.speed_scale * 0.1 | |||
| dy = cmd_vel[1] * self.speed_scale * 0.1 | |||
| dz = cmd_vel[2] * self.speed_scale * 0.1 | |||
| return J.squeeze(0).cpu().numpy() # Convert back to numpy (6, 7) | |||
| # Update target position incrementally | |||
| if abs(dx) > 0 or abs(dy) > 0 or abs(dz) > 0: | |||
| self.target_pos += np.array([dx, dy, dz]) | |||
| def process_gamepad_input(self, raw_control): | |||
| """Process gamepad input exactly like the original.""" | |||
| axes = raw_control["axes"] | |||
| buttons = raw_control["buttons"] | |||
| # Reset position with START button | |||
| if len(buttons) > 9 and buttons[9]: | |||
| # Reset target to a position based on home joint angles | |||
| if self.current_joint_pos is not None: | |||
| # Use current joint positions to get home EE position | |||
| home_ee_pos, _ = self.get_current_ee_pose(self.home_pos) | |||
| self.target_pos = home_ee_pos.copy() | |||
| print("Reset target to home position") | |||
| # Gripper control with X and Y buttons (exactly like original) | |||
| if len(buttons) > 0 and buttons[0]: # X button - Close gripper | |||
| self.gripper_state = 0.0 | |||
| print("Gripper: CLOSED") | |||
| elif len(buttons) > 3 and buttons[3]: # Y button - Open gripper | |||
| self.gripper_state = 1.0 | |||
| print("Gripper: OPEN") | |||
| self.target_pos = np.array([0.4, 0.0, 0.3]) | |||
| print("Reset target to home position") | |||
| # Speed scaling with bumpers (LB/RB) | |||
| if len(buttons) > 4 and buttons[4]: # LB | |||
| @@ -107,149 +63,130 @@ class EnhancedFrankaController: | |||
| if len(buttons) > 5 and buttons[5]: # RB | |||
| self.speed_scale = min(1.0, self.speed_scale + 0.1) | |||
| print(f"Speed: {self.speed_scale:.1f}") | |||
| # Get cartesian control inputs with deadzone | |||
| dx = self.apply_deadzone(axes[0], self.deadzone) * self.speed_scale * 0.1 | |||
| dy = -self.apply_deadzone(axes[1], self.deadzone) * self.speed_scale * 0.1 | |||
| dz = -self.apply_deadzone(axes[3], self.deadzone) * self.speed_scale * 0.1 | |||
| # Update target position incrementally | |||
| if abs(dx) > 0 or abs(dy) > 0 or abs(dz) > 0: | |||
| self.target_pos += np.array([dx, dy, dz]) | |||
| self.last_input_source = "gamepad" | |||
| # print(f"Gamepad control: dx={dx:.3f}, dy={dy:.3f}, dz={dz:.3f}") | |||
| # print(f"New target: {self.target_pos}") | |||
| def process_target_pose(self, pose_array): | |||
| """Process target pose command.""" | |||
| if len(pose_array) >= 3: | |||
| self.target_pos = np.array(pose_array[:3]) | |||
| print(f"Updated target position: {self.target_pos}") | |||
| if len(pose_array) >= 6: | |||
| self.target_rpy = list(pose_array[3:6]) | |||
| print(f"Updated target orientation (RPY): {self.target_rpy}") | |||
| self.last_input_source = "target_pose" | |||
| def get_target_rotation_matrix(self): | |||
| """Convert RPY to rotation matrix.""" | |||
| roll_rad, pitch_rad, yaw_rad = np.radians(self.target_rpy) | |||
| desired_rot = Rotation.from_euler('ZYX', [yaw_rad, pitch_rad, roll_rad]) | |||
| return desired_rot.as_matrix() | |||
| def apply_cartesian_control(self, current_joints): | |||
| """Apply Cartesian control using PyTorch Kinematics differential IK.""" | |||
| self.current_joint_pos = current_joints[:7] | |||
| # Get current end-effector pose using PyTorch Kinematics FK | |||
| current_ee_pos, current_ee_rot = self.get_current_ee_pose(self.current_joint_pos) | |||
| # Calculate position error | |||
| pos_error = self.target_pos - current_ee_pos | |||
| def update_jacobian(self, jacobian_flat, shape): | |||
| jacobian_dof = shape[1] | |||
| self.current_jacobian = jacobian_flat.reshape(shape) | |||
| if self.dof is None: | |||
| if self.current_joint_pos is not None: | |||
| self._initialize_robot(self.current_joint_pos, jacobian_dof) | |||
| else: | |||
| self.dof = jacobian_dof | |||
| elif self.dof != jacobian_dof: | |||
| self.dof = jacobian_dof | |||
| self.home_pos = np.zeros(self.dof) | |||
| def apply_differential_ik_control(self): | |||
| if self.current_ee_pose is None or self.current_jacobian is None: | |||
| return self.current_joint_pos | |||
| current_ee_pos = self.current_ee_pose['position'] | |||
| current_ee_rpy = self.current_ee_pose['rpy'] | |||
| # Construct 6D twist (3 position + 3 orientation) | |||
| pos_error = self.target_pos - current_ee_pos | |||
| twist = np.zeros(6) | |||
| twist[:3] = self.Kpos * pos_error / self.integration_dt | |||
| # Orientation control | |||
| current_rot = Rotation.from_matrix(current_ee_rot) | |||
| # Convert current RPY to rotation matrix | |||
| current_rot = Rotation.from_euler('XYZ', current_ee_rpy) | |||
| desired_rot = Rotation.from_matrix(self.get_target_rotation_matrix()) | |||
| rot_error = (desired_rot * current_rot.inv()).as_rotvec() | |||
| twist[3:] = self.Kori * rot_error / self.integration_dt | |||
| jac = self.compute_jacobian_pytorch(self.current_joint_pos) # (6, 7) | |||
| jac = self.current_jacobian | |||
| # Damped least squares inverse kinematics | |||
| diag = self.damping * np.eye(6) | |||
| dq = jac.T @ np.linalg.solve(jac @ jac.T + diag, twist) | |||
| # Nullspace control - drive towards home position in nullspace | |||
| jac_pinv = np.linalg.pinv(jac) | |||
| N = np.eye(7) - jac_pinv @ jac # Nullspace projection matrix | |||
| dq_null = self.Kn * (self.home_pos - self.current_joint_pos) # Nullspace velocity | |||
| dq += N @ dq_null # Add nullspace movement | |||
| # Apply nullspace control if we have redundant DOF | |||
| if self.dof > 6: | |||
| current_arm = self.current_joint_pos[:self.dof] | |||
| jac_pinv = np.linalg.pinv(jac) | |||
| N = np.eye(self.dof) - jac_pinv @ jac | |||
| k_null = np.ones(self.dof) * 5.0 | |||
| dq_null = k_null * (self.home_pos - current_arm) | |||
| dq += N @ dq_null | |||
| # Limit joint velocities | |||
| dq_abs_max = np.abs(dq).max() | |||
| if dq_abs_max > self.max_angvel: | |||
| dq *= self.max_angvel / dq_abs_max | |||
| # Integrate to get new joint positions | |||
| new_joints = self.current_joint_pos + dq * self.integration_dt | |||
| # Apply joint limits (Franka Panda limits) | |||
| joint_limits = np.array([ | |||
| [-2.8973, 2.8973], | |||
| [-1.7628, 1.7628], | |||
| [-2.8973, 2.8973], | |||
| [-3.0718, -0.0698], | |||
| [-2.8973, 2.8973], | |||
| [-0.0175, 3.7525], | |||
| [-2.8973, 2.8973] | |||
| ]) | |||
| for i in range(7): | |||
| new_joints[i] = np.clip(new_joints[i], joint_limits[i][0], joint_limits[i][1]) | |||
| # Return 8-dimensional control: 7 arm joints + gripper | |||
| full_commands = np.zeros(8) | |||
| full_commands[:7] = new_joints | |||
| # Map gripper state to control range | |||
| full_commands[7] = self.gripper_range[0] if self.gripper_state < 0.5 else self.gripper_range[1] | |||
| # Create full joint command | |||
| new_joints = self.current_joint_pos.copy() | |||
| new_joints[:self.dof] += dq * self.integration_dt | |||
| # Debug output | |||
| # pos_error_mag = np.linalg.norm(pos_error) | |||
| # if pos_error_mag > 0.01: # Only print if there's significant error | |||
| # print(f"Position error: {pos_error_mag:.4f}") | |||
| return full_commands | |||
| return new_joints | |||
| def main(): | |||
| node = Node("franka_controller") | |||
| controller = EnhancedFrankaController() | |||
| node = Node() | |||
| controller = GamepadFrankaController() | |||
| print("Enhanced Franka Controller Node Started") | |||
| print("\nGamepad Controls:") | |||
| print(" Left Stick X/Y: Move in X/Y plane") | |||
| print(" Right Stick Y: Move up/down (Z)") | |||
| print(" LB/RB: Decrease/Increase speed") | |||
| print(" START: Reset to home position") | |||
| print(" X: Close gripper") | |||
| print(" Y: Open gripper") | |||
| print("\nAlso accepts target_pose commands") | |||
| print("Ready to receive inputs...") | |||
| print("Gamepad Franka Controller Started") | |||
| for event in node: | |||
| if event["type"] == "INPUT": | |||
| if event["id"] == "joint_positions": | |||
| # Update current state and compute commands | |||
| joint_positions = event["value"].to_numpy() | |||
| full_commands = controller.apply_cartesian_control(joint_positions) | |||
| joint_pos = event["value"].to_numpy() | |||
| if controller.current_joint_pos is None: | |||
| controller._initialize_robot(joint_pos) | |||
| else: | |||
| controller.current_joint_pos = joint_pos | |||
| # Request FK computation | |||
| node.send_output( | |||
| "joint_commands", | |||
| pa.array(full_commands, type=pa.float64()), | |||
| metadata={"timestamp": time.time()} | |||
| "fk_request", | |||
| pa.array(controller.current_joint_pos, type=pa.float32()), | |||
| metadata={"encoding": "jointstate", "timestamp": time.time()} | |||
| ) | |||
| # Request Jacobian computation | |||
| node.send_output( | |||
| "jacobian_request", | |||
| pa.array(controller.current_joint_pos, type=pa.float32()), | |||
| metadata={"encoding": "jacobian", "timestamp": time.time()} | |||
| ) | |||
| # Send current end-effector position | |||
| if controller.current_joint_pos is not None: | |||
| current_ee_pos, _ = controller.get_current_ee_pose(controller.current_joint_pos) | |||
| node.send_output( | |||
| "ee_position", | |||
| pa.array(current_ee_pos, type=pa.float64()), | |||
| metadata={"timestamp": time.time()} | |||
| ) | |||
| # Apply differential IK control | |||
| joint_commands = controller.apply_differential_ik_control() | |||
| # Send control commands to the robot | |||
| node.send_output( | |||
| "joint_commands", | |||
| pa.array(joint_commands, type=pa.float32()), | |||
| metadata={"timestamp": time.time()} | |||
| ) | |||
| elif event["id"] == "raw_control": | |||
| raw_control = json.loads(event["value"].to_pylist()[0]) | |||
| controller.process_gamepad_input(raw_control) | |||
| elif event["id"] == "target_pose": | |||
| pose_array = event["value"].to_numpy() | |||
| controller.process_target_pose(pose_array) | |||
| elif event["id"] == "cmd_vel": | |||
| cmd_vel = event["value"].to_numpy() | |||
| controller.process_cmd_vel(cmd_vel) | |||
| # Handle FK results | |||
| if event["id"] == "fk_result": | |||
| if event["metadata"].get("encoding") == "xyzrpy": | |||
| ee_pose = event["value"].to_numpy() | |||
| controller.current_ee_pose = {'position': ee_pose[:3], 'rpy': ee_pose[3:6]} | |||
| # Handle Jacobian results | |||
| if event["id"] == "jacobian_result": | |||
| if event["metadata"].get("encoding") == "jacobian_result": | |||
| jacobian_flat = event["value"].to_numpy() | |||
| jacobian_shape = event["metadata"]["jacobian_shape"] | |||
| controller.update_jacobian(jacobian_flat, jacobian_shape) | |||
| if __name__ == "__main__": | |||
| main() | |||
| @@ -0,0 +1,289 @@ | |||
| <?xml version="1.0" ?> | |||
| <!-- ======================================================================= --> | |||
| <!-- | This document was autogenerated by xacro from lbr_iiwa.urdf.xacro | --> | |||
| <!-- | Original xacro: https://github.com/rtkg/lbr_iiwa/archive/master.zip | --> | |||
| <!-- | EDITING THIS FILE BY HAND IS NOT RECOMMENDED | --> | |||
| <!-- | Changes (kohlhoff): | --> | |||
| <!-- | * Removed gazebo tags. | --> | |||
| <!-- | * Removed unused materials. | --> | |||
| <!-- | * Made mesh paths relative. | --> | |||
| <!-- | * Removed material fields from collision segments. | --> | |||
| <!-- | * Removed the self_collision_checking segment. | --> | |||
| <!-- | * Removed transmission segments, since they didn't match the | --> | |||
| <!-- | convention, will have to added back later. | --> | |||
| <!-- ======================================================================= --> | |||
| <!--LICENSE: --> | |||
| <!--Copyright (c) 2015, Robert Krug & Todor Stoyanov, AASS Research Center, --> | |||
| <!--Orebro University, Sweden --> | |||
| <!--All rights reserved. --> | |||
| <!-- --> | |||
| <!--Redistribution and use in source and binary forms, with or without --> | |||
| <!--modification, are permitted provided that the following conditions are --> | |||
| <!--met: --> | |||
| <!-- --> | |||
| <!--1. Redistributions of source code must retain the above copyright notice,--> | |||
| <!-- this list of conditions and the following disclaimer. --> | |||
| <!-- --> | |||
| <!--2. Redistributions in binary form must reproduce the above copyright --> | |||
| <!-- notice, this list of conditions and the following disclaimer in the --> | |||
| <!-- documentation and/or other materials provided with the distribution. --> | |||
| <!-- --> | |||
| <!--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS --> | |||
| <!--IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,--> | |||
| <!--THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR --> | |||
| <!--PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR --> | |||
| <!--CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, --> | |||
| <!--EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, --> | |||
| <!--PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR --> | |||
| <!--PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF --> | |||
| <!--LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING --> | |||
| <!--NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS --> | |||
| <!--SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> | |||
| <robot name="lbr_iiwa" xmlns:xacro="http://www.ros.org/wiki/xacro"> | |||
| <!-- Import Rviz colors --> | |||
| <material name="Grey"> | |||
| <color rgba="0.2 0.2 0.2 1.0"/> | |||
| </material> | |||
| <material name="Orange"> | |||
| <color rgba="1.0 0.423529411765 0.0392156862745 1.0"/> | |||
| </material> | |||
| <material name="Blue"> | |||
| <color rgba="0.5 0.7 1.0 1.0"/> | |||
| </material> | |||
| <!--Import the lbr iiwa macro --> | |||
| <!--Import Transmissions --> | |||
| <!--Include Utilities --> | |||
| <!--The following macros are adapted from the LWR 4 definitions of the RCPRG - https://github.com/RCPRG-ros-pkg/lwr_robot --> | |||
| <!--Little helper macros to define the inertia matrix needed for links.--> | |||
| <!--Cuboid--> | |||
| <!--Cylinder: length is along the y-axis! --> | |||
| <!--lbr--> | |||
| <link name="lbr_iiwa_link_0"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="-0.1 0 0.07"/> | |||
| <!--Increase mass from 5 Kg original to provide a stable base to carry the | |||
| arm.--> | |||
| <mass value="0.0"/> | |||
| <inertia ixx="0.05" ixy="0" ixz="0" iyy="0.06" iyz="0" izz="0.03"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_0.obj"/> | |||
| </geometry> | |||
| <material name="Grey"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_0.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_0 and link_1 --> | |||
| <joint name="lbr_iiwa_joint_1" type="revolute"> | |||
| <parent link="lbr_iiwa_link_0"/> | |||
| <child link="lbr_iiwa_link_1"/> | |||
| <origin rpy="0 0 0" xyz="0 0 0.1575"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-2.96705972839" upper="2.96705972839" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_1"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0 -0.03 0.12"/> | |||
| <mass value="4"/> | |||
| <inertia ixx="0.1" ixy="0" ixz="0" iyy="0.09" iyz="0" izz="0.02"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_1.obj"/> | |||
| </geometry> | |||
| <material name="Blue"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_1.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_1 and link_2 --> | |||
| <joint name="lbr_iiwa_joint_2" type="revolute"> | |||
| <parent link="lbr_iiwa_link_1"/> | |||
| <child link="lbr_iiwa_link_2"/> | |||
| <origin rpy="1.57079632679 0 3.14159265359" xyz="0 0 0.2025"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-2.09439510239" upper="2.09439510239" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_2"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0.0003 0.059 0.042"/> | |||
| <mass value="4"/> | |||
| <inertia ixx="0.05" ixy="0" ixz="0" iyy="0.018" iyz="0" izz="0.044"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_2.obj"/> | |||
| </geometry> | |||
| <material name="Blue"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_2.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_2 and link_3 --> | |||
| <joint name="lbr_iiwa_joint_3" type="revolute"> | |||
| <parent link="lbr_iiwa_link_2"/> | |||
| <child link="lbr_iiwa_link_3"/> | |||
| <origin rpy="1.57079632679 0 3.14159265359" xyz="0 0.2045 0"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-2.96705972839" upper="2.96705972839" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_3"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0 0.03 0.13"/> | |||
| <mass value="3"/> | |||
| <inertia ixx="0.08" ixy="0" ixz="0" iyy="0.075" iyz="0" izz="0.01"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_3.obj"/> | |||
| </geometry> | |||
| <material name="Orange"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_3.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_3 and link_4 --> | |||
| <joint name="lbr_iiwa_joint_4" type="revolute"> | |||
| <parent link="lbr_iiwa_link_3"/> | |||
| <child link="lbr_iiwa_link_4"/> | |||
| <origin rpy="1.57079632679 0 0" xyz="0 0 0.2155"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-2.09439510239" upper="2.09439510239" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_4"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0 0.067 0.034"/> | |||
| <mass value="2.7"/> | |||
| <inertia ixx="0.03" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.029"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_4.obj"/> | |||
| </geometry> | |||
| <material name="Blue"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_4.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_4 and link_5 --> | |||
| <joint name="lbr_iiwa_joint_5" type="revolute"> | |||
| <parent link="lbr_iiwa_link_4"/> | |||
| <child link="lbr_iiwa_link_5"/> | |||
| <origin rpy="-1.57079632679 3.14159265359 0" xyz="0 0.1845 0"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-2.96705972839" upper="2.96705972839" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_5"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0.0001 0.021 0.076"/> | |||
| <mass value="1.7"/> | |||
| <inertia ixx="0.02" ixy="0" ixz="0" iyy="0.018" iyz="0" izz="0.005"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_5.obj"/> | |||
| </geometry> | |||
| <material name="Blue"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_5.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_5 and link_6 --> | |||
| <joint name="lbr_iiwa_joint_6" type="revolute"> | |||
| <parent link="lbr_iiwa_link_5"/> | |||
| <child link="lbr_iiwa_link_6"/> | |||
| <origin rpy="1.57079632679 0 0" xyz="0 0 0.2155"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-2.09439510239" upper="2.09439510239" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_6"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0 0.0006 0.0004"/> | |||
| <mass value="1.8"/> | |||
| <inertia ixx="0.005" ixy="0" ixz="0" iyy="0.0036" iyz="0" izz="0.0047"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_6.obj"/> | |||
| </geometry> | |||
| <material name="Orange"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_6.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| <!-- joint between link_6 and link_7 --> | |||
| <joint name="lbr_iiwa_joint_7" type="revolute"> | |||
| <parent link="lbr_iiwa_link_6"/> | |||
| <child link="lbr_iiwa_link_7"/> | |||
| <origin rpy="-1.57079632679 3.14159265359 0" xyz="0 0.081 0"/> | |||
| <axis xyz="0 0 1"/> | |||
| <limit effort="300" lower="-3.05432619099" upper="3.05432619099" velocity="10"/> | |||
| <dynamics damping="0.5"/> | |||
| </joint> | |||
| <link name="lbr_iiwa_link_7"> | |||
| <inertial> | |||
| <origin rpy="0 0 0" xyz="0 0 0.02"/> | |||
| <mass value="0.3"/> | |||
| <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> | |||
| </inertial> | |||
| <visual> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_7.obj"/> | |||
| </geometry> | |||
| <material name="Grey"/> | |||
| </visual> | |||
| <collision> | |||
| <origin rpy="0 0 0" xyz="0 0 0"/> | |||
| <geometry> | |||
| <mesh filename="meshes/link_7.stl"/> | |||
| </geometry> | |||
| </collision> | |||
| </link> | |||
| </robot> | |||
| @@ -10,9 +10,9 @@ import torch | |||
| from dora import Node | |||
| from pytorch_kinematics.transforms.rotation_conversions import matrix_to_euler_angles | |||
| TRANSFORM = np.array(os.getenv("TRANSFORM", "0. 0. 0. 0. 0. 0. 1.").split(" ")).astype( | |||
| TRANSFORM = np.array(os.getenv("TRANSFORM", "0. 0. 0. 1. 0. 0. 0.").split(" ")).astype( | |||
| np.float32, | |||
| ) | |||
| ) # wxyz format | |||
| pos = torch.tensor([TRANSFORM[0], TRANSFORM[1], TRANSFORM[2]]) | |||
| rot = torch.tensor( | |||
| [ | |||
| @@ -117,15 +117,21 @@ class RobotKinematics: | |||
| ) | |||
| if j.ndim == 1: | |||
| if j.shape[0] != self.num_joints: | |||
| raise ValueError(f"Expected {self.num_joints} joints, got {j.shape[0]}") | |||
| # Handle case with extra joints (e.g., gripper joints) | |||
| if j.shape[0] > self.num_joints: | |||
| j = j[:self.num_joints] # Truncate griper or extra joints | |||
| elif j.shape[0] < self.num_joints: | |||
| raise ValueError(f"Expected at least {self.num_joints} joints, got {j.shape[0]}") | |||
| if batch_dim_required: | |||
| j = j.unsqueeze(0) # Add batch dimension if needed (e.g., shape (1, N)) | |||
| j = j.unsqueeze(0) # Add batch dimension if needed | |||
| elif j.ndim == 2: | |||
| if j.shape[1] != self.num_joints: | |||
| raise ValueError( | |||
| f"Expected {self.num_joints} joints (dim 1), got {j.shape[1]}", | |||
| ) | |||
| # Handle batched input with extra joints | |||
| if j.shape[1] > self.num_joints: | |||
| j = j[:, :self.num_joints] # Truncate griper or extra joints | |||
| elif j.shape[1] < self.num_joints: | |||
| raise ValueError(f"Expected at least {self.num_joints} joints (dim 1), got {j.shape[1]}") | |||
| if batch_dim_required and j.shape[0] != 1: | |||
| # Most common IK solvers expect batch=1 for initial guess, FK can handle batches | |||
| # Relaxing this check slightly, but user should be aware for IK | |||
| @@ -222,6 +228,55 @@ class RobotKinematics: | |||
| return None | |||
| return solution_angles.solutions.detach() | |||
| def compute_jacobian( | |||
| self, joint_angles: Union[List[float], torch.Tensor] | |||
| ) -> torch.Tensor: | |||
| """Compute Jacobian matrix using PyTorch Kinematics. | |||
| Args: | |||
| joint_angles (Union[List[float], torch.Tensor]): Joint angles (radians). | |||
| Shape (num_joints,) or (B, num_joints). | |||
| Returns: | |||
| torch.Tensor: Jacobian matrix of shape (B, 6, num_joints) or (6, num_joints) | |||
| where rows are [linear_vel_x, linear_vel_y, linear_vel_z, | |||
| angular_vel_x, angular_vel_y, angular_vel_z] | |||
| """ | |||
| angles_tensor = self._prepare_joint_tensor( | |||
| joint_angles, batch_dim_required=False | |||
| ) | |||
| # Ensure we have batch dimension for jacobian computation | |||
| if angles_tensor.ndim == 1: | |||
| angles_tensor = angles_tensor.unsqueeze(0) | |||
| squeeze_output = True | |||
| else: | |||
| squeeze_output = False | |||
| # Compute Jacobian (returns shape: (B, 6, num_joints)) | |||
| J = self.chain.jacobian(angles_tensor) | |||
| # Remove batch dimension if input was 1D | |||
| if squeeze_output: | |||
| J = J.squeeze(0) | |||
| return J | |||
| def compute_jacobian_numpy( | |||
| self, joint_angles: Union[List[float], torch.Tensor, np.ndarray] | |||
| ) -> np.ndarray: | |||
| """Compute Jacobian matrix and return as numpy array. | |||
| Args: | |||
| joint_angles: Joint angles (radians). Can be list, torch.Tensor, or np.ndarray. | |||
| Shape (num_joints,) or (B, num_joints). | |||
| Returns: | |||
| np.ndarray: Jacobian matrix as numpy array of shape (6, num_joints) or (B, 6, num_joints) | |||
| """ | |||
| J = self.compute_jacobian(joint_angles) | |||
| return J.cpu().numpy() | |||
| def main(): | |||
| """TODO: Add docstring.""" | |||
| @@ -293,6 +348,18 @@ def main(): | |||
| target = pa.array(target.ravel(), type=pa.float32()) | |||
| metadata["encoding"] = "xyzrpy" | |||
| node.send_output(event["id"], target, metadata=metadata) | |||
| case "jacobian": | |||
| # Compute Jacobian matrix | |||
| joint_angles = event["value"].to_numpy() | |||
| jacobian = robot.compute_jacobian_numpy(joint_angles) | |||
| jacobian_flat = jacobian.flatten() | |||
| jacobian_array = pa.array(jacobian_flat, type=pa.float32()) | |||
| metadata["encoding"] = "jacobian_result" | |||
| metadata["jacobian_shape"] = list(jacobian.shape) | |||
| node.send_output(event["id"], jacobian_array, metadata=metadata) | |||
| if __name__ == "__main__": | |||
| @@ -1,9 +1,4 @@ | |||
| """Gamepad controller node for Dora. | |||
| This module provides a Dora node that reads input from a controller and publishes: | |||
| 1. velocity commands for robot control | |||
| 2. raw controller state for debugging and custom mappings | |||
| """ | |||
| """Gamepad controller node for Dora.""" | |||
| from dora import Node | |||
| import pygame | |||
| @@ -11,7 +6,7 @@ import pyarrow as pa | |||
| import json | |||
| class Controller: | |||
| """controller mapping.""" | |||
| """Controller mapping.""" | |||
| def __init__(self): | |||
| """Change this according to your controller mapping. Currently Logitech F710.""" | |||
| @@ -33,8 +28,9 @@ class Controller: | |||
| 'BACK': 8, | |||
| 'START': 9, | |||
| 'LEFT-STICK': 10, | |||
| 'RIGHT-STICK': 11 | |||
| 'RIGHT-STICK': 11, | |||
| } | |||
| self.hatIndex = 0 # Index of the D-pad hat | |||
| def main(): | |||
| node = Node("gamepad") | |||
| @@ -51,8 +47,9 @@ def main(): | |||
| controller = Controller() | |||
| max_linear_speed = 1.0 # Maximum speed in m/s | |||
| max_angular_speed = 1.5 # Maximum angular speed in rad/s | |||
| move_speed = 0.05 # Fixed increment for D-pad | |||
| max_linear_z_speed = 0.1 # Maximum linear Z speed | |||
| max_angular_speed = 0.8 # Maximum angular speed | |||
| print(f"Detected controller: {joystick.get_name()}") | |||
| print(f"Number of axes: {joystick.get_numaxes()}") | |||
| @@ -66,30 +63,64 @@ def main(): | |||
| # Get all controller states | |||
| axes = [joystick.get_axis(i) for i in range(joystick.get_numaxes())] | |||
| buttons = [joystick.get_button(i) for i in range(joystick.get_numbuttons())] | |||
| # Get hat state (D-pad) | |||
| dpad_x, dpad_y = 0, 0 | |||
| if joystick.get_numhats() > 0: | |||
| dpad_x, dpad_y = joystick.get_hat(controller.hatIndex) | |||
| # Create raw control state | |||
| raw_control = { | |||
| "axes": axes, | |||
| "buttons": buttons, | |||
| "hats": [[dpad_x, dpad_y]], | |||
| "mapping": { | |||
| "axes": controller.axisNames, | |||
| "buttons": controller.buttonNames | |||
| } | |||
| } | |||
| # print("raw_control:", raw_control) # uncomment for debugging and re-map | |||
| # Regular cmd_vel processing | |||
| forward = -joystick.get_axis(controller.axisNames['LEFT-Y']) | |||
| turn = -joystick.get_axis(controller.axisNames['LEFT-X']) | |||
| # cmd_vel processing: | |||
| # 1. D-pad vertical for X axis | |||
| # 2. D-pad horizontal for Y axis | |||
| # 3. Right stick vertical for Z axis | |||
| # 4. Right stick horizontal for rotation around Z | |||
| # 5. Left stick vertical for rotation around X | |||
| # 6. Left stick horizontal for rotation around Y | |||
| deadzone = 0.05 | |||
| forward = 0.0 if abs(forward) < deadzone else forward | |||
| turn = 0.0 if abs(turn) < deadzone else turn | |||
| # Linear X velocity from D-pad vertical | |||
| linear_x = 0.0 | |||
| if dpad_y != 0: | |||
| linear_x = dpad_y * move_speed | |||
| # Linear Y velocity from D-pad horizontal | |||
| linear_y = 0.0 | |||
| if dpad_x != 0: | |||
| linear_y = dpad_x * move_speed | |||
| forward_speed = forward * max_linear_speed | |||
| turn_speed = turn * max_angular_speed | |||
| cmd_vel = [forward_speed, 0.0, 0.0, 0.0, 0.0, turn_speed] | |||
| # Linear Z velocity from right stick vertical | |||
| right_y = -joystick.get_axis(controller.axisNames['RIGHT-Y']) | |||
| right_y = 0.0 if abs(right_y) < deadzone else right_y | |||
| linear_z = right_y * max_linear_z_speed | |||
| # Angular Z velocity (rotation) from right stick horizontal | |||
| right_x = -joystick.get_axis(controller.axisNames['RIGHT-X']) | |||
| right_x = 0.0 if abs(right_x) < deadzone else right_x | |||
| angular_z = right_x * max_angular_speed | |||
| # Angular X velocity from left stick vertical | |||
| left_y = -joystick.get_axis(controller.axisNames['LEFT-Y']) | |||
| left_y = 0.0 if abs(left_y) < deadzone else left_y | |||
| angular_x = left_y * max_angular_speed | |||
| # Angular Y velocity from left stick horizontal | |||
| left_x = -joystick.get_axis(controller.axisNames['LEFT-X']) | |||
| left_x = 0.0 if abs(left_x) < deadzone else left_x | |||
| angular_y = left_x * max_angular_speed | |||
| cmd_vel = [linear_x, linear_y, linear_z, angular_x, angular_y, angular_z] | |||
| node.send_output( | |||
| output_id="cmd_vel", | |||
| @@ -107,7 +138,6 @@ def main(): | |||
| print("\nExiting...") | |||
| finally: | |||
| pygame.quit() | |||
| # Send zero velocity at cleanup | |||
| zero_cmd = [0.0] * 6 | |||
| node.send_output( | |||
| output_id="cmd_vel", | |||
| @@ -116,4 +146,4 @@ def main(): | |||
| ) | |||
| if __name__ == "__main__": | |||
| main() | |||
| main() | |||