QT与ROS通过TCP通信指挥小车运动


项目做完有段时间了,很多细节都不太记得了,所以写得很草,细节可以看代码。 长个记性,下次一定要及时记录!


效果展示

QT与ROS通过TCP通信指挥小车运动

本文主要实现:
1、ROS作为主机(客户端),qt作为从机(服务器)进行TCP通信,通过从机给小车发布任务,在主机上实时显示小车位置、小车状态信息
2、用小乌龟代替小车,进行矩形轨迹运动


准备工作

1、开发环境说明

		 - ubuntu 18.04以及ROS Melodic环境
		 -  ubuntu下的qt creator(随机版本应该都行)
		 注:双系统或者虚拟机都行

2、代码说明

			1)IES为qt部分代码,直接打开.pro文件即可
			2)src部分为ROS的代码,需要创建自己的工作空间再进行编译
			3)运行:一定得先打开服务器(qt程序),再运行
			roslaunch my_tcp ros_tcp.launch
			注:这部分涉及到一些ros运行的基础知识

一、TCP协议

  • 本文由于是ROS与qt通信,在ROS部分,我使用的是C++编程,qt使用其自带的库函数,所以需要先了解一些C++部分编写协议的知识
    https://blog.csdn.net/cpp_learner/article/details/127813889
  • 先确定客户端和服务器,一方需要直接收接受小车位置状态信息,并且偶尔发布任务信息;一方需要一直发布小车位置状态信息,不定时接收任务,但需要一直等待(后面编程的时候发现C++写tcp协议有阻塞,等待时不能发送)。所以我选择ROS作为主机(客户端),qt作为从机(服务器)

二、搭建ros(客户端)方面的通讯

首先得建立自己的ros工作空间,然后在工作空间里面去添加节点使用TCP套接字(sockets)来发送和接收数据

1、建立客户端的流程

1、初始化TCP套接字,socket()获得一个sockfd。注意第二个参数必须SOCK_STREAM.
2、设置服务器地址结构,按通信地址请求连接(必须服务器的)
3、连接成功后,进行写或读的操作。(开放了端口,允许客户端连接)
4、关闭sockfd。

2、tcp_client.cpp

功能:订阅小车的位置和速度信息,在回调里面获取,并通过tcp协议发送给qt
注意:两种信息的格式
小车速度话题 :/turtle1/cmd_vel geometry_msgs::Twist
小车位置话题 :/turtle1/pose turtlesim::Pose
主要代码:

void carposeCallback(turtlesim::Pose poseMsg)//位置回调函数
 {
        carpose_x=poseMsg.x;
        carpose_y=poseMsg.y;
        //std::cout << "carposeCallback" << std::endl;
 }
 void carspeedCallback(geometry_msgs::Twist speedMsg)//速度回调函数
 {
        carspeed=speedMsg.linear.x;
 }

int main(int argc, char **argv)
{
    ros::init(argc, argv, "tcp_client");
    ros::NodeHandle n;
    ros::param::get("~client_port",port);//将参数引入launch文件,方便随时修改
    ros::param::get("~client_ip",ip);

    ros::Subscriber sub_carpose = n.subscribe("/turtle1/pose", 1000, carposeCallback);//订阅小乌龟位置信息
    ros::Subscriber sub_carspeed = n.subscribe("/turtle1/cmd_vel", 1000, carspeedCallback);//订阅小乌龟速度信息
    
//TCP通信部分
    //切片发送字符串的lambda函数
    auto sendString = [](int& socket, int step,std::string& send_str)
    {
      //计算待发送数据的长度
      int len = strlen(send_str.c_str());
      //发送数据
      for (int i = 0; i < len; i = i + step) 
      {
        std::string str = send_str.substr(i, step);
        send(socket, str.c_str(), strlen(str.c_str()), 0);
        usleep(1);//中间需要有停顿,不可以一直发送,否则server接受会出现错乱
      }
      //发送一个空字符,server收到长度为0的字符后会停止接收
      std::string str = "";
      send(socket, str.c_str(), strlen(str.c_str()), 0);  
    };
      //创建客户端套接字
      int sock = socket(AF_INET, SOCK_STREAM, 0);
      //向服务器(特定的IP和端口)发起请求
      struct sockaddr_in serv_addr;
      memset(&serv_addr, 0, sizeof(serv_addr));           //每个字节都用0填充
      serv_addr.sin_family = AF_INET;                     //使用IPv4地址
      serv_addr.sin_addr.s_addr = inet_addr(ip.c_str());  //具体的IP地址
      serv_addr.sin_port = htons(port);          //端口

       //连接服务器
      //connect只会链接一次,不会一直等待
       if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0)
     {
        std::cout << "can not connect socket" << std::endl;
        exit(1);
      } 

    //成功连接,在while循环内持续创建套接字,并且发送信息
    while (true) 
    {
     // 查看一次回调函数队列
    //  ros::spinOnce();
      std::cout << "successful connect " << std::endl;


     // 从服务器接收数据
        char buffer[1024];
        // if(strcmp(buffer,"0")=0)
        // {
          memset(buffer, '\0', sizeof(buffer));
          ssize_t bytesReceived = read(sock, buffer, sizeof(buffer));
    
        if (bytesReceived == -1)
         {
            std::cout <<"recv:Failed to receive data from the server." << std::endl;
            close(sock);
            return 0;
         } 
         else
          {
            std::cout << "client:Received " << bytesReceived << " bytes of data from the server:" << std::endl;
            buffer[bytesReceived] = '\0'; // 确保字符串以空字符结尾
            std::cout << buffer << std::endl;
          }
          
          //忘了这个判断是怎么定义的了
           while((strcmp(buffer,"0")!=0)||(carpose_x!=8.7&&carpose_y!=6.7))
                {
                ros::spinOnce();
                //向server发送信息
                // write(socket_fd,flag,sizeof(flag));
                std::ostringstream stream;
                stream << carpose_x << " " << carpose_y << " " << carspeed <<" ";
                std::string result = stream.str();
                sendString(sock, 1024, result);
                std::cout << "send "<<result << std::endl;
                // sleep(1);//不要用sleep函数来休眠,sleep的参数整数有效,不够精准
                // 睡眠0.9秒
                std::this_thread::sleep_for(std::chrono::milliseconds(120));
                }
          
      // close(sock);
    }
    close(sock);  
}

3、tcp_client_recv.cpp

功能:接受任务指令,订阅小车的位置信息,创建发布者控制速度,通过tcp让小车执行指令

主要代码:

// 回调函数,获取当前小乌龟的位置信息
void poseCallback(turtlesim::Pose poseMsg)
{
    current_pose = poseMsg;
}

void drawsqure(double side_length,double side_width)//小乌龟画方形
{
    // 设置运动速度和角速度
    double linear_speed = -1.2;   // 线速度为1.0单位/s,逆时针
    double angular_speed = 0.6;  // 角速度为0.5单位/s
    // 设置正方形边长和旋转次数(四个角)
    // int side_length = 2.0;   // 正方形边长为2.0单位
    int num_rotations = 4;      // 旋转次数为4
    // 创建一个Twist消息对象,用于控制小乌龟的运动
    geometry_msgs::Twist vel_msg;
    int j=0;
    // 开始画正方形
    for (int i = 0; i < num_rotations; i++)
    {
        // 移动一段距离(正方形边长)
        double distance_moved = 0.0;
        ros::Rate loop_rate(10);  // 控制循环频率为10Hz
        double distance=0;
        if(i==0||i==2)
        {
            distance=side_length;
        }
        if(i==1||i==3)
        {
            distance=side_width;
        }
        turtlesim::Pose initial_pose=current_pose;
        vel_msg.linear.x = linear_speed;
        vel_msg.angular.z = 0.0;
        // velocity_publisher.publish(vel_msg);
        while ((distance_moved < distance))
        {
            // 设置线速度和角速度
            velocity_publisher.publish(vel_msg);
            // for(int i=0;i<5;i++)
            loop_rate.sleep();
            // 计算已移动的距离(欧氏距离)
            distance_moved = sqrt(pow(current_pose.x-initial_pose.x, 2) + pow(current_pose.y-initial_pose.y, 2));
            std::cout << "distance_moved "<<distance_moved << std::endl;
            // if(i==0&&distance_moved >= distance)
            //     j++;
        }
        // 停止小乌龟的运动
        vel_msg.linear.x = 0.0;
        velocity_publisher.publish(vel_msg);
        ros::Duration(1).sleep();  // 停顿1秒钟
        // 进行旋转90°
        double initial_angle = current_pose.theta;
        // std::cout << "distance "<<distance << std::endl;
        double angle_turned = 0.0;
        vel_msg.angular.z = angular_speed;
        // velocity_publisher.publish(vel_msg);
        while (angle_turned < PI /rotate||angle_turned>5)   // 弧度表示90°角
        {
            // vel_msg.linear.x = 0.0;
            // vel_msg.angular.z = angular_speed;
            velocity_publisher.publish(vel_msg);
            loop_rate.sleep();
            angle_turned = std::abs(current_pose.theta - initial_angle);
            std::cout << "initial_angle "<<initial_angle << std::endl;
            std::cout << "angle_turned "<<angle_turned << std::endl;
        }
        // 停止小乌龟的运动
        vel_msg.angular.z = 0.0;
        velocity_publisher.publish(vel_msg);
        ros::Duration(1).sleep();  // 停顿1秒钟
    }
}

int main(int argc, char **argv)
{
    ros::init(argc,argv,"car_control");    // 初始化ROS节点
    ros::NodeHandle nh;// 初始化ROS句柄
    ros::param::get("~side_length",side_length);
    ros::param::get("~side_width",side_width);
    ros::param::get("~rotate",rotate);
    ros::param::get("~client_port",port);
    ros::param::get("~client_ip",ip);
    ros::Publisher pose_pub = nh.advertise<turtlesim::Pose>("/turtle1/pose",10);    // 创建发布者
    velocity_publisher = nh.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
    ros::Subscriber pose_subscriber = nh.subscribe("/turtle1/pose", 1000, poseCallback);// 创建一个Subscriber订阅小乌龟的位置信息
  
    //去使用小乌龟自带的服务,使其路径轨迹为白色
    ros::ServiceClient initialposeClient = nh.serviceClient<turtlesim::TeleportAbsolute>("/turtle1/teleport_absolute");
    ros::ServiceClient setpenClient = nh.serviceClient<turtlesim::SetPen>("/turtle1/set_pen");
    ros::service::waitForService("/turtle1/set_pen");
    turtlesim::SetPen::Request pen_req;
    turtlesim::SetPen::Response pen_resq;
    pen_req.r=255;
    pen_req.g=255;
    pen_req.b=255;
    pen_req.width=4;
    pen_req.off=1;
    bool ok=setpenClient.call(pen_req,pen_resq);
    if(ok)
     {
            ROS_INFO("whitepen");
    }
    else
        ROS_INFO("whitepen failed");

    //去使用小乌龟自带的服务,定义初始点位置
    ros::service::waitForService("/turtle1/teleport_absolute");
    turtlesim::TeleportAbsolute::Request req;
    turtlesim::TeleportAbsolute::Response resp;
    req.x=8.7;
    req.y=6.7;
    req.theta=0;
    ok = initialposeClient.call(req, resp);
    if (ok)
     {
            ROS_INFO("initialpose");
    }
    
    ros::AsyncSpinner spinner(2);
    spinner.start();

    turtlesim::Pose pose_msg;    // 声命变量类型,几何消息类型
    pose_msg.x = 0.0;    // 给变量赋值,线速度和角速度
    pose_msg.y = 0.0;
    ros::Rate r(10);// 设置发布频率,10Hz

//TCP协议部分
    int sockfdTCP = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfdTCP == -1)
    {
        printf("Failed to create socket.\n");
        return 0;
    }
    
    // 设置服务器地址信息
    struct sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;                     //使用IPv4地址
    serverAddr.sin_addr.s_addr = inet_addr(ip.c_str());  //具体的IP地址
    serverAddr.sin_port = htons(port);          //端口
    // serverAddr.sin_family = AF_INET;
    // serverAddr.sin_port = htons(port); //端口号,根据你实际要连接的tcp服务端的端口设置
    // inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); //服务端ip
    
    // 连接到服务器
    if (connect(sockfdTCP, reinterpret_cast<struct sockaddr*>(&serverAddr), sizeof(serverAddr)) == -1) 
    {
        printf("recv:Failed to connect to the server.\n");
        close(sockfdTCP);
        return 0;
    }
    
    while (ros::ok())
    {
        // 发布消息
        pose_pub.publish(pose_msg);
        // 添加延时函数,达到设置发布频率目的
        r.sleep();
        // 官方建议的回头函数,回头一次
        // ros::spinOnce();


         // 从服务器接收数据
        char buffer[1024];
        memset(buffer, '\0', sizeof(buffer));
        ssize_t bytesReceived = read(sockfdTCP, buffer, sizeof(buffer));
    
        if (bytesReceived == -1)
         {
            std::cout <<"recv:Failed to receive data from the server." << std::endl;
            close(sockfdTCP);
            return 0;
         } 
         else
          {
            std::cout << "Received " << bytesReceived << " bytes of data from the server:" << std::endl;
            buffer[bytesReceived] = '\0'; // 确保字符串以空字符结尾
            std::cout << buffer << std::endl;
          }
          // 关闭socket连接
        // close(sockfdTCP);

//接收到信息后进行分析,有三个任务例子
        if(strcmp(buffer,"1")==0)
        {
            std::cout << "A" << std::endl;//a为三个正方形大小的路径
            drawsqure(side_length*3,side_length);
            ros::service::waitForService("/turtle1/teleport_absolute");//因小乌龟矩形走得不标准,所以每个任务结束后需要重新设置起始点
            req.x=8.7;
            req.y=6.7;
            req.theta=0;
            bool ok = initialposeClient.call(req, resp);
            if (ok)
            {
                    ROS_INFO("initialpose");
            }
        }
        else if(strcmp(buffer,"2")==0)
        {
            std::cout << "B" << std::endl;//b为两个正方形大小的路径
            drawsqure(side_length*2,side_length);
            ros::service::waitForService("/turtle1/teleport_absolute");
            req.x=8.7;
            req.y=6.7;
            req.theta=0;
            bool ok = initialposeClient.call(req, resp);
            if (ok)
            {
                    ROS_INFO("initialpose");
            }
        }
        else if(strcmp(buffer,"3")==0)
        {
            std::cout << "C" << std::endl;
            drawsqure(side_length,side_length);
            ros::service::waitForService("/turtle1/teleport_absolute");
            req.x=8.7;
            req.y=6.7;
            req.theta=0;
            bool ok = initialposeClient.call(req, resp);
            if (ok)
            {
                    ROS_INFO("initialpose");
            }
        }
    }
    // 关闭socket连接
    close(sockfdTCP);
    
    spinner.stop();
    return 0;
}	

三、qt方面的服务器搭建

1、使用mainwindow基类,调用network库

在.pro文件里面加入QT       += sql network

2、设计ui界面

3、创建服务器

(1)先用 Open()函数打开等待传输的可读文件;
(2)用 Socket()创建套接口,并给套接口地址结构赋值;
(3)用 Bind()函数绑定套接口;
(4)用 Listen()函数在该套接口上监听请求;
(5)用 Accept()函数接受请求,产生新的套接口及描述字,并与客户端连
接;
(6)最后用 Write()函数将读取的数据存放在内存中,向客户段发送数据;
(7)传输完毕时,用 Close()关闭所有进程,结束文件传输。
注意:checkbox的三态

四、ros小车部分

使用小海龟代替小车,为了达到设计要求,需要对turtlesim的源码进行更改。
 (没写怎么做就是忘了具体改的哪儿了)

1)替换小乌龟图片;
把images里面的图片全部替换成这个:

在这里插入图片描述

2)更改小乌龟初始位置;(通过服务实现)
3)更改背景板;
4)绘制简易地图;
在turtle_frame.cpp中把函数替换为如下

 void TurtleFrame::paintEvent(QPaintEvent*)
{
  QPainter painter(this);

  painter.drawImage(QPoint(0, 0), path_image_);

  painter.drawRect(100,200,100,100);
  painter.drawRect(200,200,100,100);
  painter.drawRect(300,200,100,100);
  // QPen pen(QColor(255, 255, 255));
  // painter.setPen(pen); 

  M_Turtle::iterator it = turtles_.begin();
  M_Turtle::iterator end = turtles_.end();
  for (; it != end; ++it)
  {
    it->second->paint(painter);
  }
}

5)编写launch文件启动小乌龟。

五、qt小车部分

1)地图绘制:使用Qpianter的 drawRect 函数来绘制三个矩形,在自定义的类中重写paintevent函数,他会在窗口需要重绘时被调用
2)小车随路径移动:利用QTimer来周期性地更新图片的位置,实现图片的移动效果
具体可以看代码如何实现

小结

存在的问题:
1、本质上我并没有把TCP的协议理解透彻,当qt处于windows系统下,ROS用虚拟机时(模拟不同电脑之间),我并不知道他两端口号和IP如何去配置
2、用C++去写TCP协议的时候,发现TCP通信有阻塞,尝试用异步处理回调,多线程,中断等方法都未解决
3、视频中ROS部分实现的小车(其实就是小乌龟)移动控制不够精准,更推荐的方式是自己搭建小车模型赛道,用三维进行仿真(不过我这个任务那样搞太复杂了,其实是搞不出来)

代码下载

链接:https://pan.quark.cn/s/afbe79d4040b
提取码:Lrxp

  • 16
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了在Windows系统中与Ubuntu系统中的ROS进行通信,可以使用ROSTCP/IP协议栈。下面是实现ROS Windows TCP通信的步骤: 1. 在Windows系统中安装ROS,可以使用ROS的Windows版本或者使用虚拟机在Windows系统中运行Ubuntu系统。 2. 在Windows系统中安装ROSTCP/IP协议栈,可以使用ROSroscpp库中的TCP/IP协议栈或者使用第三方库,例如Boost.Asio。 3. 在Windows系统中编写TCP/IP Client程序,连接到Ubuntu系统中的ROS TCP/IP Server。 4. 在Windows系统中发送数据到Ubuntu系统中的ROS TCP/IP Server,可以使用ROSroscpp库中的TCP/IP协议栈提供的接口。 5. 在Ubuntu系统中编写TCP/IP Server程序,接收来自Windows系统的数据,并将其转发给ROS系统中的其他节点。 6. 在Ubuntu系统中使用ROSroscpp库中的TCP/IP协议栈提供的接口,将接收到的数据转发给ROS系统中的其他节点。 下面是一个简单的ROS Windows TCP通信的例子: ```cpp // Windows TCP/IP Client程序 #include <ros/ros.h> #include <ros/network.h> #include <boost/asio.hpp> int main(int argc, char **argv) { ros::init(argc, argv, "ros_tcp_client"); ros::NodeHandle nh; boost::asio::io_service io_service; boost::asio::ip::tcp::socket socket(io_service); boost::asio::ip::tcp::resolver resolver(io_service); boost::asio::connect(socket, resolver.resolve({"192.168.1.100", "12345"})); // 连接到Ubuntu系统中的ROS TCP/IP Server std::string message = "Hello, ROS!"; boost::asio::write(socket, boost::asio::buffer(message)); // 发送数据到Ubuntu系统中的ROS TCP/IP Server return 0; } // Ubuntu ROS TCP/IP Server程序 #include <ros/ros.h> #include <ros/network.h> #include <boost/asio.hpp> void handle_accept(boost::asio::ip::tcp::socket socket, ros::Publisher pub) { boost::asio::streambuf buffer; boost::asio::read_until(socket, buffer, "\n"); // 接收来自Windows系统的数据 std::istream is(&buffer); std::string message; std::getline(is, message); pub.publish(message); // 将接收到的数据转发给ROS系统中的其他节点 } int main(int argc, char **argv) { ros::init(argc, argv, "ros_tcp_server"); ros::NodeHandle nh; ros::Publisher pub = nh.advertise<std::string>("ros_tcp_data", 1000); boost::asio::io_service io_service; boost::asio::ip::tcp::acceptor acceptor(io_service, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 12345)); while (ros::ok()) { boost::asio::ip::tcp::socket socket(io_service); acceptor.accept(socket); handle_accept(std::move(socket), pub); } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值