从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站大佬的联合编译