ROS2海龟仿真:两只海龟跟踪实验(一只键盘控制,一只自动跟随)

控制理论与工程应用基础作业

1. 环境准备

确保ROS 2已安装,并可以正常运行turtlesim模拟环境。首先,测试turtlesim节点是否能够正常启动:

ros2 run turtlesim turtlesim_node

正常运行,关闭页面。

2. 创建功能包

进入~/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

3. 编写代码

您需要编写多个文件以实现小海龟的跟随功能。

3.1 创建海龟生成器

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;
}

3.2 发布海龟坐标信息

创建一个名为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;
}

3.3 创建跟随控制器

创建一个名为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;
}

4. 创建Launch文件

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,
    ])

5. 修改CMakeLists.txt

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}/)

6. 编译和运行

在工作空间的根目录下执行以下命令以编译您的功能包:

cd ~/colcon_ws
colcon build --packages-select cpp_a_follow_turtle_pkg

7. 启动节点

使用以下命令启动节点,启动后弹出的界面会出现两只小海龟,其中一只小海龟会向另一只移动:

source ~/colcon_ws/install/setup.bash
ros2 launch cpp_a_follow_turtle_pkg follow_turtle.launch.py

8. 启动键盘控制

新打开一个终端,运行以下命令以控制turtle1的运动:

ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r cmd_vel:=/turtle1/cmd_vel

具体键盘按键使用方法如下:

8.1 移动的键位绑定

  • i : 向前移动
  • , : 向后移动
  • j : 向左转
  • l : 向右转
  • k : 停止

8.2 更多控制

  • uo : 向前左转和向前右转
  • m. : 向后左转和向后右转
  • t : 向上移动(+Z轴)
  • b : 向下移动(-Z轴)
  • Shift + 键(如 U、J、I 等):用于侧移(全向移动)。

8.3 速度控制

  • q/z : 增加/减少最大速度 10%
  • w/x : 增加/减少线速度 10%
  • e/c : 增加/减少角速度 10%

8.4 工作原理

teleop_twist_keyboard 节点会将速度命令(Twist 消息)发布到 /turtle1/cmd_vel 主题上。海龟(或机器人)订阅这个主题,并根据接收到的命令移动。每次按键会更新线性和角速度,从而控制机器人的前进和转向。

9. 效果展示

ROS2海龟仿真:两只海龟跟踪实验(一只键盘控制,一只自动跟随)_第1张图片

10. 添加PID控制

在上述代码基础上添加PID控制,使turtle2以更加平滑和稳定的方式跟随turtle1。PID控制器可以根据误差(期望值与实际值的偏差)动态调整linear.xangular.z的值,以避免过大的加速或摆动。

10.1 PID控制的原理

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)=Kpe(t)+Kie(t)dt+Kddtde(t)

其中:

  • u ( t ) u(t) u(t):控制输出,用于调整turtle2的速度和方向。
  • e ( t ) e(t) e(t):误差,即目标位置 - 当前位置
  • K p K_p Kp:比例系数,控制器的主要增益,作用是减少误差。
  • K i K_i Ki:积分系数,用于消除系统的偏差。
  • K d K_d Kd:微分系数,抑制误差的快速变化,防止系统振荡。

在此基础上,线性速度和角速度可以分别用PID控制器计算,以实现对linear.xangular.z的平滑控制。

在每次计算控制量时,执行以下公式:

  • 线性速度(linear.x
    u l i n e a r = K p ⋅ e l i n e a r + K i ⋅ ∑ e l i n e a r + K d ⋅ ( e l i n e a r − e l i n e a r , p r e v ) u_{linear} = K_p \cdot e_{linear} + K_i \cdot \sum e_{linear} + K_d \cdot (e_{linear} - e_{linear,prev}) ulinear=Kpelinear+Kielinear+Kd(elinearelinear,prev)
  • 角速度(angular.z
    u a n g u l a r = K p ⋅ e a n g u l a r + K i ⋅ ∑ e a n g u l a r + K d ⋅ ( e a n g u l a r − e a n g u l a r , p r e v ) u_{angular} = K_p \cdot e_{angular} + K_i \cdot \sum e_{angular} + K_d \cdot (e_{angular} - e_{angular,prev}) uangular=Kpeangular+Kieangular+Kd(eangulareangular,prev)

这样可以动态调整turtle2的速度和角度,使其尽量避免过大的摆动,并更准确地跟随turtle1

10.2 PID控制的程序

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;
}

11. 添加模糊控制

11.1模糊控制的原理

在模糊控制系统中,模糊规则表用于定义输入变量与输出变量之间的关系。对于你的海龟跟随代码,我们可以构建一个模糊控制表,显示不同输入距离误差对应的控制量。

11.1.1输入模糊化(距离误差分类)

输入距离误差(distance_error)的模糊分类如下:

  • 距离分类
    • VERY CLOSE: 距离 < 0.1
    • NB (Negative Big): 0.1 <= 距离 < 0.5
    • NS (Negative Small): 0.5 <= 距离 < 1.0
    • ZO (Zero): 1.0 <= 距离 < 1.5
    • PS (Positive Small): 1.5 <= 距离 < 2.0
    • PB (Positive Big): 距离 >= 2.0

11.1.2控制信号对应表

根据不同的距离分类,输出控制信号的模糊规则如下:

距离分类 控制信号
VERY CLOSE STOP
NB (Negative Big) NS
NS (Negative Small) ZO
ZO (Zero) PS
PS (Positive Small) PB
PB (Positive Big) PB

11.1.3控制信号对应的速度值

为了进一步清晰地展示如何将控制信号转换为具体的速度,以下是控制信号和具体速度值的对应关系:

控制信号 对应速度值 (linear.x)
NB -0.5
NS -0.2
ZO 0.1
PS 0.2
PB 0.4
STOP 0.0

11.1.4示例模糊规则集

结合输入输出,我们可以定义一些示例模糊规则:

  1. 如果 距离是 VERY CLOSE ,那么控制信号是 STOP;
  2. 如果 距离是 NB ,那么控制信号是 NS;
  3. 如果 距离是 NS ,那么控制信号是 ZO;
  4. 如果 距离是 ZO ,那么控制信号是 PS;
  5. 如果 距离是 PS ,那么控制信号是 PB;
  6. 如果 距离是 PB ,那么控制信号是 PB.

通过上述模糊控制表和规则,系统能够根据与目标海龟的距离误差动态调整控制信号,从而实现平滑的跟随行为。这种模糊控制方法使得系统能够处理不确定性和非线性的问题,而无需复杂的数学模型。

11.2模糊控制的程序

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

你可能感兴趣的:(linux,c++,人工智能,机器人)