控制理论与工程应用基础作业
确保ROS 2已安装,并可以正常运行turtlesim
模拟环境。首先,测试turtlesim
节点是否能够正常启动:
ros2 run turtlesim turtlesim_node
正常运行,关闭页面。
进入~/colcon_ws/src文件目录,使用以下命令创建一个新的ROS 2功能包,命名为cpp_a_follow_turtle_pkg
,并添加必要的依赖关系:
cd ~/colcon_ws/src
ros2 pkg create --build-type ament_cmake cpp_a_follow_turtle_pkg --dependencies rclcpp tf2 tf2_ros tf2_geometry_msgs std_msgs geometry_msgs turtlesim --license Apache-2.0
您需要编写多个文件以实现小海龟的跟随功能。
在src/cpp_a_follow_turtle_pkg/src
目录下创建一个名为new_turtle_spawner.cpp
的文件,拷贝以下代码:
#include
#include
using namespace std;
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("turtle_spawner");
RCLCPP_INFO(node->get_logger(), "create turtle spawner");
auto spawnerClient = node->create_client<turtlesim::srv::Spawn>("/spawn");
auto request = std::make_shared<turtlesim::srv::Spawn::Request>();
request->name = "turtle2";
request->x = 1.5;
request->y = 2.0;
request->theta = 0.0;
while (!spawnerClient->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service");
return -1;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
auto result = spawnerClient->async_send_request(request);
if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS) {
auto r = result.get();
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "result got, %s created successfully!", r->name.c_str());
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "failed to get result, turtle created failed!");
}
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
创建一个名为turtles_pose_pub.cpp
的文件,并拷贝以下代码:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
rclcpp::Node::SharedPtr node;
string turtleName;
void CallBack(const turtlesim::msg::Pose::SharedPtr msg) {
static auto tfBr = std::make_shared<tf2_ros::TransformBroadcaster>(node);
geometry_msgs::msg::TransformStamped tfStamped;
tfStamped.header.stamp = rclcpp::Time();
tfStamped.header.frame_id = "world";
tfStamped.child_frame_id = turtleName;
tfStamped.transform.translation.x = msg->x;
tfStamped.transform.translation.y = msg->y;
tfStamped.transform.translation.z = 0.0;
tf2::Quaternion qtn;
qtn.setRPY(0.0, 0.0, msg->theta);
tfStamped.transform.rotation.x = qtn.x();
tfStamped.transform.rotation.y = qtn.y();
tfStamped.transform.rotation.z = qtn.z();
tfStamped.transform.rotation.w = qtn.w();
tfBr->sendTransform(tfStamped);
}
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
node = rclcpp::Node::make_shared("turtle_pub");
if (argc != 5) {
RCLCPP_INFO(node->get_logger(), "args not correct!");
return 0;
} else {
turtleName = argv[1];
}
auto pose_sub = node->create_subscription<turtlesim::msg::Pose>(turtleName + "/pose", 100, std::bind(&CallBack, std::placeholders::_1));
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
创建一个名为turtle2_controller.cpp
的文件,并拷贝以下代码:
#include
#include
#include
#include
#include
using namespace std;
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("turtle2_controller");
shared_ptr<tf2_ros::Buffer> buffer = make_shared<tf2_ros::Buffer>(node->get_clock());
tf2_ros::TransformListener tfListener(*buffer);
auto velPub = node->create_publisher<geometry_msgs::msg::Twist>("/turtle2/cmd_vel", 1000);
rclcpp::Rate rate(10);
while (rclcpp::ok()) {
try {
geometry_msgs::msg::TransformStamped transStamped = buffer->lookupTransform("turtle2", "turtle1", tf2::TimePointZero);
geometry_msgs::msg::Twist twist;
twist.linear.x = 0.5 * sqrt(pow(transStamped.transform.translation.x, 2) + pow(transStamped.transform.translation.y, 2));
twist.angular.z = 4 * atan2(transStamped.transform.translation.y, transStamped.transform.translation.x);
velPub->publish(twist);
} catch (tf2::TransformException &e) {
RCLCPP_WARN(node->get_logger(), "warning %s", e.what());
}
rate.sleep();
}
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
在cpp_a_follow_turtle_pkg
文件夹下新建一个launch
文件夹,在launch
文件夹中创建一个名为follow_turtle.launch.py
的文件,内容如下:
#!/usr/bin/env python3
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
turtle1 = Node(
package='turtlesim',
executable='turtlesim_node',
name='turtle1',
output='screen',
)
turtle2 = Node(
package='cpp_a_follow_turtle_pkg',
executable='turtle_spawner',
name='turtle2',
output='screen',
)
turtle_pub1 = Node(
package='cpp_a_follow_turtle_pkg',
executable='turtle_pub',
name='tf_caster1',
arguments=['turtle1'],
output='screen',
)
turtle_pub2 = Node(
package='cpp_a_follow_turtle_pkg',
executable='turtle_pub',
name='tf_caster2',
arguments=['turtle2'],
output='screen',
)
turtle2_controller = Node(
package='cpp_a_follow_turtle_pkg',
executable='turtle2_controller',
name='turtle2_controller',
output='screen',
)
return LaunchDescription([
turtle1,
turtle2,
turtle_pub1,
turtle_pub2,
turtle2_controller,
])
在CMakeLists.txt
中“ament_package()”之前添加以下内容:
add_executable(turtle_spawner src/new_turtle_spawner.cpp)
add_executable(turtle_pub src/turtles_pose_pub.cpp)
add_executable(turtle2_controller src/turtle2_controller.cpp)
ament_target_dependencies(turtle_spawner rclcpp turtlesim)
ament_target_dependencies(turtle_pub rclcpp turtlesim tf2_ros geometry_msgs)
ament_target_dependencies(turtle2_controller rclcpp tf2_ros geometry_msgs)
install(TARGETS turtle_spawner turtle_pub turtle2_controller
DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY launch
DESTINATION share/${PROJECT_NAME}/)
在工作空间的根目录下执行以下命令以编译您的功能包:
cd ~/colcon_ws
colcon build --packages-select cpp_a_follow_turtle_pkg
使用以下命令启动节点,启动后弹出的界面会出现两只小海龟,其中一只小海龟会向另一只移动:
source ~/colcon_ws/install/setup.bash
ros2 launch cpp_a_follow_turtle_pkg follow_turtle.launch.py
新打开一个终端,运行以下命令以控制turtle1
的运动:
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r cmd_vel:=/turtle1/cmd_vel
具体键盘按键使用方法如下:
teleop_twist_keyboard
节点会将速度命令(Twist
消息)发布到 /turtle1/cmd_vel
主题上。海龟(或机器人)订阅这个主题,并根据接收到的命令移动。每次按键会更新线性和角速度,从而控制机器人的前进和转向。
在上述代码基础上添加PID控制,使turtle2
以更加平滑和稳定的方式跟随turtle1
。PID控制器可以根据误差(期望值与实际值的偏差)动态调整linear.x
和angular.z
的值,以避免过大的加速或摆动。
PID(Proportional-Integral-Derivative)控制器的输出可以由以下公式表示:
u ( t ) = K p ⋅ e ( t ) + K i ∫ e ( t ) d t + K d d e ( t ) d t u(t) = K_p \cdot e(t) + K_i \int e(t) \, dt + K_d \frac{de(t)}{dt} u(t)=Kp⋅e(t)+Ki∫e(t)dt+Kddtde(t)
其中:
turtle2
的速度和方向。目标位置 - 当前位置
。在此基础上,线性速度和角速度可以分别用PID控制器计算,以实现对linear.x
和angular.z
的平滑控制。
在每次计算控制量时,执行以下公式:
linear.x
):angular.z
):这样可以动态调整turtle2
的速度和角度,使其尽量避免过大的摆动,并更准确地跟随turtle1
。
将turtle2_controller.cpp
文件中的代码替换为以下代码,重新编译运行:
#include
#include
#include
#include
#include
using namespace std;
// PID控制参数
double Kp_linear = 0.5, Ki_linear = 0.0, Kd_linear = 0.05;
double Kp_angular = 4.0, Ki_angular = 0.0, Kd_angular = 0.2;
// 误差变量
double prev_error_linear = 0.0, integral_linear = 0.0;
double prev_error_angular = 0.0, integral_angular = 0.0;
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node::make_shared("turtle2_controller");
shared_ptr<tf2_ros::Buffer> buffer = make_shared<tf2_ros::Buffer>(node->get_clock());
tf2_ros::TransformListener tfListener(*buffer);
auto velPub = node->create_publisher<geometry_msgs::msg::Twist>("/turtle2/cmd_vel", 1000);
rclcpp::Rate rate(10);
while (rclcpp::ok()) {
try {
geometry_msgs::msg::TransformStamped transStamped = buffer->lookupTransform("turtle2", "turtle1", tf2::TimePointZero);
// 计算距离误差
double distance_error = sqrt(pow(transStamped.transform.translation.x, 2) + pow(transStamped.transform.translation.y, 2));
// 计算角度误差
double angle_error = atan2(transStamped.transform.translation.y, transStamped.transform.translation.x);
// 线性速度PID控制
double error_diff_linear = distance_error - prev_error_linear;
integral_linear += distance_error;
double linear_speed = Kp_linear * distance_error + Ki_linear * integral_linear + Kd_linear * error_diff_linear;
prev_error_linear = distance_error;
// 角速度PID控制
double error_diff_angular = angle_error - prev_error_angular;
integral_angular += angle_error;
double angular_speed = Kp_angular * angle_error + Ki_angular * integral_angular + Kd_angular * error_diff_angular;
prev_error_angular = angle_error;
// 发布速度指令
geometry_msgs::msg::Twist twist;
twist.linear.x = linear_speed;
twist.angular.z = angular_speed;
velPub->publish(twist);
} catch (tf2::TransformException &e) {
RCLCPP_WARN(node->get_logger(), "Warning: %s", e.what());
}
rate.sleep();
}
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
在模糊控制系统中,模糊规则表用于定义输入变量与输出变量之间的关系。对于你的海龟跟随代码,我们可以构建一个模糊控制表,显示不同输入距离误差对应的控制量。
输入距离误差(distance_error
)的模糊分类如下:
根据不同的距离分类,输出控制信号的模糊规则如下:
距离分类 | 控制信号 |
---|---|
VERY CLOSE | STOP |
NB (Negative Big) | NS |
NS (Negative Small) | ZO |
ZO (Zero) | PS |
PS (Positive Small) | PB |
PB (Positive Big) | PB |
为了进一步清晰地展示如何将控制信号转换为具体的速度,以下是控制信号和具体速度值的对应关系:
控制信号 | 对应速度值 (linear.x) |
---|---|
NB | -0.5 |
NS | -0.2 |
ZO | 0.1 |
PS | 0.2 |
PB | 0.4 |
STOP | 0.0 |
结合输入输出,我们可以定义一些示例模糊规则:
通过上述模糊控制表和规则,系统能够根据与目标海龟的距离误差动态调整控制信号,从而实现平滑的跟随行为。这种模糊控制方法使得系统能够处理不确定性和非线性的问题,而无需复杂的数学模型。
将turtle2_controller.cpp
文件中的代码替换为以下代码,重新编译运行:
#include
#include
#include
#include
#include
#include
#include
#include
#include "rclcpp/rclcpp.hpp"
using namespace std::chrono_literals;
enum class Distance { NB, NS, ZO, PS, PB, VERY_CLOSE };
enum class ControlSignal { NB, NS, ZO, PS, PB, STOP };
Distance fuzzy_distance(double error);
ControlSignal fuzzy_control(Distance distance);
double get_control_signal(ControlSignal signal);
class TurtleFollow : public rclcpp::Node {
public:
TurtleFollow() : Node("turtle2_controller") {
velPub = this->create_publisher<geometry_msgs::msg::Twist>("turtle2/cmd_vel", 10);
tfBuffer = std::make_shared<tf2_ros::Buffer>(this->get_clock());
tfListener = std::make_shared<tf2_ros::TransformListener>(*tfBuffer);
timer_ = this->create_wall_timer(
100ms, std::bind(&TurtleFollow::follow_turtle, this));
}
private:
void follow_turtle() {
try {
geometry_msgs::msg::TransformStamped transStamped = tfBuffer->lookupTransform("turtle2", "turtle1", tf2::TimePointZero);
double distance_error = sqrt(pow(transStamped.transform.translation.x, 2) + pow(transStamped.transform.translation.y, 2));
double angle_error = atan2(transStamped.transform.translation.y, transStamped.transform.translation.x);
Distance fuzzy_dist = fuzzy_distance(distance_error);
ControlSignal control_signal = fuzzy_control(fuzzy_dist);
double speed = get_control_signal(control_signal);
geometry_msgs::msg::Twist twist;
twist.linear.x = speed;
twist.angular.z = angle_error;
velPub->publish(twist);
RCLCPP_INFO(this->get_logger(), "Distance Error: %.2f, Angle Error: %.2f, Speed: %.2f", distance_error, angle_error, speed);
} catch (tf2::TransformException &e) {
RCLCPP_WARN(this->get_logger(), "Warning: %s", e.what());
}
}
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr velPub;
std::shared_ptr<tf2_ros::Buffer> tfBuffer;
std::shared_ptr<tf2_ros::TransformListener> tfListener;
rclcpp::TimerBase::SharedPtr timer_;
};
Distance fuzzy_distance(double error) {
if (error < 0.1) return Distance::VERY_CLOSE;
else if (error < 0.5) return Distance::NB;
else if (error < 1.0) return Distance::NS;
else if (error < 1.5) return Distance::ZO;
else if (error < 2.0) return Distance::PS;
else return Distance::PB;
}
ControlSignal fuzzy_control(Distance distance) {
switch (distance) {
case Distance::VERY_CLOSE: return ControlSignal::STOP;
case Distance::NB: return ControlSignal::NS;
case Distance::NS: return ControlSignal::ZO;
case Distance::ZO: return ControlSignal::PS;
case Distance::PS: return ControlSignal::PB;
case Distance::PB: return ControlSignal::PB;
}
return ControlSignal::ZO;
}
double get_control_signal(ControlSignal signal) {
switch (signal) {
case ControlSignal::NB: return -0.5;
case ControlSignal::NS: return -0.2;
case ControlSignal::ZO: return 0.1;
case ControlSignal::PS: return 0.2;
case ControlSignal::PB: return 0.4;
case ControlSignal::STOP: return 0.0;
}
return 0.0;
}
int main(int argc, char **argv) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<TurtleFollow>());
rclcpp::shutdown();
return 0;
}
参考网址:figoowen2003.github.io