从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 COMPONENTSroscpprospystd_msgssensor_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)
{// 获取触发信号的对象,向下转型为QPushButtongeometry_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状态和objectNameqDebug() << "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_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic: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 转换为 QStringQ_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_OBJECTpublic: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 &);//显示左、右轮速度、imuvoid 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站大佬的联合编译