1.Controller Manager
Controller Manager
/ \
/ \
joint_trajectory_controller InexbotHardware
↓ ↓
产生目标关节角 调用 servoJ / 读真实角度
是一个大管家,统管很多控制器
2.joint_trajectory_controller
是ros2里面专门执行,关节轨迹的标准控制器,
joint_trajectory_controller:
接收轨迹
管理 time_from_start
插值
输出目标关节角
判断轨迹执行状态
它接收的是:
control_msgs/action/FollowJointTrajectory这个 action 里面装着一条:
trajectory_msgs/JointTrajectory大概结构是这样:
control_msgs::action::FollowJointTrajectory::Goal goal; goal.trajectory.joint_names = { "Joint_1", "Joint_2", "Joint_3", "Joint_4", "Joint_5", "Joint_6" }; goal.trajectory.points[0].positions = {0.0, 0.2, -0.5, 0.1, 0.0, 0.0}; goal.trajectory.points[0].time_from_start = rclcpp::Duration::from_seconds(0.0); goal.trajectory.points[1].positions = {0.1, 0.3, -0.4, 0.2, 0.0, 0.1}; goal.trajectory.points[1].time_from_start = rclcpp::Duration::from_seconds(1.0); goal.trajectory.points[2].positions = {0.2, 0.4, -0.3, 0.3, 0.1, 0.2}; goal.trajectory.points[2].time_from_start = rclcpp::Duration::from_seconds(2.0);这条轨迹的意思是:
第 0.0 秒:6 个关节在 positions[0] 第 1.0 秒:6 个关节到 positions[1] 第 2.0 秒:6 个关节到 positions[2]joint_trajectory_controller的核心工作就是严格按着这些time_from_start来执行轨迹,但是它不负责执行,只负责下发
它怎么执行 time_from_start?
假设 MoveIt 发来这样一条轨迹:
t = 0.0s q = [0, 0, 0, 0, 0, 0] t = 1.0s q = [10, 5, -8, 3, 0, 2] t = 2.0s q = [20, 8, -12, 5, 1, 4]如果控制器频率是 100Hz,也就是每 0.01 秒运行一次,那么joint_trajectory_controller会不断计算:
当前时间 elapsed = 0.37s 应该处在第 0 秒和第 1 秒之间 所以算出当前目标关节角 q_command例如简单理解:
double ratio = (elapsed - t0) / (t1 - t0); q_command[j] = q0[j] + ratio * (q1[j] - q0[j]);当然真实的joint_trajectory_controller内部比这个更完整,会处理速度、加速度、容差、停止、取消等情况。但你入门阶段可以先这样理解,它按照时间在轨迹点之间插值,周期性产生目标关节角。
joint_trajectory_controller不会直接调用:set_servoJ_pos(...),它只会把每个周期算出来的目标关节角,写到 ros2_control 的 command interface 里。如果你配置的是:command_interfaces:
- position
意思就是:joint_trajectory_controller 每个周期输出“目标关节位置”
完整闭环是:joint_trajectory_controller
↓ command position
hardware_interface::write()
↓ servoJ
真实机械臂
↓ get_current_position
hardware_interface::read()
↓ state position
joint_trajectory_controller / joint_state_broadcaster
3.hardware_interface
把 ros2_control 的标准关节命令
转换成 Inexbot SDK / servoJ 能理解的命令
同时把 Inexbot 真实反馈关节角
转换成 ROS2 能理解的关节状态
FollowJointTrajectory action → joint_trajectory_controller 轨迹时间执行和插值 → joint_trajectory_controller 控制循环 read → update → write → Controller Manager 调用 set_servoJ_pos() → hardware_interface::write() 调用 get_current_position() → hardware_interface::read() ros_rad 和 ctrl_deg 转换 → hardware_interface 连接机器人 IP / 端口 → hardware_interface::on_configure() 或 on_activate() servoJ 初始化 → hardware_interface::on_activate()完整周期是这样的:
1. Controller Manager 调用 hardware_interface::read() 2. read() 从 Inexbot 读取真实角度 填入 state_positions_ 3. joint_trajectory_controller 读取 state_positions_ 知道当前机械臂在哪里 4. joint_trajectory_controller 根据轨迹 time_from_start 算出当前目标角 5. joint_trajectory_controller 写入 command_positions_ 6. Controller Manager 调用 hardware_interface::write() 7. write() 把 command_positions_ 转成 ctrl_deg 调用 set_servoJ_pos()4.joint_state_broadcaster
是ros2_controller里面负责发布关节状态的标准组件,把 hardware_interface::read() 读到的关节状态,发布成 ROS2 标准话题 /joint_states。
一条是“命令方向”:
MoveIt ↓ joint_trajectory_controller ↓ hardware_interface::write() ↓ servoJ ↓ 真实机械臂另一条是“状态方向”:
真实机械臂 ↓ hardware_interface::read() ↓ joint_state_broadcaster ↓ /joint_states ↓ MoveIt / RViz在hardware_interface中,你的read()以后大概会这样:
hardware_interface::return_type InexbotHardware::read( const rclcpp::Time& time, const rclcpp::Duration& period) { std::vector<double> now_ctrl_deg; readCtrlJointsDeg(now_ctrl_deg); for (size_t i = 0; i < 6; ++i) { double ros_deg = (now_ctrl_deg[i] - OFF[i]) / SGN[i]; state_positions_[i] = ros_deg * M_PI / 180.0; } return hardware_interface::return_type::OK; }这里的重点是:
state_positions_[i] = ...state_positions_里保存的是当前真实机械臂的关节角,单位一般是rad。
但是read()本身不会自动发布/joint_states。
它只是把真实状态写进 ros2_control 的状态接口里。
然后joint_state_broadcaster会读取这些状态接口,再发布出去。
其中,state_interface可以理解为hardware_interface 暴露给 ros2_control 的状态插口
比如你的硬件接口声明:
std::vector<hardware_interface::StateInterface> InexbotHardware::export_state_interfaces() { std::vector<hardware_interface::StateInterface> interfaces; for (size_t i = 0; i < 6; ++i) { interfaces.emplace_back( joint_names_[i], hardware_interface::HW_IF_POSITION, &state_positions_[i]); } return interfaces; }这表示你的硬件提供了这些状态:
Joint_1/position Joint_2/position Joint_3/position Joint_4/position Joint_5/position Joint_6/position每个状态背后对应一个变量:
state_positions_[0] state_positions_[1] state_positions_[2] ...joint_state_broadcaster就是从这些 state interface 里取数据,然后发布/joint_states。
moveit规划出来的轨迹是一串离散的关节目标点,不是连续曲线,
它大概长这样:
trajectory: joint_names: - Joint_1 - Joint_2 - Joint_3 - Joint_4 - Joint_5 - Joint_6 points: 第 0 个点: time_from_start = 0.0s positions = [0.00, 0.10, -0.30, 0.20, 0.00, 0.00] 第 1 个点: time_from_start = 0.8s positions = [0.15, 0.20, -0.25, 0.25, 0.03, 0.02] 第 2 个点: time_from_start = 1.6s positions = [0.30, 0.25, -0.15, 0.35, 0.08, 0.05] 第 3 个点: time_from_start = 2.5s positions = [0.45, 0.30, -0.05, 0.40, 0.10, 0.08]假设机械臂的频率是100hz,那就是一毫秒发一个点,但是moveit一般只给出几个关键点位,所以就需要进行插值
MoveIt 规划出来的点里有什么?
一个JointTrajectoryPoint通常可能包含:
trajectory_msgs::msg::JointTrajectoryPoint point; point.positions; point.velocities; point.accelerations; point.effort; point.time_from_start;也就是:
positions:这个时间点的关节位置 velocities:这个时间点的关节速度 accelerations:这个时间点的关节加速度 time_from_start:从轨迹开始到这个点的时间