扩展【从0制作自己的ros导航小车】C++_ROS_QT5联合编译,简单界面为ROS开发增添交互

从0制作自己的ros导航小车

前言

前面已经实现了导航功能,对于之后的一些开发,有交互能力是比较重要的,比如小车上连接一块屏幕,通过屏幕来选择模式,可视化等等。QT是不错的选择,但是需要做一些额外的工作,让QT与ROS能够建立联系,实现通信。本文基于linux操作系统搭建C++_ROS_QT5联合编译环境,让ros与qt数据互通起来,为之后的开发打下基础。

环境是直接使用ubuntu20.04安装之后默认的一些比如cmake等等,自己只需要再安装一下完整的QT5、Vscode即可。

一、环境搭建

一般在虚拟机的ubuntu里安装VScode进行开发,因为开发板端性能较差,所以本文都是在虚拟机操作。对于开发板的话,旭日x3派好像是没有预留触摸屏的接口,所以也只能在虚拟机里先玩着了,如果想放到板端运行可以去看正点原子的qt教程,需要交叉编译的。
①安装VScode
ubuntu下安装VScode

②安装QT5
ubuntu下安装QT5

③VScode扩展安装

switcher
include-autocomplete
vscode-switcher
gbktoutf8
vscode-typora
code-runner
todo-tree
cppsnippets
c-cpp-flylint
qt-support
vscode-clang
vscode-language-pack-zh-hans
vscode-dotnet-runtime
vscode-ros
debugpy
python
pylance
cmake-tools
cpptools
azurerm-vscode-tools
devicetree
tabnine-vscode
rainbow-bracket
qt-cpp-pack
qt-official
qtvsctools
cmake
qtconfigure
vscode-icons
material-theme

二、联合编译

1、创建自己的工程项目:

①终端输入:qtcreator

②创建新QT项目:




接下来全点下一步即可。

③新建文件夹qt_ros,将刚刚新建的qt工程中的四个文件复制过去:

新建文件夹qt_ros中新建build文件夹、CMakeLists.txt、rosNode.cpp、rosNode.h、style.qss,最后目录格式如下:

2、各部分代码及介绍:

①CMakeList.txt:

这是最重要的,联合编译靠的就是这里面的设置,大致就是添加QT和ROS的库并链接,实现联合编译。

#*************************
# 声明cmake最低的版本号
#************************
cmake_minimum_required(VERSION 3.16)

#*************************
# 项目名称
#************************
project(qt_ros)

#*************************
# 设置编译类型
#************************
# set(DEFAULT_BUILD_TYPE "Release")
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Release")
endif()

#*************************
# 指定C++标准 
#************************
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 确保编译器支持所选标准

#************************
# 设置优化等级
#************************
set(CMAKE_CXX_FLAGS "-w")# 忽略警告信息
set(CMAKE_CXX_FLAGS_DEBUG "-g -ggdb ${CMAKE_CXX_FLAGS}")# 设置DEBUG模式的编译选项
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -g -ggdb ${CMAKE_CXX_FLAGS}")# 设置RELEASE模式的编译选项


#*************************
# 添加头文件
#************************
set(CMAKE_INCLUDE_CURRENT_DIR ON)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})


#************************
# 导入QT库
#************************
find_package(Qt5 COMPONENTS Widgets Core REQUIRED)

find_package(catkin REQUIRED COMPONENTS
            roscpp
            rospy
            std_msgs
            sensor_msgs
            )
include_directories(${catkin_INCLUDE_DIRS})


#************************
# 添加源文件
#************************
file(GLOB QT_FORMS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.ui)
file(GLOB QT_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.qrc)
file(GLOB_RECURSE QT_MOC RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} FOLLOW_SYMLINKS *.h)
set(QT_SOURCES mainwindow.cpp rosNode.cpp) #手动添加cpp文件
set(QSS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/style.qss) #手动添加qss文件

#三个用于处理Qt特定资源和代码生成的命令
QT5_WRAP_UI(QT_FORMS_HPP ${QT_FORMS}) #从 .ui 文件自动生成用户界面类的头文件和源文件
QT5_ADD_RESOURCES(QT_RESOURCES_CPP ${QT_RESOURCES})#将 .qrc 资源文件转换成可编译的C++源文件
QT5_WRAP_CPP(QT_MOC_HPP ${QT_MOC}) #.h 文件(包含Qt对象的头文件)自动生成moc(元对象编译器)文件,这些文件是Qt信号和槽机制所必需的

#*************************
# 生成动态库
#************************
add_library(${PROJECT_NAME} SHARED  ${QT_SOURCES} 
                                    ${QT_RESOURCES_CPP} 
                                    ${QT_FORMS_HPP} 
                                    ${QT_MOC_HPP})
target_link_libraries(${PROJECT_NAME} Qt5::Widgets ${catkin_LIBRARIES})

#*************************
# 生成可执行文件
#************************
add_executable(${PROJECT_NAME}_test main.cpp)

target_link_libraries(${PROJECT_NAME}_test 
                      ${PROJECT_NAME} 
                      )

②main.cpp:

删除掉自带的构造函数,然后主函数里添加打开qss文件的代码。

#include "mainwindow.h"

#include <QApplication>
#include <QFile>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QFile file("style.qss");//因为后续会用到qss样式表,所以这里要打开文件
    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QString style = QLatin1String(file.readAll());
        a.setStyleSheet(style);
        file.close();
    }
    MainWindow w(argc, argv);
    w.show();
    return a.exec();
}

③mainwindow.cpp:

在写这个cpp之前需要去ui界面添加几个pushbutton,vscode中打开.ui文件,在点击.ui文件时会显示使用qt designer打开,这时候右下角有东西弹出来,点进去,将qtcreator的路径填进去即可,路径查找使用此命令:which qtcreator

接下来拖5个pushbutton到ui界面中:

选中五个按钮,右键选择指定到按钮组,然后选择新建按钮组即可。

单独勾选每一个按钮的checkable:

#include "mainwindow.h"
#include "./ui_mainwindow.h"

MainWindow::MainWindow(int argc,char **argv,QWidget *parent)
    : QMainWindow(parent)
    , ui_(new Ui::MainWindow)
    , rosNode_(argc,argv)
{
    ui_->setupUi(this);
    
    //仪表盘,显示当前左右轮速、偏航角
    dial_left = new QDial(this);//堆区存储实例化的左轮仪表盘对象
    dial_left->setGeometry(330, 200, 200, 200);//设置位置与大小
    dial_left->setPageStep(10);
    dial_left->setNotchesVisible(true);
    dial_left->setNotchTarget(1.00);//设置步长
    dial_left->setRange(0,100);//设置范围
    dial_right = new QDial(this);
    dial_right->setGeometry(580, 200, 200, 200);
    dial_right->setPageStep(10);
    dial_right->setNotchesVisible(true);
    dial_right->setNotchTarget(1.00);
    dial_right->setRange(0,100);
    dial_imu = new QDial(this);
    dial_imu->setGeometry(500, 70, 100, 100);
    dial_imu->setPageStep(10);
    dial_imu->setNotchesVisible(true);
    dial_imu->setNotchTarget(1.00);
    dial_imu->setRange(0,360);
    //各仪表盘下的label,用于显示文字
    label_left = new QLabel(this);
    label_left->setGeometry(380, 400, 200, 50);
    label_left->setText("左轮速0cm/s");
    label_right = new QLabel(this);
    label_right->setGeometry(630, 400, 200, 50);
    label_right->setText("右轮速0cm/s");
    label_imu = new QLabel(this);
    label_imu->setGeometry(520, 170, 100, 30);
    label_imu->setText("yaw:无数据");
    connect(dial_left, SIGNAL(valueChanged(int)), this, SLOT(dialValueChanged_left(int)));//将仪表盘数据变换信号与槽函数进行绑定,来刷新label显示。
    connect(dial_right, SIGNAL(valueChanged(int)), this, SLOT(dialValueChanged_right(int)));
    connect(dial_imu, SIGNAL(valueChanged(int)), this, SLOT(dialValueChanged_imu(int)));
    QObject::connect(&rosNode_,&RosNode::signal_wheeldata,this,&MainWindow::updateSpeedDisplay);//ros中订阅的速度信息刷新之后会发布signal_wheeldata信号,这里与updateSpeedDisplay槽函数进行绑定,来刷新仪表盘的显示,这里仪表盘显示变换之后就会触发上面的label槽函数。
    QObject::connect(&rosNode_,&RosNode::signal_imu,this,&MainWindow::updateImuDisplay);

    //遥控,前后左右停止
    rosNode_.init();
    ros::NodeHandle nh_;
    pub_ctrl_car = nh_.advertise<geometry_msgs::Twist>("/car_keyboard/cmd_vel", 10);//发布小车控制指令的话题
    //下面是将按键翻转时产生的信号与槽函数绑定起来,这里多个信号绑定同一个槽函数,根据objectName方法,来判断此时是哪个按键翻转了。
    QObject::connect(ui_->pushButton,&QPushButton::toggled,this,&MainWindow::slot_ros_ctrl_car_goforward);
    QObject::connect(ui_->pushButton_2,&QPushButton::toggled,this,&MainWindow::slot_ros_ctrl_car_goforward);
    QObject::connect(ui_->pushButton_3,&QPushButton::toggled,this,&MainWindow::slot_ros_ctrl_car_goforward);
    QObject::connect(ui_->pushButton_4,&QPushButton::toggled,this,&MainWindow::slot_ros_ctrl_car_goforward);
    QObject::connect(ui_->pushButton_5,&QPushButton::toggled,this,&MainWindow::slot_ros_ctrl_car_goforward);
}

MainWindow::~MainWindow()
{
    delete ui_;
}
//下面三个是仪表盘下面label的槽函数,当仪表盘数据变换时触发
void MainWindow::dialValueChanged_left(int val)
{
    /* QString::number()转换成字符串 */
    label_left->setText("左轮速" + QString::number(val) + "cm/s");
}
void MainWindow::dialValueChanged_right(int val)
{
    /* QString::number()转换成字符串 */
    label_right->setText("右轮速" + QString::number(val) + "cm/s");
}
void MainWindow::dialValueChanged_imu(int val)
{
    /* QString::number()转换成字符串 */
    label_imu->setText("yaw:" + QString::number(val) + " 度");
}
//下面两个是轮速、偏航角对应仪表盘的槽函数,触发条件上面初始化时说了
void MainWindow::updateSpeedDisplay(const QByteArray &data)
{
    // 确保数据长度至少为1字节
    int firstByte,secondByte,thirdByte;
    if (data.size() >= 1) {
        // 获取第一个字节,并将其转换为int类型
        if(data[0] <= 0)
        {
            firstByte = static_cast<int>(static_cast<unsigned char>(0.0));
        }
        else
        {
            firstByte = static_cast<int>(static_cast<unsigned char>(data[0]));
        }
        if(data[1] <= 0)
        {
            secondByte = static_cast<int>(static_cast<unsigned char>(0.0));
        }
        else
        {
            secondByte = static_cast<int>(static_cast<unsigned char>(data[1]));
        }
        // 这里可以添加逻辑来确保firstByte在期望的范围内,例如0到100
        //firstByte = qBound(0, firstByte, 100);
        //secondByte = qBound(0, secondByte, 100);

        // QDial控件来显示速度和角度
        dial_left->setValue(firstByte);
        dial_right->setValue(secondByte);
    } else {
        // 数据长度不足,无法获取第一个字节
        qDebug() << "数据长度不足,无法获取第一个字节";
    }
}

void MainWindow::updateImuDisplay(const QString & msg)
{
    int value = msg.toInt();
    dial_imu->setValue(value);
}

//这个是按键翻转时触发的槽函数
void MainWindow::slot_ros_ctrl_car_goforward(bool checked)
{
    // 获取触发信号的对象,向下转型为QPushButton
    geometry_msgs::Twist pubmsg;
    QPushButton *button = qobject_cast<QPushButton *>(sender());
    if (button) {
        if(button->objectName() == "pushButton" && checked)pubmsg.linear.x = 10;
        if(button->objectName() == "pushButton_2" && checked)pubmsg.linear.x = 20;//左转
        if(button->objectName() == "pushButton_3" && checked)pubmsg.linear.x = 30;//右转
        if(button->objectName() == "pushButton_4" && checked)pubmsg.linear.x = 40;//后退
        if(button->objectName() == "pushButton_5" && checked)pubmsg.linear.x = 50;//停车

        // 打印按钮的toggle状态和objectName

        qDebug() << "Button toggled:" << (checked ? "on" : "off");
        qDebug() << "Button objectName:" << button->objectName();
        pub_ctrl_car.publish(pubmsg);
    }
}

④mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QString>
#include <QDebug>
#include <QLabel>
#include <QRadioButton>
#include <QPainter>
#include <QPixmap>
#include <QComboBox>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QDateTimeEdit>
#include <QTimeEdit>
#include <QDateEdit>
#include <QDial>
#include <QLabel>
#include <QDataStream>
#include <QTimer>

#include <ros/ros.h>
#include "rosNode.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(int argc,char **argv,QWidget *parent = nullptr);
    ~MainWindow();

    ros::Publisher pub_ctrl_car;

public slots:
    //九的
    void dialValueChanged_left(int);
    void dialValueChanged_right(int);
    void dialValueChanged_imu(int);
    void updateSpeedDisplay(const QByteArray &data);
    void updateImuDisplay(const QString & msg);
    void slot_ros_ctrl_car_goforward(bool checked);

private:
    Ui::MainWindow *ui_;
    RosNode rosNode_;
    
    QDial *dial_left;
    QDial *dial_right;
    QDial *dial_imu;
    QLabel *label_left;
    QLabel *label_right;
    QLabel *label_imu;
};


#endif // MAINWINDOW_H

⑤rosNode.cpp:

#include "rosNode.h"
#include <QCoreApplication>

RosNode::RosNode(int argc,char **argv):init_argc_(argc),init_argv_(argv)
{

}

RosNode::~RosNode()
{
    if(ros::isStarted())
    {
        ros::shutdown();
        ros::waitForShutdown();
    }
}
//ros的初始化函数,这是必要的
bool RosNode::init()
{
    ros::init(init_argc_, init_argv_, "qt_ui");
    if(!ros::master::check())
    {
        return false;
    }
    ros::start();
    ros::NodeHandle nh;

    // 订阅轮速话题
    sub_wheelspeed_msg = nh.subscribe("/wheel_speed", 10, &RosNode::callback_wheelspeed, this);

    start();
    return true;
}

void RosNode::run()
{
    ros::start();
    ros::NodeHandle nh;
    ros::Rate rate(10);
    while(ros::ok())
    {
        QCoreApplication::processEvents();
        ros::spinOnce();
        rate.sleep();
    }

    Q_EMIT signal_rosShutdown();
}
//轮速、imu话题的回调函数,会发射signal_wheeldata、signal_imu信号,触发mainwindow.cpp的updateSpeedDisplay和updateImuDisplay
void RosNode::callback_wheelspeed(const geometry_msgs::TwistConstPtr& msg)
{
    //ROS_INFO("Received: [%.2f]", msg->angular.x);
    QByteArray data;
    data.resize(2);
    data[0] = static_cast<char>(msg->linear.x);
    data[1] = static_cast<char>(msg->linear.y); // 存储第二个字节

    double angular_x = msg->angular.x;
    QString msgStr = QString::number(angular_x); // 将 double 转换为 QString
    Q_EMIT signal_wheeldata(data);
    Q_EMIT signal_imu(msgStr);
}

⑥rosNode.h:

#pragma once

#ifndef Q_MOC_RUN
#include <ros/ros.h>
#endif

#include <QThread>
#include <QString>

#include <std_msgs/String.h>
#include "geometry_msgs/Twist.h"


class RosNode:public QThread 
{
    Q_OBJECT

public:
    RosNode(int argc,char **argv);
    virtual ~RosNode();
    
    bool init();
    void run() override;

    void callback_wheelspeed(const geometry_msgs::TwistConstPtr& msg);

    ros::Subscriber sub_wheelspeed_msg;


Q_SIGNALS:
    void signal_rosShutdown();

    void signal_imu(const QString &);//显示左、右轮速度、imu
    void signal_wheeldata(const QByteArray &data);


private:
    int init_argc_;
    char **init_argv_;
};

⑦style.qss:

这里用于设置五个按钮的样式。

QPushButton { background-color: #8f8888;border-radius: 30px }
QPushButton::checked { background-color: green; color:white }

上述代码全部填好之后,进入build目录:

cmake ..
make

然后就可以运行观察ui是否正确了:./qt_ros_test

没问题的话,ui界面如下,比较丑陋,对于排版和其它样式的需求,可以自己看QT文档:


三、测试

修改小车的uart.cpp,添加发布小车轮速和偏航角的话题;添加订阅qt发布的控制小车的话题:

pub = nh_.advertise<geometry_msgs::Twist>("/wheel_speed", 10);
subscription_ = nh_.subscribe("car_keyboard/cmd_vel", 10, &MyNode::cmdVelCallback, this);

ros::Publisher pub;
ros::Subscriber subscription_;

void cmdVelCallback(const geometry_msgs::Twist::ConstPtr& msg) {
    /*void cmdVelCallback(const geometry_msgs::msg::Twist::SharedPtr msg) {*/
        double linear_vel = msg->linear.x;
        double angular_vel = msg->angular.z;

        if (linear_vel == 10.0) {
            writeSpeed(15, 15, 0x01);
            writeSpeed(15, 15, 0x01);
        } 
        else if (linear_vel == 20.0) {
            writeSpeed(10, 20, 0x01);
            writeSpeed(10, 20, 0x01);
        }
                else if (linear_vel == 30.0) {
            writeSpeed(20, 10, 0x01);
            writeSpeed(20, 10, 0x01);
        }
                else if (linear_vel == 40.0) {
            writeSpeed(-15,-15, 0x01);
            writeSpeed(-15,-15, 0x01);
        }
                else if (linear_vel == 50.0) {
            writeSpeed(0, 0, 0x01);
             writeSpeed(0, 0, 0x01);
        }
        
        ROS_INFO("keyboard_ctrl: linear=%.2f", linear_vel);
 }
    
//在原有的基础上添加几行发布的即可
void MyNode::controlLoop() {
        double left_speed_now = 0.0;
        double right_speed_now = 0.0;
        double angle = 0.0;
        unsigned char testRece4 = 0x00;

        readSpeed(left_speed_now, right_speed_now, angle, testRece4);
        twist.linear.x = left_speed_now;
        twist.linear.y = right_speed_now;
        twist.angular.x = angle;
        pub.publish(twist);
    }

上面就是简单的固定速度,有兴趣的可以改一改,发布线速度和角速度然后进行解算,做更好看的界面等等。

最终效果如下视频所示,界面中还有一些东西是我自己加的练习,不用在意:


本文参考:

1、正点原子QT开发指导书

2、b站大佬的联合编译

相关推荐
初晴~16 分钟前
【spring】参数校验Validation
java·c++·spring boot·后端·python·spring·validation
古月居GYH42 分钟前
ROS一键安装脚本
人工智能·机器人·ros
daily_23331 小时前
数据结构——小小二叉树第二幕(二叉树链式结构的实现以及二叉树的遍历)超详细!!!
数据结构·c++
时光の尘1 小时前
C语言菜鸟入门·关键字·void的用法
c语言·开发语言·c++·算法·c#·c·关键字
小林熬夜学编程1 小时前
【Linux系统编程】第四十九弹---日志系统构建指南:从基础结构到时间处理与Log类实现
linux·运维·服务器·c语言·开发语言·c++
DeepAlchemy1 小时前
ROSSERIAL与Arduino IDE交叉开发(UBUNTU环境,包含ESP32、arduino nano)
c++·单片机·ros·rosserial
螺旋天光极锐斩空闪壹式!1 小时前
第十三课 二维数组(2)方向数组
开发语言·c++·算法
此生只爱蛋1 小时前
【C++】类和对象-深度剖析默认成员函数-下
c++
是店小二呀1 小时前
【C++】右值引用与移动语义详解:如何利用万能引用实现完美转发
c++·redis
十五年专注C++开发2 小时前
C++不完整类型(Incomplete Type)的检测与避免
开发语言·c++·算法·设计模式