reachy2_sdk.parts.arm
Reachy Arm module.
Handles all specific methods to an Arm (left and/or right).
1"""Reachy Arm module. 2 3Handles all specific methods to an Arm (left and/or right). 4""" 5 6import time 7from typing import Any, Dict, List, Optional, overload 8 9import grpc 10import numpy as np 11import numpy.typing as npt 12from google.protobuf.wrappers_pb2 import FloatValue 13from reachy2_sdk_api.arm_pb2 import Arm as Arm_proto 14from reachy2_sdk_api.arm_pb2 import ( # ArmLimits,; ArmTemperatures, 15 ArmCartesianGoal, 16 ArmComponentsCommands, 17 ArmEndEffector, 18 ArmFKRequest, 19 ArmIKRequest, 20 ArmJointGoal, 21 ArmJoints, 22 ArmState, 23 ArmStatus, 24 CustomArmJoints, 25) 26from reachy2_sdk_api.arm_pb2_grpc import ArmServiceStub 27from reachy2_sdk_api.goto_pb2 import ( 28 CartesianGoal, 29 CustomJointGoal, 30 EllipticalGoToParameters, 31 GoToId, 32 GoToRequest, 33 JointsGoal, 34) 35from reachy2_sdk_api.goto_pb2_grpc import GoToServiceStub 36from reachy2_sdk_api.hand_pb2 import Hand as HandState 37from reachy2_sdk_api.hand_pb2 import Hand as Hand_proto 38from reachy2_sdk_api.hand_pb2 import HandType 39from reachy2_sdk_api.kinematics_pb2 import Matrix4x4 40 41from ..grippers.parallel_gripper import ParallelGripper 42from ..orbita.orbita2d import Orbita2d 43from ..orbita.orbita3d import Orbita3d 44from ..utils.utils import ( 45 arm_position_to_list, 46 get_grpc_arc_direction, 47 get_grpc_interpolation_mode, 48 get_grpc_interpolation_space, 49 list_to_arm_position, 50 matrix_from_euler_angles, 51 recompose_matrix, 52 rotate_in_self, 53 translate_in_self, 54) 55from .goto_based_part import IGoToBasedPart 56from .hand import Hand 57from .joints_based_part import JointsBasedPart 58 59 60class Arm(JointsBasedPart, IGoToBasedPart): 61 """Reachy Arm module. 62 63 Handles specific functionalities for the arm (left and/or right), including: 64 - Forward and inverse kinematics 65 - Goto functions for movement 66 - Turning the arm on and off 67 - Cartesian interpolation for movements 68 69 Attributes: 70 shoulder (Orbita2d): The shoulder actuator of the arm. 71 elbow (Orbita2d): The elbow actuator of the arm. 72 wrist (Orbita3d): The wrist actuator of the arm. 73 gripper (Optional[Hand]): The gripper of the arm, if initialized. 74 """ 75 76 def __init__( 77 self, 78 arm_msg: Arm_proto, 79 initial_state: ArmState, 80 grpc_channel: grpc.Channel, 81 goto_stub: GoToServiceStub, 82 ) -> None: 83 """Initialize an Arm instance. 84 85 This constructor sets up the arm's gRPC communication and initializes its actuators 86 (shoulder, elbow, and wrist). Optionally, a gripper can also be configured. 87 88 Args: 89 arm_msg: The protobuf message containing the arm's configuration details. 90 initial_state: The initial state of the arm's actuators. 91 grpc_channel: The gRPC channel used for communication with the arm's server. 92 goto_stub: The gRPC stub for controlling goto movements. 93 """ 94 JointsBasedPart.__init__(self, arm_msg, grpc_channel, ArmServiceStub(grpc_channel)) 95 IGoToBasedPart.__init__(self, self._part_id, goto_stub) 96 97 self._setup_arm(arm_msg, initial_state) 98 self._gripper: Optional[Hand] = None 99 100 self._actuators: Dict[str, Orbita2d | Orbita3d | Hand] = {} 101 self._actuators["shoulder"] = self.shoulder 102 self._actuators["elbow"] = self.elbow 103 self._actuators["wrist"] = self.wrist 104 105 def _setup_arm(self, arm: Arm_proto, initial_state: ArmState) -> None: 106 """Initialize the arm's actuators (shoulder, elbow, and wrist) based on the arm's description and initial state. 107 108 Args: 109 arm: The arm description used to set up the actuators, including the shoulder, 110 elbow, and wrist. The method creates instances of `Orbita2d` for the shoulder and 111 elbow, and an instance of `Orbita3d` for the wrist. 112 initial_state: The initial state of the arm's actuators, containing the starting 113 positions or states of the shoulder, elbow, and wrist. This information is used to 114 initialize the corresponding actuators. 115 """ 116 description = arm.description 117 self._shoulder = Orbita2d( 118 uid=description.shoulder.id.id, 119 name=description.shoulder.id.name, 120 axis1=description.shoulder.axis_1, 121 axis2=description.shoulder.axis_2, 122 initial_state=initial_state.shoulder_state, 123 grpc_channel=self._grpc_channel, 124 part=self, 125 joints_position_order=[ArmJoints.SHOULDER_PITCH, ArmJoints.SHOULDER_ROLL], 126 ) 127 self._elbow = Orbita2d( 128 uid=description.elbow.id.id, 129 name=description.elbow.id.name, 130 axis1=description.elbow.axis_1, 131 axis2=description.elbow.axis_2, 132 initial_state=initial_state.elbow_state, 133 grpc_channel=self._grpc_channel, 134 part=self, 135 joints_position_order=[ArmJoints.ELBOW_YAW, ArmJoints.ELBOW_PITCH], 136 ) 137 self._wrist = Orbita3d( 138 uid=description.wrist.id.id, 139 name=description.wrist.id.name, 140 initial_state=initial_state.wrist_state, 141 grpc_channel=self._grpc_channel, 142 part=self, 143 joints_position_order=[ArmJoints.WRIST_ROLL, ArmJoints.WRIST_PITCH, ArmJoints.WRIST_YAW], 144 ) 145 146 def _init_hand(self, hand: Hand_proto, hand_initial_state: HandState) -> None: 147 if hand.type == HandType.PARALLEL_GRIPPER: 148 self._gripper = ParallelGripper(hand, hand_initial_state, self._grpc_channel, self._goto_stub) 149 self._actuators["gripper"] = self._gripper 150 151 @property 152 def shoulder(self) -> Orbita2d: 153 """Get the shoulder actuator of the arm.""" 154 return self._shoulder 155 156 @property 157 def elbow(self) -> Orbita2d: 158 """Get the elbow actuator of the arm.""" 159 return self._elbow 160 161 @property 162 def wrist(self) -> Orbita3d: 163 """Get the wrist actuator of the arm.""" 164 return self._wrist 165 166 @property 167 def gripper(self) -> Optional[Hand]: 168 """Get the gripper of the arm, or None if not set.""" 169 return self._gripper 170 171 def __repr__(self) -> str: 172 """Clean representation of an Arm.""" 173 s = "\n\t".join([act_name + ": " + str(actuator) for act_name, actuator in self._actuators.items()]) 174 return f"""<Arm on={self.is_on()} actuators=\n\t{ 175 s 176 }\n>""" 177 178 def turn_on(self) -> None: 179 """Turn on all motors of the part, making all arm motors stiff. 180 181 If a gripper is present, it will also be turned on. 182 """ 183 if self._gripper is not None: 184 self._gripper._turn_on() 185 super().turn_on() 186 187 def turn_off(self) -> None: 188 """Turn off all motors of the part, making all arm motors compliant. 189 190 If a gripper is present, it will also be turned off. 191 """ 192 if self._gripper is not None: 193 self._gripper._turn_off() 194 super().turn_off() 195 196 def turn_off_smoothly(self) -> None: 197 """Gradually reduce the torque limit of all motors over 3 seconds before turning them off. 198 199 This function decreases the torque limit in steps until the motors are turned off. 200 It then restores the torque limit to its original value. 201 """ 202 torque_limit_low = 35 203 torque_limit_high = 100 204 duration = 3 205 206 self.set_torque_limits(torque_limit_low) 207 self.goto_posture(duration=duration, wait_for_goto_end=False) 208 209 countingTime = 0 210 while countingTime < duration: 211 time.sleep(1) 212 torque_limit_low -= 10 213 self.set_torque_limits(torque_limit_low) 214 countingTime += 1 215 216 super().turn_off() 217 self.set_torque_limits(torque_limit_high) 218 219 def _turn_on(self) -> None: 220 """Turn on all motors of the part. 221 222 This will make all arm motors stiff. If a gripper is present, it will also be turned on. 223 """ 224 if self._gripper is not None: 225 self._gripper._turn_on() 226 super()._turn_on() 227 228 def _turn_off(self) -> None: 229 """Turn off all motors of the part. 230 231 This will make all arm motors compliant. If a gripper is present, it will also be turned off. 232 """ 233 if self._gripper is not None: 234 self._gripper._turn_off() 235 super()._turn_off() 236 237 def is_on(self, check_gripper: bool = True) -> bool: 238 """Check if all actuators of the arm are stiff. 239 240 Returns: 241 `True` if all actuators of the arm are stiff, `False` otherwise. 242 """ 243 if not check_gripper: 244 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 245 if not actuator.is_on(): 246 return False 247 return True 248 return super().is_on() 249 250 def is_off(self, check_gripper: bool = True) -> bool: 251 """Check if all actuators of the arm are compliant. 252 253 Returns: 254 `True` if all actuators of the arm are compliant, `False` otherwise. 255 """ 256 if not check_gripper: 257 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 258 if not actuator.is_off(): 259 return False 260 return True 261 return super().is_off() 262 263 def get_current_positions(self, degrees: bool = True) -> List[float]: 264 """Return the current joint positions of the arm, either in degrees or radians. 265 266 Args: 267 degrees: Specifies whether the joint positions should be returned in degrees. 268 If set to `True`, the positions are returned in degrees; otherwise, they are returned in radians. 269 Defaults to `True`. 270 271 Returns: 272 A list of float values representing the current joint positions of the arm in the 273 following order: [shoulder_pitch, shoulder_roll, elbow_yaw, elbow_pitch, wrist_roll, wrist_pitch, 274 wrist_yaw]. 275 """ 276 response = self._stub.GetJointPosition(self._part_id) 277 positions: List[float] = arm_position_to_list(response, degrees) 278 return positions 279 280 def forward_kinematics( 281 self, joints_positions: Optional[List[float]] = None, degrees: bool = True 282 ) -> npt.NDArray[np.float64]: 283 """Compute the forward kinematics of the arm and return a 4x4 pose matrix. 284 285 The pose matrix is expressed in Reachy coordinate system. 286 287 Args: 288 joints_positions: A list of float values representing the positions of the joints 289 in the arm. If not provided, the current robot joints positions are used. Defaults to None. 290 degrees: Indicates whether the joint positions are in degrees or radians. 291 If `True`, the positions are in degrees; if `False`, in radians. Defaults to True. 292 293 Returns: 294 A 4x4 pose matrix as a NumPy array, expressed in Reachy coordinate system. 295 296 Raises: 297 ValueError: If `joints_positions` is provided and its length is not 7. 298 ValueError: If no solution is found for the given joint positions. 299 """ 300 req_params = { 301 "id": self._part_id, 302 } 303 if joints_positions is None: 304 present_joints_positions = [ 305 joint.present_position for orbita in self._actuators.values() for joint in orbita._joints.values() 306 ] 307 req_params["position"] = list_to_arm_position(present_joints_positions, degrees) 308 309 else: 310 if len(joints_positions) != 7: 311 raise ValueError(f"joints_positions should be length 7 (got {len(joints_positions)} instead)!") 312 req_params["position"] = list_to_arm_position(joints_positions, degrees) 313 req = ArmFKRequest(**req_params) 314 resp = self._stub.ComputeArmFK(req) 315 if not resp.success: 316 raise ValueError(f"No solution found for the given joints ({joints_positions})!") 317 318 return np.array(resp.end_effector.pose.data).reshape((4, 4)) 319 320 def inverse_kinematics( 321 self, 322 target: npt.NDArray[np.float64], 323 q0: Optional[List[float]] = None, 324 degrees: bool = True, 325 ) -> List[float]: 326 """Compute a joint configuration to reach a specified target pose for the arm end-effector. 327 328 Args: 329 target: A 4x4 homogeneous pose matrix representing the target pose in 330 Reachy coordinate system, provided as a NumPy array. 331 q0: An optional initial joint configuration for the arm. If provided, the 332 algorithm will use it as a starting point for finding a solution. Defaults to None. 333 degrees: Indicates whether the returned joint angles should be in degrees or radians. 334 If `True`, angles are in degrees; if `False`, in radians. Defaults to True. 335 round: Number of decimal places to round the computed joint angles to before 336 returning. If None, no rounding is performed. Defaults to None. 337 338 Returns: 339 A list of joint angles representing the solution to reach the target pose, in the following order: 340 [shoulder_pitch, shoulder_roll, elbo_yaw, elbow_pitch, wrist.roll, wrist.pitch, wrist.yaw]. 341 342 Raises: 343 ValueError: If the target shape is not (4, 4). 344 ValueError: If the length of `q0` is not 7. 345 ValueError: If vectorized kinematics is attempted (unsupported). 346 ValueError: If no solution is found for the given target. 347 """ 348 if target.shape != (4, 4): 349 raise ValueError("target shape should be (4, 4) (got {target.shape} instead)!") 350 351 if q0 is not None and (len(q0) != 7): 352 raise ValueError(f"q0 should be length 7 (got {len(q0)} instead)!") 353 354 if isinstance(q0, np.ndarray) and len(q0.shape) > 1: 355 raise ValueError("Vectorized kinematics not supported!") 356 357 req_params = { 358 "target": ArmEndEffector( 359 pose=Matrix4x4(data=target.flatten().tolist()), 360 ), 361 "id": self._part_id, 362 } 363 364 if q0 is not None: 365 req_params["q0"] = list_to_arm_position(q0, degrees) 366 367 else: 368 present_joints_positions = [ 369 joint.present_position for orbita in self._actuators.values() for joint in orbita._joints.values() 370 ] 371 req_params["q0"] = list_to_arm_position(present_joints_positions, degrees) 372 373 req = ArmIKRequest(**req_params) 374 resp = self._stub.ComputeArmIK(req) 375 376 if not resp.success: 377 raise ValueError(f"No solution found for the given target ({target})!") 378 379 answer: List[float] = arm_position_to_list(resp.arm_position, degrees) 380 return answer 381 382 @overload 383 def goto( 384 self, 385 target: List[float], 386 duration: float = 2, 387 wait: bool = False, 388 interpolation_space: str = "joint_space", 389 interpolation_mode: str = "minimum_jerk", 390 degrees: bool = True, 391 q0: Optional[List[float]] = None, 392 ) -> GoToId: 393 ... # pragma: no cover 394 395 @overload 396 def goto( 397 self, 398 target: npt.NDArray[np.float64], 399 duration: float = 2, 400 wait: bool = False, 401 interpolation_space: str = "joint_space", 402 interpolation_mode: str = "minimum_jerk", 403 degrees: bool = True, 404 q0: Optional[List[float]] = None, 405 arc_direction: str = "above", 406 secondary_radius: Optional[float] = None, 407 ) -> GoToId: 408 ... # pragma: no cover 409 410 def goto( 411 self, 412 target: Any, 413 duration: float = 2, 414 wait: bool = False, 415 interpolation_space: str = "joint_space", 416 interpolation_mode: str = "minimum_jerk", 417 degrees: bool = True, 418 q0: Optional[List[float]] = None, 419 arc_direction: str = "above", 420 secondary_radius: Optional[float] = None, 421 ) -> GoToId: 422 """Move the arm to a specified target position, either in joint space or Cartesian space. 423 424 This function allows the arm to move to a specified target using either: 425 - A list of 7 joint positions, or 426 - A 4x4 pose matrix representing the desired end-effector position. 427 428 The function also supports an optional initial configuration `q0` for 429 computing the inverse kinematics solution when the target is in Cartesian space. 430 431 Args: 432 target: The target position. It can either be a list of 7 joint values (for joint space) 433 or a 4x4 NumPy array (for Cartesian space). 434 duration: The time in seconds for the movement to be completed. Defaults to 2. 435 wait: If True, the function waits until the movement is completed before returning. 436 Defaults to False. 437 interpolation_space: The space in which the interpolation should be performed. It can 438 be either "joint_space" or "cartesian_space". Defaults to "joint_space". 439 interpolation_mode: The interpolation method to be used. It can be either "minimum_jerk", 440 "linear" or "elliptical". Defaults to "minimum_jerk". 441 degrees: If True, the joint values in the `target` argument are treated as degrees. 442 Defaults to True. 443 q0: An optional list of 7 joint values representing the initial configuration 444 for inverse kinematics. Defaults to None. 445 arc_direction: The direction of the arc to be followed during elliptical interpolation. 446 Can be "above", "below", "front", "back", "left" or "right" . Defaults to "above". 447 secondary_radius: The secondary radius of the ellipse for elliptical interpolation, in meters. 448 449 Returns: 450 GoToId: The unique GoToId identifier for the movement command. 451 452 Raises: 453 TypeError: If the `target` is neither a list nor a pose matrix. 454 TypeError: If the `q0` is not a list. 455 ValueError: If the `target` list has a length other than 7, or the pose matrix is not 456 of shape (4, 4). 457 ValueError: If the `q0` list has a length other than 7. 458 ValueError: If the `duration` is set to 0. 459 """ 460 self._check_goto_parameters(target, duration, q0) 461 462 if self.is_off(check_gripper=False): 463 self._logger.warning(f"{self._part_id.name} is off. Goto not sent.") 464 return GoToId(id=-1) 465 466 if interpolation_space == "joint_space" and interpolation_mode == "elliptical": 467 self._logger.warning("Elliptical interpolation is not supported in joint space. Switching to linear.") 468 interpolation_mode = "linear" 469 if secondary_radius is not None and secondary_radius > 0.3: 470 self._logger.warning("Interpolation secondary_radius was too large, reduced to 0.3") 471 secondary_radius = 0.3 472 473 if isinstance(target, list) and len(target) == 7: 474 response = self._goto_joints( 475 target, 476 duration, 477 interpolation_space, 478 interpolation_mode, 479 degrees, 480 ) 481 elif isinstance(target, np.ndarray) and target.shape == (4, 4): 482 response = self._goto_from_matrix( 483 target, duration, interpolation_space, interpolation_mode, q0, arc_direction, secondary_radius 484 ) 485 486 if response.id == -1: 487 self._logger.error("Target was not reachable. No command sent.") 488 elif wait: 489 self._wait_goto(response, duration) 490 491 return response 492 493 def _goto_joints( 494 self, 495 target: List[float], 496 duration: float, 497 interpolation_space: str, 498 interpolation_mode: str, 499 degrees: bool, 500 ) -> GoToId: 501 """Handle movement to a specified position in joint space. 502 503 Args: 504 target: A list of 7 joint positions to move the arm to. 505 duration: The time in seconds for the movement to be completed. 506 interpolation_space: The space in which the interpolation should be performed. 507 Only "joint_space" is supported for joints target. 508 interpolation_mode: The interpolation method to be used. Can be "minimum_jerk" or "linear". 509 degrees: If True, the joint positions are interpreted as degrees; otherwise, as radians. 510 511 Returns: 512 GoToId: A unique identifier for the movement command. 513 """ 514 if isinstance(target, np.ndarray): 515 target = target.tolist() 516 arm_pos = list_to_arm_position(target, degrees) 517 518 if interpolation_space == "cartesian_space": 519 self._logger.warning( 520 "cartesian_space interpolation is not supported using joints target. Switching to joint_space interpolation." 521 ) 522 interpolation_space == "joint_space" 523 if interpolation_mode == "elliptical": 524 self._logger.warning("Elliptical interpolation is not supported in joint space. Switching to linear.") 525 interpolation_mode = "linear" 526 527 req_params = { 528 "joints_goal": JointsGoal( 529 arm_joint_goal=ArmJointGoal(id=self._part_id, joints_goal=arm_pos, duration=FloatValue(value=duration)) 530 ), 531 "interpolation_space": get_grpc_interpolation_space(interpolation_space), 532 "interpolation_mode": get_grpc_interpolation_mode(interpolation_mode), 533 } 534 535 request = GoToRequest(**req_params) 536 537 return self._goto_stub.GoToJoints(request) 538 539 def _goto_from_matrix( 540 self, 541 target: npt.NDArray[np.float64], 542 duration: float, 543 interpolation_space: str, 544 interpolation_mode: str, 545 q0: Optional[List[float]], 546 arc_direction: str, 547 secondary_radius: Optional[float], 548 ) -> GoToId: 549 """Handle movement to a Cartesian target using a 4x4 transformation matrix. 550 551 This function computes and sends a command to move the arm to a Cartesian target specified by a 552 4x4 homogeneous transformation matrix. Optionally, an initial joint configuration (`q0`) can be provided 553 for the inverse kinematics calculation. 554 555 Args: 556 target: A 4x4 NumPy array representing the Cartesian target pose. 557 duration: The time in seconds for the movement to be completed. 558 interpolation_space: The space in which the interpolation should be performed. Can be "joint_space" 559 or "cartesian_space". 560 interpolation_mode: The interpolation method to be used. Can be "minimum_jerk", "linear" or "elliptical". 561 q0: An optional list of 7 joint positions representing the initial configuration. Defaults to None. 562 arc_direction: The direction of the arc to be followed during elliptical interpolation. Can be "above", 563 "below", "front", "back", "left" or "right". 564 secondary_radius: The secondary radius of the ellipse for elliptical interpolation, in meters. 565 566 Returns: 567 GoToId: A unique identifier for the movement command. 568 569 Raises: 570 ValueError: If the length of `q0` is not 7. 571 """ 572 goal_pose = Matrix4x4(data=target.flatten().tolist()) 573 574 req_params = { 575 "cartesian_goal": CartesianGoal( 576 arm_cartesian_goal=ArmCartesianGoal( 577 id=self._part_id, 578 goal_pose=goal_pose, 579 duration=FloatValue(value=duration), 580 q0=list_to_arm_position(q0) if q0 is not None else None, 581 ) 582 ), 583 "interpolation_space": get_grpc_interpolation_space(interpolation_space), 584 "interpolation_mode": get_grpc_interpolation_mode(interpolation_mode), 585 } 586 587 if interpolation_mode == "elliptical": 588 ellipse_params = { 589 "arc_direction": get_grpc_arc_direction(arc_direction), 590 } 591 if secondary_radius is not None: 592 ellipse_params["secondary_radius"] = FloatValue(value=secondary_radius) 593 elliptical_params = EllipticalGoToParameters(**ellipse_params) 594 req_params["elliptical_parameters"] = elliptical_params 595 596 request = GoToRequest(**req_params) 597 598 return self._goto_stub.GoToCartesian(request) 599 600 def _check_goto_parameters(self, target: Any, duration: Optional[float] = 0, q0: Optional[List[float]] = None) -> None: 601 """Check the validity of the parameters for the `goto` method. 602 603 Args: 604 duration: The time in seconds for the movement to be completed. 605 target: The target position, either a list of joint positions or a 4x4 pose matrix. 606 q0: An optional initial joint configuration for inverse kinematics. Defaults to None. 607 608 Raises: 609 TypeError: If the target is not a list or a NumPy matrix. 610 ValueError: If the target list has a length other than 7, or the pose matrix is not of 611 shape (4, 4). 612 ValueError: If the duration is set to 0. 613 ValueError: If the length of `q0` is not 7. 614 """ 615 if not (isinstance(target, list) or isinstance(target, np.ndarray)): 616 raise TypeError(f"Invalid target: must be either a list or a np matrix, got {type(target)} instead.") 617 618 elif isinstance(target, list) and not (len(target) == 7): 619 raise ValueError(f"The joints list should be of length 7 (got {len(target)} instead).") 620 elif isinstance(target, np.ndarray) and not (target.shape == (4, 4)): 621 raise ValueError(f"The pose matrix should be of shape (4, 4) (got {target.shape} instead).") 622 623 elif q0 is not None: 624 if not isinstance(q0, list): 625 raise TypeError("Invalid q0: must be a list.") 626 elif len(q0) != 7: 627 raise ValueError(f"q0 should be of length 7 (got {len(q0)} instead)!") 628 629 elif duration == 0: 630 raise ValueError("duration cannot be set to 0.") 631 632 def _goto_single_joint( 633 self, 634 arm_joint: int, 635 goal_position: float, 636 duration: float = 2, 637 wait: bool = False, 638 interpolation_mode: str = "minimum_jerk", 639 degrees: bool = True, 640 ) -> GoToId: 641 """Move a single joint of the arm to a specified position. 642 643 The function allows for optional parameters for duration, interpolation mode, and waiting for completion. 644 645 Args: 646 arm_joint: The specific joint of the arm to move, identified by an integer value. 647 goal_position: The target position for the specified arm joint, given as a float. 648 The value can be in radians or degrees, depending on the `degrees` parameter. 649 duration: The time duration in seconds for the joint to reach the specified goal 650 position. Defaults to 2. 651 wait: Determines whether the program should wait for the movement to finish before 652 returning. If set to `True`, the program waits for the movement to complete before continuing 653 execution. Defaults to `False`. 654 interpolation_mode: The type of interpolation to use when moving the arm's joint. 655 Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 656 degrees: Specifies whether the joint positions are in degrees. If set to `True`, 657 the goal position is interpreted as degrees. Defaults to `True`. 658 659 Returns: 660 A unique GoToId identifier corresponding to this specific goto movement. 661 """ 662 if degrees: 663 goal_position = np.deg2rad(goal_position) 664 request = GoToRequest( 665 joints_goal=JointsGoal( 666 custom_joint_goal=CustomJointGoal( 667 id=self._part_id, 668 arm_joints=CustomArmJoints(joints=[arm_joint]), 669 joints_goals=[FloatValue(value=goal_position)], 670 duration=FloatValue(value=duration), 671 ) 672 ), 673 interpolation_mode=get_grpc_interpolation_mode(interpolation_mode), 674 ) 675 response = self._goto_stub.GoToJoints(request) 676 677 if response.id == -1: 678 self._logger.error(f"Position {goal_position} was not reachable. No command sent.") 679 elif wait: 680 self._wait_goto(response, duration) 681 return response 682 683 def goto_posture( 684 self, 685 common_posture: str = "default", 686 duration: float = 2, 687 wait: bool = False, 688 wait_for_goto_end: bool = True, 689 interpolation_mode: str = "minimum_jerk", 690 open_gripper: bool = False, 691 ) -> GoToId: 692 """Send all joints to standard positions with optional parameters for duration, waiting, and interpolation mode. 693 694 Args: 695 common_posture: The standard positions to which all joints will be sent. 696 It can be 'default' or 'elbow_90'. Defaults to 'default'. 697 duration: The time duration in seconds for the robot to move to the specified posture. 698 Defaults to 2. 699 wait: Determines whether the program should wait for the movement to finish before 700 returning. If set to `True`, the program waits for the movement to complete before continuing 701 execution. Defaults to `False`. 702 wait_for_goto_end: Specifies whether commands will be sent to a part immediately or 703 only after all previous commands in the queue have been executed. If set to `False`, the program 704 will cancel all executing moves and queues. Defaults to `True`. 705 interpolation_mode: The type of interpolation used when moving the arm's joints. 706 Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 707 open_gripper: If `True`, the gripper will open, if `False`, it stays in its current position. 708 Defaults to `False`. 709 710 Returns: 711 A unique GoToId identifier for this specific movement. 712 """ 713 joints = self.get_default_posture_joints(common_posture=common_posture) 714 if self._gripper is not None and self._gripper.is_on() and open_gripper: 715 self._gripper.open() 716 if not wait_for_goto_end: 717 self.cancel_all_goto() 718 if self.is_on(): 719 return self.goto(joints, duration, wait, interpolation_mode=interpolation_mode) 720 else: 721 self._logger.warning(f"{self._part_id.name} is off. No command sent.") 722 return GoToId(id=-1) 723 724 def get_default_posture_joints(self, common_posture: str = "default") -> List[float]: 725 """Get the list of joint positions for default or elbow_90 poses. 726 727 Args: 728 common_posture: The name of the posture to retrieve. Can be "default" or "elbow_90". 729 Defaults to "default". 730 731 Returns: 732 A list of joint positions in degrees for the specified posture. 733 734 Raises: 735 ValueError: If `common_posture` is not "default" or "elbow_90". 736 """ 737 if common_posture not in ["default", "elbow_90"]: 738 raise ValueError(f"common_posture {common_posture} not supported! Should be 'default' or 'elbow_90'") 739 if common_posture == "elbow_90": 740 elbow_pitch = -90 741 else: 742 elbow_pitch = 0 743 if self._part_id.name == "r_arm": 744 return [0, 10, -10, elbow_pitch, 0, 0, 0] 745 else: 746 return [0, -10, 10, elbow_pitch, 0, 0, 0] 747 748 def get_default_posture_matrix(self, common_posture: str = "default") -> npt.NDArray[np.float64]: 749 """Get the 4x4 pose matrix in Reachy coordinate system for a default robot posture. 750 751 Args: 752 common_posture: The posture to retrieve. Can be "default" or "elbow_90". 753 Defaults to "default". 754 755 Returns: 756 The 4x4 homogeneous pose matrix for the specified posture in Reachy coordinate system. 757 """ 758 joints = self.get_default_posture_joints(common_posture) 759 return self.forward_kinematics(joints) 760 761 def get_translation_by( 762 self, 763 x: float, 764 y: float, 765 z: float, 766 initial_pose: Optional[npt.NDArray[np.float64]] = None, 767 frame: str = "robot", 768 ) -> npt.NDArray[np.float64]: 769 """Return a 4x4 matrix representing a pose translated by specified x, y, z values. 770 771 The translation is performed in either the robot or gripper coordinate system. 772 773 Args: 774 x: Translation along the x-axis in meters (forwards direction) to apply 775 to the pose matrix. 776 y: Translation along the y-axis in meters (left direction) to apply 777 to the pose matrix. 778 z: Translation along the z-axis in meters (upwards direction) to apply 779 to the pose matrix. 780 initial_pose: A 4x4 matrix representing the initial pose of the end-effector in Reachy coordinate system, 781 expressed as a NumPy array of type `np.float64`. 782 If not provided, the current pose of the arm is used. Defaults to `None`. 783 frame: The coordinate system in which the translation should be performed. 784 Can be either "robot" or "gripper". Defaults to "robot". 785 786 Returns: 787 A 4x4 pose matrix, expressed in Reachy coordinate system, 788 translated by the specified x, y, z values from the initial pose. 789 790 Raises: 791 ValueError: If the `frame` is not "robot" or "gripper". 792 """ 793 if frame not in ["robot", "gripper"]: 794 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 795 796 if initial_pose is None: 797 initial_pose = self.forward_kinematics() 798 799 pose = initial_pose.copy() 800 801 if frame == "robot": 802 pose[0, 3] += x 803 pose[1, 3] += y 804 pose[2, 3] += z 805 elif frame == "gripper": 806 pose = translate_in_self(initial_pose, [x, y, z]) 807 return pose 808 809 def translate_by( 810 self, 811 x: float, 812 y: float, 813 z: float, 814 duration: float = 2, 815 wait: bool = False, 816 frame: str = "robot", 817 interpolation_space: str = "cartesian_space", 818 interpolation_mode: str = "minimum_jerk", 819 arc_direction: str = "above", 820 secondary_radius: Optional[float] = None, 821 ) -> GoToId: 822 """Create a translation movement for the arm's end effector. 823 824 The movement is based on the last sent position or the current position. 825 826 Args: 827 x: Translation along the x-axis in meters (forwards direction) to apply 828 to the pose matrix. 829 y: Translation along the y-axis in meters (left direction) to apply 830 to the pose matrix. 831 z: Translation along the z-axis in meters (vertical direction) to apply 832 to the pose matrix. 833 duration: Time duration in seconds for the translation movement to be completed. 834 Defaults to 2. 835 wait: Determines whether the program should wait for the movement to finish before 836 returning. If set to `True`, the program waits for the movement to complete before continuing 837 execution. Defaults to `False`. 838 frame: The coordinate system in which the translation should be performed. 839 Can be "robot" or "gripper". Defaults to "robot". 840 interpolation_mode: The type of interpolation to be used when moving the arm's 841 joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 842 843 Returns: 844 The GoToId of the movement command, created using the `goto_from_matrix` method with the 845 translated pose computed in the specified frame. 846 847 Raises: 848 ValueError: If the `frame` is not "robot" or "gripper". 849 """ 850 try: 851 goto = self.get_goto_queue()[-1] 852 except IndexError: 853 goto = self.get_goto_playing() 854 855 if goto.id != -1: 856 joints_request = self._get_goto_request(goto) 857 else: 858 joints_request = None 859 860 if joints_request is not None: 861 if joints_request.request.target.joints is not None: 862 pose = self.forward_kinematics(joints_request.request.target.joints) 863 else: 864 pose = joints_request.request.target.pose 865 else: 866 pose = self.forward_kinematics() 867 868 pose = self.get_translation_by(x, y, z, initial_pose=pose, frame=frame) 869 return self.goto( 870 pose, 871 duration=duration, 872 wait=wait, 873 interpolation_space=interpolation_space, 874 interpolation_mode=interpolation_mode, 875 arc_direction=arc_direction, 876 secondary_radius=secondary_radius, 877 ) 878 879 def get_rotation_by( 880 self, 881 roll: float, 882 pitch: float, 883 yaw: float, 884 initial_pose: Optional[npt.NDArray[np.float64]] = None, 885 degrees: bool = True, 886 frame: str = "robot", 887 ) -> npt.NDArray[np.float64]: 888 """Calculate a new pose matrix by rotating an initial pose matrix by specified roll, pitch, and yaw angles. 889 890 The rotation is performed in either the robot or gripper coordinate system. 891 892 Args: 893 roll: Rotation around the x-axis in the Euler angles representation, specified 894 in radians or degrees (based on the `degrees` parameter). 895 pitch: Rotation around the y-axis in the Euler angles representation, specified 896 in radians or degrees (based on the `degrees` parameter). 897 yaw: Rotation around the z-axis in the Euler angles representation, specified 898 in radians or degrees (based on the `degrees` parameter). 899 initial_pose: A 4x4 matrix representing the initial 900 pose of the end-effector, expressed as a NumPy array of type `np.float64`. If not provided, 901 the current pose of the arm is used. Defaults to `None`. 902 degrees: Specifies whether the rotation angles are provided in degrees. If set to 903 `True`, the angles are interpreted as degrees. Defaults to `True`. 904 frame: The coordinate system in which the rotation should be performed. Can be 905 "robot" or "gripper". Defaults to "robot". 906 907 Returns: 908 A 4x4 pose matrix, expressed in the Reachy coordinate system, rotated 909 by the specified roll, pitch, and yaw angles from the initial pose, in the specified frame. 910 911 Raises: 912 ValueError: If the `frame` is not "robot" or "gripper". 913 """ 914 if frame not in ["robot", "gripper"]: 915 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 916 917 if initial_pose is None: 918 initial_pose = self.forward_kinematics() 919 920 pose = initial_pose.copy() 921 rotation = matrix_from_euler_angles(roll, pitch, yaw, degrees=degrees) 922 923 if frame == "robot": 924 pose_rotation = np.eye(4) 925 pose_rotation[:3, :3] = pose.copy()[:3, :3] 926 pose_translation = pose.copy()[:3, 3] 927 pose_rotation = rotation @ pose_rotation 928 pose = recompose_matrix(pose_rotation[:3, :3], pose_translation) 929 elif frame == "gripper": 930 pose = rotate_in_self(initial_pose, [roll, pitch, yaw], degrees=degrees) 931 932 return pose 933 934 def rotate_by( 935 self, 936 roll: float, 937 pitch: float, 938 yaw: float, 939 duration: float = 2, 940 wait: bool = False, 941 degrees: bool = True, 942 frame: str = "robot", 943 interpolation_mode: str = "minimum_jerk", 944 ) -> GoToId: 945 """Create a rotation movement for the arm's end effector based on the specified roll, pitch, and yaw angles. 946 947 The rotation is performed in either the robot or gripper frame. 948 949 Args: 950 roll: Rotation around the x-axis in the Euler angles representation, specified 951 in radians or degrees (based on the `degrees` parameter). 952 pitch: Rotation around the y-axis in the Euler angles representation, specified 953 in radians or degrees (based on the `degrees` parameter). 954 yaw: Rotation around the z-axis in the Euler angles representation, specified 955 in radians or degrees (based on the `degrees` parameter). 956 duration: Time duration in seconds for the rotation movement to be completed. 957 Defaults to 2. 958 wait: Determines whether the program should wait for the movement to finish before 959 returning. If set to `True`, the program waits for the movement to complete before continuing 960 execution. Defaults to `False`. 961 degrees: Specifies whether the rotation angles are provided in degrees. If set to 962 `True`, the angles are interpreted as degrees. Defaults to `True`. 963 frame: The coordinate system in which the rotation should be performed. Can be 964 "robot" or "gripper". Defaults to "robot". 965 interpolation_mode: The type of interpolation to be used when moving the arm's 966 joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 967 968 Returns: 969 The GoToId of the movement command, created by calling the `goto_from_matrix` method with 970 the rotated pose computed in the specified frame. 971 972 Raises: 973 ValueError: If the `frame` is not "robot" or "gripper". 974 """ 975 if frame not in ["robot", "gripper"]: 976 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 977 978 try: 979 goto = self.get_goto_queue()[-1] 980 except IndexError: 981 goto = self.get_goto_playing() 982 983 if goto.id != -1: 984 joints_request = self._get_goto_request(goto) 985 else: 986 joints_request = None 987 988 if joints_request is not None: 989 if joints_request.request.target.joints is not None: 990 pose = self.forward_kinematics(joints_request.request.target.joints) 991 else: 992 pose = joints_request.request.target.pose 993 else: 994 pose = self.forward_kinematics() 995 996 pose = self.get_rotation_by(roll, pitch, yaw, initial_pose=pose, degrees=degrees, frame=frame) 997 return self.goto(pose, duration=duration, wait=wait, interpolation_mode=interpolation_mode) 998 999 # @property 1000 # def joints_limits(self) -> ArmLimits: 1001 # """Get limits of all the part's joints""" 1002 # limits = self._arm_stub.GetJointsLimits(self._part_id) 1003 # return limits 1004 1005 # @property 1006 # def temperatures(self) -> ArmTemperatures: 1007 # """Get temperatures of all the part's motors""" 1008 # temperatures = self._arm_stub.GetTemperatures(self._part_id) 1009 # return temperatures 1010 1011 def _get_goal_positions_message(self) -> ArmComponentsCommands: 1012 """Get the ArmComponentsCommands message to send the goal positions to the actuator.""" 1013 commands = {} 1014 for actuator_name, actuator in self._actuators.items(): 1015 if actuator_name != "gripper": 1016 actuator_command = actuator._get_goal_positions_message() 1017 if actuator_command is not None: 1018 commands[f"{actuator_name}_command"] = actuator_command 1019 return ArmComponentsCommands(**commands) 1020 1021 def _clean_outgoing_goal_positions(self) -> None: 1022 """Clean the outgoing goal positions.""" 1023 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 1024 actuator._clean_outgoing_goal_positions() 1025 1026 def _post_send_goal_positions(self) -> None: 1027 """Monitor the joint positions to check if they reach the specified goals.""" 1028 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 1029 actuator._post_send_goal_positions() 1030 1031 def send_goal_positions(self, check_positions: bool = False) -> None: 1032 """Send goal positions to the arm's joints, including the gripper. 1033 1034 If goal positions have been specified for any joint of the part, sends them to the robot. 1035 1036 Args : 1037 check_positions: A boolean indicating whether to check the positions after sending the command. 1038 Defaults to True. 1039 """ 1040 super().send_goal_positions(check_positions) 1041 if self.gripper is not None: 1042 self.gripper.send_goal_positions(check_positions) 1043 1044 def _update_with(self, new_state: ArmState) -> None: 1045 """Update the arm with a newly received (partial) state from the gRPC server. 1046 1047 Updating the shoulder, elbow, and wrist states accordingly. 1048 1049 Args: 1050 new_state: current state of the arm, including the states of the shoulder, elbow, and wrist. 1051 """ 1052 self.shoulder._update_with(new_state.shoulder_state) 1053 self.elbow._update_with(new_state.elbow_state) 1054 self.wrist._update_with(new_state.wrist_state) 1055 1056 def _update_audit_status(self, new_status: ArmStatus) -> None: 1057 """Update the audit status of different components based on a new overall status. 1058 1059 Args: 1060 new_status: new status of the shoulder, elbow, and wrist. 1061 """ 1062 self.shoulder._update_audit_status(new_status.shoulder_status) 1063 self.elbow._update_audit_status(new_status.elbow_status) 1064 self.wrist._update_audit_status(new_status.wrist_status)
61class Arm(JointsBasedPart, IGoToBasedPart): 62 """Reachy Arm module. 63 64 Handles specific functionalities for the arm (left and/or right), including: 65 - Forward and inverse kinematics 66 - Goto functions for movement 67 - Turning the arm on and off 68 - Cartesian interpolation for movements 69 70 Attributes: 71 shoulder (Orbita2d): The shoulder actuator of the arm. 72 elbow (Orbita2d): The elbow actuator of the arm. 73 wrist (Orbita3d): The wrist actuator of the arm. 74 gripper (Optional[Hand]): The gripper of the arm, if initialized. 75 """ 76 77 def __init__( 78 self, 79 arm_msg: Arm_proto, 80 initial_state: ArmState, 81 grpc_channel: grpc.Channel, 82 goto_stub: GoToServiceStub, 83 ) -> None: 84 """Initialize an Arm instance. 85 86 This constructor sets up the arm's gRPC communication and initializes its actuators 87 (shoulder, elbow, and wrist). Optionally, a gripper can also be configured. 88 89 Args: 90 arm_msg: The protobuf message containing the arm's configuration details. 91 initial_state: The initial state of the arm's actuators. 92 grpc_channel: The gRPC channel used for communication with the arm's server. 93 goto_stub: The gRPC stub for controlling goto movements. 94 """ 95 JointsBasedPart.__init__(self, arm_msg, grpc_channel, ArmServiceStub(grpc_channel)) 96 IGoToBasedPart.__init__(self, self._part_id, goto_stub) 97 98 self._setup_arm(arm_msg, initial_state) 99 self._gripper: Optional[Hand] = None 100 101 self._actuators: Dict[str, Orbita2d | Orbita3d | Hand] = {} 102 self._actuators["shoulder"] = self.shoulder 103 self._actuators["elbow"] = self.elbow 104 self._actuators["wrist"] = self.wrist 105 106 def _setup_arm(self, arm: Arm_proto, initial_state: ArmState) -> None: 107 """Initialize the arm's actuators (shoulder, elbow, and wrist) based on the arm's description and initial state. 108 109 Args: 110 arm: The arm description used to set up the actuators, including the shoulder, 111 elbow, and wrist. The method creates instances of `Orbita2d` for the shoulder and 112 elbow, and an instance of `Orbita3d` for the wrist. 113 initial_state: The initial state of the arm's actuators, containing the starting 114 positions or states of the shoulder, elbow, and wrist. This information is used to 115 initialize the corresponding actuators. 116 """ 117 description = arm.description 118 self._shoulder = Orbita2d( 119 uid=description.shoulder.id.id, 120 name=description.shoulder.id.name, 121 axis1=description.shoulder.axis_1, 122 axis2=description.shoulder.axis_2, 123 initial_state=initial_state.shoulder_state, 124 grpc_channel=self._grpc_channel, 125 part=self, 126 joints_position_order=[ArmJoints.SHOULDER_PITCH, ArmJoints.SHOULDER_ROLL], 127 ) 128 self._elbow = Orbita2d( 129 uid=description.elbow.id.id, 130 name=description.elbow.id.name, 131 axis1=description.elbow.axis_1, 132 axis2=description.elbow.axis_2, 133 initial_state=initial_state.elbow_state, 134 grpc_channel=self._grpc_channel, 135 part=self, 136 joints_position_order=[ArmJoints.ELBOW_YAW, ArmJoints.ELBOW_PITCH], 137 ) 138 self._wrist = Orbita3d( 139 uid=description.wrist.id.id, 140 name=description.wrist.id.name, 141 initial_state=initial_state.wrist_state, 142 grpc_channel=self._grpc_channel, 143 part=self, 144 joints_position_order=[ArmJoints.WRIST_ROLL, ArmJoints.WRIST_PITCH, ArmJoints.WRIST_YAW], 145 ) 146 147 def _init_hand(self, hand: Hand_proto, hand_initial_state: HandState) -> None: 148 if hand.type == HandType.PARALLEL_GRIPPER: 149 self._gripper = ParallelGripper(hand, hand_initial_state, self._grpc_channel, self._goto_stub) 150 self._actuators["gripper"] = self._gripper 151 152 @property 153 def shoulder(self) -> Orbita2d: 154 """Get the shoulder actuator of the arm.""" 155 return self._shoulder 156 157 @property 158 def elbow(self) -> Orbita2d: 159 """Get the elbow actuator of the arm.""" 160 return self._elbow 161 162 @property 163 def wrist(self) -> Orbita3d: 164 """Get the wrist actuator of the arm.""" 165 return self._wrist 166 167 @property 168 def gripper(self) -> Optional[Hand]: 169 """Get the gripper of the arm, or None if not set.""" 170 return self._gripper 171 172 def __repr__(self) -> str: 173 """Clean representation of an Arm.""" 174 s = "\n\t".join([act_name + ": " + str(actuator) for act_name, actuator in self._actuators.items()]) 175 return f"""<Arm on={self.is_on()} actuators=\n\t{ 176 s 177 }\n>""" 178 179 def turn_on(self) -> None: 180 """Turn on all motors of the part, making all arm motors stiff. 181 182 If a gripper is present, it will also be turned on. 183 """ 184 if self._gripper is not None: 185 self._gripper._turn_on() 186 super().turn_on() 187 188 def turn_off(self) -> None: 189 """Turn off all motors of the part, making all arm motors compliant. 190 191 If a gripper is present, it will also be turned off. 192 """ 193 if self._gripper is not None: 194 self._gripper._turn_off() 195 super().turn_off() 196 197 def turn_off_smoothly(self) -> None: 198 """Gradually reduce the torque limit of all motors over 3 seconds before turning them off. 199 200 This function decreases the torque limit in steps until the motors are turned off. 201 It then restores the torque limit to its original value. 202 """ 203 torque_limit_low = 35 204 torque_limit_high = 100 205 duration = 3 206 207 self.set_torque_limits(torque_limit_low) 208 self.goto_posture(duration=duration, wait_for_goto_end=False) 209 210 countingTime = 0 211 while countingTime < duration: 212 time.sleep(1) 213 torque_limit_low -= 10 214 self.set_torque_limits(torque_limit_low) 215 countingTime += 1 216 217 super().turn_off() 218 self.set_torque_limits(torque_limit_high) 219 220 def _turn_on(self) -> None: 221 """Turn on all motors of the part. 222 223 This will make all arm motors stiff. If a gripper is present, it will also be turned on. 224 """ 225 if self._gripper is not None: 226 self._gripper._turn_on() 227 super()._turn_on() 228 229 def _turn_off(self) -> None: 230 """Turn off all motors of the part. 231 232 This will make all arm motors compliant. If a gripper is present, it will also be turned off. 233 """ 234 if self._gripper is not None: 235 self._gripper._turn_off() 236 super()._turn_off() 237 238 def is_on(self, check_gripper: bool = True) -> bool: 239 """Check if all actuators of the arm are stiff. 240 241 Returns: 242 `True` if all actuators of the arm are stiff, `False` otherwise. 243 """ 244 if not check_gripper: 245 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 246 if not actuator.is_on(): 247 return False 248 return True 249 return super().is_on() 250 251 def is_off(self, check_gripper: bool = True) -> bool: 252 """Check if all actuators of the arm are compliant. 253 254 Returns: 255 `True` if all actuators of the arm are compliant, `False` otherwise. 256 """ 257 if not check_gripper: 258 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 259 if not actuator.is_off(): 260 return False 261 return True 262 return super().is_off() 263 264 def get_current_positions(self, degrees: bool = True) -> List[float]: 265 """Return the current joint positions of the arm, either in degrees or radians. 266 267 Args: 268 degrees: Specifies whether the joint positions should be returned in degrees. 269 If set to `True`, the positions are returned in degrees; otherwise, they are returned in radians. 270 Defaults to `True`. 271 272 Returns: 273 A list of float values representing the current joint positions of the arm in the 274 following order: [shoulder_pitch, shoulder_roll, elbow_yaw, elbow_pitch, wrist_roll, wrist_pitch, 275 wrist_yaw]. 276 """ 277 response = self._stub.GetJointPosition(self._part_id) 278 positions: List[float] = arm_position_to_list(response, degrees) 279 return positions 280 281 def forward_kinematics( 282 self, joints_positions: Optional[List[float]] = None, degrees: bool = True 283 ) -> npt.NDArray[np.float64]: 284 """Compute the forward kinematics of the arm and return a 4x4 pose matrix. 285 286 The pose matrix is expressed in Reachy coordinate system. 287 288 Args: 289 joints_positions: A list of float values representing the positions of the joints 290 in the arm. If not provided, the current robot joints positions are used. Defaults to None. 291 degrees: Indicates whether the joint positions are in degrees or radians. 292 If `True`, the positions are in degrees; if `False`, in radians. Defaults to True. 293 294 Returns: 295 A 4x4 pose matrix as a NumPy array, expressed in Reachy coordinate system. 296 297 Raises: 298 ValueError: If `joints_positions` is provided and its length is not 7. 299 ValueError: If no solution is found for the given joint positions. 300 """ 301 req_params = { 302 "id": self._part_id, 303 } 304 if joints_positions is None: 305 present_joints_positions = [ 306 joint.present_position for orbita in self._actuators.values() for joint in orbita._joints.values() 307 ] 308 req_params["position"] = list_to_arm_position(present_joints_positions, degrees) 309 310 else: 311 if len(joints_positions) != 7: 312 raise ValueError(f"joints_positions should be length 7 (got {len(joints_positions)} instead)!") 313 req_params["position"] = list_to_arm_position(joints_positions, degrees) 314 req = ArmFKRequest(**req_params) 315 resp = self._stub.ComputeArmFK(req) 316 if not resp.success: 317 raise ValueError(f"No solution found for the given joints ({joints_positions})!") 318 319 return np.array(resp.end_effector.pose.data).reshape((4, 4)) 320 321 def inverse_kinematics( 322 self, 323 target: npt.NDArray[np.float64], 324 q0: Optional[List[float]] = None, 325 degrees: bool = True, 326 ) -> List[float]: 327 """Compute a joint configuration to reach a specified target pose for the arm end-effector. 328 329 Args: 330 target: A 4x4 homogeneous pose matrix representing the target pose in 331 Reachy coordinate system, provided as a NumPy array. 332 q0: An optional initial joint configuration for the arm. If provided, the 333 algorithm will use it as a starting point for finding a solution. Defaults to None. 334 degrees: Indicates whether the returned joint angles should be in degrees or radians. 335 If `True`, angles are in degrees; if `False`, in radians. Defaults to True. 336 round: Number of decimal places to round the computed joint angles to before 337 returning. If None, no rounding is performed. Defaults to None. 338 339 Returns: 340 A list of joint angles representing the solution to reach the target pose, in the following order: 341 [shoulder_pitch, shoulder_roll, elbo_yaw, elbow_pitch, wrist.roll, wrist.pitch, wrist.yaw]. 342 343 Raises: 344 ValueError: If the target shape is not (4, 4). 345 ValueError: If the length of `q0` is not 7. 346 ValueError: If vectorized kinematics is attempted (unsupported). 347 ValueError: If no solution is found for the given target. 348 """ 349 if target.shape != (4, 4): 350 raise ValueError("target shape should be (4, 4) (got {target.shape} instead)!") 351 352 if q0 is not None and (len(q0) != 7): 353 raise ValueError(f"q0 should be length 7 (got {len(q0)} instead)!") 354 355 if isinstance(q0, np.ndarray) and len(q0.shape) > 1: 356 raise ValueError("Vectorized kinematics not supported!") 357 358 req_params = { 359 "target": ArmEndEffector( 360 pose=Matrix4x4(data=target.flatten().tolist()), 361 ), 362 "id": self._part_id, 363 } 364 365 if q0 is not None: 366 req_params["q0"] = list_to_arm_position(q0, degrees) 367 368 else: 369 present_joints_positions = [ 370 joint.present_position for orbita in self._actuators.values() for joint in orbita._joints.values() 371 ] 372 req_params["q0"] = list_to_arm_position(present_joints_positions, degrees) 373 374 req = ArmIKRequest(**req_params) 375 resp = self._stub.ComputeArmIK(req) 376 377 if not resp.success: 378 raise ValueError(f"No solution found for the given target ({target})!") 379 380 answer: List[float] = arm_position_to_list(resp.arm_position, degrees) 381 return answer 382 383 @overload 384 def goto( 385 self, 386 target: List[float], 387 duration: float = 2, 388 wait: bool = False, 389 interpolation_space: str = "joint_space", 390 interpolation_mode: str = "minimum_jerk", 391 degrees: bool = True, 392 q0: Optional[List[float]] = None, 393 ) -> GoToId: 394 ... # pragma: no cover 395 396 @overload 397 def goto( 398 self, 399 target: npt.NDArray[np.float64], 400 duration: float = 2, 401 wait: bool = False, 402 interpolation_space: str = "joint_space", 403 interpolation_mode: str = "minimum_jerk", 404 degrees: bool = True, 405 q0: Optional[List[float]] = None, 406 arc_direction: str = "above", 407 secondary_radius: Optional[float] = None, 408 ) -> GoToId: 409 ... # pragma: no cover 410 411 def goto( 412 self, 413 target: Any, 414 duration: float = 2, 415 wait: bool = False, 416 interpolation_space: str = "joint_space", 417 interpolation_mode: str = "minimum_jerk", 418 degrees: bool = True, 419 q0: Optional[List[float]] = None, 420 arc_direction: str = "above", 421 secondary_radius: Optional[float] = None, 422 ) -> GoToId: 423 """Move the arm to a specified target position, either in joint space or Cartesian space. 424 425 This function allows the arm to move to a specified target using either: 426 - A list of 7 joint positions, or 427 - A 4x4 pose matrix representing the desired end-effector position. 428 429 The function also supports an optional initial configuration `q0` for 430 computing the inverse kinematics solution when the target is in Cartesian space. 431 432 Args: 433 target: The target position. It can either be a list of 7 joint values (for joint space) 434 or a 4x4 NumPy array (for Cartesian space). 435 duration: The time in seconds for the movement to be completed. Defaults to 2. 436 wait: If True, the function waits until the movement is completed before returning. 437 Defaults to False. 438 interpolation_space: The space in which the interpolation should be performed. It can 439 be either "joint_space" or "cartesian_space". Defaults to "joint_space". 440 interpolation_mode: The interpolation method to be used. It can be either "minimum_jerk", 441 "linear" or "elliptical". Defaults to "minimum_jerk". 442 degrees: If True, the joint values in the `target` argument are treated as degrees. 443 Defaults to True. 444 q0: An optional list of 7 joint values representing the initial configuration 445 for inverse kinematics. Defaults to None. 446 arc_direction: The direction of the arc to be followed during elliptical interpolation. 447 Can be "above", "below", "front", "back", "left" or "right" . Defaults to "above". 448 secondary_radius: The secondary radius of the ellipse for elliptical interpolation, in meters. 449 450 Returns: 451 GoToId: The unique GoToId identifier for the movement command. 452 453 Raises: 454 TypeError: If the `target` is neither a list nor a pose matrix. 455 TypeError: If the `q0` is not a list. 456 ValueError: If the `target` list has a length other than 7, or the pose matrix is not 457 of shape (4, 4). 458 ValueError: If the `q0` list has a length other than 7. 459 ValueError: If the `duration` is set to 0. 460 """ 461 self._check_goto_parameters(target, duration, q0) 462 463 if self.is_off(check_gripper=False): 464 self._logger.warning(f"{self._part_id.name} is off. Goto not sent.") 465 return GoToId(id=-1) 466 467 if interpolation_space == "joint_space" and interpolation_mode == "elliptical": 468 self._logger.warning("Elliptical interpolation is not supported in joint space. Switching to linear.") 469 interpolation_mode = "linear" 470 if secondary_radius is not None and secondary_radius > 0.3: 471 self._logger.warning("Interpolation secondary_radius was too large, reduced to 0.3") 472 secondary_radius = 0.3 473 474 if isinstance(target, list) and len(target) == 7: 475 response = self._goto_joints( 476 target, 477 duration, 478 interpolation_space, 479 interpolation_mode, 480 degrees, 481 ) 482 elif isinstance(target, np.ndarray) and target.shape == (4, 4): 483 response = self._goto_from_matrix( 484 target, duration, interpolation_space, interpolation_mode, q0, arc_direction, secondary_radius 485 ) 486 487 if response.id == -1: 488 self._logger.error("Target was not reachable. No command sent.") 489 elif wait: 490 self._wait_goto(response, duration) 491 492 return response 493 494 def _goto_joints( 495 self, 496 target: List[float], 497 duration: float, 498 interpolation_space: str, 499 interpolation_mode: str, 500 degrees: bool, 501 ) -> GoToId: 502 """Handle movement to a specified position in joint space. 503 504 Args: 505 target: A list of 7 joint positions to move the arm to. 506 duration: The time in seconds for the movement to be completed. 507 interpolation_space: The space in which the interpolation should be performed. 508 Only "joint_space" is supported for joints target. 509 interpolation_mode: The interpolation method to be used. Can be "minimum_jerk" or "linear". 510 degrees: If True, the joint positions are interpreted as degrees; otherwise, as radians. 511 512 Returns: 513 GoToId: A unique identifier for the movement command. 514 """ 515 if isinstance(target, np.ndarray): 516 target = target.tolist() 517 arm_pos = list_to_arm_position(target, degrees) 518 519 if interpolation_space == "cartesian_space": 520 self._logger.warning( 521 "cartesian_space interpolation is not supported using joints target. Switching to joint_space interpolation." 522 ) 523 interpolation_space == "joint_space" 524 if interpolation_mode == "elliptical": 525 self._logger.warning("Elliptical interpolation is not supported in joint space. Switching to linear.") 526 interpolation_mode = "linear" 527 528 req_params = { 529 "joints_goal": JointsGoal( 530 arm_joint_goal=ArmJointGoal(id=self._part_id, joints_goal=arm_pos, duration=FloatValue(value=duration)) 531 ), 532 "interpolation_space": get_grpc_interpolation_space(interpolation_space), 533 "interpolation_mode": get_grpc_interpolation_mode(interpolation_mode), 534 } 535 536 request = GoToRequest(**req_params) 537 538 return self._goto_stub.GoToJoints(request) 539 540 def _goto_from_matrix( 541 self, 542 target: npt.NDArray[np.float64], 543 duration: float, 544 interpolation_space: str, 545 interpolation_mode: str, 546 q0: Optional[List[float]], 547 arc_direction: str, 548 secondary_radius: Optional[float], 549 ) -> GoToId: 550 """Handle movement to a Cartesian target using a 4x4 transformation matrix. 551 552 This function computes and sends a command to move the arm to a Cartesian target specified by a 553 4x4 homogeneous transformation matrix. Optionally, an initial joint configuration (`q0`) can be provided 554 for the inverse kinematics calculation. 555 556 Args: 557 target: A 4x4 NumPy array representing the Cartesian target pose. 558 duration: The time in seconds for the movement to be completed. 559 interpolation_space: The space in which the interpolation should be performed. Can be "joint_space" 560 or "cartesian_space". 561 interpolation_mode: The interpolation method to be used. Can be "minimum_jerk", "linear" or "elliptical". 562 q0: An optional list of 7 joint positions representing the initial configuration. Defaults to None. 563 arc_direction: The direction of the arc to be followed during elliptical interpolation. Can be "above", 564 "below", "front", "back", "left" or "right". 565 secondary_radius: The secondary radius of the ellipse for elliptical interpolation, in meters. 566 567 Returns: 568 GoToId: A unique identifier for the movement command. 569 570 Raises: 571 ValueError: If the length of `q0` is not 7. 572 """ 573 goal_pose = Matrix4x4(data=target.flatten().tolist()) 574 575 req_params = { 576 "cartesian_goal": CartesianGoal( 577 arm_cartesian_goal=ArmCartesianGoal( 578 id=self._part_id, 579 goal_pose=goal_pose, 580 duration=FloatValue(value=duration), 581 q0=list_to_arm_position(q0) if q0 is not None else None, 582 ) 583 ), 584 "interpolation_space": get_grpc_interpolation_space(interpolation_space), 585 "interpolation_mode": get_grpc_interpolation_mode(interpolation_mode), 586 } 587 588 if interpolation_mode == "elliptical": 589 ellipse_params = { 590 "arc_direction": get_grpc_arc_direction(arc_direction), 591 } 592 if secondary_radius is not None: 593 ellipse_params["secondary_radius"] = FloatValue(value=secondary_radius) 594 elliptical_params = EllipticalGoToParameters(**ellipse_params) 595 req_params["elliptical_parameters"] = elliptical_params 596 597 request = GoToRequest(**req_params) 598 599 return self._goto_stub.GoToCartesian(request) 600 601 def _check_goto_parameters(self, target: Any, duration: Optional[float] = 0, q0: Optional[List[float]] = None) -> None: 602 """Check the validity of the parameters for the `goto` method. 603 604 Args: 605 duration: The time in seconds for the movement to be completed. 606 target: The target position, either a list of joint positions or a 4x4 pose matrix. 607 q0: An optional initial joint configuration for inverse kinematics. Defaults to None. 608 609 Raises: 610 TypeError: If the target is not a list or a NumPy matrix. 611 ValueError: If the target list has a length other than 7, or the pose matrix is not of 612 shape (4, 4). 613 ValueError: If the duration is set to 0. 614 ValueError: If the length of `q0` is not 7. 615 """ 616 if not (isinstance(target, list) or isinstance(target, np.ndarray)): 617 raise TypeError(f"Invalid target: must be either a list or a np matrix, got {type(target)} instead.") 618 619 elif isinstance(target, list) and not (len(target) == 7): 620 raise ValueError(f"The joints list should be of length 7 (got {len(target)} instead).") 621 elif isinstance(target, np.ndarray) and not (target.shape == (4, 4)): 622 raise ValueError(f"The pose matrix should be of shape (4, 4) (got {target.shape} instead).") 623 624 elif q0 is not None: 625 if not isinstance(q0, list): 626 raise TypeError("Invalid q0: must be a list.") 627 elif len(q0) != 7: 628 raise ValueError(f"q0 should be of length 7 (got {len(q0)} instead)!") 629 630 elif duration == 0: 631 raise ValueError("duration cannot be set to 0.") 632 633 def _goto_single_joint( 634 self, 635 arm_joint: int, 636 goal_position: float, 637 duration: float = 2, 638 wait: bool = False, 639 interpolation_mode: str = "minimum_jerk", 640 degrees: bool = True, 641 ) -> GoToId: 642 """Move a single joint of the arm to a specified position. 643 644 The function allows for optional parameters for duration, interpolation mode, and waiting for completion. 645 646 Args: 647 arm_joint: The specific joint of the arm to move, identified by an integer value. 648 goal_position: The target position for the specified arm joint, given as a float. 649 The value can be in radians or degrees, depending on the `degrees` parameter. 650 duration: The time duration in seconds for the joint to reach the specified goal 651 position. Defaults to 2. 652 wait: Determines whether the program should wait for the movement to finish before 653 returning. If set to `True`, the program waits for the movement to complete before continuing 654 execution. Defaults to `False`. 655 interpolation_mode: The type of interpolation to use when moving the arm's joint. 656 Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 657 degrees: Specifies whether the joint positions are in degrees. If set to `True`, 658 the goal position is interpreted as degrees. Defaults to `True`. 659 660 Returns: 661 A unique GoToId identifier corresponding to this specific goto movement. 662 """ 663 if degrees: 664 goal_position = np.deg2rad(goal_position) 665 request = GoToRequest( 666 joints_goal=JointsGoal( 667 custom_joint_goal=CustomJointGoal( 668 id=self._part_id, 669 arm_joints=CustomArmJoints(joints=[arm_joint]), 670 joints_goals=[FloatValue(value=goal_position)], 671 duration=FloatValue(value=duration), 672 ) 673 ), 674 interpolation_mode=get_grpc_interpolation_mode(interpolation_mode), 675 ) 676 response = self._goto_stub.GoToJoints(request) 677 678 if response.id == -1: 679 self._logger.error(f"Position {goal_position} was not reachable. No command sent.") 680 elif wait: 681 self._wait_goto(response, duration) 682 return response 683 684 def goto_posture( 685 self, 686 common_posture: str = "default", 687 duration: float = 2, 688 wait: bool = False, 689 wait_for_goto_end: bool = True, 690 interpolation_mode: str = "minimum_jerk", 691 open_gripper: bool = False, 692 ) -> GoToId: 693 """Send all joints to standard positions with optional parameters for duration, waiting, and interpolation mode. 694 695 Args: 696 common_posture: The standard positions to which all joints will be sent. 697 It can be 'default' or 'elbow_90'. Defaults to 'default'. 698 duration: The time duration in seconds for the robot to move to the specified posture. 699 Defaults to 2. 700 wait: Determines whether the program should wait for the movement to finish before 701 returning. If set to `True`, the program waits for the movement to complete before continuing 702 execution. Defaults to `False`. 703 wait_for_goto_end: Specifies whether commands will be sent to a part immediately or 704 only after all previous commands in the queue have been executed. If set to `False`, the program 705 will cancel all executing moves and queues. Defaults to `True`. 706 interpolation_mode: The type of interpolation used when moving the arm's joints. 707 Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 708 open_gripper: If `True`, the gripper will open, if `False`, it stays in its current position. 709 Defaults to `False`. 710 711 Returns: 712 A unique GoToId identifier for this specific movement. 713 """ 714 joints = self.get_default_posture_joints(common_posture=common_posture) 715 if self._gripper is not None and self._gripper.is_on() and open_gripper: 716 self._gripper.open() 717 if not wait_for_goto_end: 718 self.cancel_all_goto() 719 if self.is_on(): 720 return self.goto(joints, duration, wait, interpolation_mode=interpolation_mode) 721 else: 722 self._logger.warning(f"{self._part_id.name} is off. No command sent.") 723 return GoToId(id=-1) 724 725 def get_default_posture_joints(self, common_posture: str = "default") -> List[float]: 726 """Get the list of joint positions for default or elbow_90 poses. 727 728 Args: 729 common_posture: The name of the posture to retrieve. Can be "default" or "elbow_90". 730 Defaults to "default". 731 732 Returns: 733 A list of joint positions in degrees for the specified posture. 734 735 Raises: 736 ValueError: If `common_posture` is not "default" or "elbow_90". 737 """ 738 if common_posture not in ["default", "elbow_90"]: 739 raise ValueError(f"common_posture {common_posture} not supported! Should be 'default' or 'elbow_90'") 740 if common_posture == "elbow_90": 741 elbow_pitch = -90 742 else: 743 elbow_pitch = 0 744 if self._part_id.name == "r_arm": 745 return [0, 10, -10, elbow_pitch, 0, 0, 0] 746 else: 747 return [0, -10, 10, elbow_pitch, 0, 0, 0] 748 749 def get_default_posture_matrix(self, common_posture: str = "default") -> npt.NDArray[np.float64]: 750 """Get the 4x4 pose matrix in Reachy coordinate system for a default robot posture. 751 752 Args: 753 common_posture: The posture to retrieve. Can be "default" or "elbow_90". 754 Defaults to "default". 755 756 Returns: 757 The 4x4 homogeneous pose matrix for the specified posture in Reachy coordinate system. 758 """ 759 joints = self.get_default_posture_joints(common_posture) 760 return self.forward_kinematics(joints) 761 762 def get_translation_by( 763 self, 764 x: float, 765 y: float, 766 z: float, 767 initial_pose: Optional[npt.NDArray[np.float64]] = None, 768 frame: str = "robot", 769 ) -> npt.NDArray[np.float64]: 770 """Return a 4x4 matrix representing a pose translated by specified x, y, z values. 771 772 The translation is performed in either the robot or gripper coordinate system. 773 774 Args: 775 x: Translation along the x-axis in meters (forwards direction) to apply 776 to the pose matrix. 777 y: Translation along the y-axis in meters (left direction) to apply 778 to the pose matrix. 779 z: Translation along the z-axis in meters (upwards direction) to apply 780 to the pose matrix. 781 initial_pose: A 4x4 matrix representing the initial pose of the end-effector in Reachy coordinate system, 782 expressed as a NumPy array of type `np.float64`. 783 If not provided, the current pose of the arm is used. Defaults to `None`. 784 frame: The coordinate system in which the translation should be performed. 785 Can be either "robot" or "gripper". Defaults to "robot". 786 787 Returns: 788 A 4x4 pose matrix, expressed in Reachy coordinate system, 789 translated by the specified x, y, z values from the initial pose. 790 791 Raises: 792 ValueError: If the `frame` is not "robot" or "gripper". 793 """ 794 if frame not in ["robot", "gripper"]: 795 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 796 797 if initial_pose is None: 798 initial_pose = self.forward_kinematics() 799 800 pose = initial_pose.copy() 801 802 if frame == "robot": 803 pose[0, 3] += x 804 pose[1, 3] += y 805 pose[2, 3] += z 806 elif frame == "gripper": 807 pose = translate_in_self(initial_pose, [x, y, z]) 808 return pose 809 810 def translate_by( 811 self, 812 x: float, 813 y: float, 814 z: float, 815 duration: float = 2, 816 wait: bool = False, 817 frame: str = "robot", 818 interpolation_space: str = "cartesian_space", 819 interpolation_mode: str = "minimum_jerk", 820 arc_direction: str = "above", 821 secondary_radius: Optional[float] = None, 822 ) -> GoToId: 823 """Create a translation movement for the arm's end effector. 824 825 The movement is based on the last sent position or the current position. 826 827 Args: 828 x: Translation along the x-axis in meters (forwards direction) to apply 829 to the pose matrix. 830 y: Translation along the y-axis in meters (left direction) to apply 831 to the pose matrix. 832 z: Translation along the z-axis in meters (vertical direction) to apply 833 to the pose matrix. 834 duration: Time duration in seconds for the translation movement to be completed. 835 Defaults to 2. 836 wait: Determines whether the program should wait for the movement to finish before 837 returning. If set to `True`, the program waits for the movement to complete before continuing 838 execution. Defaults to `False`. 839 frame: The coordinate system in which the translation should be performed. 840 Can be "robot" or "gripper". Defaults to "robot". 841 interpolation_mode: The type of interpolation to be used when moving the arm's 842 joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 843 844 Returns: 845 The GoToId of the movement command, created using the `goto_from_matrix` method with the 846 translated pose computed in the specified frame. 847 848 Raises: 849 ValueError: If the `frame` is not "robot" or "gripper". 850 """ 851 try: 852 goto = self.get_goto_queue()[-1] 853 except IndexError: 854 goto = self.get_goto_playing() 855 856 if goto.id != -1: 857 joints_request = self._get_goto_request(goto) 858 else: 859 joints_request = None 860 861 if joints_request is not None: 862 if joints_request.request.target.joints is not None: 863 pose = self.forward_kinematics(joints_request.request.target.joints) 864 else: 865 pose = joints_request.request.target.pose 866 else: 867 pose = self.forward_kinematics() 868 869 pose = self.get_translation_by(x, y, z, initial_pose=pose, frame=frame) 870 return self.goto( 871 pose, 872 duration=duration, 873 wait=wait, 874 interpolation_space=interpolation_space, 875 interpolation_mode=interpolation_mode, 876 arc_direction=arc_direction, 877 secondary_radius=secondary_radius, 878 ) 879 880 def get_rotation_by( 881 self, 882 roll: float, 883 pitch: float, 884 yaw: float, 885 initial_pose: Optional[npt.NDArray[np.float64]] = None, 886 degrees: bool = True, 887 frame: str = "robot", 888 ) -> npt.NDArray[np.float64]: 889 """Calculate a new pose matrix by rotating an initial pose matrix by specified roll, pitch, and yaw angles. 890 891 The rotation is performed in either the robot or gripper coordinate system. 892 893 Args: 894 roll: Rotation around the x-axis in the Euler angles representation, specified 895 in radians or degrees (based on the `degrees` parameter). 896 pitch: Rotation around the y-axis in the Euler angles representation, specified 897 in radians or degrees (based on the `degrees` parameter). 898 yaw: Rotation around the z-axis in the Euler angles representation, specified 899 in radians or degrees (based on the `degrees` parameter). 900 initial_pose: A 4x4 matrix representing the initial 901 pose of the end-effector, expressed as a NumPy array of type `np.float64`. If not provided, 902 the current pose of the arm is used. Defaults to `None`. 903 degrees: Specifies whether the rotation angles are provided in degrees. If set to 904 `True`, the angles are interpreted as degrees. Defaults to `True`. 905 frame: The coordinate system in which the rotation should be performed. Can be 906 "robot" or "gripper". Defaults to "robot". 907 908 Returns: 909 A 4x4 pose matrix, expressed in the Reachy coordinate system, rotated 910 by the specified roll, pitch, and yaw angles from the initial pose, in the specified frame. 911 912 Raises: 913 ValueError: If the `frame` is not "robot" or "gripper". 914 """ 915 if frame not in ["robot", "gripper"]: 916 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 917 918 if initial_pose is None: 919 initial_pose = self.forward_kinematics() 920 921 pose = initial_pose.copy() 922 rotation = matrix_from_euler_angles(roll, pitch, yaw, degrees=degrees) 923 924 if frame == "robot": 925 pose_rotation = np.eye(4) 926 pose_rotation[:3, :3] = pose.copy()[:3, :3] 927 pose_translation = pose.copy()[:3, 3] 928 pose_rotation = rotation @ pose_rotation 929 pose = recompose_matrix(pose_rotation[:3, :3], pose_translation) 930 elif frame == "gripper": 931 pose = rotate_in_self(initial_pose, [roll, pitch, yaw], degrees=degrees) 932 933 return pose 934 935 def rotate_by( 936 self, 937 roll: float, 938 pitch: float, 939 yaw: float, 940 duration: float = 2, 941 wait: bool = False, 942 degrees: bool = True, 943 frame: str = "robot", 944 interpolation_mode: str = "minimum_jerk", 945 ) -> GoToId: 946 """Create a rotation movement for the arm's end effector based on the specified roll, pitch, and yaw angles. 947 948 The rotation is performed in either the robot or gripper frame. 949 950 Args: 951 roll: Rotation around the x-axis in the Euler angles representation, specified 952 in radians or degrees (based on the `degrees` parameter). 953 pitch: Rotation around the y-axis in the Euler angles representation, specified 954 in radians or degrees (based on the `degrees` parameter). 955 yaw: Rotation around the z-axis in the Euler angles representation, specified 956 in radians or degrees (based on the `degrees` parameter). 957 duration: Time duration in seconds for the rotation movement to be completed. 958 Defaults to 2. 959 wait: Determines whether the program should wait for the movement to finish before 960 returning. If set to `True`, the program waits for the movement to complete before continuing 961 execution. Defaults to `False`. 962 degrees: Specifies whether the rotation angles are provided in degrees. If set to 963 `True`, the angles are interpreted as degrees. Defaults to `True`. 964 frame: The coordinate system in which the rotation should be performed. Can be 965 "robot" or "gripper". Defaults to "robot". 966 interpolation_mode: The type of interpolation to be used when moving the arm's 967 joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 968 969 Returns: 970 The GoToId of the movement command, created by calling the `goto_from_matrix` method with 971 the rotated pose computed in the specified frame. 972 973 Raises: 974 ValueError: If the `frame` is not "robot" or "gripper". 975 """ 976 if frame not in ["robot", "gripper"]: 977 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 978 979 try: 980 goto = self.get_goto_queue()[-1] 981 except IndexError: 982 goto = self.get_goto_playing() 983 984 if goto.id != -1: 985 joints_request = self._get_goto_request(goto) 986 else: 987 joints_request = None 988 989 if joints_request is not None: 990 if joints_request.request.target.joints is not None: 991 pose = self.forward_kinematics(joints_request.request.target.joints) 992 else: 993 pose = joints_request.request.target.pose 994 else: 995 pose = self.forward_kinematics() 996 997 pose = self.get_rotation_by(roll, pitch, yaw, initial_pose=pose, degrees=degrees, frame=frame) 998 return self.goto(pose, duration=duration, wait=wait, interpolation_mode=interpolation_mode) 999 1000 # @property 1001 # def joints_limits(self) -> ArmLimits: 1002 # """Get limits of all the part's joints""" 1003 # limits = self._arm_stub.GetJointsLimits(self._part_id) 1004 # return limits 1005 1006 # @property 1007 # def temperatures(self) -> ArmTemperatures: 1008 # """Get temperatures of all the part's motors""" 1009 # temperatures = self._arm_stub.GetTemperatures(self._part_id) 1010 # return temperatures 1011 1012 def _get_goal_positions_message(self) -> ArmComponentsCommands: 1013 """Get the ArmComponentsCommands message to send the goal positions to the actuator.""" 1014 commands = {} 1015 for actuator_name, actuator in self._actuators.items(): 1016 if actuator_name != "gripper": 1017 actuator_command = actuator._get_goal_positions_message() 1018 if actuator_command is not None: 1019 commands[f"{actuator_name}_command"] = actuator_command 1020 return ArmComponentsCommands(**commands) 1021 1022 def _clean_outgoing_goal_positions(self) -> None: 1023 """Clean the outgoing goal positions.""" 1024 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 1025 actuator._clean_outgoing_goal_positions() 1026 1027 def _post_send_goal_positions(self) -> None: 1028 """Monitor the joint positions to check if they reach the specified goals.""" 1029 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 1030 actuator._post_send_goal_positions() 1031 1032 def send_goal_positions(self, check_positions: bool = False) -> None: 1033 """Send goal positions to the arm's joints, including the gripper. 1034 1035 If goal positions have been specified for any joint of the part, sends them to the robot. 1036 1037 Args : 1038 check_positions: A boolean indicating whether to check the positions after sending the command. 1039 Defaults to True. 1040 """ 1041 super().send_goal_positions(check_positions) 1042 if self.gripper is not None: 1043 self.gripper.send_goal_positions(check_positions) 1044 1045 def _update_with(self, new_state: ArmState) -> None: 1046 """Update the arm with a newly received (partial) state from the gRPC server. 1047 1048 Updating the shoulder, elbow, and wrist states accordingly. 1049 1050 Args: 1051 new_state: current state of the arm, including the states of the shoulder, elbow, and wrist. 1052 """ 1053 self.shoulder._update_with(new_state.shoulder_state) 1054 self.elbow._update_with(new_state.elbow_state) 1055 self.wrist._update_with(new_state.wrist_state) 1056 1057 def _update_audit_status(self, new_status: ArmStatus) -> None: 1058 """Update the audit status of different components based on a new overall status. 1059 1060 Args: 1061 new_status: new status of the shoulder, elbow, and wrist. 1062 """ 1063 self.shoulder._update_audit_status(new_status.shoulder_status) 1064 self.elbow._update_audit_status(new_status.elbow_status) 1065 self.wrist._update_audit_status(new_status.wrist_status)
Reachy Arm module.
Handles specific functionalities for the arm (left and/or right), including:
- Forward and inverse kinematics
- Goto functions for movement
- Turning the arm on and off
- Cartesian interpolation for movements
Attributes:
- shoulder (Orbita2d): The shoulder actuator of the arm.
- elbow (Orbita2d): The elbow actuator of the arm.
- wrist (Orbita3d): The wrist actuator of the arm.
- gripper (Optional[Hand]): The gripper of the arm, if initialized.
77 def __init__( 78 self, 79 arm_msg: Arm_proto, 80 initial_state: ArmState, 81 grpc_channel: grpc.Channel, 82 goto_stub: GoToServiceStub, 83 ) -> None: 84 """Initialize an Arm instance. 85 86 This constructor sets up the arm's gRPC communication and initializes its actuators 87 (shoulder, elbow, and wrist). Optionally, a gripper can also be configured. 88 89 Args: 90 arm_msg: The protobuf message containing the arm's configuration details. 91 initial_state: The initial state of the arm's actuators. 92 grpc_channel: The gRPC channel used for communication with the arm's server. 93 goto_stub: The gRPC stub for controlling goto movements. 94 """ 95 JointsBasedPart.__init__(self, arm_msg, grpc_channel, ArmServiceStub(grpc_channel)) 96 IGoToBasedPart.__init__(self, self._part_id, goto_stub) 97 98 self._setup_arm(arm_msg, initial_state) 99 self._gripper: Optional[Hand] = None 100 101 self._actuators: Dict[str, Orbita2d | Orbita3d | Hand] = {} 102 self._actuators["shoulder"] = self.shoulder 103 self._actuators["elbow"] = self.elbow 104 self._actuators["wrist"] = self.wrist
Initialize an Arm instance.
This constructor sets up the arm's gRPC communication and initializes its actuators (shoulder, elbow, and wrist). Optionally, a gripper can also be configured.
Arguments:
- arm_msg: The protobuf message containing the arm's configuration details.
- initial_state: The initial state of the arm's actuators.
- grpc_channel: The gRPC channel used for communication with the arm's server.
- goto_stub: The gRPC stub for controlling goto movements.
152 @property 153 def shoulder(self) -> Orbita2d: 154 """Get the shoulder actuator of the arm.""" 155 return self._shoulder
Get the shoulder actuator of the arm.
157 @property 158 def elbow(self) -> Orbita2d: 159 """Get the elbow actuator of the arm.""" 160 return self._elbow
Get the elbow actuator of the arm.
162 @property 163 def wrist(self) -> Orbita3d: 164 """Get the wrist actuator of the arm.""" 165 return self._wrist
Get the wrist actuator of the arm.
167 @property 168 def gripper(self) -> Optional[Hand]: 169 """Get the gripper of the arm, or None if not set.""" 170 return self._gripper
Get the gripper of the arm, or None if not set.
179 def turn_on(self) -> None: 180 """Turn on all motors of the part, making all arm motors stiff. 181 182 If a gripper is present, it will also be turned on. 183 """ 184 if self._gripper is not None: 185 self._gripper._turn_on() 186 super().turn_on()
Turn on all motors of the part, making all arm motors stiff.
If a gripper is present, it will also be turned on.
188 def turn_off(self) -> None: 189 """Turn off all motors of the part, making all arm motors compliant. 190 191 If a gripper is present, it will also be turned off. 192 """ 193 if self._gripper is not None: 194 self._gripper._turn_off() 195 super().turn_off()
Turn off all motors of the part, making all arm motors compliant.
If a gripper is present, it will also be turned off.
197 def turn_off_smoothly(self) -> None: 198 """Gradually reduce the torque limit of all motors over 3 seconds before turning them off. 199 200 This function decreases the torque limit in steps until the motors are turned off. 201 It then restores the torque limit to its original value. 202 """ 203 torque_limit_low = 35 204 torque_limit_high = 100 205 duration = 3 206 207 self.set_torque_limits(torque_limit_low) 208 self.goto_posture(duration=duration, wait_for_goto_end=False) 209 210 countingTime = 0 211 while countingTime < duration: 212 time.sleep(1) 213 torque_limit_low -= 10 214 self.set_torque_limits(torque_limit_low) 215 countingTime += 1 216 217 super().turn_off() 218 self.set_torque_limits(torque_limit_high)
Gradually reduce the torque limit of all motors over 3 seconds before turning them off.
This function decreases the torque limit in steps until the motors are turned off. It then restores the torque limit to its original value.
238 def is_on(self, check_gripper: bool = True) -> bool: 239 """Check if all actuators of the arm are stiff. 240 241 Returns: 242 `True` if all actuators of the arm are stiff, `False` otherwise. 243 """ 244 if not check_gripper: 245 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 246 if not actuator.is_on(): 247 return False 248 return True 249 return super().is_on()
Check if all actuators of the arm are stiff.
Returns:
True
if all actuators of the arm are stiff,False
otherwise.
251 def is_off(self, check_gripper: bool = True) -> bool: 252 """Check if all actuators of the arm are compliant. 253 254 Returns: 255 `True` if all actuators of the arm are compliant, `False` otherwise. 256 """ 257 if not check_gripper: 258 for actuator in [self._actuators[act] for act in self._actuators.keys() if act not in ["gripper"]]: 259 if not actuator.is_off(): 260 return False 261 return True 262 return super().is_off()
Check if all actuators of the arm are compliant.
Returns:
True
if all actuators of the arm are compliant,False
otherwise.
264 def get_current_positions(self, degrees: bool = True) -> List[float]: 265 """Return the current joint positions of the arm, either in degrees or radians. 266 267 Args: 268 degrees: Specifies whether the joint positions should be returned in degrees. 269 If set to `True`, the positions are returned in degrees; otherwise, they are returned in radians. 270 Defaults to `True`. 271 272 Returns: 273 A list of float values representing the current joint positions of the arm in the 274 following order: [shoulder_pitch, shoulder_roll, elbow_yaw, elbow_pitch, wrist_roll, wrist_pitch, 275 wrist_yaw]. 276 """ 277 response = self._stub.GetJointPosition(self._part_id) 278 positions: List[float] = arm_position_to_list(response, degrees) 279 return positions
Return the current joint positions of the arm, either in degrees or radians.
Arguments:
- degrees: Specifies whether the joint positions should be returned in degrees.
If set to
True
, the positions are returned in degrees; otherwise, they are returned in radians. Defaults toTrue
.
Returns:
A list of float values representing the current joint positions of the arm in the following order: [shoulder_pitch, shoulder_roll, elbow_yaw, elbow_pitch, wrist_roll, wrist_pitch, wrist_yaw].
281 def forward_kinematics( 282 self, joints_positions: Optional[List[float]] = None, degrees: bool = True 283 ) -> npt.NDArray[np.float64]: 284 """Compute the forward kinematics of the arm and return a 4x4 pose matrix. 285 286 The pose matrix is expressed in Reachy coordinate system. 287 288 Args: 289 joints_positions: A list of float values representing the positions of the joints 290 in the arm. If not provided, the current robot joints positions are used. Defaults to None. 291 degrees: Indicates whether the joint positions are in degrees or radians. 292 If `True`, the positions are in degrees; if `False`, in radians. Defaults to True. 293 294 Returns: 295 A 4x4 pose matrix as a NumPy array, expressed in Reachy coordinate system. 296 297 Raises: 298 ValueError: If `joints_positions` is provided and its length is not 7. 299 ValueError: If no solution is found for the given joint positions. 300 """ 301 req_params = { 302 "id": self._part_id, 303 } 304 if joints_positions is None: 305 present_joints_positions = [ 306 joint.present_position for orbita in self._actuators.values() for joint in orbita._joints.values() 307 ] 308 req_params["position"] = list_to_arm_position(present_joints_positions, degrees) 309 310 else: 311 if len(joints_positions) != 7: 312 raise ValueError(f"joints_positions should be length 7 (got {len(joints_positions)} instead)!") 313 req_params["position"] = list_to_arm_position(joints_positions, degrees) 314 req = ArmFKRequest(**req_params) 315 resp = self._stub.ComputeArmFK(req) 316 if not resp.success: 317 raise ValueError(f"No solution found for the given joints ({joints_positions})!") 318 319 return np.array(resp.end_effector.pose.data).reshape((4, 4))
Compute the forward kinematics of the arm and return a 4x4 pose matrix.
The pose matrix is expressed in Reachy coordinate system.
Arguments:
- joints_positions: A list of float values representing the positions of the joints in the arm. If not provided, the current robot joints positions are used. Defaults to None.
- degrees: Indicates whether the joint positions are in degrees or radians.
If
True
, the positions are in degrees; ifFalse
, in radians. Defaults to True.
Returns:
A 4x4 pose matrix as a NumPy array, expressed in Reachy coordinate system.
Raises:
- ValueError: If
joints_positions
is provided and its length is not 7. - ValueError: If no solution is found for the given joint positions.
321 def inverse_kinematics( 322 self, 323 target: npt.NDArray[np.float64], 324 q0: Optional[List[float]] = None, 325 degrees: bool = True, 326 ) -> List[float]: 327 """Compute a joint configuration to reach a specified target pose for the arm end-effector. 328 329 Args: 330 target: A 4x4 homogeneous pose matrix representing the target pose in 331 Reachy coordinate system, provided as a NumPy array. 332 q0: An optional initial joint configuration for the arm. If provided, the 333 algorithm will use it as a starting point for finding a solution. Defaults to None. 334 degrees: Indicates whether the returned joint angles should be in degrees or radians. 335 If `True`, angles are in degrees; if `False`, in radians. Defaults to True. 336 round: Number of decimal places to round the computed joint angles to before 337 returning. If None, no rounding is performed. Defaults to None. 338 339 Returns: 340 A list of joint angles representing the solution to reach the target pose, in the following order: 341 [shoulder_pitch, shoulder_roll, elbo_yaw, elbow_pitch, wrist.roll, wrist.pitch, wrist.yaw]. 342 343 Raises: 344 ValueError: If the target shape is not (4, 4). 345 ValueError: If the length of `q0` is not 7. 346 ValueError: If vectorized kinematics is attempted (unsupported). 347 ValueError: If no solution is found for the given target. 348 """ 349 if target.shape != (4, 4): 350 raise ValueError("target shape should be (4, 4) (got {target.shape} instead)!") 351 352 if q0 is not None and (len(q0) != 7): 353 raise ValueError(f"q0 should be length 7 (got {len(q0)} instead)!") 354 355 if isinstance(q0, np.ndarray) and len(q0.shape) > 1: 356 raise ValueError("Vectorized kinematics not supported!") 357 358 req_params = { 359 "target": ArmEndEffector( 360 pose=Matrix4x4(data=target.flatten().tolist()), 361 ), 362 "id": self._part_id, 363 } 364 365 if q0 is not None: 366 req_params["q0"] = list_to_arm_position(q0, degrees) 367 368 else: 369 present_joints_positions = [ 370 joint.present_position for orbita in self._actuators.values() for joint in orbita._joints.values() 371 ] 372 req_params["q0"] = list_to_arm_position(present_joints_positions, degrees) 373 374 req = ArmIKRequest(**req_params) 375 resp = self._stub.ComputeArmIK(req) 376 377 if not resp.success: 378 raise ValueError(f"No solution found for the given target ({target})!") 379 380 answer: List[float] = arm_position_to_list(resp.arm_position, degrees) 381 return answer
Compute a joint configuration to reach a specified target pose for the arm end-effector.
Arguments:
- target: A 4x4 homogeneous pose matrix representing the target pose in Reachy coordinate system, provided as a NumPy array.
- q0: An optional initial joint configuration for the arm. If provided, the algorithm will use it as a starting point for finding a solution. Defaults to None.
- degrees: Indicates whether the returned joint angles should be in degrees or radians.
If
True
, angles are in degrees; ifFalse
, in radians. Defaults to True. - round: Number of decimal places to round the computed joint angles to before returning. If None, no rounding is performed. Defaults to None.
Returns:
A list of joint angles representing the solution to reach the target pose, in the following order: [shoulder_pitch, shoulder_roll, elbo_yaw, elbow_pitch, wrist.roll, wrist.pitch, wrist.yaw].
Raises:
- ValueError: If the target shape is not (4, 4).
- ValueError: If the length of
q0
is not 7. - ValueError: If vectorized kinematics is attempted (unsupported).
- ValueError: If no solution is found for the given target.
411 def goto( 412 self, 413 target: Any, 414 duration: float = 2, 415 wait: bool = False, 416 interpolation_space: str = "joint_space", 417 interpolation_mode: str = "minimum_jerk", 418 degrees: bool = True, 419 q0: Optional[List[float]] = None, 420 arc_direction: str = "above", 421 secondary_radius: Optional[float] = None, 422 ) -> GoToId: 423 """Move the arm to a specified target position, either in joint space or Cartesian space. 424 425 This function allows the arm to move to a specified target using either: 426 - A list of 7 joint positions, or 427 - A 4x4 pose matrix representing the desired end-effector position. 428 429 The function also supports an optional initial configuration `q0` for 430 computing the inverse kinematics solution when the target is in Cartesian space. 431 432 Args: 433 target: The target position. It can either be a list of 7 joint values (for joint space) 434 or a 4x4 NumPy array (for Cartesian space). 435 duration: The time in seconds for the movement to be completed. Defaults to 2. 436 wait: If True, the function waits until the movement is completed before returning. 437 Defaults to False. 438 interpolation_space: The space in which the interpolation should be performed. It can 439 be either "joint_space" or "cartesian_space". Defaults to "joint_space". 440 interpolation_mode: The interpolation method to be used. It can be either "minimum_jerk", 441 "linear" or "elliptical". Defaults to "minimum_jerk". 442 degrees: If True, the joint values in the `target` argument are treated as degrees. 443 Defaults to True. 444 q0: An optional list of 7 joint values representing the initial configuration 445 for inverse kinematics. Defaults to None. 446 arc_direction: The direction of the arc to be followed during elliptical interpolation. 447 Can be "above", "below", "front", "back", "left" or "right" . Defaults to "above". 448 secondary_radius: The secondary radius of the ellipse for elliptical interpolation, in meters. 449 450 Returns: 451 GoToId: The unique GoToId identifier for the movement command. 452 453 Raises: 454 TypeError: If the `target` is neither a list nor a pose matrix. 455 TypeError: If the `q0` is not a list. 456 ValueError: If the `target` list has a length other than 7, or the pose matrix is not 457 of shape (4, 4). 458 ValueError: If the `q0` list has a length other than 7. 459 ValueError: If the `duration` is set to 0. 460 """ 461 self._check_goto_parameters(target, duration, q0) 462 463 if self.is_off(check_gripper=False): 464 self._logger.warning(f"{self._part_id.name} is off. Goto not sent.") 465 return GoToId(id=-1) 466 467 if interpolation_space == "joint_space" and interpolation_mode == "elliptical": 468 self._logger.warning("Elliptical interpolation is not supported in joint space. Switching to linear.") 469 interpolation_mode = "linear" 470 if secondary_radius is not None and secondary_radius > 0.3: 471 self._logger.warning("Interpolation secondary_radius was too large, reduced to 0.3") 472 secondary_radius = 0.3 473 474 if isinstance(target, list) and len(target) == 7: 475 response = self._goto_joints( 476 target, 477 duration, 478 interpolation_space, 479 interpolation_mode, 480 degrees, 481 ) 482 elif isinstance(target, np.ndarray) and target.shape == (4, 4): 483 response = self._goto_from_matrix( 484 target, duration, interpolation_space, interpolation_mode, q0, arc_direction, secondary_radius 485 ) 486 487 if response.id == -1: 488 self._logger.error("Target was not reachable. No command sent.") 489 elif wait: 490 self._wait_goto(response, duration) 491 492 return response
Move the arm to a specified target position, either in joint space or Cartesian space.
This function allows the arm to move to a specified target using either:
- A list of 7 joint positions, or
- A 4x4 pose matrix representing the desired end-effector position.
The function also supports an optional initial configuration q0
for
computing the inverse kinematics solution when the target is in Cartesian space.
Arguments:
- target: The target position. It can either be a list of 7 joint values (for joint space) or a 4x4 NumPy array (for Cartesian space).
- duration: The time in seconds for the movement to be completed. Defaults to 2.
- wait: If True, the function waits until the movement is completed before returning. Defaults to False.
- interpolation_space: The space in which the interpolation should be performed. It can be either "joint_space" or "cartesian_space". Defaults to "joint_space".
- interpolation_mode: The interpolation method to be used. It can be either "minimum_jerk", "linear" or "elliptical". Defaults to "minimum_jerk".
- degrees: If True, the joint values in the
target
argument are treated as degrees. Defaults to True. - q0: An optional list of 7 joint values representing the initial configuration for inverse kinematics. Defaults to None.
- arc_direction: The direction of the arc to be followed during elliptical interpolation. Can be "above", "below", "front", "back", "left" or "right" . Defaults to "above".
- secondary_radius: The secondary radius of the ellipse for elliptical interpolation, in meters.
Returns:
GoToId: The unique GoToId identifier for the movement command.
Raises:
- TypeError: If the
target
is neither a list nor a pose matrix. - TypeError: If the
q0
is not a list. - ValueError: If the
target
list has a length other than 7, or the pose matrix is not of shape (4, 4). - ValueError: If the
q0
list has a length other than 7. - ValueError: If the
duration
is set to 0.
684 def goto_posture( 685 self, 686 common_posture: str = "default", 687 duration: float = 2, 688 wait: bool = False, 689 wait_for_goto_end: bool = True, 690 interpolation_mode: str = "minimum_jerk", 691 open_gripper: bool = False, 692 ) -> GoToId: 693 """Send all joints to standard positions with optional parameters for duration, waiting, and interpolation mode. 694 695 Args: 696 common_posture: The standard positions to which all joints will be sent. 697 It can be 'default' or 'elbow_90'. Defaults to 'default'. 698 duration: The time duration in seconds for the robot to move to the specified posture. 699 Defaults to 2. 700 wait: Determines whether the program should wait for the movement to finish before 701 returning. If set to `True`, the program waits for the movement to complete before continuing 702 execution. Defaults to `False`. 703 wait_for_goto_end: Specifies whether commands will be sent to a part immediately or 704 only after all previous commands in the queue have been executed. If set to `False`, the program 705 will cancel all executing moves and queues. Defaults to `True`. 706 interpolation_mode: The type of interpolation used when moving the arm's joints. 707 Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 708 open_gripper: If `True`, the gripper will open, if `False`, it stays in its current position. 709 Defaults to `False`. 710 711 Returns: 712 A unique GoToId identifier for this specific movement. 713 """ 714 joints = self.get_default_posture_joints(common_posture=common_posture) 715 if self._gripper is not None and self._gripper.is_on() and open_gripper: 716 self._gripper.open() 717 if not wait_for_goto_end: 718 self.cancel_all_goto() 719 if self.is_on(): 720 return self.goto(joints, duration, wait, interpolation_mode=interpolation_mode) 721 else: 722 self._logger.warning(f"{self._part_id.name} is off. No command sent.") 723 return GoToId(id=-1)
Send all joints to standard positions with optional parameters for duration, waiting, and interpolation mode.
Arguments:
- common_posture: The standard positions to which all joints will be sent. It can be 'default' or 'elbow_90'. Defaults to 'default'.
- duration: The time duration in seconds for the robot to move to the specified posture. Defaults to 2.
- wait: Determines whether the program should wait for the movement to finish before
returning. If set to
True
, the program waits for the movement to complete before continuing execution. Defaults toFalse
. - wait_for_goto_end: Specifies whether commands will be sent to a part immediately or
only after all previous commands in the queue have been executed. If set to
False
, the program will cancel all executing moves and queues. Defaults toTrue
. - interpolation_mode: The type of interpolation used when moving the arm's joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'.
- open_gripper: If
True
, the gripper will open, ifFalse
, it stays in its current position. Defaults toFalse
.
Returns:
A unique GoToId identifier for this specific movement.
725 def get_default_posture_joints(self, common_posture: str = "default") -> List[float]: 726 """Get the list of joint positions for default or elbow_90 poses. 727 728 Args: 729 common_posture: The name of the posture to retrieve. Can be "default" or "elbow_90". 730 Defaults to "default". 731 732 Returns: 733 A list of joint positions in degrees for the specified posture. 734 735 Raises: 736 ValueError: If `common_posture` is not "default" or "elbow_90". 737 """ 738 if common_posture not in ["default", "elbow_90"]: 739 raise ValueError(f"common_posture {common_posture} not supported! Should be 'default' or 'elbow_90'") 740 if common_posture == "elbow_90": 741 elbow_pitch = -90 742 else: 743 elbow_pitch = 0 744 if self._part_id.name == "r_arm": 745 return [0, 10, -10, elbow_pitch, 0, 0, 0] 746 else: 747 return [0, -10, 10, elbow_pitch, 0, 0, 0]
Get the list of joint positions for default or elbow_90 poses.
Arguments:
- common_posture: The name of the posture to retrieve. Can be "default" or "elbow_90". Defaults to "default".
Returns:
A list of joint positions in degrees for the specified posture.
Raises:
- ValueError: If
common_posture
is not "default" or "elbow_90".
749 def get_default_posture_matrix(self, common_posture: str = "default") -> npt.NDArray[np.float64]: 750 """Get the 4x4 pose matrix in Reachy coordinate system for a default robot posture. 751 752 Args: 753 common_posture: The posture to retrieve. Can be "default" or "elbow_90". 754 Defaults to "default". 755 756 Returns: 757 The 4x4 homogeneous pose matrix for the specified posture in Reachy coordinate system. 758 """ 759 joints = self.get_default_posture_joints(common_posture) 760 return self.forward_kinematics(joints)
Get the 4x4 pose matrix in Reachy coordinate system for a default robot posture.
Arguments:
- common_posture: The posture to retrieve. Can be "default" or "elbow_90". Defaults to "default".
Returns:
The 4x4 homogeneous pose matrix for the specified posture in Reachy coordinate system.
762 def get_translation_by( 763 self, 764 x: float, 765 y: float, 766 z: float, 767 initial_pose: Optional[npt.NDArray[np.float64]] = None, 768 frame: str = "robot", 769 ) -> npt.NDArray[np.float64]: 770 """Return a 4x4 matrix representing a pose translated by specified x, y, z values. 771 772 The translation is performed in either the robot or gripper coordinate system. 773 774 Args: 775 x: Translation along the x-axis in meters (forwards direction) to apply 776 to the pose matrix. 777 y: Translation along the y-axis in meters (left direction) to apply 778 to the pose matrix. 779 z: Translation along the z-axis in meters (upwards direction) to apply 780 to the pose matrix. 781 initial_pose: A 4x4 matrix representing the initial pose of the end-effector in Reachy coordinate system, 782 expressed as a NumPy array of type `np.float64`. 783 If not provided, the current pose of the arm is used. Defaults to `None`. 784 frame: The coordinate system in which the translation should be performed. 785 Can be either "robot" or "gripper". Defaults to "robot". 786 787 Returns: 788 A 4x4 pose matrix, expressed in Reachy coordinate system, 789 translated by the specified x, y, z values from the initial pose. 790 791 Raises: 792 ValueError: If the `frame` is not "robot" or "gripper". 793 """ 794 if frame not in ["robot", "gripper"]: 795 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 796 797 if initial_pose is None: 798 initial_pose = self.forward_kinematics() 799 800 pose = initial_pose.copy() 801 802 if frame == "robot": 803 pose[0, 3] += x 804 pose[1, 3] += y 805 pose[2, 3] += z 806 elif frame == "gripper": 807 pose = translate_in_self(initial_pose, [x, y, z]) 808 return pose
Return a 4x4 matrix representing a pose translated by specified x, y, z values.
The translation is performed in either the robot or gripper coordinate system.
Arguments:
- x: Translation along the x-axis in meters (forwards direction) to apply to the pose matrix.
- y: Translation along the y-axis in meters (left direction) to apply to the pose matrix.
- z: Translation along the z-axis in meters (upwards direction) to apply to the pose matrix.
- initial_pose: A 4x4 matrix representing the initial pose of the end-effector in Reachy coordinate system,
expressed as a NumPy array of type
np.float64
. If not provided, the current pose of the arm is used. Defaults toNone
. - frame: The coordinate system in which the translation should be performed. Can be either "robot" or "gripper". Defaults to "robot".
Returns:
A 4x4 pose matrix, expressed in Reachy coordinate system, translated by the specified x, y, z values from the initial pose.
Raises:
- ValueError: If the
frame
is not "robot" or "gripper".
810 def translate_by( 811 self, 812 x: float, 813 y: float, 814 z: float, 815 duration: float = 2, 816 wait: bool = False, 817 frame: str = "robot", 818 interpolation_space: str = "cartesian_space", 819 interpolation_mode: str = "minimum_jerk", 820 arc_direction: str = "above", 821 secondary_radius: Optional[float] = None, 822 ) -> GoToId: 823 """Create a translation movement for the arm's end effector. 824 825 The movement is based on the last sent position or the current position. 826 827 Args: 828 x: Translation along the x-axis in meters (forwards direction) to apply 829 to the pose matrix. 830 y: Translation along the y-axis in meters (left direction) to apply 831 to the pose matrix. 832 z: Translation along the z-axis in meters (vertical direction) to apply 833 to the pose matrix. 834 duration: Time duration in seconds for the translation movement to be completed. 835 Defaults to 2. 836 wait: Determines whether the program should wait for the movement to finish before 837 returning. If set to `True`, the program waits for the movement to complete before continuing 838 execution. Defaults to `False`. 839 frame: The coordinate system in which the translation should be performed. 840 Can be "robot" or "gripper". Defaults to "robot". 841 interpolation_mode: The type of interpolation to be used when moving the arm's 842 joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 843 844 Returns: 845 The GoToId of the movement command, created using the `goto_from_matrix` method with the 846 translated pose computed in the specified frame. 847 848 Raises: 849 ValueError: If the `frame` is not "robot" or "gripper". 850 """ 851 try: 852 goto = self.get_goto_queue()[-1] 853 except IndexError: 854 goto = self.get_goto_playing() 855 856 if goto.id != -1: 857 joints_request = self._get_goto_request(goto) 858 else: 859 joints_request = None 860 861 if joints_request is not None: 862 if joints_request.request.target.joints is not None: 863 pose = self.forward_kinematics(joints_request.request.target.joints) 864 else: 865 pose = joints_request.request.target.pose 866 else: 867 pose = self.forward_kinematics() 868 869 pose = self.get_translation_by(x, y, z, initial_pose=pose, frame=frame) 870 return self.goto( 871 pose, 872 duration=duration, 873 wait=wait, 874 interpolation_space=interpolation_space, 875 interpolation_mode=interpolation_mode, 876 arc_direction=arc_direction, 877 secondary_radius=secondary_radius, 878 )
Create a translation movement for the arm's end effector.
The movement is based on the last sent position or the current position.
Arguments:
- x: Translation along the x-axis in meters (forwards direction) to apply to the pose matrix.
- y: Translation along the y-axis in meters (left direction) to apply to the pose matrix.
- z: Translation along the z-axis in meters (vertical direction) to apply to the pose matrix.
- duration: Time duration in seconds for the translation movement to be completed. Defaults to 2.
- wait: Determines whether the program should wait for the movement to finish before
returning. If set to
True
, the program waits for the movement to complete before continuing execution. Defaults toFalse
. - frame: The coordinate system in which the translation should be performed. Can be "robot" or "gripper". Defaults to "robot".
- interpolation_mode: The type of interpolation to be used when moving the arm's joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'.
Returns:
The GoToId of the movement command, created using the
goto_from_matrix
method with the translated pose computed in the specified frame.
Raises:
- ValueError: If the
frame
is not "robot" or "gripper".
880 def get_rotation_by( 881 self, 882 roll: float, 883 pitch: float, 884 yaw: float, 885 initial_pose: Optional[npt.NDArray[np.float64]] = None, 886 degrees: bool = True, 887 frame: str = "robot", 888 ) -> npt.NDArray[np.float64]: 889 """Calculate a new pose matrix by rotating an initial pose matrix by specified roll, pitch, and yaw angles. 890 891 The rotation is performed in either the robot or gripper coordinate system. 892 893 Args: 894 roll: Rotation around the x-axis in the Euler angles representation, specified 895 in radians or degrees (based on the `degrees` parameter). 896 pitch: Rotation around the y-axis in the Euler angles representation, specified 897 in radians or degrees (based on the `degrees` parameter). 898 yaw: Rotation around the z-axis in the Euler angles representation, specified 899 in radians or degrees (based on the `degrees` parameter). 900 initial_pose: A 4x4 matrix representing the initial 901 pose of the end-effector, expressed as a NumPy array of type `np.float64`. If not provided, 902 the current pose of the arm is used. Defaults to `None`. 903 degrees: Specifies whether the rotation angles are provided in degrees. If set to 904 `True`, the angles are interpreted as degrees. Defaults to `True`. 905 frame: The coordinate system in which the rotation should be performed. Can be 906 "robot" or "gripper". Defaults to "robot". 907 908 Returns: 909 A 4x4 pose matrix, expressed in the Reachy coordinate system, rotated 910 by the specified roll, pitch, and yaw angles from the initial pose, in the specified frame. 911 912 Raises: 913 ValueError: If the `frame` is not "robot" or "gripper". 914 """ 915 if frame not in ["robot", "gripper"]: 916 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 917 918 if initial_pose is None: 919 initial_pose = self.forward_kinematics() 920 921 pose = initial_pose.copy() 922 rotation = matrix_from_euler_angles(roll, pitch, yaw, degrees=degrees) 923 924 if frame == "robot": 925 pose_rotation = np.eye(4) 926 pose_rotation[:3, :3] = pose.copy()[:3, :3] 927 pose_translation = pose.copy()[:3, 3] 928 pose_rotation = rotation @ pose_rotation 929 pose = recompose_matrix(pose_rotation[:3, :3], pose_translation) 930 elif frame == "gripper": 931 pose = rotate_in_self(initial_pose, [roll, pitch, yaw], degrees=degrees) 932 933 return pose
Calculate a new pose matrix by rotating an initial pose matrix by specified roll, pitch, and yaw angles.
The rotation is performed in either the robot or gripper coordinate system.
Arguments:
- roll: Rotation around the x-axis in the Euler angles representation, specified
in radians or degrees (based on the
degrees
parameter). - pitch: Rotation around the y-axis in the Euler angles representation, specified
in radians or degrees (based on the
degrees
parameter). - yaw: Rotation around the z-axis in the Euler angles representation, specified
in radians or degrees (based on the
degrees
parameter). - initial_pose: A 4x4 matrix representing the initial
pose of the end-effector, expressed as a NumPy array of type
np.float64
. If not provided, the current pose of the arm is used. Defaults toNone
. - degrees: Specifies whether the rotation angles are provided in degrees. If set to
True
, the angles are interpreted as degrees. Defaults toTrue
. - frame: The coordinate system in which the rotation should be performed. Can be "robot" or "gripper". Defaults to "robot".
Returns:
A 4x4 pose matrix, expressed in the Reachy coordinate system, rotated by the specified roll, pitch, and yaw angles from the initial pose, in the specified frame.
Raises:
- ValueError: If the
frame
is not "robot" or "gripper".
935 def rotate_by( 936 self, 937 roll: float, 938 pitch: float, 939 yaw: float, 940 duration: float = 2, 941 wait: bool = False, 942 degrees: bool = True, 943 frame: str = "robot", 944 interpolation_mode: str = "minimum_jerk", 945 ) -> GoToId: 946 """Create a rotation movement for the arm's end effector based on the specified roll, pitch, and yaw angles. 947 948 The rotation is performed in either the robot or gripper frame. 949 950 Args: 951 roll: Rotation around the x-axis in the Euler angles representation, specified 952 in radians or degrees (based on the `degrees` parameter). 953 pitch: Rotation around the y-axis in the Euler angles representation, specified 954 in radians or degrees (based on the `degrees` parameter). 955 yaw: Rotation around the z-axis in the Euler angles representation, specified 956 in radians or degrees (based on the `degrees` parameter). 957 duration: Time duration in seconds for the rotation movement to be completed. 958 Defaults to 2. 959 wait: Determines whether the program should wait for the movement to finish before 960 returning. If set to `True`, the program waits for the movement to complete before continuing 961 execution. Defaults to `False`. 962 degrees: Specifies whether the rotation angles are provided in degrees. If set to 963 `True`, the angles are interpreted as degrees. Defaults to `True`. 964 frame: The coordinate system in which the rotation should be performed. Can be 965 "robot" or "gripper". Defaults to "robot". 966 interpolation_mode: The type of interpolation to be used when moving the arm's 967 joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'. 968 969 Returns: 970 The GoToId of the movement command, created by calling the `goto_from_matrix` method with 971 the rotated pose computed in the specified frame. 972 973 Raises: 974 ValueError: If the `frame` is not "robot" or "gripper". 975 """ 976 if frame not in ["robot", "gripper"]: 977 raise ValueError(f"Unknown frame {frame}! Should be 'robot' or 'gripper'") 978 979 try: 980 goto = self.get_goto_queue()[-1] 981 except IndexError: 982 goto = self.get_goto_playing() 983 984 if goto.id != -1: 985 joints_request = self._get_goto_request(goto) 986 else: 987 joints_request = None 988 989 if joints_request is not None: 990 if joints_request.request.target.joints is not None: 991 pose = self.forward_kinematics(joints_request.request.target.joints) 992 else: 993 pose = joints_request.request.target.pose 994 else: 995 pose = self.forward_kinematics() 996 997 pose = self.get_rotation_by(roll, pitch, yaw, initial_pose=pose, degrees=degrees, frame=frame) 998 return self.goto(pose, duration=duration, wait=wait, interpolation_mode=interpolation_mode)
Create a rotation movement for the arm's end effector based on the specified roll, pitch, and yaw angles.
The rotation is performed in either the robot or gripper frame.
Arguments:
- roll: Rotation around the x-axis in the Euler angles representation, specified
in radians or degrees (based on the
degrees
parameter). - pitch: Rotation around the y-axis in the Euler angles representation, specified
in radians or degrees (based on the
degrees
parameter). - yaw: Rotation around the z-axis in the Euler angles representation, specified
in radians or degrees (based on the
degrees
parameter). - duration: Time duration in seconds for the rotation movement to be completed. Defaults to 2.
- wait: Determines whether the program should wait for the movement to finish before
returning. If set to
True
, the program waits for the movement to complete before continuing execution. Defaults toFalse
. - degrees: Specifies whether the rotation angles are provided in degrees. If set to
True
, the angles are interpreted as degrees. Defaults toTrue
. - frame: The coordinate system in which the rotation should be performed. Can be "robot" or "gripper". Defaults to "robot".
- interpolation_mode: The type of interpolation to be used when moving the arm's joints. Can be 'minimum_jerk' or 'linear'. Defaults to 'minimum_jerk'.
Returns:
The GoToId of the movement command, created by calling the
goto_from_matrix
method with the rotated pose computed in the specified frame.
Raises:
- ValueError: If the
frame
is not "robot" or "gripper".
1032 def send_goal_positions(self, check_positions: bool = False) -> None: 1033 """Send goal positions to the arm's joints, including the gripper. 1034 1035 If goal positions have been specified for any joint of the part, sends them to the robot. 1036 1037 Args : 1038 check_positions: A boolean indicating whether to check the positions after sending the command. 1039 Defaults to True. 1040 """ 1041 super().send_goal_positions(check_positions) 1042 if self.gripper is not None: 1043 self.gripper.send_goal_positions(check_positions)
Send goal positions to the arm's joints, including the gripper.
If goal positions have been specified for any joint of the part, sends them to the robot.
Args :
check_positions: A boolean indicating whether to check the positions after sending the command. Defaults to True.