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

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

时间:2024-08-10 17:23:37浏览次数:17  
标签:dial include ROS C++ QT5 imu ros MainWindow QT

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

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

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

<iframe allowfullscreen="true" data-mediaembed="youku" frameborder="0" id="YMzneTC0-1723090881813" src="https://player.youku.com/embed/XNjQxMjcyMDUwMA=="></iframe>


本文参考:
1、正点原子QT开发指导书
2、b站大佬的联合编译

标签:dial,include,ROS,C++,QT5,imu,ros,MainWindow,QT
From: https://blog.csdn.net/m0_71523511/article/details/140903124

相关文章

  • 集合相似度c++
    初入新蒟蒻一多多关照。弱弱问一句,有没有东营区一中的学哥学姐                               集合相似度题目是这样的——题目描述给定两个整数集合,它们的相似度定义为:Nc/Nt×100%。其中Nc是两个集合......
  • C++基础入门
    一·命名空间(namespace)正常namespace的使用include<stdio.h>#include<stdlib.h>//1.正常的命名空间定义//wzh是命名空间的名字,⼀般开发中是⽤项⽬名字做命名空间名。namespacebit{//命名空间中可以定义变量/函数/类型intrand=10;intAdd(in......
  • 1.动手编写第一个makefile编译c++多文件项目
    1.动手编写第一个makefile编译c++多文件项目1.1ubuntu开发环境安装•apt-getupdate#更新安装源•apt-getinstallg++#安装gcc和c++的开发库•apt-getinstallgdb#调试工具•apt-getinstallmake•apt-getinstallopenssh-server#远程连接工具•apt-getin......
  • 2024年华为OD机试真题-推荐多样性-C++-OD统一考试(C卷D卷)
    2024年OD统一考试(D卷)完整题库:华为OD机试2024年最新题库(Python、JAVA、C++合集) 题目描述:推荐多样性需要从多个列表中选择元素,一次性要返回N屏数据(窗口数量),每屏展示K个元素(窗口大小),选择策略:1.各个列表元素需要做穿插处理,即先从第一个列表中为每屏选择一个元素,再从第二个列......
  • c++习题18-计算鞍点
    目录一,题目二,思路三,代码一,题目给定一个5×5的矩阵,每行只有一个最大值,每列只有一个最小值,寻找这个矩阵的鞍点。鞍点指的是矩阵中的一个元素,它是所在行的最大值,并且是所在列的最小值。例如:在下面的例子中(第4行第1列的元素就是鞍点,值为8)。11356912478101056......
  • c++习题20-百钱百鸡
    目录一,题目二,思路三,代码 一,题目农夫约翰今天来到了畜牧市场,想给自己的农场里买点鸡回去。已知公鸡一只5块钱,母鸡3块钱,小鸡崽子一块钱三只。农夫手里有N元,他想买N只鸡,但是他跟ljc一样数学不好,想让你帮他算一算有几种买法,以方便他挑选。输入描述一个整数N,约翰手里的钱......
  • C++ 类型转换
    目录0.前言1.C语言类型转换1.1隐式类型转换1.2显式类型转换2.C++强制类型转换2.1static_cast2.2reinterpret_cast2.3const_cast2.4dynamic_cast3.为什么C++需要4种强制类型转换3.1类型转换的多样性需求3.2提高类型转换的安全性3.3提供更明确的语义3.4支持高......
  • C++标准库函数
    很多人用C++就是因为C++有很多方便的函数,所以我来科普一下方便的输入输出函数:scanf(); 高效的输入(TLE的可以和printf一起用,很快!)格式:输入变量a:scanf(...,&a);一定要加&(取地址)...是按a的类型来定的:%d:int%lld:longlong%lf:double%c:charprintf(); ......
  • 【数据结构与算法】输出二叉树中从每个叶子结点到根结点的路径 C++实现(二叉树+栈+深度
    二叉树叶子节点到根节点的路径题目描述给定一棵二叉树的后序遍历序列post[s1..t1]和中序遍历序列in[s2..t2],设计一个算法,输出二叉树中从每个叶子节点到根节点的路径。请使用栈的数据结构解决。输入格式输入包括两行:第一行为后序遍历序列post[s1..t1]。第二行为中序......
  • C++类和对象(上)
    文章目录一、类的定义1、类的定义格式2、访问限定符3、类域二、实例化1、实例化概念2、对象的大小三、this指针一、类的定义1、类的定义格式calss是定义类的关键词,用法更C语言中的结构体struct关键词用法一样,区别是类可以在里面创建函数,当然在C++中也是兼容结......