controller experiments - dingdongdengdong/astra_ws GitHub Wiki
leader_arm_controller
This Python code (`teleop_spacemouse_node.py`) is a ROS 2 node designed to control a robot arm using input from a 3D mouse (like a SpaceMouse). It involves coordinate transformations, robot control commands, and ROS 2 communication.
**Overall Functionality**
The node reads input from a SpaceMouse, converts it into robot arm movements, and publishes commands to control the arm and its gripper. It also handles coordinate frame transformations to ensure the robot moves as intended.
**Detailed Explanation**
**1. Imports**
The code starts by importing necessary Python libraries and ROS 2 modules:
* `rclpy`: The core ROS 2 Python client library.
* `rclpy.node`, `rclpy.publisher`, `rclpy.qos`: ROS 2 modules for creating nodes, publishers, and managing Quality of Service (QoS) settings.
* `sensor_msgs.msg`, `std_msgs.msg`, `geometry_msgs.msg`, `astra_controller_interfaces.msg`: ROS 2 message definitions for various data types (e.g., sensor data, poses, joint commands).
* `tf2_ros`: ROS 2 libraries for working with coordinate frame transformations.
* `numpy` (`np`): A library for numerical operations, especially for handling matrices and arrays.
* `pytransform3d`: A Python library for 3D transformations and rotations.
* `modern_robotics` (`mr`): A library for robot kinematics.
* `mr_urdf_loader`: A utility to load robot URDF files.
* `pathlib`, `ament_index_python`: Libraries for handling file paths and ROS 2 package resources.
* `astra_controller.experiments.spacemouse_agent`: A module specific to the robot setup, likely for interfacing with the SpaceMouse.
* `astra_controller.astra_controller`: Another module related to the robot control.
* `astra_teleop_web.teleoprator`: A module possibly related to web-based teleoperation.
* `math`, `time`: Standard Python libraries.
**2. QoS Profile**
```python
qos_profile_sensor_data_reliable = rclpy.qos.QoSProfile(**rclpy.impl.implementation_singleton.rclpy_implementation.rmw_qos_profile_t.predefined('qos_profile_sensor_data').to_dict())
qos_profile_sensor_data_reliable.reliability = 1
This section configures the Quality of Service (QoS) settings for ROS 2 communication. QoS determines how messages are delivered (e.g., reliability, history). Here, it sets up a qos_profile_sensor_data_reliable
profile, making it reliable, which means the system will ensure messages are delivered.
3. main
Function
The main
function is the entry point of the ROS 2 node.
-
Initialization
rclpy.init(args=args) node = rclpy.node.Node("teleop_spacemouse_node") logger = node.get_logger()
Initializes ROS 2, creates a node named "teleop_spacemouse_node", and gets a logger for outputting messages.
-
pub_T
Functiondef pub_T(pub: rclpy.publisher.Publisher, T, frame_id='base_link'): msg = geometry_msgs.msg.PoseStamped() msg.header.frame_id = frame_id msg.header.stamp = node.get_clock().now().to_msg() pq = pt.pq_from_transform(T) msg.pose.position.x = pq[0] msg.pose.position.y = pq[1] msg.pose.position.z = pq[2] msg.pose.orientation.w = pq[3] msg.pose.orientation.x = pq[4] msg.pose.orientation.y = pq[5] msg.pose.orientation.z = pq[6] pub.publish(msg)
This function publishes a 4x4 transformation matrix (
T
) as a ROS 2PoseStamped
message. Here's a breakdown:- It takes a publisher (
pub
), a transformation matrixT
, and a frame ID (frame_id
) as input. - It creates a
PoseStamped
message, which includes a pose and a header with timestamp and frame ID. pt.pq_from_transform(T)
converts the 4x4 transformation matrixT
into a position-quaternion representation (pq
). A transformation matrix combines both rotation and translation. The position is a 3D vector, and the orientation is represented as a quaternion (w, x, y, z).- It populates the message with the position and quaternion.
- It publishes the message.
- It takes a publisher (
-
Publishers
goal_pose_publisher = {} goal_pose_inactive_publisher = {} cam_pose_publisher = {} arm_gripper_joint_command_publisher = {} arm_joint_command_publisher = {} lift_joint_command_publisher = {} for side in ["left", "right"]: goal_pose_publisher[side] = node.create_publisher(geometry_msgs.msg.PoseStamped, f"{side}/goal_pose", 10) goal_pose_inactive_publisher[side] = node.create_publisher(geometry_msgs.msg.PoseStamped, f"{side}/goal_pose_inactive", 10) cam_pose_publisher[side] = node.create_publisher(geometry_msgs.msg.PoseStamped, f"{side}/cam_pose", 10) arm_gripper_joint_command_publisher[side] = node.create_publisher(astra_controller_interfaces.msg.JointCommand, f"{side}/arm/gripper_joint_command", 10) arm_joint_command_publisher[side] = node.create_publisher(astra_controller_interfaces.msg.JointCommand, f"{side}/arm/joint_command", 10) lift_joint_command_publisher[side] = node.create_publisher(astra_controller_interfaces.msg.JointCommand, f"{side}/lift/joint_command", 10)
This code creates several ROS 2 publishers for sending data to different topics. It appears to handle control for both "left" and "right" sides (possibly for a dual-arm robot):
goal_pose_publisher
: Publishes the desired end-effector pose.goal_pose_inactive_publisher
: Publishes a "goal pose inactive".cam_pose_publisher
: Publishes a camera pose.arm_gripper_joint_command_publisher
: Publishes commands to control the robot's gripper.arm_joint_command_publisher
: Publishes commands to control the robot's arm joints.lift_joint_command_publisher
: Publishes commands to control a lift joint.
-
pub_goal_cb
Functiondef pub_goal_cb(side, Tsgoal, Tscam=None, Tsgoal_inactive=None): if Tsgoal is not None: pub_T(goal_pose_publisher[side], Tsgoal) if Tsgoal_inactive is not None: pub_T(goal_pose_inactive_publisher[side], Tsgoal_inactive) if Tscam is not None: pub_T(cam_pose_publisher[side], Tscam)
This is a callback function to publish goal poses and camera poses. It takes a side ("left" or "right") and transformation matrices as input and uses the
pub_T
function to publish them. -
pub_gripper_cb
Functiondef pub_gripper_cb(side, gripper_open): arm_gripper_joint_command_publisher[side].publish(astra_controller_interfaces.msg.JointCommand( name=["joint_r7r" if side == "right" else "joint_l7r"], position_cmd=[gripper_open] ))
This function publishes gripper control commands. It takes the side and a gripper opening value (
gripper_open
) and sends aJointCommand
message. -
Transform Listener
tf_buffer = Buffer() tf_listener = TransformListener(tf_buffer, node) # Wait until the transform from 'base_link' to 'link_ree_teleop' becomes available. while rclpy.ok(): try: Tsgoal_msg: geometry_msgs.msg.TransformStamped = tf_buffer.lookup_transform('base_link', 'link_ree_teleop', rclpy.time.Time()) break except Exception as e: logger.warn(str(e)) # logger.warn("Transform lookup failed (base_link may not exist yet). Retrying...") rclpy.spin_once(node) Tsgoal = pt.transform_from_pq(np.array(pq_from_ros_transform(Tsgoal_msg.transform))) goal_gripper = GRIPPER_MAX
This section handles coordinate frame transformations using
tf2_ros
.- It creates a
TransformListener
to listen for transformations. - It waits until the transformation from the 'base_link' frame to the 'link_ree_teleop' frame is available. This is crucial because it establishes the initial position and orientation of the robot's end-effector (where the SpaceMouse control starts from) relative to the robot's base.
base_link
is typically the robot's base frame, andlink_ree_teleop
is likely a frame associated with the end-effector and the teleoperation setup. tf_buffer.lookup_transform
retrieves this transformation.- It converts the ROS 2 transform message to a 4x4 transformation matrix (
Tsgoal
) usingpt.transform_from_pq
andpq_from_ros_transform
. - It initializes
goal_gripper
to a maximum value.
- It creates a
-
Spacemouse Agent and Control Loop
agent = SpacemouseAgent() control_T = 0.01 while True: action, buttons = agent.act(None) action[:3] *= 200.0 / 1000.0 * control_T # mm/s action[3:] *= 60 / 180 * math.pi * control_T # deg/s Tv = pt.transform_from(pr.matrix_from_euler(action[3:], 0, 1, 2, True), action[:3]) # # Transform frame: cam # Tsgoal = Tsgoal @ Tv # Transform frame: base R = Tsgoal[:3, :3] p = Tsgoal[:3, 3] Tsgoal = pt.transform_from(np.eye(3), p) @ Tv @ pt.transform_from(R, np.zeros(3)) if buttons[0]: goal_gripper += 100.0 / 1000.0 * control_T if goal_gripper > GRIPPER_MAX: goal_gripper = GRIPPER_MAX elif buttons[1]: goal_gripper -= 100.0 / 1000.0 * control_T if goal_gripper < 0: goal_gripper = 0 pub_goal_cb("right", Tsgoal) pub_gripper_cb("right", goal_gripper) time.sleep(control_T)
This is the main control loop of the node:
- It creates an instance of
SpacemouseAgent
to handle SpaceMouse input. control_T
sets the control loop's time step (0.01 seconds).- The
while True
loop runs continuously:-
agent.act(None)
reads the SpaceMouse input, returningaction
(6D vector of translation and rotation) andbuttons
. -
action[:3] *= 200.0 / 1000.0 * control_T
scales the translational velocity from the SpaceMouse to mm/s. -
action[3:] *= 60 / 180 * math.pi * control_T
scales the rotational velocity from the SpaceMouse to rad/s. -
Tv = pt.transform_from(pr.matrix_from_euler(action[3:], 0, 1, 2, True), action[:3])
creates a 4x4 transformation matrixTv
from the SpaceMouse input.action[:3]
is the translation, andaction[3:]
(roll, pitch, yaw) is used to create a rotation matrix using Euler angles. -
The core transformation logic is:
R = Tsgoal[:3, :3] p = Tsgoal[:3, 3] Tsgoal_new = pt.transform_from(np.eye(3), p) @ Tv @ pt.transform_from(R, np.zeros(3))
This updates the goal pose (
Tsgoal
) based on the SpaceMouse input (Tv
). It decomposes the current goal pose into rotation (R) and position (p), calculates the new transformation, and updatesTsgoal
. Let's break this down with a mathematical representation:Tsgoal
: Current transformation matrix representing the goal pose of the robot's end-effector.R
: 3x3 rotation matrix extracted fromTsgoal
.p
: 3x1 position vector extracted fromTsgoal
.Tv
: 4x4 transformation matrix representing the change in pose commanded by the SpaceMouse.pt.transform_from(np.eye(3), p)
: Creates a transformation matrix representing only the translationp
.pt.transform_from(R, np.zeros(3))
: Creates a transformation matrix representing only the rotationR
.
The update equation is:
$$T_{goal_new} = T_{base_goal} \cdot T_v \cdot R_{goal_old}$$
Where:
T_goal_new
: The new goal transformation matrix.T_base_goal
: A transformation matrix representing the current position (p
) of the goal, but with no rotation (identity rotation).T_v
: The transformation matrix from the SpaceMouse input.R_goal_old
: A transformation matrix representing the current rotation (R
) of the goal, but with no translation (zero translation).
This way, the translation from the SpaceMouse is applied in the base frame, while the rotation is applied relative to the current orientation.
-
The code then checks for button presses on the SpaceMouse to control the gripper:
- Button 0 opens the gripper.
- Button 1 closes the gripper.
-
pub_goal_cb
andpub_gripper_cb
publish the updated goal pose and gripper commands. -
time.sleep(control_T)
introduces a delay to control the loop rate.
-
- It creates an instance of
-
Shutdown
rclpy.spin(node) # Destroy the node explicitly node.destroy_node() rclpy.shutdown()
rclpy.spin(node)
keeps the node alive and listening for callbacks (though it's not reached in the currentwhile True
loop). It's more typical to userclpy.spin
to handle callbacks for subscriptions.node.destroy_node()
andrclpy.shutdown()
properly clean up the ROS 2 node and resources.
-
Entry Point
if __name__ == '__main__': main()
This ensures that the
main
function is executed when the script is run.
Summary
This code sets up a ROS 2 node that:
- Receives 6D motion input from a SpaceMouse.
- Transforms this motion into desired end-effector poses for a robot arm.
- Publishes these pose commands and gripper control commands to the appropriate ROS 2 topics.
- Uses
tf2_ros
to manage coordinate frame transformations, ensuring the robot's movement is correctly interpreted.
The core of the code lies in the transformation update logic, where the SpaceMouse input is used to incrementally adjust the robot's goal pose. The separation of translation and rotation in the transformation update is important for controlling how the robot moves and rotates in 3D space.
math
The core of the robot's pose update is performed by this formula:
$$T_{goal_new} = T_{base_goal} \cdot T_v \cdot R_{goal_old}$$
To fully understand this, we need to break down each component:
-
T_goal_new
: This is the new 4x4 homogeneous transformation matrix that represents the desired pose (position and orientation) of the robot's end-effector after applying the SpaceMouse's input. It's what the code calculates in each iteration of the control loop. -
T_base_goal
: This 4x4 matrix represents the translational part of the current goal pose, but without any rotation. It's constructed in the code as:pt.transform_from(np.eye(3), p)
p
is a 3x1 vector representing the position (x, y, z) extracted from the current goal pose (Tsgoal
).np.eye(3)
creates a 3x3 identity matrix, representing no rotation.pt.transform_from
is a function from thepytransform3d
library that constructs a 4x4 homogeneous transformation matrix from a rotation matrix and a translation vector. In this case, it creates a matrix that only translates (moves) the end-effector without rotating it.
This matrix has the form:
[ 1 0 0 x ] [ 0 1 0 y ] [ 0 0 1 z ] [ 0 0 0 1 ]
Where
x
,y
, andz
are the coordinates of the current goal position. -
T_v
: This 4x4 matrix represents the transformation (translation and rotation) commanded by the SpaceMouse. It's calculated in the code as:Tv = pt.transform_from(pr.matrix_from_euler(action[3:], 0, 1, 2, True), action[:3])
action
is a 6x1 vector received from the SpaceMouse, whereaction[:3]
represents the translational velocity (dx, dy, dz) andaction[3:]
represents the rotational velocity (roll, pitch, yaw).pr.matrix_from_euler(action[3:], 0, 1, 2, True)
converts the rotational velocity (Euler angles) into a 3x3 rotation matrix. The0, 1, 2
specify the order of the Euler angles (x, y, z), andTrue
indicates that the input is an extrinsic rotation (rotation about fixed axes).pt.transform_from
then combines this rotation matrix and the translational velocity (action[:3]
) to create the 4x4 transformation matrixTv
.
This matrix represents the change in pose that the SpaceMouse is commanding.
-
R_goal_old
: This 4x4 matrix represents the rotational part of the current goal pose, but without any translation. It's constructed in the code as:pt.transform_from(R, np.zeros(3))
R
is the 3x3 rotation matrix extracted from the current goal pose (Tsgoal
).np.zeros(3)
creates a 3x1 zero vector, representing no translation.pt.transform_from
creates a 4x4 homogeneous transformation matrix from the rotation matrixR
and a zero translation vector.
This matrix has the form:
[ r11 r12 r13 0 ] [ r21 r22 r23 0 ] [ r31 r32 r33 0 ] [ 0 0 0 1 ]
Where
r11
tor33
are the elements of the rotation matrixR
.
Explanation of the Formula
The formula
$$T_{goal_new} = T_{base_goal} \cdot T_v \cdot R_{goal_old}$$
effectively updates the robot's goal pose in a way that:
-
Translates in the base frame:
T_base_goal . T_v
This part first applies the SpaceMouse's transformation (T_v
) as if the robot were starting from its base frame's origin, but only considers the translation component of the current goal. This means the translational movement from the SpaceMouse is applied relative to the robot's base. -
Rotates relative to the current orientation:
(... ) . R_goal_old
Then, it multiplies the result byR_goal_old
. This applies the current rotation of the end-effector to the result from the previous step. The rotational part of the SpaceMouse input (T_v
) is thus applied relative to the robot's current orientation, not the base frame.
Why this approach?
This approach provides more intuitive control. If you were to simply do Tsgoal = Tsgoal @ Tv
, both the translation and rotation from the SpaceMouse would be applied relative to the current pose of the end-effector. This can lead to less predictable and harder-to-control movements, especially for the translational part.
By separating the translation and rotation and applying the translation in the base frame, the control becomes more aligned with how a user might intuitively expect a robot to respond to 3D mouse input. Moving the mouse "forward" translates the end-effector in the general direction it's currently facing, rather than moving it relative to its current rotated coordinate frame.
# teleop_leader_arm_node