在这里插入图片描述
这个是本人在大三期间做的项目 —— 基于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. 键盘控制机器人运动
    在这里插入图片描述

/* 以下是滑条的嵌套函数截取,仅供参考 */

/**
 * @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);
    });

信号发生器界面

 

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

/* 以下是本项目中键盘控制的部分代码截取,仅供参考 */

/**
 * @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;
    }
}

曲线观测界面

用于接收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();
}

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