joint sensor增加双臂控制
This commit is contained in:
parent
6934e6c7a3
commit
96b00bd0a1
15
src/openarm_joint_sensor/config/joint_control_dual_arm.yaml
Normal file
15
src/openarm_joint_sensor/config/joint_control_dual_arm.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
joint_control_node:
|
||||||
|
ros__parameters:
|
||||||
|
enable_filter: true
|
||||||
|
filter_type: ema
|
||||||
|
ema_alpha: 0.15
|
||||||
|
kalman_q: 1.0e-5
|
||||||
|
kalman_r: 1.0e-2
|
||||||
|
|
||||||
|
right_joint_states_topic: /right_joint_sensor_states
|
||||||
|
left_joint_states_topic: /left_joint_sensor_states
|
||||||
|
|
||||||
|
right_arm_cmd_topic: /right_forward_position_controller/commands
|
||||||
|
right_gripper_cmd_topic: /right_gripper_controller/commands
|
||||||
|
left_arm_cmd_topic: /left_forward_position_controller/commands
|
||||||
|
left_gripper_cmd_topic: /left_gripper_controller/commands
|
||||||
@ -1,6 +1,36 @@
|
|||||||
joint_sensor_node:
|
joint_sensor_node:
|
||||||
ros__parameters:
|
ros__parameters:
|
||||||
|
# CAN and topic settings (dual-arm)
|
||||||
|
can_interface: can2
|
||||||
|
right_can_interface: can2
|
||||||
|
left_can_interface: can3
|
||||||
|
enable_left_arm: true
|
||||||
|
right_joint_states_topic: /right_joint_sensor_states
|
||||||
|
left_joint_states_topic: /left_joint_sensor_states
|
||||||
|
|
||||||
|
# Right arm calibration
|
||||||
zero_offsets: [2.7995149378912956, 2.5233983960718795, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
zero_offsets: [2.7995149378912956, 2.5233983960718795, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||||
sensor_directions: [-1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0]
|
sensor_directions: [-1.0, 1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0]
|
||||||
auto_zero_on_first_sample: true
|
auto_zero_on_first_sample: true
|
||||||
can_interface: 'can2'
|
|
||||||
|
# Left arm calibration
|
||||||
|
left_zero_offsets: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||||
|
left_sensor_directions: [-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0]
|
||||||
|
left_auto_zero_on_first_sample: true
|
||||||
|
|
||||||
|
# Runtime calibration triggers
|
||||||
|
calibrate_now: false
|
||||||
|
left_calibrate_now: false
|
||||||
|
|
||||||
|
# Diagnostics and anomaly filtering
|
||||||
|
enable_stage_diag_log: false
|
||||||
|
stage_log_min_interval_sec: 0.5
|
||||||
|
enable_speed_jump_log: false
|
||||||
|
speed_jump_threshold_rad_s: 3.0
|
||||||
|
jump_log_min_interval_sec: 0.2
|
||||||
|
joint_update_epsilon_rad: 1.0e-4
|
||||||
|
enable_raw_jump_log: false
|
||||||
|
raw_jump_threshold_rad: 0.2
|
||||||
|
raw_jump_log_min_interval_sec: 0.2
|
||||||
|
enable_raw_jump_reject: true
|
||||||
|
raw_jump_reject_threshold_rad: 0.35
|
||||||
|
|||||||
@ -13,12 +13,22 @@ def generate_launch_description():
|
|||||||
'config',
|
'config',
|
||||||
'joint_sensor_zero_offsets.yaml',
|
'joint_sensor_zero_offsets.yaml',
|
||||||
)
|
)
|
||||||
|
default_control_params_file = os.path.join(
|
||||||
|
package_share,
|
||||||
|
'config',
|
||||||
|
'joint_control_dual_arm.yaml',
|
||||||
|
)
|
||||||
|
|
||||||
params_file_arg = DeclareLaunchArgument(
|
params_file_arg = DeclareLaunchArgument(
|
||||||
'params_file',
|
'params_file',
|
||||||
default_value=default_params_file,
|
default_value=default_params_file,
|
||||||
description='Path to the YAML parameter file for joint_sensor_can node.',
|
description='Path to the YAML parameter file for joint_sensor_can node.',
|
||||||
)
|
)
|
||||||
|
control_params_file_arg = DeclareLaunchArgument(
|
||||||
|
'control_params_file',
|
||||||
|
default_value=default_control_params_file,
|
||||||
|
description='Path to the YAML parameter file for joint_control_node.',
|
||||||
|
)
|
||||||
|
|
||||||
joint_sensor_node = Node(
|
joint_sensor_node = Node(
|
||||||
package='openarm_joint_sensor',
|
package='openarm_joint_sensor',
|
||||||
@ -33,10 +43,12 @@ def generate_launch_description():
|
|||||||
executable='joint_control',
|
executable='joint_control',
|
||||||
name='joint_control_node',
|
name='joint_control_node',
|
||||||
output='screen',
|
output='screen',
|
||||||
|
parameters=[LaunchConfiguration('control_params_file')],
|
||||||
)
|
)
|
||||||
delayed_joint_control_node = TimerAction(period=3.0, actions=[joint_control_node])
|
delayed_joint_control_node = TimerAction(period=1.0, actions=[joint_control_node])
|
||||||
return LaunchDescription([
|
return LaunchDescription([
|
||||||
params_file_arg,
|
params_file_arg,
|
||||||
|
control_params_file_arg,
|
||||||
joint_sensor_node,
|
joint_sensor_node,
|
||||||
delayed_joint_control_node,
|
delayed_joint_control_node,
|
||||||
])
|
])
|
||||||
|
|||||||
@ -91,112 +91,92 @@ class PositionFilter:
|
|||||||
return float(z)
|
return float(z)
|
||||||
|
|
||||||
class ProgressActionClient(Node):
|
class ProgressActionClient(Node):
|
||||||
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('progress_action_client')
|
super().__init__('progress_action_client')
|
||||||
|
|
||||||
# Parameters for optional position filtering
|
self.num_joints = 8
|
||||||
|
|
||||||
|
# Parameters for optional position filtering and dual-arm topics
|
||||||
self.declare_parameter('enable_filter', True)
|
self.declare_parameter('enable_filter', True)
|
||||||
self.declare_parameter('filter_type', 'ema')
|
self.declare_parameter('filter_type', 'ema')
|
||||||
self.declare_parameter('ema_alpha', 0.15)
|
self.declare_parameter('ema_alpha', 0.15)
|
||||||
self.declare_parameter('kalman_q', 1e-5)
|
self.declare_parameter('kalman_q', 1e-5)
|
||||||
self.declare_parameter('kalman_r', 1e-2)
|
self.declare_parameter('kalman_r', 1e-2)
|
||||||
self.declare_parameter('enable_jump_log', False)
|
|
||||||
self.declare_parameter('jump_velocity_threshold_rad_s', 3.0)
|
self.declare_parameter('right_joint_states_topic', '/right_joint_sensor_states')
|
||||||
self.declare_parameter('jump_log_min_interval_sec', 0.2)
|
self.declare_parameter('left_joint_states_topic', '/left_joint_sensor_states')
|
||||||
self.declare_parameter('joint_update_epsilon_rad', 1e-4)
|
self.declare_parameter('right_arm_cmd_topic', '/right_forward_position_controller/commands')
|
||||||
|
self.declare_parameter('right_gripper_cmd_topic', '/right_gripper_controller/commands')
|
||||||
|
self.declare_parameter('left_arm_cmd_topic', '/left_forward_position_controller/commands')
|
||||||
|
self.declare_parameter('left_gripper_cmd_topic', '/left_gripper_controller/commands')
|
||||||
|
|
||||||
self.enable_filter = self.get_parameter('enable_filter').get_parameter_value().bool_value
|
self.enable_filter = self.get_parameter('enable_filter').get_parameter_value().bool_value
|
||||||
self.filter_type = self.get_parameter('filter_type').get_parameter_value().string_value
|
self.filter_type = self.get_parameter('filter_type').get_parameter_value().string_value
|
||||||
self.ema_alpha = self.get_parameter('ema_alpha').get_parameter_value().double_value
|
self.ema_alpha = self.get_parameter('ema_alpha').get_parameter_value().double_value
|
||||||
self.kalman_q = self.get_parameter('kalman_q').get_parameter_value().double_value
|
self.kalman_q = self.get_parameter('kalman_q').get_parameter_value().double_value
|
||||||
self.kalman_r = self.get_parameter('kalman_r').get_parameter_value().double_value
|
self.kalman_r = self.get_parameter('kalman_r').get_parameter_value().double_value
|
||||||
self.enable_jump_log = self.get_parameter('enable_jump_log').get_parameter_value().bool_value
|
|
||||||
self.jump_velocity_threshold_rad_s = self.get_parameter('jump_velocity_threshold_rad_s').get_parameter_value().double_value
|
|
||||||
self.jump_log_min_interval_sec = self.get_parameter('jump_log_min_interval_sec').get_parameter_value().double_value
|
|
||||||
self.joint_update_epsilon_rad = self.get_parameter('joint_update_epsilon_rad').get_parameter_value().double_value
|
|
||||||
|
|
||||||
# Create per-joint filters
|
self.right_joint_states_topic = self.get_parameter('right_joint_states_topic').get_parameter_value().string_value
|
||||||
self.num_joints = 8
|
self.left_joint_states_topic = self.get_parameter('left_joint_states_topic').get_parameter_value().string_value
|
||||||
self.filters = [PositionFilter(filter_type=self.filter_type,
|
self.right_arm_cmd_topic = self.get_parameter('right_arm_cmd_topic').get_parameter_value().string_value
|
||||||
alpha=self.ema_alpha,
|
self.right_gripper_cmd_topic = self.get_parameter('right_gripper_cmd_topic').get_parameter_value().string_value
|
||||||
q=self.kalman_q,
|
self.left_arm_cmd_topic = self.get_parameter('left_arm_cmd_topic').get_parameter_value().string_value
|
||||||
r=self.kalman_r)
|
self.left_gripper_cmd_topic = self.get_parameter('left_gripper_cmd_topic').get_parameter_value().string_value
|
||||||
for _ in range(self.num_joints)]
|
|
||||||
self.prev_raw_positions = [None] * self.num_joints
|
|
||||||
self.last_update_time_sec = [None] * self.num_joints
|
|
||||||
self.last_jump_log_time_sec = [0.0] * self.num_joints
|
|
||||||
|
|
||||||
# Buffer outgoing joint commands and send them at timer rate.
|
# Right arm runtime states
|
||||||
self.command_queue = deque(maxlen=20)
|
self.right_filters = [PositionFilter(filter_type=self.filter_type,
|
||||||
|
alpha=self.ema_alpha,
|
||||||
|
q=self.kalman_q,
|
||||||
|
r=self.kalman_r)
|
||||||
|
for _ in range(self.num_joints)]
|
||||||
|
|
||||||
self.subscriber_ = self.create_subscription(
|
self.right_command_queue = deque(maxlen=20)
|
||||||
|
|
||||||
|
# Left arm runtime states
|
||||||
|
self.left_filters = [PositionFilter(filter_type=self.filter_type,
|
||||||
|
alpha=self.ema_alpha,
|
||||||
|
q=self.kalman_q,
|
||||||
|
r=self.kalman_r)
|
||||||
|
for _ in range(self.num_joints)]
|
||||||
|
|
||||||
|
self.left_command_queue = deque(maxlen=20)
|
||||||
|
|
||||||
|
self.right_subscriber_ = self.create_subscription(
|
||||||
JointState,
|
JointState,
|
||||||
"/joint_sensor_states",
|
self.right_joint_states_topic,
|
||||||
self.listener_callback,
|
self.right_listener_callback,
|
||||||
10
|
10
|
||||||
)
|
)
|
||||||
|
|
||||||
self.armPosPublisher_ = self.create_publisher(Float64MultiArray, "/right_forward_position_controller/commands", 10)
|
self.left_subscriber_ = self.create_subscription(
|
||||||
self.gipperPosPublisher_ = self.create_publisher(Float64MultiArray, "/right_gripper_controller/commands", 10)
|
JointState,
|
||||||
# 3-1.创建动作客户端;
|
self.left_joint_states_topic,
|
||||||
# self._action_client = ActionClient(self, FollowJointTrajectory, '/right_joint_trajectory_controller/follow_joint_trajectory')
|
self.left_listener_callback,
|
||||||
# self.right_gripper_action_client = ActionClient(self, FollowJointTrajectory, '/right_joint_trajectory_controller/follow_joint_trajectory')
|
10,
|
||||||
|
)
|
||||||
|
|
||||||
self.create_timer(0.01, self.timer_callback) # 定时器,周期为0.02秒
|
self.right_arm_publisher = self.create_publisher(Float64MultiArray, self.right_arm_cmd_topic, 10)
|
||||||
# 3. 创建订阅者
|
self.right_gripper_publisher = self.create_publisher(Float64MultiArray, self.right_gripper_cmd_topic, 10)
|
||||||
# 参数1:消息类型(如String,与发布者一致)
|
self.left_arm_publisher = self.create_publisher(Float64MultiArray, self.left_arm_cmd_topic, 10)
|
||||||
# 参数2:话题名称(如"chatter",与发布者一致)
|
self.left_gripper_publisher = self.create_publisher(Float64MultiArray, self.left_gripper_cmd_topic, 10)
|
||||||
# 参数3:回调函数(消息到达时触发,用于处理消息)
|
|
||||||
# 参数4:队列长度(10,缓存消息的最大数量)
|
|
||||||
|
|
||||||
self.subscriber_ # 防止未使用变量警告
|
self.create_timer(0.01, self.timer_callback)
|
||||||
|
|
||||||
# 4. 消息处理回调函数(核心)
|
def right_listener_callback(self, msg):
|
||||||
# 当订阅到消息时,自动调用此函数处理消息
|
self._process_arm_joint_state(
|
||||||
def listener_callback(self, msg):
|
msg=msg,
|
||||||
|
filters=self.right_filters,
|
||||||
|
command_queue=self.right_command_queue,
|
||||||
|
)
|
||||||
|
|
||||||
# self.send_goal([count, count1, 0.0, 0.0, 0.0, 0.0, 0.0])
|
def left_listener_callback(self, msg):
|
||||||
# self.get_logger().info(f'Received joint states: {list(msg.name)} positions: {list(msg.position)}')
|
self._process_arm_joint_state(
|
||||||
# Apply position filtering if enabled
|
msg=msg,
|
||||||
|
filters=self.left_filters,
|
||||||
|
command_queue=self.left_command_queue,
|
||||||
|
)
|
||||||
|
def _process_arm_joint_state(self, msg, filters, command_queue):
|
||||||
arm_pos = list(msg.position)
|
arm_pos = list(msg.position)
|
||||||
|
|
||||||
# 基于“每关节速度阈值”检测异常,减少仅靠角度差导致的误报
|
|
||||||
if self.enable_jump_log and len(arm_pos) >= self.num_joints:
|
|
||||||
now_sec = self.get_clock().now().nanoseconds * 1e-9
|
|
||||||
for i in range(self.num_joints):
|
|
||||||
try:
|
|
||||||
curr = float(arm_pos[i])
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
prev = self.prev_raw_positions[i]
|
|
||||||
if prev is None:
|
|
||||||
self.prev_raw_positions[i] = curr
|
|
||||||
self.last_update_time_sec[i] = now_sec
|
|
||||||
continue
|
|
||||||
|
|
||||||
delta = curr - prev
|
|
||||||
if abs(delta) <= self.joint_update_epsilon_rad:
|
|
||||||
continue
|
|
||||||
|
|
||||||
last_time = self.last_update_time_sec[i]
|
|
||||||
dt = 0.0 if last_time is None else (now_sec - last_time)
|
|
||||||
if dt > 1e-6:
|
|
||||||
speed = abs(delta) / dt
|
|
||||||
if speed >= self.jump_velocity_threshold_rad_s:
|
|
||||||
since_last_log = now_sec - self.last_jump_log_time_sec[i]
|
|
||||||
if since_last_log >= self.jump_log_min_interval_sec:
|
|
||||||
self.get_logger().warning(
|
|
||||||
f'Joint{i + 1} jump detected: prev={prev:.6f} curr={curr:.6f} '
|
|
||||||
f'delta={delta:.6f} rad dt={dt:.4f} s speed={speed:.3f} rad/s'
|
|
||||||
)
|
|
||||||
self.last_jump_log_time_sec[i] = now_sec
|
|
||||||
|
|
||||||
self.prev_raw_positions[i] = curr
|
|
||||||
self.last_update_time_sec[i] = now_sec
|
|
||||||
|
|
||||||
if self.enable_filter:
|
if self.enable_filter:
|
||||||
if len(arm_pos) >= self.num_joints:
|
if len(arm_pos) >= self.num_joints:
|
||||||
filtered = []
|
filtered = []
|
||||||
@ -205,7 +185,7 @@ class ProgressActionClient(Node):
|
|||||||
v = float(arm_pos[i])
|
v = float(arm_pos[i])
|
||||||
except Exception:
|
except Exception:
|
||||||
v = None
|
v = None
|
||||||
fv = self.filters[i].update(v)
|
fv = filters[i].update(v)
|
||||||
# If filter returned None (no prior), fall back to raw
|
# If filter returned None (no prior), fall back to raw
|
||||||
filtered.append(fv if fv is not None else (arm_pos[i] if i < len(arm_pos) else 0.0))
|
filtered.append(fv if fv is not None else (arm_pos[i] if i < len(arm_pos) else 0.0))
|
||||||
arm_pos_to_send = filtered
|
arm_pos_to_send = filtered
|
||||||
@ -226,68 +206,31 @@ class ProgressActionClient(Node):
|
|||||||
arm7_pos_to_send = arm_pos_to_send[:7]
|
arm7_pos_to_send = arm_pos_to_send[:7]
|
||||||
gripper_pos_to_send = arm_pos_to_send[7]
|
gripper_pos_to_send = arm_pos_to_send[7]
|
||||||
|
|
||||||
self.command_queue.append((arm7_pos_to_send, gripper_pos_to_send))
|
command_queue.append((arm7_pos_to_send, gripper_pos_to_send))
|
||||||
# self.send_goal(arm_pos_to_send)
|
|
||||||
|
|
||||||
def timer_callback(self):
|
def timer_callback(self):
|
||||||
# 定时器回调函数,可以用于周期性任务
|
if self.right_command_queue:
|
||||||
if self.command_queue:
|
arm7_pos_to_send, gripper_pos_to_send = self.right_command_queue.popleft()
|
||||||
arm7_pos_to_send, gripper_pos_to_send = self.command_queue.popleft()
|
self.send_goal('right', arm7_pos_to_send, gripper_pos_to_send)
|
||||||
self.send_goal(arm7_pos_to_send, gripper_pos_to_send)
|
|
||||||
|
|
||||||
def send_goal(self, arm7_pos, gripper_pos):
|
if self.left_command_queue:
|
||||||
arm_joint = ['openarm_right_joint1', 'openarm_right_joint2', 'openarm_right_joint3', 'openarm_right_joint4', 'openarm_right_joint5', 'openarm_right_joint6', 'openarm_right_joint7']
|
arm7_pos_to_send, gripper_pos_to_send = self.left_command_queue.popleft()
|
||||||
arm_effort = [10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0]
|
self.send_goal('left', arm7_pos_to_send, gripper_pos_to_send)
|
||||||
arm_zero = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
|
||||||
# 3-2.发送请求;
|
|
||||||
# goal_msg = FollowJointTrajectory.Goal()
|
|
||||||
# goal_msg.trajectory.joint_names = arm_joint
|
|
||||||
# goal_msg.trajectory.points.append(JointTrajectoryPoint())
|
|
||||||
# goal_msg.trajectory.points[0].positions = arm_pos
|
|
||||||
# goal_msg.trajectory.points[0].effort = arm_effort
|
|
||||||
# print(goal_msg)
|
|
||||||
|
|
||||||
# self._action_client.wait_for_server()
|
def send_goal(self, arm_name, arm7_pos, gripper_pos):
|
||||||
# self._send_goal_future = self._action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_callback)
|
|
||||||
# self._send_goal_future.add_done_callback(self.goal_response_callback)
|
|
||||||
arm_msg = Float64MultiArray()
|
arm_msg = Float64MultiArray()
|
||||||
arm_msg.data = arm7_pos
|
arm_msg.data = arm7_pos
|
||||||
self.armPosPublisher_.publish(arm_msg)
|
|
||||||
|
|
||||||
gripper_msg = Float64MultiArray()
|
gripper_msg = Float64MultiArray()
|
||||||
gripper_msg.data = [map_float_range(gripper_pos, 0.0, 1.18, 0.0, 0.045)]
|
gripper_msg.data = [map_float_range(gripper_pos, 0.0, 1.18, 0.0, 0.045)]
|
||||||
self.gipperPosPublisher_.publish(gripper_msg)
|
|
||||||
|
|
||||||
# self.get_logger().info(
|
if arm_name == 'left':
|
||||||
# f'Published arm joints(1-7): {arm7_pos}, gripper joint8: {gripper_pos}'
|
self.left_arm_publisher.publish(arm_msg)
|
||||||
# )
|
self.left_gripper_publisher.publish(gripper_msg)
|
||||||
# self.get_logger().info(f'Published gripper joint8: {gripper_pos}')
|
else:
|
||||||
|
self.right_arm_publisher.publish(arm_msg)
|
||||||
|
self.right_gripper_publisher.publish(gripper_msg)
|
||||||
|
|
||||||
# def goal_response_callback(self, future):
|
|
||||||
# # 3-3.处理目标发送后的反馈;
|
|
||||||
# goal_handle = future.result()
|
|
||||||
# print(goal_handle)
|
|
||||||
# if not goal_handle.accepted:
|
|
||||||
# self.get_logger().info('请求被拒绝')
|
|
||||||
# return
|
|
||||||
|
|
||||||
# self.get_logger().info('请求被接收,开始执行任务!')
|
|
||||||
|
|
||||||
# self._get_result_future = goal_handle.get_result_async()
|
|
||||||
# self._get_result_future.add_done_callback(self.get_result_callback)
|
|
||||||
|
|
||||||
# # 3-5.处理最终响应。
|
|
||||||
# def get_result_callback(self, future):
|
|
||||||
# return
|
|
||||||
# # result = future.result().result
|
|
||||||
# # self.get_logger().info('最终计算结果:sum = %d' % result.sum)
|
|
||||||
# # 5.释放资源。
|
|
||||||
# # rclpy.shutdown()
|
|
||||||
|
|
||||||
# # 3-4.处理连续反馈;
|
|
||||||
# def feedback_callback(self, feedback_msg):
|
|
||||||
# feedback = (int)(feedback_msg.feedback.progress * 100)
|
|
||||||
# self.get_logger().info('当前进度: %d%%' % feedback)
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
rclpy.init()
|
rclpy.init()
|
||||||
|
|||||||
@ -23,45 +23,27 @@ class JointSensorNode(Node):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__('joint_sensor_node')
|
super().__init__('joint_sensor_node')
|
||||||
self.num_joints = 8
|
self.num_joints = 8
|
||||||
self.declare_parameter('can_interface', 'can2')
|
self.arm_labels = {'right': 'RIGHT', 'left': 'LEFT'}
|
||||||
self.get_logger().info('正在初始化CAN接口...')
|
|
||||||
can_interface = self.get_parameter('can_interface').value
|
|
||||||
self.bus = can.interface.Bus(channel=can_interface, bustype='socketcan')
|
|
||||||
# ROS2 publisher: 发布 JointState 到话题 'joint_states'
|
|
||||||
self.publisher_ = self.create_publisher(JointState, 'joint_sensor_states', 10)
|
|
||||||
|
|
||||||
self.joint_names = [f'joint{i}' for i in range(1, self.num_joints + 1)]
|
# CAN interface and topic parameters
|
||||||
# 8路传感器 CAN ID -> 8个关节索引
|
self.declare_parameter('can_interface', 'can2') # backward-compatible alias for right arm
|
||||||
self.can_to_joint_index = {
|
self.declare_parameter('right_can_interface', 'can2')
|
||||||
0x01: 0,
|
self.declare_parameter('left_can_interface', 'can3')
|
||||||
0x02: 1,
|
self.declare_parameter('enable_left_arm', True)
|
||||||
0x03: 2,
|
self.declare_parameter('right_joint_states_topic', 'right_joint_sensor_states')
|
||||||
0x04: 3,
|
self.declare_parameter('left_joint_states_topic', 'left_joint_sensor_states')
|
||||||
0x05: 4,
|
|
||||||
0x06: 5,
|
|
||||||
0x07: 6,
|
|
||||||
0x08: 7,
|
|
||||||
}
|
|
||||||
self.current_positions = [0.0] * self.num_joints
|
|
||||||
self.current_velocities = [0.0] * self.num_joints
|
|
||||||
self.has_position = [False] * self.num_joints
|
|
||||||
self.last_wrapped_positions = [0.0] * self.num_joints
|
|
||||||
self.has_wrapped_position = [False] * self.num_joints
|
|
||||||
self.last_joint_update_sec = [None] * self.num_joints
|
|
||||||
self.last_jump_log_sec = [0.0] * self.num_joints
|
|
||||||
self.last_stage_log_sec = [0.0] * self.num_joints
|
|
||||||
self.prev_raw_sensor_angle = [None] * self.num_joints
|
|
||||||
self.last_raw_jump_log_sec = [0.0] * self.num_joints
|
|
||||||
self.last_raw_reject_log_sec = [0.0] * self.num_joints
|
|
||||||
# 传感器原始单位:'centi-rad' 表示原始值/100后得到弧度;'centi-deg' 表示原始值/100后得到角度
|
|
||||||
self.raw_angle_unit = 'centi-rad'
|
|
||||||
|
|
||||||
# 每个关节零点偏置(弧度),发布角度时会先减去该偏置
|
# Calibration and direction parameters
|
||||||
self.declare_parameter('zero_offsets', [0.0] * self.num_joints)
|
self.declare_parameter('zero_offsets', [0.0] * self.num_joints)
|
||||||
# 每个关节转向系数,1.0 为正向,-1.0 为反向
|
|
||||||
self.declare_parameter('sensor_directions', [1.0] * self.num_joints)
|
self.declare_parameter('sensor_directions', [1.0] * self.num_joints)
|
||||||
|
self.declare_parameter('left_zero_offsets', [0.0] * self.num_joints)
|
||||||
|
self.declare_parameter('left_sensor_directions', [1.0] * self.num_joints)
|
||||||
self.declare_parameter('auto_zero_on_first_sample', False)
|
self.declare_parameter('auto_zero_on_first_sample', False)
|
||||||
|
self.declare_parameter('left_auto_zero_on_first_sample', False)
|
||||||
self.declare_parameter('calibrate_now', False)
|
self.declare_parameter('calibrate_now', False)
|
||||||
|
self.declare_parameter('left_calibrate_now', False)
|
||||||
|
|
||||||
|
# Diagnostics and anomaly filters
|
||||||
self.declare_parameter('enable_speed_jump_log', False)
|
self.declare_parameter('enable_speed_jump_log', False)
|
||||||
self.declare_parameter('speed_jump_threshold_rad_s', 3.0)
|
self.declare_parameter('speed_jump_threshold_rad_s', 3.0)
|
||||||
self.declare_parameter('jump_log_min_interval_sec', 0.2)
|
self.declare_parameter('jump_log_min_interval_sec', 0.2)
|
||||||
@ -73,9 +55,9 @@ class JointSensorNode(Node):
|
|||||||
self.declare_parameter('raw_jump_log_min_interval_sec', 0.2)
|
self.declare_parameter('raw_jump_log_min_interval_sec', 0.2)
|
||||||
self.declare_parameter('enable_raw_jump_reject', True)
|
self.declare_parameter('enable_raw_jump_reject', True)
|
||||||
self.declare_parameter('raw_jump_reject_threshold_rad', 0.35)
|
self.declare_parameter('raw_jump_reject_threshold_rad', 0.35)
|
||||||
self.zero_offsets = self._load_zero_offsets_from_param()
|
|
||||||
self.sensor_directions = self._load_sensor_directions_from_param()
|
# Runtime flags
|
||||||
self.auto_zero_on_first_sample = bool(self.get_parameter('auto_zero_on_first_sample').value)
|
self.enable_left_arm = bool(self.get_parameter('enable_left_arm').value)
|
||||||
self.enable_speed_jump_log = bool(self.get_parameter('enable_speed_jump_log').value)
|
self.enable_speed_jump_log = bool(self.get_parameter('enable_speed_jump_log').value)
|
||||||
self.speed_jump_threshold_rad_s = float(self.get_parameter('speed_jump_threshold_rad_s').value)
|
self.speed_jump_threshold_rad_s = float(self.get_parameter('speed_jump_threshold_rad_s').value)
|
||||||
self.jump_log_min_interval_sec = float(self.get_parameter('jump_log_min_interval_sec').value)
|
self.jump_log_min_interval_sec = float(self.get_parameter('jump_log_min_interval_sec').value)
|
||||||
@ -87,16 +69,87 @@ class JointSensorNode(Node):
|
|||||||
self.raw_jump_log_min_interval_sec = float(self.get_parameter('raw_jump_log_min_interval_sec').value)
|
self.raw_jump_log_min_interval_sec = float(self.get_parameter('raw_jump_log_min_interval_sec').value)
|
||||||
self.enable_raw_jump_reject = bool(self.get_parameter('enable_raw_jump_reject').value)
|
self.enable_raw_jump_reject = bool(self.get_parameter('enable_raw_jump_reject').value)
|
||||||
self.raw_jump_reject_threshold_rad = float(self.get_parameter('raw_jump_reject_threshold_rad').value)
|
self.raw_jump_reject_threshold_rad = float(self.get_parameter('raw_jump_reject_threshold_rad').value)
|
||||||
self.auto_zero_done = [False] * self.num_joints
|
|
||||||
|
# Sensor unit setting
|
||||||
|
self.raw_angle_unit = 'centi-rad'
|
||||||
|
|
||||||
|
self.get_logger().info('正在初始化CAN接口...')
|
||||||
|
|
||||||
|
legacy_can = str(self.get_parameter('can_interface').value)
|
||||||
|
right_can_interface = str(self.get_parameter('right_can_interface').value or legacy_can)
|
||||||
|
left_can_interface = str(self.get_parameter('left_can_interface').value)
|
||||||
|
right_topic = str(self.get_parameter('right_joint_states_topic').value)
|
||||||
|
left_topic = str(self.get_parameter('left_joint_states_topic').value)
|
||||||
|
|
||||||
|
self.joint_names = [f'joint{i}' for i in range(1, self.num_joints + 1)]
|
||||||
|
self.can_to_joint_index = {
|
||||||
|
0x01: 0,
|
||||||
|
0x02: 1,
|
||||||
|
0x03: 2,
|
||||||
|
0x04: 3,
|
||||||
|
0x05: 4,
|
||||||
|
0x06: 5,
|
||||||
|
0x07: 6,
|
||||||
|
0x08: 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.arms = {
|
||||||
|
'right': self._new_arm_state(
|
||||||
|
zero_offsets=self._load_offsets_param('zero_offsets'),
|
||||||
|
sensor_directions=self._load_directions_param('sensor_directions'),
|
||||||
|
auto_zero=bool(self.get_parameter('auto_zero_on_first_sample').value),
|
||||||
|
publisher=self.create_publisher(JointState, right_topic, 10),
|
||||||
|
bus=can.interface.Bus(channel=right_can_interface, bustype='socketcan'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if self.enable_left_arm:
|
||||||
|
self.arms['left'] = self._new_arm_state(
|
||||||
|
zero_offsets=self._load_offsets_param('left_zero_offsets'),
|
||||||
|
sensor_directions=self._load_directions_param('left_sensor_directions'),
|
||||||
|
auto_zero=bool(self.get_parameter('left_auto_zero_on_first_sample').value),
|
||||||
|
publisher=self.create_publisher(JointState, left_topic, 10),
|
||||||
|
bus=can.interface.Bus(channel=left_can_interface, bustype='socketcan'),
|
||||||
|
)
|
||||||
|
|
||||||
self.add_on_set_parameters_callback(self.on_set_parameters)
|
self.add_on_set_parameters_callback(self.on_set_parameters)
|
||||||
|
|
||||||
self.listener = CallbackListener(self.listen_can_callback)
|
self.notifiers = []
|
||||||
self.notifier = can.Notifier(self.bus, [self.listener])
|
for arm_name, state in self.arms.items():
|
||||||
|
listener = CallbackListener(lambda msg, a=arm_name: self.listen_can_callback(msg, a))
|
||||||
|
notifier = can.Notifier(state['bus'], [listener])
|
||||||
|
state['listener'] = listener
|
||||||
|
state['notifier'] = notifier
|
||||||
|
self.notifiers.append(notifier)
|
||||||
|
|
||||||
def listen_can_callback(self, msg: can.Message):
|
def _new_arm_state(self, zero_offsets, sensor_directions, auto_zero, publisher, bus):
|
||||||
|
return {
|
||||||
|
'publisher': publisher,
|
||||||
|
'bus': bus,
|
||||||
|
'notifier': None,
|
||||||
|
'listener': None,
|
||||||
|
'positions': [0.0] * self.num_joints,
|
||||||
|
'velocities': [0.0] * self.num_joints,
|
||||||
|
'has_position': [False] * self.num_joints,
|
||||||
|
'last_wrapped_positions': [0.0] * self.num_joints,
|
||||||
|
'has_wrapped_position': [False] * self.num_joints,
|
||||||
|
'last_joint_update_sec': [None] * self.num_joints,
|
||||||
|
'last_jump_log_sec': [0.0] * self.num_joints,
|
||||||
|
'last_stage_log_sec': [0.0] * self.num_joints,
|
||||||
|
'prev_raw_sensor_angle': [None] * self.num_joints,
|
||||||
|
'last_raw_jump_log_sec': [0.0] * self.num_joints,
|
||||||
|
'last_raw_reject_log_sec': [0.0] * self.num_joints,
|
||||||
|
'zero_offsets': zero_offsets,
|
||||||
|
'sensor_directions': sensor_directions,
|
||||||
|
'auto_zero_on_first_sample': auto_zero,
|
||||||
|
'auto_zero_done': [False] * self.num_joints,
|
||||||
|
}
|
||||||
|
|
||||||
|
def listen_can_callback(self, msg: can.Message, arm_name: str):
|
||||||
# 用户自定义的回调函数
|
# 用户自定义的回调函数
|
||||||
try:
|
try:
|
||||||
# self.get_logger().info(f"Received: id=0x{msg.arbitration_id:X} dlc={msg.dlc} data={msg.data.hex()} ts={msg.timestamp}")
|
state = self.arms.get(arm_name)
|
||||||
|
if state is None:
|
||||||
|
return
|
||||||
|
|
||||||
joint_index = self.can_to_joint_index.get(msg.arbitration_id)
|
joint_index = self.can_to_joint_index.get(msg.arbitration_id)
|
||||||
if joint_index is None:
|
if joint_index is None:
|
||||||
@ -107,28 +160,30 @@ class JointSensorNode(Node):
|
|||||||
raw_sensor_angle = self.extract_raw_sensor_angle_rad(msg.data)
|
raw_sensor_angle = self.extract_raw_sensor_angle_rad(msg.data)
|
||||||
|
|
||||||
# 原始数据跳变过大时直接丢弃该帧,避免异常值进入后续链路
|
# 原始数据跳变过大时直接丢弃该帧,避免异常值进入后续链路
|
||||||
if self._should_reject_raw_jump(joint_index, raw_sensor_angle, msg_time_sec, msg):
|
if self._should_reject_raw_jump(state, arm_name, joint_index, raw_sensor_angle, msg_time_sec, msg):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._log_raw_jump(joint_index, raw_sensor_angle, msg_time_sec, msg)
|
self._log_raw_jump(state, arm_name, joint_index, raw_sensor_angle, msg_time_sec, msg)
|
||||||
|
|
||||||
# 每条CAN报文默认对应一个关节角和速度
|
# 每条CAN报文默认对应一个关节角和速度
|
||||||
parsed_angle, vel = self.parse_sensor_from_can(msg.data)
|
parsed_angle, vel = self.parse_sensor_from_can(msg.data)
|
||||||
direction = self.sensor_directions[joint_index]
|
direction = state['sensor_directions'][joint_index]
|
||||||
directed_angle = self.wrap_to_pi(parsed_angle * direction)
|
directed_angle = self.wrap_to_pi(parsed_angle * direction)
|
||||||
vel = vel * direction
|
vel = vel * direction
|
||||||
|
|
||||||
self.last_wrapped_positions[joint_index] = directed_angle
|
state['last_wrapped_positions'][joint_index] = directed_angle
|
||||||
self.has_wrapped_position[joint_index] = True
|
state['has_wrapped_position'][joint_index] = True
|
||||||
|
|
||||||
if self.auto_zero_on_first_sample and not self.auto_zero_done[joint_index]:
|
if state['auto_zero_on_first_sample'] and not state['auto_zero_done'][joint_index]:
|
||||||
self.zero_offsets[joint_index] = directed_angle
|
state['zero_offsets'][joint_index] = directed_angle
|
||||||
self.auto_zero_done[joint_index] = True
|
state['auto_zero_done'][joint_index] = True
|
||||||
|
|
||||||
zeroed_angle = self.wrap_to_pi(directed_angle - self.zero_offsets[joint_index])
|
zeroed_angle = self.wrap_to_pi(directed_angle - state['zero_offsets'][joint_index])
|
||||||
angle = self.unwrap_joint_angle(joint_index, zeroed_angle)
|
angle = self.unwrap_joint_angle(joint_index, zeroed_angle, state['positions'], state['has_position'])
|
||||||
|
|
||||||
self._log_stage_diag(
|
self._log_stage_diag(
|
||||||
|
state,
|
||||||
|
arm_name,
|
||||||
joint_index,
|
joint_index,
|
||||||
parsed_angle,
|
parsed_angle,
|
||||||
directed_angle,
|
directed_angle,
|
||||||
@ -137,41 +192,39 @@ class JointSensorNode(Node):
|
|||||||
vel,
|
vel,
|
||||||
msg_time_sec,
|
msg_time_sec,
|
||||||
)
|
)
|
||||||
self._log_speed_jump(joint_index, angle, msg_time_sec)
|
self._log_speed_jump(state, arm_name, joint_index, angle, msg_time_sec)
|
||||||
|
|
||||||
self.current_positions[joint_index] = angle
|
state['positions'][joint_index] = angle
|
||||||
self.current_velocities[joint_index] = vel
|
state['velocities'][joint_index] = vel
|
||||||
|
|
||||||
# 发布完整8关节状态(位置/速度单位均为弧度)
|
# 发布完整8关节状态(位置/速度单位均为弧度)
|
||||||
js = JointState()
|
js = JointState()
|
||||||
js.header.stamp = self.get_clock().now().to_msg()
|
js.header.stamp = self.get_clock().now().to_msg()
|
||||||
js.name = self.joint_names
|
js.name = self.joint_names
|
||||||
js.position = self.current_positions
|
js.position = state['positions']
|
||||||
js.velocity = self.current_velocities
|
js.velocity = state['velocities']
|
||||||
self.publisher_.publish(js)
|
state['publisher'].publish(js)
|
||||||
|
|
||||||
# self.get_logger().info(
|
|
||||||
# f"Published JointState: id=0x{msg.arbitration_id:X} "
|
|
||||||
# f"joint={self.joint_names[joint_index]} pos={angle:.6f} vel={vel:.6f}"
|
|
||||||
# )
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.get_logger().error(f"Error handling CAN msg: {e}")
|
self.get_logger().error(f"Error handling CAN msg: {e}")
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.notifier.stop()
|
for state in self.arms.values():
|
||||||
self.bus.shutdown()
|
if state['notifier'] is not None:
|
||||||
|
state['notifier'].stop()
|
||||||
|
if state['bus'] is not None:
|
||||||
|
state['bus'].shutdown()
|
||||||
|
|
||||||
def _load_zero_offsets_from_param(self):
|
def _load_offsets_param(self, name):
|
||||||
raw_offsets = self.get_parameter('zero_offsets').value
|
raw_offsets = self.get_parameter(name).value
|
||||||
if not isinstance(raw_offsets, (list, tuple)) or len(raw_offsets) != self.num_joints:
|
if not isinstance(raw_offsets, (list, tuple)) or len(raw_offsets) != self.num_joints:
|
||||||
self.get_logger().warn(f'参数 zero_offsets 长度不是{self.num_joints},已使用默认零偏。')
|
self.get_logger().warn(f'参数 {name} 长度不是{self.num_joints},已使用默认零偏。')
|
||||||
return [0.0] * self.num_joints
|
return [0.0] * self.num_joints
|
||||||
return [float(v) for v in raw_offsets]
|
return [float(v) for v in raw_offsets]
|
||||||
|
|
||||||
def _load_sensor_directions_from_param(self):
|
def _load_directions_param(self, name):
|
||||||
raw_dirs = self.get_parameter('sensor_directions').value
|
raw_dirs = self.get_parameter(name).value
|
||||||
if not isinstance(raw_dirs, (list, tuple)) or len(raw_dirs) != self.num_joints:
|
if not isinstance(raw_dirs, (list, tuple)) or len(raw_dirs) != self.num_joints:
|
||||||
self.get_logger().warn(f'参数 sensor_directions 长度不是{self.num_joints},已使用默认方向。')
|
self.get_logger().warn(f'参数 {name} 长度不是{self.num_joints},已使用默认方向。')
|
||||||
return [1.0] * self.num_joints
|
return [1.0] * self.num_joints
|
||||||
|
|
||||||
dirs = [float(v) for v in raw_dirs]
|
dirs = [float(v) for v in raw_dirs]
|
||||||
@ -181,41 +234,49 @@ class JointSensorNode(Node):
|
|||||||
dirs[i] = 1.0
|
dirs[i] = 1.0
|
||||||
return dirs
|
return dirs
|
||||||
|
|
||||||
def calibrate_zero_from_current(self):
|
def calibrate_zero_from_current(self, arm_name):
|
||||||
|
state = self.arms.get(arm_name)
|
||||||
|
if state is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
param_name = 'left_zero_offsets' if arm_name == 'left' else 'zero_offsets'
|
||||||
updated = 0
|
updated = 0
|
||||||
for i in range(self.num_joints):
|
for i in range(self.num_joints):
|
||||||
if self.has_wrapped_position[i]:
|
if state['has_wrapped_position'][i]:
|
||||||
self.zero_offsets[i] = self.last_wrapped_positions[i]
|
state['zero_offsets'][i] = state['last_wrapped_positions'][i]
|
||||||
self.has_position[i] = False
|
state['has_position'][i] = False
|
||||||
updated += 1
|
updated += 1
|
||||||
|
|
||||||
if updated > 0:
|
if updated > 0:
|
||||||
self.set_parameters([
|
self.set_parameters([
|
||||||
rclpy.parameter.Parameter(
|
rclpy.parameter.Parameter(
|
||||||
'zero_offsets',
|
param_name,
|
||||||
rclpy.Parameter.Type.DOUBLE_ARRAY,
|
rclpy.Parameter.Type.DOUBLE_ARRAY,
|
||||||
self.zero_offsets,
|
state['zero_offsets'],
|
||||||
)
|
)
|
||||||
])
|
])
|
||||||
self.get_logger().info(f'零点校准完成,已更新 {updated}/{self.num_joints} 个关节零偏。')
|
self.get_logger().info(f'[{self.arm_labels[arm_name]}] 零点校准完成,已更新 {updated}/{self.num_joints} 个关节零偏。')
|
||||||
else:
|
else:
|
||||||
self.get_logger().warn('零点校准失败:当前还没有可用的关节采样数据。')
|
self.get_logger().warn('零点校准失败:当前还没有可用的关节采样数据。')
|
||||||
|
|
||||||
def on_set_parameters(self, params):
|
def on_set_parameters(self, params):
|
||||||
for param in params:
|
for param in params:
|
||||||
if param.name == 'zero_offsets':
|
if param.name in ('zero_offsets', 'left_zero_offsets'):
|
||||||
if not isinstance(param.value, (list, tuple)) or len(param.value) != self.num_joints:
|
if not isinstance(param.value, (list, tuple)) or len(param.value) != self.num_joints:
|
||||||
return SetParametersResult(successful=False, reason=f'zero_offsets 必须是长度为{self.num_joints}的数组')
|
return SetParametersResult(successful=False, reason=f'{param.name} 必须是长度为{self.num_joints}的数组')
|
||||||
|
|
||||||
if param.name == 'sensor_directions':
|
if param.name in ('sensor_directions', 'left_sensor_directions'):
|
||||||
if not isinstance(param.value, (list, tuple)) or len(param.value) != self.num_joints:
|
if not isinstance(param.value, (list, tuple)) or len(param.value) != self.num_joints:
|
||||||
return SetParametersResult(successful=False, reason=f'sensor_directions 必须是长度为{self.num_joints}的数组')
|
return SetParametersResult(successful=False, reason=f'{param.name} 必须是长度为{self.num_joints}的数组')
|
||||||
for i, v in enumerate(param.value):
|
for i, v in enumerate(param.value):
|
||||||
if abs(float(v)) < 1e-9:
|
if abs(float(v)) < 1e-9:
|
||||||
return SetParametersResult(successful=False, reason=f'sensor_directions[{i}] 不能为0')
|
return SetParametersResult(successful=False, reason=f'{param.name}[{i}] 不能为0')
|
||||||
|
|
||||||
if param.name == 'auto_zero_on_first_sample':
|
if param.name == 'auto_zero_on_first_sample':
|
||||||
self.auto_zero_on_first_sample = bool(param.value)
|
self.arms['right']['auto_zero_on_first_sample'] = bool(param.value)
|
||||||
|
|
||||||
|
if param.name == 'left_auto_zero_on_first_sample' and 'left' in self.arms:
|
||||||
|
self.arms['left']['auto_zero_on_first_sample'] = bool(param.value)
|
||||||
|
|
||||||
if param.name == 'enable_speed_jump_log':
|
if param.name == 'enable_speed_jump_log':
|
||||||
self.enable_speed_jump_log = bool(param.value)
|
self.enable_speed_jump_log = bool(param.value)
|
||||||
@ -251,21 +312,34 @@ class JointSensorNode(Node):
|
|||||||
self.raw_jump_reject_threshold_rad = float(param.value)
|
self.raw_jump_reject_threshold_rad = float(param.value)
|
||||||
|
|
||||||
if param.name == 'calibrate_now' and bool(param.value):
|
if param.name == 'calibrate_now' and bool(param.value):
|
||||||
self.calibrate_zero_from_current()
|
self.calibrate_zero_from_current('right')
|
||||||
|
|
||||||
|
if param.name == 'left_calibrate_now' and bool(param.value):
|
||||||
|
self.calibrate_zero_from_current('left')
|
||||||
|
|
||||||
for param in params:
|
for param in params:
|
||||||
if param.name == 'zero_offsets':
|
if param.name == 'zero_offsets':
|
||||||
self.zero_offsets = [float(v) for v in param.value]
|
self.arms['right']['zero_offsets'] = [float(v) for v in param.value]
|
||||||
self.has_position = [False] * self.num_joints
|
self.arms['right']['has_position'] = [False] * self.num_joints
|
||||||
|
|
||||||
if param.name == 'sensor_directions':
|
if param.name == 'sensor_directions':
|
||||||
self.sensor_directions = [float(v) for v in param.value]
|
self.arms['right']['sensor_directions'] = [float(v) for v in param.value]
|
||||||
self.has_position = [False] * self.num_joints
|
self.arms['right']['has_position'] = [False] * self.num_joints
|
||||||
|
|
||||||
|
if param.name == 'left_zero_offsets' and 'left' in self.arms:
|
||||||
|
self.arms['left']['zero_offsets'] = [float(v) for v in param.value]
|
||||||
|
self.arms['left']['has_position'] = [False] * self.num_joints
|
||||||
|
|
||||||
|
if param.name == 'left_sensor_directions' and 'left' in self.arms:
|
||||||
|
self.arms['left']['sensor_directions'] = [float(v) for v in param.value]
|
||||||
|
self.arms['left']['has_position'] = [False] * self.num_joints
|
||||||
|
|
||||||
return SetParametersResult(successful=True)
|
return SetParametersResult(successful=True)
|
||||||
|
|
||||||
def _log_stage_diag(
|
def _log_stage_diag(
|
||||||
self,
|
self,
|
||||||
|
state,
|
||||||
|
arm_name,
|
||||||
joint_index: int,
|
joint_index: int,
|
||||||
parsed_angle: float,
|
parsed_angle: float,
|
||||||
directed_angle: float,
|
directed_angle: float,
|
||||||
@ -277,80 +351,80 @@ class JointSensorNode(Node):
|
|||||||
if not self.enable_stage_diag_log:
|
if not self.enable_stage_diag_log:
|
||||||
return
|
return
|
||||||
|
|
||||||
since_last = now_sec - self.last_stage_log_sec[joint_index]
|
since_last = now_sec - state['last_stage_log_sec'][joint_index]
|
||||||
if since_last < self.stage_log_min_interval_sec:
|
if since_last < self.stage_log_min_interval_sec:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.last_stage_log_sec[joint_index] = now_sec
|
state['last_stage_log_sec'][joint_index] = now_sec
|
||||||
self.get_logger().info(
|
self.get_logger().info(
|
||||||
f"Joint{joint_index + 1} stages: parsed={parsed_angle:.6f} dir={directed_angle:.6f} "
|
f"[{self.arm_labels[arm_name]}] Joint{joint_index + 1} stages: parsed={parsed_angle:.6f} dir={directed_angle:.6f} "
|
||||||
f"zeroed={zeroed_angle:.6f} unwrapped={unwrapped_angle:.6f} vel={vel:.6f}"
|
f"zeroed={zeroed_angle:.6f} unwrapped={unwrapped_angle:.6f} vel={vel:.6f}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _log_speed_jump(self, joint_index: int, curr_unwrapped_angle: float, now_sec: float):
|
def _log_speed_jump(self, state, arm_name, joint_index: int, curr_unwrapped_angle: float, now_sec: float):
|
||||||
if not self.enable_speed_jump_log:
|
if not self.enable_speed_jump_log:
|
||||||
self.last_joint_update_sec[joint_index] = now_sec
|
state['last_joint_update_sec'][joint_index] = now_sec
|
||||||
return
|
return
|
||||||
|
|
||||||
prev_time = self.last_joint_update_sec[joint_index]
|
prev_time = state['last_joint_update_sec'][joint_index]
|
||||||
if prev_time is None:
|
if prev_time is None:
|
||||||
self.last_joint_update_sec[joint_index] = now_sec
|
state['last_joint_update_sec'][joint_index] = now_sec
|
||||||
return
|
return
|
||||||
|
|
||||||
dt = now_sec - prev_time
|
dt = now_sec - prev_time
|
||||||
if dt <= 1e-6:
|
if dt <= 1e-6:
|
||||||
self.last_joint_update_sec[joint_index] = now_sec
|
state['last_joint_update_sec'][joint_index] = now_sec
|
||||||
return
|
return
|
||||||
|
|
||||||
prev_unwrapped = self.current_positions[joint_index]
|
prev_unwrapped = state['positions'][joint_index]
|
||||||
delta = curr_unwrapped_angle - prev_unwrapped
|
delta = curr_unwrapped_angle - prev_unwrapped
|
||||||
if abs(delta) <= self.joint_update_epsilon_rad:
|
if abs(delta) <= self.joint_update_epsilon_rad:
|
||||||
self.last_joint_update_sec[joint_index] = now_sec
|
state['last_joint_update_sec'][joint_index] = now_sec
|
||||||
return
|
return
|
||||||
|
|
||||||
speed = abs(delta) / dt
|
speed = abs(delta) / dt
|
||||||
if speed >= self.speed_jump_threshold_rad_s:
|
if speed >= self.speed_jump_threshold_rad_s:
|
||||||
since_last_log = now_sec - self.last_jump_log_sec[joint_index]
|
since_last_log = now_sec - state['last_jump_log_sec'][joint_index]
|
||||||
if since_last_log >= self.jump_log_min_interval_sec:
|
if since_last_log >= self.jump_log_min_interval_sec:
|
||||||
self.get_logger().warning(
|
self.get_logger().warning(
|
||||||
f"Joint{joint_index + 1} speed jump: prev={prev_unwrapped:.6f} curr={curr_unwrapped_angle:.6f} "
|
f"[{self.arm_labels[arm_name]}] Joint{joint_index + 1} speed jump: prev={prev_unwrapped:.6f} curr={curr_unwrapped_angle:.6f} "
|
||||||
f"delta={delta:.6f} rad dt={dt:.4f} s speed={speed:.3f} rad/s"
|
f"delta={delta:.6f} rad dt={dt:.4f} s speed={speed:.3f} rad/s"
|
||||||
)
|
)
|
||||||
self.last_jump_log_sec[joint_index] = now_sec
|
state['last_jump_log_sec'][joint_index] = now_sec
|
||||||
|
|
||||||
self.last_joint_update_sec[joint_index] = now_sec
|
state['last_joint_update_sec'][joint_index] = now_sec
|
||||||
|
|
||||||
def _log_raw_jump(self, joint_index: int, raw_sensor_angle: float, now_sec: float, msg: can.Message):
|
def _log_raw_jump(self, state, arm_name, joint_index: int, raw_sensor_angle: float, now_sec: float, msg: can.Message):
|
||||||
if not self.enable_raw_jump_log or raw_sensor_angle is None:
|
if not self.enable_raw_jump_log or raw_sensor_angle is None:
|
||||||
if raw_sensor_angle is not None:
|
if raw_sensor_angle is not None:
|
||||||
self.prev_raw_sensor_angle[joint_index] = raw_sensor_angle
|
state['prev_raw_sensor_angle'][joint_index] = raw_sensor_angle
|
||||||
return
|
return
|
||||||
|
|
||||||
prev = self.prev_raw_sensor_angle[joint_index]
|
prev = state['prev_raw_sensor_angle'][joint_index]
|
||||||
if prev is None:
|
if prev is None:
|
||||||
self.prev_raw_sensor_angle[joint_index] = raw_sensor_angle
|
state['prev_raw_sensor_angle'][joint_index] = raw_sensor_angle
|
||||||
return
|
return
|
||||||
|
|
||||||
# 原始角度做最短角距离,避免 0/2pi 边界导致假跳变
|
# 原始角度做最短角距离,避免 0/2pi 边界导致假跳变
|
||||||
delta = self.wrap_to_pi(raw_sensor_angle - prev)
|
delta = self.wrap_to_pi(raw_sensor_angle - prev)
|
||||||
if abs(delta) >= self.raw_jump_threshold_rad:
|
if abs(delta) >= self.raw_jump_threshold_rad:
|
||||||
since_last = now_sec - self.last_raw_jump_log_sec[joint_index]
|
since_last = now_sec - state['last_raw_jump_log_sec'][joint_index]
|
||||||
if since_last >= self.raw_jump_log_min_interval_sec:
|
if since_last >= self.raw_jump_log_min_interval_sec:
|
||||||
self.get_logger().warning(
|
self.get_logger().warning(
|
||||||
f"Joint{joint_index + 1} RAW jump: prev_raw={prev:.6f} curr_raw={raw_sensor_angle:.6f} "
|
f"[{self.arm_labels[arm_name]}] Joint{joint_index + 1} RAW jump: prev_raw={prev:.6f} curr_raw={raw_sensor_angle:.6f} "
|
||||||
f"delta={delta:.6f} rad can_id=0x{msg.arbitration_id:X} data={msg.data.hex()}"
|
f"delta={delta:.6f} rad can_id=0x{msg.arbitration_id:X} data={msg.data.hex()}"
|
||||||
)
|
)
|
||||||
self.last_raw_jump_log_sec[joint_index] = now_sec
|
state['last_raw_jump_log_sec'][joint_index] = now_sec
|
||||||
|
|
||||||
self.prev_raw_sensor_angle[joint_index] = raw_sensor_angle
|
state['prev_raw_sensor_angle'][joint_index] = raw_sensor_angle
|
||||||
|
|
||||||
def _should_reject_raw_jump(self, joint_index: int, raw_sensor_angle: float, now_sec: float, msg: can.Message) -> bool:
|
def _should_reject_raw_jump(self, state, arm_name, joint_index: int, raw_sensor_angle: float, now_sec: float, msg: can.Message) -> bool:
|
||||||
if raw_sensor_angle is None:
|
if raw_sensor_angle is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
prev = self.prev_raw_sensor_angle[joint_index]
|
prev = state['prev_raw_sensor_angle'][joint_index]
|
||||||
if prev is None:
|
if prev is None:
|
||||||
self.prev_raw_sensor_angle[joint_index] = raw_sensor_angle
|
state['prev_raw_sensor_angle'][joint_index] = raw_sensor_angle
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not self.enable_raw_jump_reject:
|
if not self.enable_raw_jump_reject:
|
||||||
@ -360,14 +434,14 @@ class JointSensorNode(Node):
|
|||||||
if abs(delta) < self.raw_jump_reject_threshold_rad:
|
if abs(delta) < self.raw_jump_reject_threshold_rad:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
since_last = now_sec - self.last_raw_reject_log_sec[joint_index]
|
since_last = now_sec - state['last_raw_reject_log_sec'][joint_index]
|
||||||
if since_last >= self.raw_jump_log_min_interval_sec:
|
if since_last >= self.raw_jump_log_min_interval_sec:
|
||||||
self.get_logger().warning(
|
self.get_logger().warning(
|
||||||
f"Joint{joint_index + 1} RAW frame dropped: prev_raw={prev:.6f} curr_raw={raw_sensor_angle:.6f} "
|
f"[{self.arm_labels[arm_name]}] Joint{joint_index + 1} RAW frame dropped: prev_raw={prev:.6f} curr_raw={raw_sensor_angle:.6f} "
|
||||||
f"delta={delta:.6f} rad threshold={self.raw_jump_reject_threshold_rad:.6f} "
|
f"delta={delta:.6f} rad threshold={self.raw_jump_reject_threshold_rad:.6f} "
|
||||||
f"can_id=0x{msg.arbitration_id:X} data={msg.data.hex()}"
|
f"can_id=0x{msg.arbitration_id:X} data={msg.data.hex()}"
|
||||||
)
|
)
|
||||||
self.last_raw_reject_log_sec[joint_index] = now_sec
|
state['last_raw_reject_log_sec'][joint_index] = now_sec
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -393,13 +467,13 @@ class JointSensorNode(Node):
|
|||||||
# 归一化到 [-pi, pi),解决 0~2pi 到 ±pi 的过零映射问题
|
# 归一化到 [-pi, pi),解决 0~2pi 到 ±pi 的过零映射问题
|
||||||
return (angle_rad + math.pi) % (2.0 * math.pi) - math.pi
|
return (angle_rad + math.pi) % (2.0 * math.pi) - math.pi
|
||||||
|
|
||||||
def unwrap_joint_angle(self, joint_index: int, wrapped_angle: float) -> float:
|
def unwrap_joint_angle(self, joint_index: int, wrapped_angle: float, positions, has_position) -> float:
|
||||||
# 基于上一帧做相位展开,避免在 -pi/+pi 边界发生数值跳变
|
# 基于上一帧做相位展开,避免在 -pi/+pi 边界发生数值跳变
|
||||||
if not self.has_position[joint_index]:
|
if not has_position[joint_index]:
|
||||||
self.has_position[joint_index] = True
|
has_position[joint_index] = True
|
||||||
return wrapped_angle
|
return wrapped_angle
|
||||||
|
|
||||||
prev = self.current_positions[joint_index]
|
prev = positions[joint_index]
|
||||||
prev_wrapped = self.wrap_to_pi(prev)
|
prev_wrapped = self.wrap_to_pi(prev)
|
||||||
delta = wrapped_angle - prev_wrapped
|
delta = wrapped_angle - prev_wrapped
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user