ROS Control
参考链接 rosroboticslearning.com/ros-control
本章我们将学习一个有用但难以理解的包,ros_control。
我们将从简单了解什么是控制及其在机器人学中的重要性开始。然后了解ros_control包是如何派上用场来控制我们的机器人的。
让我列出我们将要讨论的话题:
内容:
-
为什么控制。
-
什么是ROS控制。
-
ros_control包中控制器的类型。
-
硬件_接口怎么写?
-
如何在你的机器人上使用ros_control?
1 为什么要控制…?
我们在工程中听过很多关于控制系统和PID控制器的内容。如今,控制系统无处不在,从汽车(用于控制巡航模式下的速度)到月球着陆器(用于控制软着陆的方向和速度)。
无论如何,这一页没有解释控制系统,但我们将看到如何使用机器人ros控制包。我继续假设你有什么是PID控制系统及其工作原理的先验知识。
首先让我们谈谈为什么我们需要机器人控制。?
当我们处理机器人时,我们需要确保它的运动是可控的,即动作既不太慢也不太快,并且遵循指定的轨迹。
想象一下,你想从桌子上拿起一个物体,然后你移动你的手,这样你/物体不会猛烈地撞击桌子,而且你可以在很短的时间内放置它。在这里,你在整个动作中控制手的位置和速度。我们必须把同样的道理应用到机器人身上。每当我们需要机器人在正确的时间将物体放置在正确的位置时,我们需要控制它的运动。
ROS为我们提供了一套软件包,可以使用PID控制器来控制我们机器人的运动。如果你愿意,你也可以写你自己的控制器插件。但是,当我们开始编写自己的控制器时,我们需要考虑很多事情,如采样率、导数跳动、动态调整参数、复位饱和缓解等。所有这些方面都经过精心包装,供我们在ROS中使用。
ROS提供了一个被称为ros_control的通用控制包,可以用于我们的机器人,节省我们重写控制器代码的时间。
2 什么是 ROS Control…?
ROS_Control是一套软件包,包括控制器接口/controller interface、控制器管理器/controller manager,、传输/transmissions、硬件接口/hardware interfaces 和控制工具箱/control toolbox。所有这些包一起将允许您交互和控制机器人的关节执行器。
ros_control将关节状态数据和输入设定点(目标)作为来自用户/第三方应用的输入,如图3所示,并将适当的命令作为输出发送给致动器【减速比】。为了实现所提供的设定点(目标),它使用通用控制回路反馈机制(通常为PID控制器)来控制输出。该输出通过硬件接口传递给机器人。
第三方块(移动/导航堆栈)表示将目标发送到ros_control包的块。控制器(base _ controller/arm _ controller/base controller)负责计算实现设定目标所需的输出命令。为了移动关节,控制器必须与致动器通信。硬件接口节点(RobotHW)位于控制器和真正的硬件之间,它将命令致动器移动并从关节传感器获得反馈。这就是ros_control的工作方式。
查看下面的视频以便更好地理解,在上面的视频中,我使用了效果控制器。单击此处获取源代码。
还有其他控制器。让我们检查控制器列表。
3 ROS控制包中的控制器类型
ROS控制包提供了一组控制器插件,以不同的方式与机器人的关节进行交互。该列表如下:
- joint_state_controller/ 关节状态控制器:该控制器读取所有关节位置并将其发布到主题“/joint_states”上。该控制器不向致动器发送任何命令。用作“joint_state_controller/JointStateController”。
-
effort_controllers:当您想向力矩发送命令时使用。这意味着您想要控制的关节执行器接受effort【电流(或)电压】命令。
-
- JointPositionController:
用作“effort _ controllers/JointPositionController”。这个控制器插件接受位置【弧度(或)米】值作为输入。误差(目标位置-当前位置)通过PID回路映射到输出努力命令。
- JointPositionController:
-
- JointVelocityController:用作“effort _ controllers/JointVelocityController”。这个控制器插件接受速度【弧度/秒(或)米/秒】值作为输入。误差(目标速度-当前速度)通过PID回路映射到输出努力命令。
-
- JointEffortController:用作“effort _ controllers/JointEffortController”。这个控制器插件接受effort【力/force(or)扭矩/torque】值作为输入。输入力被简单地作为输出力命令传递给关节致动器。P、I和D的值对输出努力没有影响。
-
- JointGroupPositionController:用作“effort _ controllers/jointgroupositioncontroller”。该控制器的功能与一组关节的effort _ controllers/JointPositionController相同。
-
- JointGroupEffortController:用作“effort _ controllers/jointgroupeffertcontroller”。该控制器的功能与一组关节的effort _ controllers/JointEffortController相同。
- JointGroupEffortController:用作“effort _ controllers/jointgroupeffertcontroller”。该控制器的功能与一组关节的effort _ controllers/JointEffortController相同。
-
velocity_controllers:当您想向velocity接口发送命令时使用。这意味着您想要控制的关节执行器直接接受速度命令。
-
- JointPositionController:用作“velocity_controllers/JointPositionController”。这个控制器插件接受位置【弧度(或)米】值作为输入。误差(目标位置-当前位置)通过PID回路映射到输出速度命令。
-
- JointVelocityController:用作“velocity_controllers/JointVelocityController”。这个控制器插件接受速度【弧度/秒(或)米/秒】值作为输入。输入速度值简单地作为输出命令转发给关节致动器。P、I和D的值对输出命令没有影响。
-
- JointGroupVelocityController:用作“velocity_controllers/JointGroupVelocityController”。该控制器的功能与关节组的速度控制器/关节速度控制器相同。
-
position_controllers:当您想要向位置接口发送命令时使用。这意味着您要控制的关节执行器直接接受位置命令。
-
- JointPositionController:用作“position _ controllers/JointPositionController”。这个控制器插件接受位置【弧度(或)米】值作为输入。输入位置值被简单地作为输出命令转发给关节致动器。P、I和D的值对输出命令没有影响。
-
- JointGroupPositionController:用作“position _ controllers/jointGroupspositioncontroller”。该控制器的功能与一组关节的position _ controllers/JointPositionController相同。
-
joint_trajectory_controllers: 该控制器用于在一组关节上执行关节空间轨迹。轨迹被指定为要到达的一组路点。航路点由位置以及可选的速度和加速度组成。有关此控制器的更多信息,请单击此处。
-
- effort_controllers:用作“effort _ controllers/JointTrajectoryController”。该控制器插件用于接受作用力【电流(或)电压】的关节执行器。位置+速度轨迹跟随误差通过PID回路映射到输出力命令。
-
- velocity_controllers:用作“velocity_controllers/JointTrajectoryController”。该控制器插件用于直接接受速度命令的关节执行器。位置+速度轨迹跟随误差通过PID回路映射到输出速度命令。
-
- position_controllers:用作“position _ controllers/JointTrajectoryController”。该控制器插件用于直接接受位置命令的关节执行器。指定轨迹中的期望位置被简单地转发到关节。P、I和D的值对输出命令没有影响。
呼呼…!!名单很大,不是吗。你认为控制者的名单到此为止了吗?
还没有,我上面列出的控制器类型是基本的和最常用的。ros_control包提供了很少更有用的控制器。我把它留给你的兴趣。如果你想知道控制器的完整列表,那么请浏览ros_control包中ros_controllers的源代码。
现在,您必须根据您正在使用的关节致动器/电机的类型以及您想要控制的内容(位置、速度、轨迹或力)来决定使用哪个控制器插件,除了关节状态控制器,因为关节状态控制器始终设置为提供机器人的当前关节状态。顺便说一句,不要忘记你不仅限于使用ros_control包中的控制器。如果你对现有的控制器不满意,你可以随时编写自己的控制器插件并使用它。
到目前为止一切顺利。
现在我们已经准备好了我们的 actuators/motors(真实硬件)和ros控制器,但我们仍然需要其他东西来控制机器人。你能猜出缺少的部分是什么吗?
它是 hardware interface 节点。现在让我们学习如何编写一个硬件接口节点来使ros控制器和真正的硬件进行通信。
4 如何编写硬件接口:
hardware_interface实现了构建机器人硬件抽象的所有构件。它是机器人的软件表示。我们为不同类型的电机/执行器和传感器提供了一套接口,如下所示:
关节致动器的接口:
-
EffortJointInterface:该接口用于接受作用力(电压或电流)命令的关节致动器。您可能已经猜到了,这个接口与effort_controllers一起使用。
-
VelocityJointInterface:该接口用于直接接受速度命令的关节致动器。与velocity _ controllers一起使用。
-
PositionJointInterface:该接口用于直接接受位置命令的关节致动器。与position_controllers一起使用。
关节传感器的接口:
-
JointStateInterface:当你有传感器获取关节的当前位置或/和速度或/和作用力(力/扭矩)时,使用这个接口。与关节状态控制器一起使用。
JointStateInterface用于几乎所有机器人,因为它获得机器人的当前关节位置,这些数据反过来被tf/tf2用来计算机器人的正向运动学。 -
IMU sensor接口:当您有IMU传感器用于获取关节/机器人的方向、角速度和线加速度时,将使用该接口。这与imu_sensor_controller一起使用。
根据您使用的电机和传感器的类型,您必须选择相应的接口。现在我们已经了解了关节执行器和传感器可用的接口类型,让我们学习如何编写hardware_interface节点(我们机器人的软件表示)。
所以让我们假设一个有三个关节的机器人作为例子,并为此编写硬件接口。我们开始吧。
我们的机器人有三个致动器JointA、JointB和JointC,每个关节都有位置传感器。假设JointA & JointB 接受 effort命令,JointC 接受 position 命令。好了,我们有足够的关于机器人的信息来编写硬件接口,所以让我们开始写代码。
嗯,我们需要在机器人的柳絮包中创建一个头文件和一个cpp文件。让我把它们命名为MyRobot_hardware_interface.h和MyRobot_hardware_interface.cpp。
卷起袖子,让我们干吧…!!!让我们先来看看MyRobot_hardware_interface.h。
#include <hardware_interface/joint_state_interface.h>
#include <hardware_interface/joint_command_interface.h>
#include <hardware_interface/robot_hw.h>
#include <joint_limits_interface/joint_limits.h>
#include <joint_limits_interface/joint_limits_interface.h>
#include <controller_manager/controller_manager.h>
#include <boost/scoped_ptr.hpp>
#include <ros/ros.h>
class MyRobot : public hardware_interface::RobotHW
{
public:
MyRobot(ros::NodeHandle& nh);
~MyRobot();
void init();
void update(const ros::TimerEvent& e);
void read();
void write(ros::Duration elapsed_time);
protected:
hardware_interface::JointStateInterface joint_state_interface_;
hardware_interface::EffortJointInterface effort_joint_interface_;
hardware_interface::PositionJointInterface position_joint_interface_;
joint_limits_interface::JointLimits limits;
joint_limits_interface::EffortJointSaturationInterface effortJointSaturationInterface;
joint_limits_interface::PositionJointSaturationInterface positionJointSaturationInterface;
double joint_position_[3];
double joint_velocity_[3];
double joint_effort_[3];
double joint_effort_command_[2];
double joint_position_command_;
ros::NodeHandle nh_;
ros::Timer my_control_loop_;
ros::Duration elapsed_time_;
double loop_hz_;
boost::shared_ptr<controller_manager::ControllerManager> controller_manager_;
};
让我们分解上述头文件的代码并理解它。
#include <hardware_interface/joint_state_interface.h>
#include <hardware_interface/joint_command_interface.h>
#include <hardware_interface/robot_hw.h>
#include <joint_limits_interface/joint_limits.h>
#include <joint_limits_interface/joint_limits_interface.h>
#include <controller_manager/controller_manager.h>
#include <boost/scoped_ptr.hpp>
#include <ros/ros.h>
包括所有必需的头文件。
class MyRobot : public hardware_interface::RobotHW
{
这是一类机器人硬件,包括读取关节传感器数据、向电机发送命令、控制回路以及关节接口数据的方法。
public:
MyRobot(ros::NodeHandle& nh);
~MyRobot();
void init();
void update(const ros::TimerEvent& e);
void read();
void write(ros::Duration elapsed_time);
下面是MyRobot类的构造函数、析构函数和其他方法。
- init()方法是我们定义所有关节句柄、关节的接口和关节限制接口的地方。
- update()方法是控制循环()。
- read()方法用于读取关节传感器数据。
- 向电机发送命令的write()方法。
我们将在cpp文件中看到这些定义。
protected:
hardware_interface::JointStateInterface joint_state_interface_;
hardware_interface::EffortJointInterface effort_joint_interface_;
hardware_interface::PositionJointInterface position_joint_interface_;
joint_limits_interface::JointLimits limits;
joint_limits_interface::EffortJointSaturationInterface effortJointSaturationInterface;
joint_limits_interface::PositionJointSaturationInterface positionJointSaturationInterface;
声明您的机器人执行器/电机正在使用的关节接口和关节限制接口的类型。由于我们的示例机器人只有力和位置接受电机,因此我为力和位置声明了关节接口和限制接口。如果您有一些接受速度命令的电机,则声明速度对应的接口。
double joint_position_[3];
double joint_velocity_[3];
double joint_effort_[3];
double joint_effort_command_[2];
double joint_position_command_;
joint _ position _【3】、joint _ velocity _【3】、joint _ effort _【3】是用于读取机器人关节的位置、速度和作用力的数组变量。这些变量由联合状态接口使用。数组的大小为3,因为我们的示例机器人总共有3个关节。joint _ effort _ command _【2】数组用于向JointA和JointB发送命令。joint_position_command_用于向JointC发送命令。
ros::NodeHandle nh_;
ros::Timer my_control_loop_;
ros::Duration elapsed_time_;
double loop_hz_;
boost::shared_ptr<controller_manager::ControllerManager> controller_manager_;
};
这里是在我们的hardware_interface节点中使用的一些其他变量。my_control_loop_是一个定时器,它以设定的频率(loop_hz_)定期调用控制循环(更新方法)。
好了,我们已经理解了hearder文件。接下来让我们看看MyRobot_hardware_interface.cpp,
#include <YOUR_PACKAGE_NAME/MYRobot_hardware_interface.h>
MyRobot::MyRobot(ros::NodeHandle& nh) : nh_(nh) {
// Declare all JointHandles, JointInterfaces and JointLimitInterfaces of the robot.
init();
// Create the controller manager
controller_manager_.reset(new controller_manager::ControllerManager(this, nh_));
//Set the frequency of the control loop.
loop_hz_=10;
ros::Duration update_freq = ros::Duration(1.0/loop_hz_);
//Run the control loop
my_control_loop_ = nh_.createTimer(update_freq, &MyRobot::update, this);
}
MyRobot::~MyRobot() {
}
void MyRobot::init() {
// Create joint_state_interface for JointA
hardware_interface::JointStateHandle jointStateHandleA("JointA", &joint_position_[0], &joint_velocity_[0], &joint_effort_[0]);
joint_state_interface_.registerHandle(jointStateHandleA);
// Create effort joint interface as JointA accepts effort command.
hardware_interface::JointHandle jointEffortHandleA(jointStateHandleA, &joint_effort_command_[0]);
effort_joint_interface_.registerHandle(jointEffortHandleA);
// Create Joint Limit interface for JointA
joint_limits_interface::getJointLimits("JointA", nh_, limits);
joint_limits_interface::EffortJointSaturationHandle jointLimitsHandleA(jointEffortHandleA, limits);
effortJointSaturationInterface.registerHandle(jointLimitsHandleA);
// Create joint_state_interface for JointB
hardware_interface::JointStateHandle jointStateHandleB("JointB", &joint_position_[1], &joint_velocity_[1], &joint_effort_[1]);
joint_state_interface_.registerHandle(jointStateHandleB);
// Create effort joint interface as JointB accepts effort command..
hardware_interface::JointHandle jointEffortHandleB(jointStateHandleB, &joint_effort_command_[1]);
effort_joint_interface_.registerHandle(jointEffortHandleB);
// Create Joint Limit interface for JointB
joint_limits_interface::getJointLimits("JointB", nh_, limits);
joint_limits_interface::EffortJointSaturationHandle jointLimitsHandleB(jointEffortHandleB, limits);
effortJointSaturationInterface.registerHandle(jointLimitsHandleB);
// Create joint_state_interface for JointC
hardware_interface::JointStateHandle jointStateHandleC("JointC", &joint_position_[2], &joint_velocity_[2], &joint_effort_[2]);
joint_state_interface_.registerHandle(jointStateHandleC);
// Create position joint interface as JointC accepts position command.
hardware_interface::JointHandle jointPositionHandleC(jointStateHandleC, &joint_position_command_);
position_joint_interface_.registerHandle(jointPositionHandleC);
// Create Joint Limit interface for JointC
joint_limits_interface::getJointLimits("JointC", nh_, limits);
joint_limits_interface::PositionJointSaturationHandle jointLimitsHandleC(jointPositionHandleC, limits);
positionJointSaturationInterface.registerHandle(jointLimitsHandleC);
// Register all joints interfaces
registerInterface(&joint_state_interface_);
registerInterface(&effort_joint_interface_);
registerInterface(&position_joint_interface_);
registerInterface(&effortJointSaturationInterface);
registerInterface(&positionJointSaturationInterface);
}
//This is the control loop
void MyRobot::update(const ros::TimerEvent& e) {
elapsed_time_ = ros::Duration(e.current_real - e.last_real);
read();
controller_manager_->update(ros::Time::now(), elapsed_time_);
write(elapsed_time_);
}
void MyRobot::read() {
// Write the protocol (I2C/CAN/ros_serial/ros_industrial)used to get the current joint position and/or velocity and/or effort
//from robot.
// and fill JointStateHandle variables joint_position_[i], joint_velocity_[i] and joint_effort_[i]
}
void MyRobot::write(ros::Duration elapsed_time) {
// Safety
effortJointSaturationInterface.enforceLimits(elapsed_time); // enforce limits for JointA and JointB
positionJointSaturationInterface.enforceLimits(elapsed_time); // enforce limits for JointC
// Write the protocol (I2C/CAN/ros_serial/ros_industrial)used to send the commands to the robot's actuators.
// the output commands need to send are joint_effort_command_[0] for JointA, joint_effort_command_[1] for JointB and
//joint_position_command_ for JointC.
}
int main(int argc, char** argv)
{
//Initialze the ROS node.
ros::init(argc, argv, "MyRobot_hardware_inerface_node");
ros::NodeHandle nh;
//Separate Sinner thread for the Non-Real time callbacks such as service callbacks to load controllers
ros::MultiThreadedspinner(2);
// Create the object of the robot hardware_interface class and spin the thread.
MyRobot ROBOT(nh);
spinner.spin();
return 0;
}
我希望上述cpp文件的大部分内容在注释的帮助下可以自我解释,我将只解释重要的几行。
my_control_loop_ = nh_.createTimer(update_freq, &MyRobot::update, this);
这一行将使update()方法(PID控制循环)以指定的频率周期性地循环。
//This is the control loop
void MyRobot::update(const ros::TimerEvent& e) {
elapsed_time_ = ros::Duration(e.current_real - e.last_real);
read();
controller_manager_->update(ros::Time::now(), elapsed_time_);
write(elapsed_time_);
}
update()方法将简单地调用read()方法来获取当前的关节状态。然后,当前关节状态被更新到控制器管理器,以计算误差(当前目标),并使用PID回路为每个关节生成输出命令。最后调用write()方法向执行器/关节发送输出命令。
这样我们就完全理解了如何为任何机器人编写硬件接口。您可以使用上述代码作为样板代码,为您的机器人编写硬件接口和控制回路。
衷心感谢Slate Robotics。这个博客帮助我理解了如何编写一个硬件接口节点。你可以在这里查看TR1机器人的hardware_interface节点。
到目前为止,我们已经了解了ros控制器的类型、硬件接口以及如何为定制机器人编写硬件接口节点,但我们仍然没有开始控制机器人。我们只差一步就可以开始控制机器人了。所以让我们迈出这一步,看看还需要什么来开始控制机器人。
5 如何在机器人上使用ros_control:
为机器人编写硬件接口节点后,您必须编写一些配置文件来声明控制器和关节执行器的关节限制以控制机器人。让我们跳回我们的3关节机器人,编写配置文件和启动文件来开始控制我们的机器人。
在编写配置文件之前,我们必须首先决定我们实际上想要控制什么(是关节的位置、力度还是速度)。如果你还记得,在举这个机器人例子时,我说过我们只有关节位置传感器,这意味着我们只能获得关节的位置反馈。因此,我们将编写配置文件来控制机器人的位置。让我将用于定义控制器的配置文件命名为controllers.yaml和joint_limits.yaml,以描述关节限制。
所以让我们浏览一下controllers.yaml文件,
MyRobot:
# Publish all joint states
joints_update:
type: joint_state_controller/JointStateController
publish_rate: 50
JointA_EffortController: # Name of the controller
type: effort_controllers/JointPositionController # Since JointA uses effort interface this controller type is used
joint: JointA # Name of the joint for which this controller belongs to.
pid: {
p: 100.0, i: 10.0, d: 1.0} # PID values
JointB_EffortController:
type: effort_controllers/JointPositionController # Since JointB uses effort interface this controller type is used
joint: JointB
pid: {
p: 1.0, i: 1.0, d: 0.0}
JointC_PositionController:
type: position_controllers/JointPositionController # Since JointC uses position interface this controller type is used
joint: JointC
# No PID values defined since this controller simply passes the input position command to the actuators.
我们的示例机器人的controllers.yaml文件到此结束。因此,根据关节接口和需要控制的内容,决定控制器的类型。希望你已经过了上面的控制器类型部分,然后你可以轻松地决定哪种控制器类型将用于你的机器人的关节。
现在让我们检查格式以定义关节的限制。看看下面的joint_limts.yaml文件,
joint_limits:
JointA:
has_position_limits: true
min_position: -1.57
max_position: 1.57
has_velocity_limits: true
max_velocity: 1.5
has_acceleration_limits: false
max_acceleration: 0.0
has_jerk_limits: false
max_jerk: 0
has_effort_limits: true
max_effort: 255
JointB:
has_position_limits: true
min_position: 0
max_position: 3.14
has_velocity_limits: true
max_velocity: 1.5
has_acceleration_limits: false
max_acceleration: 0.0
has_jerk_limits: false
max_jerk: 0
has_effort_limits: true
max_effort: 255
JointC:
has_position_limits: true
min_position: 0
max_position: 3.14
has_velocity_limits: false
max_velocity: 0
has_acceleration_limits: false
max_acceleration: 0.0
has_jerk_limits: false
max_jerk: 0
has_effort_limits: false
max_effort: 0
这是书写机器人关节限制的格式。硬件接口节点中定义的关节限制接口使用这些关节限制值,通过执行输出命令将机器人保持在安全限制范围内。
这就完成了我们的机器人配置文件的编写。因此,让我们编写一个启动文件来加载配置文件,运行hardware_interface节点,启动控制器管理器并加载控制器。让我将其命名为MyRobot_control.launch。
让我们看看下面的MyRobot_control.launch文件,
<launch>
<rosparam file="$(find YOUR_PACKAGE_NAME)/config/controllers.yaml" command="load"/>
<rosparam file="$(find YOUR_PACKAGE_NAME)/config/joint_limits.yaml" command="load"/>
<node name="MyRobotHardwareInterface" pkg="YOUR_PACKAGE_NAME" type="MyRobot_hardware_inerface_node" output="screen"/>
<node name="robot_state_publisher" pkg="robot_state_publisher" type="state_publisher"/ >
<node name="controller_spawner" pkg="controller_manager" type="spawner" respawn="false" output="screen"
args="
/MyRobot/joints_update
/MyRobot/JointA_EffortController
/MyRobot/JointB_EffortController
/MyRobot/JointC_PositionController
"/>
</launch>
同样,上面的启动文件是不言自明的,但我想谈谈控制器管理器。在上面的启动文件中,控制器通过控制器“spawner”python脚本加载到controller_manager中。还有另一种加载控制器的方法。控制器通过服务请求加载到controller_manager中。这些服务请求可以在命令行中手动发送。点击此处了解更多关于如何使用命令行加载/启动/停止控制器的信息。
一旦加载并启动了控制器,您就可以通过下面的主题界面发送所需的目标。ros_controllers将向致动器发送适当的输出命令来实现目标。
控制示例机器人的主题名称。
JointA:/my robot/JointA _ effort controller/command
JointB:/my robot/JointB _ effort controller/command
JointC:/my robot/JointC _ position controller/command
最后,我们来到了这篇文章的结尾。让我最后一次总结一下在你的机器人上使用ros_control需要什么。
总结:
- 你必须为你的机器人编写硬件接口节点。
- 编写配置文件,根据接头接口和应用以及PID值(如果需要)为接头选择控制器类型。
- 写一个配置文件来定义你的机器人的关节限制。
- 通过controller_manager加载和启动控制器。
- 使用主题界面将输入目标发送给控制器。
因此,您可以使用上面的示例机器人代码作为模板,在您自己的机器人上设置ros_control。我做的所有ROS项目,都在用ros_control。因此,您可以查看我的项目中使用ros_control的机器人的工作示例。