@ -1,6 +1,6 @@
""" RM75 单臂 XR 相对位姿透传遥操作节点。
节点订阅左/右手柄位姿,在 grip 按下时锁定手柄和 TCP 起点,把手柄相对位移
节点订阅左/右手柄位姿,在 grip 按下时锁定手柄和 TCP 起点,把手柄相对位姿
映射成机器人坐标系中的目标 TCP, 并通过 rm_movep_canfd 持续下发目标位姿。
"""
@ -30,8 +30,123 @@ def _clamp(value: float, low: float, high: float) -> float:
return min ( max ( value , low ) , high )
def _wrap_angle ( angle : float ) - > float :
return math . atan2 ( math . sin ( angle ) , math . cos ( angle ) )
def _angle_delta ( target : float , current : float ) - > float :
return _wrap_angle ( target - current )
def _normalize_quaternion ( values : Iterable [ float ] ) - > tuple [ float , float , float , float ] :
x , y , z , w = [ float ( value ) for value in values ]
norm = math . sqrt ( x * x + y * y + z * z + w * w )
if norm < = 1e-9 or not math . isfinite ( norm ) :
raise ValueError ( " quaternion norm is zero or non-finite " )
return x / norm , y / norm , z / norm , w / norm
def _quaternion_conjugate (
quat : tuple [ float , float , float , float ] ,
) - > tuple [ float , float , float , float ] :
x , y , z , w = quat
return - x , - y , - z , w
def _quaternion_multiply (
left : tuple [ float , float , float , float ] ,
right : tuple [ float , float , float , float ] ,
) - > tuple [ float , float , float , float ] :
lx , ly , lz , lw = left
rx , ry , rz , rw = right
return _normalize_quaternion (
(
lw * rx + lx * rw + ly * rz - lz * ry ,
lw * ry - lx * rz + ly * rw + lz * rx ,
lw * rz + lx * ry - ly * rx + lz * rw ,
lw * rw - lx * rx - ly * ry - lz * rz ,
)
)
def _quaternion_to_matrix ( quat : tuple [ float , float , float , float ] ) - > list [ float ] :
x , y , z , w = quat
xx = x * x
yy = y * y
zz = z * z
xy = x * y
xz = x * z
yz = y * z
wx = w * x
wy = w * y
wz = w * z
return [
1.0 - 2.0 * ( yy + zz ) ,
2.0 * ( xy - wz ) ,
2.0 * ( xz + wy ) ,
2.0 * ( xy + wz ) ,
1.0 - 2.0 * ( xx + zz ) ,
2.0 * ( yz - wx ) ,
2.0 * ( xz - wy ) ,
2.0 * ( yz + wx ) ,
1.0 - 2.0 * ( xx + yy ) ,
]
def _matrix_multiply ( left : list [ float ] , right : list [ float ] ) - > list [ float ] :
return [
sum ( left [ row * 3 + k ] * right [ k * 3 + col ] for k in range ( 3 ) )
for row in range ( 3 )
for col in range ( 3 )
]
def _matrix_transpose ( matrix : list [ float ] ) - > list [ float ] :
return [
matrix [ 0 ] , matrix [ 3 ] , matrix [ 6 ] ,
matrix [ 1 ] , matrix [ 4 ] , matrix [ 7 ] ,
matrix [ 2 ] , matrix [ 5 ] , matrix [ 8 ] ,
]
def _euler_to_quaternion ( roll : float , pitch : float , yaw : float ) - > tuple [ float , float , float , float ] :
cy = math . cos ( yaw * 0.5 )
sy = math . sin ( yaw * 0.5 )
cp = math . cos ( pitch * 0.5 )
sp = math . sin ( pitch * 0.5 )
cr = math . cos ( roll * 0.5 )
sr = math . sin ( roll * 0.5 )
qw = cr * cp * cy + sr * sp * sy
qx = sr * cp * cy - cr * sp * sy
qy = cr * sp * cy + sr * cp * sy
qz = cr * cp * sy - sr * sp * cy
return _normalize_quaternion ( ( qx , qy , qz , qw ) )
def _euler_to_matrix ( roll : float , pitch : float , yaw : float ) - > list [ float ] :
return _quaternion_to_matrix ( _euler_to_quaternion ( roll , pitch , yaw ) )
def _matrix_to_euler ( matrix : list [ float ] ) - > tuple [ float , float , float ] :
sy = - _clamp ( matrix [ 6 ] , - 1.0 , 1.0 )
pitch = math . asin ( sy )
cp = math . cos ( pitch )
if abs ( cp ) > 1e-9 :
roll = math . atan2 ( matrix [ 7 ] , matrix [ 8 ] )
yaw = math . atan2 ( matrix [ 3 ] , matrix [ 0 ] )
else :
roll = math . atan2 ( - matrix [ 5 ] , matrix [ 4 ] )
yaw = 0.0
return _wrap_angle ( roll ) , _wrap_angle ( pitch ) , _wrap_angle ( yaw )
def _quaternion_to_euler ( quat : tuple [ float , float , float , float ] ) - > tuple [ float , float , float ] :
return _matrix_to_euler ( _quaternion_to_matrix ( quat ) )
class SingleArmVelocityTeleop ( Node ) :
""" 基于 XR 手柄相对位移 的 RM75 单臂位姿透传遥操节点。
""" 基于 XR 手柄相对位姿 的 RM75 单臂位姿透传遥操节点。
类名和可执行入口沿用旧名称,避免已有 launch、UI 和命令失效。
"""
@ -50,6 +165,11 @@ class SingleArmVelocityTeleop(Node):
self . declare_parameter ( " target_filter_fast_threshold_m " , 0.03 )
self . declare_parameter ( " max_linear_speed " , 0.25 )
self . declare_parameter ( " enable_position_axes " , [ True , True , True ] )
self . declare_parameter ( " enable_orientation_control " , False )
self . declare_parameter ( " enable_orientation_axes " , [ True , True , True ] )
self . declare_parameter ( " orientation_deadband_rad " , 0.005 )
self . declare_parameter ( " orientation_filter_alpha " , 0.65 )
self . declare_parameter ( " max_orientation_speed " , 0.6 )
self . declare_parameter ( " workspace_min " , [ 0.20 , - 0.35 , 0.10 ] )
self . declare_parameter ( " workspace_max " , [ 0.65 , 0.35 , 0.60 ] )
self . declare_parameter ( " cyl_radius_limit " , [ 0.20 , 0.60 ] )
@ -102,6 +222,11 @@ class SingleArmVelocityTeleop(Node):
)
self . _max_linear_speed = float ( self . get_parameter ( " max_linear_speed " ) . value )
self . _enable_position_axes = self . _bool_list_parameter ( " enable_position_axes " , 3 )
self . _enable_orientation_control = self . _bool_parameter ( " enable_orientation_control " )
self . _enable_orientation_axes = self . _bool_list_parameter ( " enable_orientation_axes " , 3 )
self . _orientation_deadband_rad = float ( self . get_parameter ( " orientation_deadband_rad " ) . value )
self . _orientation_filter_alpha = float ( self . get_parameter ( " orientation_filter_alpha " ) . value )
self . _max_orientation_speed = float ( self . get_parameter ( " max_orientation_speed " ) . value )
self . _workspace_min = self . _float_list_parameter ( " workspace_min " , 3 )
self . _workspace_max = self . _float_list_parameter ( " workspace_max " , 3 )
self . _cyl_radius_limit = self . _float_list_parameter ( " cyl_radius_limit " , 2 )
@ -122,9 +247,12 @@ class SingleArmVelocityTeleop(Node):
self . _last_msg_time : Time | None = None
self . _active = False
self . _controller_start : list [ float ] | None = None
self . _controller_orientation_start : tuple [ float , float , float , float ] | None = None
self . _robot_start_pose : ArmPose | None = None
self . _filtered_target : list [ float ] | None = None
self . _filtered_orientation_target : list [ float ] | None = None
self . _last_sent_target : list [ float ] | None = None
self . _last_sent_orientation : list [ float ] | None = None
self . _last_command_time : Time | None = None
self . _last_current_pose : ArmPose | None = None
self . _last_current_pose_time : Time | None = None
@ -150,7 +278,8 @@ class SingleArmVelocityTeleop(Node):
self . create_timer ( self . _dt , self . _control_tick )
self . get_logger ( ) . info (
f " { self . _arm_name } 位姿透传遥操节点已启动,监听话题: { topic } , "
f " dt= { self . _dt : .4f } s, follow= { self . _follow } "
f " dt= { self . _dt : .4f } s, follow= { self . _follow } , "
f " orientation_control= { self . _enable_orientation_control } "
)
def _make_adapter ( self ) :
@ -332,39 +461,66 @@ class SingleArmVelocityTeleop(Node):
return
controller_now = self . _controller_xyz ( self . _last_msg )
try :
controller_quat = self . _controller_quaternion ( self . _last_msg )
except ValueError as exc :
self . get_logger ( ) . warn (
f " { self . _arm_name } XR 手柄姿态无效,停止输出: { exc } " ,
throttle_duration_sec = 1.0 ,
)
self . _safe_stop ( reset_active = True )
return
if not self . _active :
self . _enter_active_control ( controller_now , now )
self . _enter_active_control ( controller_now , controller_quat , now )
return
assert self . _controller_start is not None
assert self . _robot_start_pose is not None
self . _maybe_refresh_current_pose ( now )
raw_target = self . _raw_target_from_controller ( controller_now )
workspace_target , workspace_clamped = self . _clamp_workspace_with_flag ( raw_targe t)
raw_target_xyz = self . _raw_target_from_controller ( controller_now )
raw_target_rpy = self . _raw_orientation_from_controller ( controller_qua t)
workspace_target , workspace_clamped = self . _clamp_workspace_with_flag ( raw_target_xyz )
desired_target = self . _apply_deadband ( workspace_target )
filtered_target = self . _filter_target ( desired_target )
sent_target , step_limited = self . _limit_target_step ( filtered_target )
sent_target , final_clamped = self . _clamp_workspace_with_flag ( sent_target )
desired_orientation = self . _apply_orientation_deadband ( raw_target_rpy )
filtered_orientation = self . _filter_orientation_target ( desired_orientation )
sent_orientation , orientation_step_limited = self . _limit_orientation_step ( filtered_orientation )
velocity = self . _estimate_command_velocity ( sent_target , now )
target_clamped = workspace_clamped or step_limited or final_clamped
velocity = self . _estimate_command_velocity ( sent_target , sent_orientation , now )
target_clamped = workspace_clamped or step_limited or final_clamped or orientation_step_limited
raw_target_pose = ArmPose (
x = raw_target_xyz [ 0 ] ,
y = raw_target_xyz [ 1 ] ,
z = raw_target_xyz [ 2 ] ,
rx = raw_target_rpy [ 0 ] ,
ry = raw_target_rpy [ 1 ] ,
rz = raw_target_rpy [ 2 ] ,
)
target_pose = ArmPose (
x = sent_target [ 0 ] ,
y = sent_target [ 1 ] ,
z = sent_target [ 2 ] ,
rx = self . _robot_start_pose . rx ,
ry = self . _robot_start_pose . ry ,
rz = self . _robot_start_pose . rz ,
rx = sent_orientation [ 0 ] ,
ry = sent_orientation [ 1 ] ,
rz = sent_orientation [ 2 ] ,
)
self . _publish_debug ( raw_target , sent_ target, velocity , target_clamped )
self . _publish_debug ( raw_target_pose , target_pose , velocity , target_clamped )
if self . _send_cartesian_target ( target_pose ) :
self . _last_sent_target = sent_target
self . _last_sent_orientation = sent_orientation
self . _last_command_time = now
self . _stop_sent = False
def _enter_active_control ( self , controller_now : list [ float ] , now : Time ) - > None :
def _enter_active_control (
self ,
controller_now : list [ float ] ,
controller_quat : tuple [ float , float , float , float ] ,
now : Time ,
) - > None :
try :
robot_pose = self . _read_current_pose_for_control ( now )
except Exception as exc :
@ -378,18 +534,33 @@ class SingleArmVelocityTeleop(Node):
robot_xyz = robot_pose . xyz ( )
self . _active = True
self . _controller_start = controller_now
self . _controller_orientation_start = controller_quat
self . _robot_start_pose = robot_pose
self . _filtered_target = robot_xyz
self . _filtered_orientation_target = robot_pose . rpy ( )
self . _last_sent_target = robot_xyz
self . _last_sent_orientation = robot_pose . rpy ( )
self . _last_command_time = now
self . _stop_sent = True
self . get_logger ( ) . info ( f " { self . _arm_name } Grip 按下,已锁定手柄和机械臂初始位姿。 " )
self . _publish_debug ( robot_xyz , robot_xyz , [ 0.0 ] * 6 , False )
self . _publish_debug ( robot_pose , robot_pose , [ 0.0 ] * 6 , False )
@staticmethod
def _controller_xyz ( msg : XrController ) - > list [ float ] :
return [ msg . pose . position . x , msg . pose . position . y , msg . pose . position . z ]
def _controller_quaternion ( self , msg : XrController ) - > tuple [ float , float , float , float ] :
if not self . _enable_orientation_control :
return 0.0 , 0.0 , 0.0 , 1.0
return _normalize_quaternion (
(
msg . pose . orientation . x ,
msg . pose . orientation . y ,
msg . pose . orientation . z ,
msg . pose . orientation . w ,
)
)
def _raw_target_from_controller ( self , controller_now : list [ float ] ) - > list [ float ] :
assert self . _controller_start is not None
assert self . _robot_start_pose is not None
@ -410,6 +581,38 @@ class SingleArmVelocityTeleop(Node):
matrix [ 6 ] * delta [ 0 ] + matrix [ 7 ] * delta [ 1 ] + matrix [ 8 ] * delta [ 2 ] ,
]
def _raw_orientation_from_controller (
self ,
controller_quat : tuple [ float , float , float , float ] ,
) - > list [ float ] :
assert self . _robot_start_pose is not None
if not self . _enable_orientation_control :
return self . _robot_start_pose . rpy ( )
assert self . _controller_orientation_start is not None
xr_delta_quat = _quaternion_multiply (
controller_quat ,
_quaternion_conjugate ( self . _controller_orientation_start ) ,
)
xr_delta_matrix = _quaternion_to_matrix ( xr_delta_quat )
matrix = self . _xr_to_robot_matrix
robot_delta_matrix = _matrix_multiply (
_matrix_multiply ( matrix , xr_delta_matrix ) ,
_matrix_transpose ( matrix ) ,
)
robot_start_matrix = _euler_to_matrix (
self . _robot_start_pose . rx ,
self . _robot_start_pose . ry ,
self . _robot_start_pose . rz ,
)
target_matrix = _matrix_multiply ( robot_delta_matrix , robot_start_matrix )
target_rpy = list ( _matrix_to_euler ( target_matrix ) )
start_rpy = self . _robot_start_pose . rpy ( )
return [
target_rpy [ i ] if self . _enable_orientation_axes [ i ] else start_rpy [ i ]
for i in range ( 3 )
]
def _apply_deadband ( self , target : list [ float ] ) - > list [ float ] :
if self . _deadband_m < = 0.0 or self . _last_sent_target is None :
return target
@ -457,6 +660,51 @@ class SingleArmVelocityTeleop(Node):
for i in range ( 3 )
] , True
def _apply_orientation_deadband ( self , target_rpy : list [ float ] ) - > list [ float ] :
if self . _orientation_deadband_rad < = 0.0 or self . _last_sent_orientation is None :
return [ _wrap_angle ( value ) for value in target_rpy ]
delta = [
_angle_delta ( target_rpy [ i ] , self . _last_sent_orientation [ i ] )
for i in range ( 3 )
]
if _norm ( delta ) < self . _orientation_deadband_rad :
return list ( self . _last_sent_orientation )
return [ _wrap_angle ( value ) for value in target_rpy ]
def _filter_orientation_target ( self , target_rpy : list [ float ] ) - > list [ float ] :
if self . _filtered_orientation_target is None :
self . _filtered_orientation_target = [ _wrap_angle ( value ) for value in target_rpy ]
return list ( self . _filtered_orientation_target )
self . _filtered_orientation_target = [
_wrap_angle (
self . _filtered_orientation_target [ i ]
+ self . _orientation_filter_alpha
* _angle_delta ( target_rpy [ i ] , self . _filtered_orientation_target [ i ] )
)
for i in range ( 3 )
]
return list ( self . _filtered_orientation_target )
def _limit_orientation_step ( self , target_rpy : list [ float ] ) - > tuple [ list [ float ] , bool ] :
if self . _last_sent_orientation is None :
return [ _wrap_angle ( value ) for value in target_rpy ] , False
delta = [
_angle_delta ( target_rpy [ i ] , self . _last_sent_orientation [ i ] )
for i in range ( 3 )
]
distance = _norm ( delta )
max_step = self . _max_orientation_speed * self . _dt
if distance < = max_step or distance < = 1e-9 :
return [ _wrap_angle ( value ) for value in target_rpy ] , False
scale = max_step / distance
return [
_wrap_angle ( self . _last_sent_orientation [ i ] + delta [ i ] * scale )
for i in range ( 3 )
] , True
def _clamp_workspace_with_flag ( self , target : list [ float ] ) - > tuple [ list [ float ] , bool ] :
clamped = [
_clamp ( target [ i ] , self . _workspace_min [ i ] , self . _workspace_max [ i ] )
@ -487,8 +735,13 @@ class SingleArmVelocityTeleop(Node):
return target
def _estimate_command_velocity ( self , target : list [ float ] , now : Time ) - > list [ float ] :
if self . _last_sent_target is None :
def _estimate_command_velocity (
self ,
target_xyz : list [ float ] ,
target_rpy : list [ float ] ,
now : Time ,
) - > list [ float ] :
if self . _last_sent_target is None or self . _last_sent_orientation is None :
return [ 0.0 ] * 6
dt = self . _dt
@ -497,9 +750,12 @@ class SingleArmVelocityTeleop(Node):
if measured_dt > 1e-6 :
dt = measured_dt
return [
( target [ i ] - self . _last_sent_target [ i ] ) / dt
( target_xyz [ i ] - self . _last_sent_target [ i ] ) / dt
for i in range ( 3 )
] + [ 0.0 , 0.0 , 0.0 ]
] + [
_angle_delta ( target_rpy [ i ] , self . _last_sent_orientation [ i ] ) / dt
for i in range ( 3 )
]
def _read_current_pose_for_control ( self , now : Time ) - > ArmPose :
pose = self . _adapter . get_current_pose ( )
@ -529,9 +785,12 @@ class SingleArmVelocityTeleop(Node):
if reset_active :
self . _active = False
self . _controller_start = None
self . _controller_orientation_start = None
self . _robot_start_pose = None
self . _filtered_target = None
self . _filtered_orientation_target = None
self . _last_sent_target = None
self . _last_sent_orientation = None
self . _last_command_time = None
self . _publish_stop_debug ( )
@ -549,7 +808,7 @@ class SingleArmVelocityTeleop(Node):
pose = self . _debug_pose_fallback ( )
if pose is None :
return
self . _publish_debug ( pose . xyz ( ) , pose . xyz ( ) , [ 0.0 ] * 6 , False )
self . _publish_debug ( pose , pose , [ 0.0 ] * 6 , False )
def _debug_pose_fallback ( self ) - > ArmPose | None :
if self . _last_current_pose is not None :
@ -557,7 +816,8 @@ class SingleArmVelocityTeleop(Node):
if self . _robot_start_pose is not None :
return self . _robot_start_pose
if self . _last_sent_target is not None :
return ArmPose ( * self . _last_sent_target )
rpy = self . _last_sent_orientation or [ 0.0 , 0.0 , 0.0 ]
return ArmPose ( * self . _last_sent_target , * rpy )
return None
def _send_cartesian_target ( self , pose : ArmPose ) - > bool :
@ -575,38 +835,18 @@ class SingleArmVelocityTeleop(Node):
def _publish_debug (
self ,
raw_target_xyz : list [ float ] ,
target_xyz : list [ float ] ,
raw_target_pose : ArmPose ,
target_pose : ArmPose ,
command_velocity : list [ float ] ,
target_clamped : bool ,
) - > None :
stamp = self . get_clock ( ) . now ( ) . to_msg ( )
current_pose = self . _debug_pose_fallback ( )
if current_pose is None :
current_pose = ArmPose ( * target_xyz )
current_pose = target_pose
raw_pose = self . _pose_msg (
stamp ,
ArmPose (
x = raw_target_xyz [ 0 ] ,
y = raw_target_xyz [ 1 ] ,
z = raw_target_xyz [ 2 ] ,
rx = current_pose . rx ,
ry = current_pose . ry ,
rz = current_pose . rz ,
) ,
)
target_msg = self . _pose_msg (
stamp ,
ArmPose (
x = target_xyz [ 0 ] ,
y = target_xyz [ 1 ] ,
z = target_xyz [ 2 ] ,
rx = current_pose . rx ,
ry = current_pose . ry ,
rz = current_pose . rz ,
) ,
)
raw_pose = self . _pose_msg ( stamp , raw_target_pose )
target_msg = self . _pose_msg ( stamp , target_pose )
velocity_msg = TwistStamped ( )
velocity_msg . header . stamp = stamp
velocity_msg . header . frame_id = " rm_base "
@ -628,7 +868,7 @@ class SingleArmVelocityTeleop(Node):
@staticmethod
def _pose_msg ( stamp , pose : ArmPose ) - > PoseStamped :
qx , qy , qz , qw = SingleArmVelocityTeleop . _euler_to_quaternion( pose . rx , pose . ry , pose . rz )
qx , qy , qz , qw = _euler_to_quaternion ( pose . rx , pose . ry , pose . rz )
msg = PoseStamped ( )
msg . header . stamp = stamp
msg . header . frame_id = " rm_base "
@ -641,21 +881,6 @@ class SingleArmVelocityTeleop(Node):
msg . pose . orientation . w = qw
return msg
@staticmethod
def _euler_to_quaternion ( roll : float , pitch : float , yaw : float ) - > tuple [ float , float , float , float ] :
cy = math . cos ( yaw * 0.5 )
sy = math . sin ( yaw * 0.5 )
cp = math . cos ( pitch * 0.5 )
sp = math . sin ( pitch * 0.5 )
cr = math . cos ( roll * 0.5 )
sr = math . sin ( roll * 0.5 )
qw = cr * cp * cy + sr * sp * sy
qx = sr * cp * cy - cr * sp * sy
qy = cr * sp * cy + sr * cp * sy
qz = cr * cp * sy - sr * sp * cy
return qx , qy , qz , qw
def _float_list_parameter ( self , name : str , expected_length : int ) - > list [ float ] :
values = [ float ( value ) for value in self . get_parameter ( name ) . value ]
if len ( values ) != expected_length :
@ -696,6 +921,12 @@ class SingleArmVelocityTeleop(Node):
raise ValueError ( " max_linear_speed must be > 0 " )
if self . _current_pose_poll_hz < 0.0 :
raise ValueError ( " current_pose_poll_hz must be >= 0 " )
if self . _orientation_deadband_rad < 0.0 :
raise ValueError ( " orientation_deadband_rad must be >= 0 " )
if not 0.0 < = self . _orientation_filter_alpha < = 1.0 :
raise ValueError ( " orientation_filter_alpha must be in [0, 1] " )
if self . _max_orientation_speed < = 0.0 :
raise ValueError ( " max_orientation_speed must be > 0 " )
if not 0.0 < = self . _trigger_close_threshold < = 1.0 :
raise ValueError ( " trigger_close_threshold must be in [0, 1] " )
for axis , ( low , high ) in enumerate ( zip ( self . _workspace_min , self . _workspace_max ) ) :