• 并联四足机器人项目开源教程(四) --- 使用QT创建控制上位机


    在这里插入图片描述
    这个是本人在大三期间做的项目 ---- 基于MIT的Cheetah方案设计的十二自由度并联四足机器人,这个项目获得过两个国家级奖项和一个省级奖项。接下来我会将这个机器人的控制部分所有代码进行开源,并配有相关的教程博客,希望能够帮助到在学习相关领域知识或者进行项目开发的同学。

    学习建议

    QT是一个跨平台的 C++ 开发库,主要用来开发图形用户界面(Graphical User Interface,GUI)程序,当然也可以开发不带界面的命令行。由于它可视化界面的操作便捷,拓展性强以及对各平台的兼容性高,非常适用于机器人操作UI的开发。
    因此,本项目基于QT开发平台,并结合了ROS,设计了一套用于人机交互的四足机器人操作UI界面

    学习资料:因为QT的生态已经非常成熟了,网上有数不清的QT开源项目,所以学习成本很低。并且制作上位机最难的不是各种语法(只要有一定的C++代码基础),而是审美,所以建议找到相关的开源项目直接上手,实践出真知。
    B站相关教程:QT入门到实战
    相关开源项目及教程:ROS&Qt5 人机交互界面开发
    本项目上位机开源代码: Quadruped_UpperMonitor

    学习内容

    这里不展开讲QT的语法实现,主要讲本项目所需的上位机功能以及界面设计

    QT与ROS的关系: QT上位机一般作为一个节点接入到ROS网络中,并且将ROS的通信函数嵌入到QT的控件SLOT函数中,进行数据交互。

    机器人操作界面

    单关节控制界面(用于驱动测试以及极性测试)
    在这里插入图片描述

    /* 以下是滑条的嵌套函数截取,仅供参考 */
    
    /**
     * @brief 控制单关节
     * @param id 关节号(1-12)
     */
    void MainWindow::JointCtrl(JOINT l, float data)
    {
        sensor_msgs::JointState msg;
        msg.name.push_back(std::to_string(l));
        msg.position.push_back(data);
        qnode->JointCmdPuber.publish(msg);
    }
    
    /**
     * @brief 控件初始化函数
     */
    void MainWindow::InitWidget(void)
    {
        // 滑块移动时显示数值
        connect(ui->Slider_LBjoint1,&QSlider::sliderMoved,[=]() {
            ui->Num_LBjoint1->display(ui->Slider_LBjoint1->value());
        });
        connect(ui->Slider_LBjoint2,&QSlider::sliderMoved,[=]() {
            ui->Num_LBjoint2->display(ui->Slider_LBjoint2->value());
        });
        connect(ui->Slider_LBjoint3,&QSlider::sliderMoved,[=]() {
            ui->Num_LBjoint3->display(ui->Slider_LBjoint3->value());
        });
    
        // 滑块松开时发布控制数据,ang_bias是角度偏置,与腿部坐标系建立和初始相位有关。
        connect(ui->Slider_LBjoint1,&QSlider::sliderReleased,[=]() {
           JointCtrl(JOINT_LB1, ui->Slider_LBjoint1->value()*Angle2Pi);
        });
        connect(ui->Slider_LBjoint2,&QSlider::sliderReleased,[=]() {
           JointCtrl(JOINT_LB2, ui->Slider_LBjoint2->value()*Angle2Pi - ang_bias);
        });
        connect(ui->Slider_LBjoint3,&QSlider::sliderReleased,[=]() {
           JointCtrl(JOINT_LB3, ui->Slider_LBjoint3->value()*Angle2Pi + ang_bias);
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    运动控制界面(人机交互主要的界面)
    功能描述:

    1. 支持修改机器人运动参数:步态类型,速度,身高,步高等
    2. 键盘控制机器人运动
      在这里插入图片描述
    /* 以下是本项目中键盘控制的部分代码截取,仅供参考 */
    
    /**
     * @brief 键盘按键松开回调函数
     */
    void MainWindow::keyReleaseEvent(QKeyEvent *event){
        // 判断模式
        if(ui->rBtn_Joystick->isChecked() && ctrlEnable){
            switch(event->key()){
    	        // 状态控制
    	        case Qt::Key_Space:
    	            joystick.jump_triggle = 1;
    	            break;
    	
    	        // 速度控制
    	        case Qt::Key_W: joystick.v_des[0] = 0; 	break;
    	        case Qt::Key_S: joystick.v_des[0] = 0; 	break;
    	        case Qt::Key_A: joystick.v_des[1] = 0; 	break;
    	        case Qt::Key_D: joystick.v_des[1] = 0; 	break;
    	        case Qt::Key_Q: joystick.v_des[2] = 0;  	break;
    	        case Qt::Key_E: joystick.v_des[2] = 0; 	break;
    	
    	        // 参数控制
    	        case Qt::Key_G:
    	            cur_gait = (cur_gait + 1) % Gait_Num;
    	            ui->tBtn_Gait->menu()->actions()[cur_gait]->trigger();
    	            break;
    	        case Qt::Key_H:
    	            cur_jump = (cur_jump + 1) % 3;
    	            ui->tBtn_Jump->menu()->actions()[cur_jump]->trigger();
    	            break;
    	        case Qt::Key_U:
    	            ui->pBar_Velocity->setValue(ui->pBar_Velocity->value() + 10);
    	            break;
    	        case Qt::Key_J:
    	            ui->pBar_Velocity->setValue(ui->pBar_Velocity->value() - 10);
    	            break;
    	        case Qt::Key_I:
    	            ui->pBar_Omega->setValue(ui->pBar_Omega->value() + 10);
    	            break;
    	        case Qt::Key_K:
    	            ui->pBar_Omega->setValue(ui->pBar_Omega->value() - 10);
    	            break;
         	}
        }
    }
    
    /**
     * @brief 键盘按键按下回调函数
     */
    void MainWindow::keyPressEvent(QKeyEvent *event){
        switch(event->key()){
            case Qt::Key_Return:
      
                // 步态类型
                if(gait_type == QString("Stand")) joystick.gait = 1;
                else if(gait_type == QString("Trot")) joystick.gait = 2;
                else if(gait_type == QString("Walk")) joystick.gait = 3;
    
                // 跳跃类型
                if(_jump == QString("Jump")) joystick.jump_type = 0;
                else if(_jump == QString("Jump_L")) joystick.jump_type = 1;
                else if(_jump == QString("Jump_H")) joystick.jump_type = 2;
    
    	        // 速度控制
    	        case Qt::Key_W: joystick.v_des[0] = ui->pBar_Velocity->value() / 100. * 0.3f; 		break;
    	        case Qt::Key_S: joystick.v_des[0] = -ui->pBar_Velocity->value() / 100. * 0.3f; 		break;
    	        case Qt::Key_A: joystick.v_des[1] = ui->pBar_Velocity->value() / 100. * 0.15f;  		break;
    	        case Qt::Key_D: joystick.v_des[1] = -ui->pBar_Velocity->value() / 100. * 0.15f; 		break;
    	        case Qt::Key_Q: joystick.v_des[2] = ui->pBar_Omega->value() / 100. * 0.3f;			break;
    	        case Qt::Key_E: joystick.v_des[2] = -ui->pBar_Omega->value() / 100. * 0.3f; 			break;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73

    信号发生器界面

    用于发布正弦波,三角波等控制信号,以测试控制器的响应性能。
    在这里插入图片描述

    /* 以下是本项目中曲线控制信号生成的部分代码截取,仅供参考 */
    
    /**
    曲线参数结构体
    **/
    typedef struct{
    	// 
        Curve_Type curve_type;	// 曲线类型
        // 3代表xyz3个轴系,因为是对每个轴系分别施加控制信号,所以需要分开存储曲线参数
        uint32_t freq[3];			// 频率
        float amp[3];					// 幅值
        float origin[3];				// 初始相位
        float Kp[3];					// PID的比例系数
        float Kd[3];					// PID的微分系数
        bool axis_enable[3];	// 轴系使能标志位
        bool legs_enable[4];	// 腿部使能标志位,4代表4条腿
    }Curve_Inf;
    
    /**
     * @brief 曲线输出信号计算函数
     */
    void calculateCurve(Curve_Inf* _curve, std::vector<sensor_msgs::JointState>& _msgs){
    		
    	    float scale, pDes[3], vDes[3];
    	    int real_time = QDateTime::currentDateTime().toMSecsSinceEpoch();
    	    // 根据曲线类型确定输出
    	    for(int i = 0; i < 3; i ++){
    	        if(_curve->axis_enable[i]){
    	        		// 参数存储
    		           float start = _curve->origin[i];
    		            float phase = (real_time % int(1. / _curve->freq[i] * 1000.)) / (1. / _curve->freq[i] * 1000.);
    		            float amp = _curve->amp[i];
    		            float freq = _curve->freq[i];
    		            switch (_curve->curve_type) {
    		            	// 正弦波
    			            case Curve_Sin:
    			                scale = phase < 0.5f ? sin(M_PI * 2 * phase) : 0;
    			                vDes[i] = amp * cos(M_PI * 2 * phase);
    			                break;
    			                
    			             // 三角波
    			            case Curve_Triangle:
    			                vDes[i] = (phase < 0.5f) ? amp * freq * 4.f : -amp * freq * 4.f;
    			                if(phase < 0.5f){
    			                    start -=  amp;
    			                    scale = 2 * vDes[i] * phase;
    			                }
    			                else{
    			                    start +=  0.5 * amp;
    			                    scale = 2 * vDes[i] * (phase - 0.5f);
    			                }
    			                break;
    			                
    			             // 方波
    			            case Curve_Quare:
    			                scale = phase > 0.5 ? -1. : 1.;
    			                vDes[i] = 0;
    			                break;
    			            default:
    			                break;
    			            }
    			            // 记录目标位置
    			            pDes[i] = start + scale * amp;
    			        }
        }
    
    
        // 信号输出
        _msgs.clear();
        for(int leg = 0; leg < 4; leg ++){
            if(_curve->legs_enable[leg]){
            	// 输出离散目标位置
                msg.position.push_back(_curve->axis_enable[0]? pDes[0] : _curve->origin[0]);
                msg.position.push_back(_curve->axis_enable[1]? pDes[1] : _curve->origin[1]);
                msg.position.push_back(_curve->axis_enable[2]? pDes[2] : _curve->origin[2]);
                for(int i = 0; i < 3; i ++) msg.position.push_back(_curve->Kp[i]);
    			// 输出离散目标速度 
                msg.velocity.push_back(_curve->axis_enable[0]? vDes[0] : 0);
                msg.velocity.push_back(_curve->axis_enable[1]? vDes[1] : 0);
                msg.velocity.push_back(_curve->axis_enable[2]? vDes[2] : 0);
                for(int i = 0; i < 3; i ++) msg.velocity.push_back(_curve->Kd[i]);
    
                msg.name.push_back(std::to_string(leg));
                _msgs.push_back(msg);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    曲线观测界面

    用于接收ROS网络中其他节点发布过来的数据,并定频实时显示。
    多条曲线的实时显示,需要用到定时器以及多线程,即QT中的QTime和QThread。
    在这里插入图片描述

    /* 以下是本项目中曲线绘制的部分代码截取,仅供参考 */
    
    
    /**
     * @brief 曲线输出信号计算函数
     */
    void GraphThread::createItem()
    {
    	int m_iThreadCount = 4;//开启的线程个数
        for(int i = 0;i < m_iThreadCount;i++)
        {
            QTimer *timer = new QTimer();
            QThread  *thread = new QThread();
            m_qTimerList.append(timer);
            m_threadList.append(thread);
        }
    }
    
    /**
     * @brief 多线程开启
     */
    void GraphThread::startMultThread(int dt = 5)
    {
        // 设置图表更新时间间隔
        dtGraph = dt;
    
        for(int i = 0; i < m_qTimerList.size(); i++)
        {
            m_qTimerList.value(i)->start(dtGraph);
            m_qTimerList.value(i)->moveToThread(m_threadList.value(i));
            switch(i){
                case 0: QObject::connect(m_qTimerList.value(i),SIGNAL(timeout()),this,SLOT(realtimeGraph1()),Qt::DirectConnection); break;
                case 1: QObject::connect(m_qTimerList.value(i),SIGNAL(timeout()),this,SLOT(realtimeGraph2()),Qt::DirectConnection); break;
                case 2: QObject::connect(m_qTimerList.value(i),SIGNAL(timeout()),this,SLOT(realtimeGraph3()),Qt::DirectConnection); break;
                case 3: QObject::connect(m_qTimerList.value(i),SIGNAL(timeout()),this,SLOT(realtimeGraph4()),Qt::DirectConnection); break;
            }
            m_threadList.value(i)->start();
        }
    }
    
    /**
     * @brief UI绘制曲线
     * @param id
     * @return
     */
    void graphplot::slot_plotGraph(int id){
        if(enabled[id]){
            for(int i = 0; i < 3; i ++){
                graph_checkboxes[id][i]->setText(graphList.at(id)->graph(i)->name());
                if(!graph_checkboxes[id][i]->isChecked()) graphList.operator [](id)->graph(i)->setVisible(false);
                else graphList.operator [](id)->graph(i)->setVisible(true);
            }
            graphList.operator [](id)->replot();
        }
    }
    
    /**
     * @brief 控件初始化函数
     */
    void graphplot::initWidget(void){
        graph_threads->createItem();
        graph_threads->startMultThread(25);
        QObject::connect(graph_threads, SIGNAL(plotGraph(int)), this, SLOT(slot_plotGraph(int)));
    }
    
    /**
     * @brief 更新曲线数据
     */
    void GraphThread::slot_updateGraphData(float* value, QString* name){
        // 获取当前时间
        double key = QDateTime::currentDateTime().toMSecsSinceEpoch()/1000.0;
        if(Timer_firstRun){
            Timer_startTime = key;
            Timer_firstRun = 0;
        }
    
        // 上写锁,更新曲线的存储数据
        rwlocker.lockForWrite();
        for(int i = 0; i < 4; i ++){
            if(clearflag[i]){
                for(int j = 0; j < 3; j ++){
                    if(!graph_container[i][j].empty())
                    graph_container[i][j].clear();
                }
            }
            setClearFlag(i,false);
        }
    
        PointType p;
        p.stamp = key-Timer_startTime;
        for(int i = 0; i < 4; i ++){
            for(int j = 0; j < 3; j ++){
                if(graph_container[i][j].size() > 100) graph_container[i][j].clear();
                else{
                    p.name = name[i*3 + j];
                    p.value = value[i*3 + j];
                    graph_container[i][j].append(p);
                }
    
            }
        }
        // 解锁
        rwlocker.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    说明:上面列举的所有代码,都是项目中的部分代码截取,也是所对应界面的核心部分代码,能够理解的话,重新编写自己的功能代码应该不难。完整的项目代码也在上方开源了,欢迎下载。

  • 相关阅读:
    互联网时代结束了吗?
    微信小程序(一)
    软件测试要达到一个什么水平才能找到一份9K的工作?
    Volatile型变量
    NeuralODF: Learning Omnidirectional Distance Fields for 3D Shape Representation
    C++中GDAL批量创建多个栅格图像文件并批量写入数据
    WPF向Avalonia迁移(一、一些通用迁移项目)
    学术团体的机器人相关分会和机器人相关大赛的说明
    string类接口介绍及应用
    优思学院|六西格玛黑带大师MBB是什么?兩大认证比较
  • 原文地址:https://blog.csdn.net/weixin_45728705/article/details/127162344