这个是本人在大三期间做的项目 ---- 基于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);
});
运动控制界面(人机交互主要的界面)
功能描述:
- 支持修改机器人运动参数:步态类型,速度,身高,步高等
- 键盘控制机器人运动
/* 以下是本项目中键盘控制的部分代码截取,仅供参考 */
/**
* @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;
}
}
信号发生器界面
用于发布正弦波,三角波等控制信号,以测试控制器的响应性能。
/* 以下是本项目中曲线控制信号生成的部分代码截取,仅供参考 */
/**
曲线参数结构体
**/
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);
}
}
}
曲线观测界面
用于接收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();
}
说明:上面列举的所有代码,都是项目中的部分代码截取,也是所对应界面的核心部分代码,能够理解的话,重新编写自己的功能代码应该不难。完整的项目代码也在上方开源了,欢迎下载。