266 lines
11 KiB
C++
266 lines
11 KiB
C++
|
||
#include "arm.h"
|
||
#include "titlebar.h"
|
||
#include "statusstrip.h"
|
||
#include "alarmbanner.h"
|
||
#include "datacard.h"
|
||
#include "car.h"
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
//#include <QGridLayout> // 末端按钮原为 2×3 网格,改为竖列后暂不需要
|
||
#include <QPushButton>
|
||
#include <QLabel>
|
||
#include <QTimer>
|
||
#include "global.h"
|
||
|
||
ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
|
||
: BasePage(ctx, parent)
|
||
{
|
||
QVBoxLayout* root = new QVBoxLayout(this);
|
||
root->setContentsMargins(0, 0, 0, 0);
|
||
root->setSpacing(0);
|
||
|
||
m_titleBar = new TitleBar(ctx, this);
|
||
m_titleBar->setTitle("机械臂");
|
||
root->addWidget(m_titleBar);
|
||
|
||
m_banner = new AlarmBanner(this);
|
||
root->addWidget(m_banner);
|
||
|
||
QHBoxLayout* main = new QHBoxLayout();
|
||
main->setContentsMargins(36, 16, 36, 16);
|
||
main->setSpacing(32);
|
||
|
||
// 左控制列(6 运动按钮 + 系统回位/暂停)
|
||
QWidget* left = new QWidget(this);
|
||
QVBoxLayout* leftLay = new QVBoxLayout(left);
|
||
leftLay->setContentsMargins(10,10,20,10);
|
||
leftLay->setSpacing(30);
|
||
|
||
auto addMove = [&](const QString& t, const QString& axis, int dir) {
|
||
QPushButton* b = makeMoveButton(t, axis, dir);
|
||
m_moveButtons << b;
|
||
leftLay->addWidget(b);
|
||
};
|
||
addMove("左平移", "x", -1);
|
||
addMove("右平移", "x", +1);
|
||
addMove("上升", "z", +1);
|
||
addMove("下降", "z", -1);
|
||
addMove("前平移", "y", +1);
|
||
addMove("后平移", "y", -1);
|
||
|
||
m_returnBtn = button("系统回位/\n暂停", "", left);
|
||
m_returnBtn->setCheckable(true);
|
||
m_returnBtn->setFixedSize(270, 120); // 两行文字,故加高
|
||
m_returnBtn->setStyleSheet(QString(
|
||
"QPushButton{font-size:40px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;}"
|
||
"QPushButton:hover,QPushButton:focus,QPushButton:checked"
|
||
"{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}")
|
||
.arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY));
|
||
connect(m_returnBtn, &QPushButton::toggled, this, &ArmPage::onReturnToggled);
|
||
leftLay->addWidget(m_returnBtn);
|
||
|
||
m_returnStatus = label("", 18, C::TEXT_SUB, false, left);
|
||
m_returnStatus->setAlignment(Qt::AlignCenter);
|
||
m_returnStatus->setWordWrap(true);
|
||
leftLay->addWidget(m_returnStatus);
|
||
|
||
leftLay->addStretch(1);
|
||
left->setFixedWidth(300); // 270 按钮 + 左右边距 30,避免按钮被裁
|
||
main->addWidget(left);
|
||
|
||
// 中央区:左侧三张状态卡 + 机械臂参数卡,并排放在中间
|
||
QWidget* center = new QWidget(this);
|
||
QHBoxLayout* centerLay = new QHBoxLayout(center);
|
||
centerLay->setContentsMargins(0, 0, 0, 0);
|
||
centerLay->setSpacing(12);
|
||
|
||
QWidget* cardsCol = new QWidget(center);
|
||
QVBoxLayout* cardsLay = new QVBoxLayout(cardsCol);
|
||
cardsLay->setContentsMargins(0, 0, 0, 0);
|
||
cardsLay->setSpacing(12);
|
||
|
||
m_chassisCard = new DataCard("底盘", cardsCol);
|
||
m_chassisCard->addField("底盘电量SOC", "--");
|
||
m_chassisCard->addField("底盘电压", "--");
|
||
|
||
m_washCard = new DataCard("洗消", cardsCol);
|
||
m_washCard->addField("A组分/清水液位", "--");
|
||
m_washCard->addField("泡沫箱液位", "--");
|
||
|
||
m_gasCard = new DataCard("气体检测仪", cardsCol);
|
||
m_gasCard->addField("主机状态", "--");
|
||
m_gasCard->addField("有毒物种类", "--");
|
||
m_gasCard->addField("有毒物等级", "--");
|
||
|
||
cardsLay->addWidget(m_chassisCard, 1);
|
||
cardsLay->addWidget(m_washCard, 1);
|
||
cardsLay->addWidget(m_gasCard, 1);
|
||
|
||
m_armCard = new DataCard("机械臂参数", center);
|
||
m_armCard->addField("末端位置", "--");
|
||
m_armCard->addField("末端姿态", "--");
|
||
m_armCard->addField("末端速度", "--");
|
||
m_armCard->addField("Joint1关节角", "--");
|
||
m_armCard->addField("Joint2关节角", "--");
|
||
m_armCard->addField("Joint3关节角", "--");
|
||
m_armCard->addField("Joint4关节角", "--");
|
||
m_armCard->addField("Joint5关节角", "--");
|
||
m_armCard->addField("Joint6关节角", "--");
|
||
|
||
centerLay->addWidget(cardsCol, 1);
|
||
centerLay->addWidget(m_armCard, 1);
|
||
main->addWidget(center, 1);
|
||
|
||
// 右控制列(末端 6 姿态按钮,与左侧按钮列对称)
|
||
QWidget* right = new QWidget(this);
|
||
QVBoxLayout* rightLay = new QVBoxLayout(right);
|
||
rightLay->setContentsMargins(10, 20, 20, 20);
|
||
rightLay->setSpacing(15);
|
||
|
||
auto addEnd = [&](const QString& t, const QString& axis, int dir) {
|
||
QPushButton* b = makeMoveButton(t, axis, dir);
|
||
m_endButtons << b;
|
||
rightLay->addWidget(b);
|
||
};
|
||
addEnd("末端仰", "pitch", +1);
|
||
addEnd("末端俯", "pitch", -1);
|
||
addEnd("末端正偏航", "yaw", +1);
|
||
addEnd("末端负偏航", "yaw", -1);
|
||
addEnd("末端左横滚", "roll", -1);
|
||
addEnd("末端右横滚", "roll", +1);
|
||
|
||
right->setFixedWidth(300); // 与左列同宽,按钮 270 不被裁
|
||
main->addWidget(right);
|
||
|
||
root->addLayout(main, 1);
|
||
|
||
m_statusStrip = new StatusStrip(ctx, this);
|
||
connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() { emit navigate(PageJob); });
|
||
root->addWidget(m_statusStrip);
|
||
|
||
m_statusTimer = new QTimer(this);
|
||
m_statusTimer->setSingleShot(true);
|
||
m_statusTimer->setInterval(3000);
|
||
connect(m_statusTimer, &QTimer::timeout, this, [this]() {
|
||
m_returnStatus->setText("");
|
||
});
|
||
|
||
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &ArmPage::refresh);
|
||
connect(ctx, &AppContext::modeChanged, this, [this](Mode) { updateModeGating(); });
|
||
connect(ctx, &AppContext::estopChanged, this, [this](bool) { updateModeGating(); });
|
||
connect(ctx, &AppContext::rcDrivingChanged, this, [this](bool) {
|
||
updateModeGating();
|
||
// 同底盘页:让位给遥控器期间按下的轴被忽略,遥控器松手后把按着的键重发一遍。
|
||
// 注意写 this->ctx():构造函数形参也叫 ctx,直接写 ctx() 会被它遮住
|
||
if (this->ctx()->rcDriving()) return;
|
||
for (QPushButton* b : m_moveButtons)
|
||
if (b->isDown())
|
||
this->ctx()->comm()->sendArmMove(b->property("armAxis").toString(),
|
||
b->property("armDir").toInt());
|
||
});
|
||
updateModeGating();
|
||
refresh();
|
||
}
|
||
|
||
void ArmPage::showEvent(QShowEvent* ev)
|
||
{
|
||
BasePage::showEvent(ev);
|
||
// 每次进入机械臂页自动切到机械臂模式:Arm_ControlCmd1 的 Arm_modeCmd/Arm_enable
|
||
// 挂在它上面,不切的话车端会忽略这里发出的动作指令。遥控器在线时模式由遥控器报文
|
||
// 决定,不抢——和底盘页进页面切行车模式是同一套处理。
|
||
if (!ctx()->rc()->linkOk())
|
||
ctx()->setMode(Mode::Arm);
|
||
}
|
||
|
||
QPushButton* ArmPage::makeMoveButton(const QString& text, const QString& axis, int dir)
|
||
{
|
||
QPushButton* b = button(text, "", this);
|
||
b->setFixedSize(270, 76);
|
||
// 记下这根按钮管哪个轴/往哪边,好让"遥控器松手后重发按住的键"找得到它
|
||
b->setProperty("armAxis", axis);
|
||
b->setProperty("armDir", dir);
|
||
b->setStyleSheet(QString(
|
||
"QPushButton{font-size:45px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;}"
|
||
"QPushButton:hover,QPushButton:focus,QPushButton:pressed"
|
||
"{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}")
|
||
.arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY));
|
||
connect(b, &QPushButton::pressed, this, [this, axis, dir]() {
|
||
ctx()->comm()->sendArmMove(axis, dir);
|
||
});
|
||
connect(b, &QPushButton::released, this, [this, axis]() {
|
||
ctx()->comm()->sendArmMove(axis, 0);
|
||
});
|
||
return b;
|
||
}
|
||
|
||
void ArmPage::updateModeGating()
|
||
{
|
||
// 和底盘页一样:现在只有**急停**会禁用动作按钮。模式不再限制(进页面即用),
|
||
// 遥控器也不再让位——面板和遥控器可以同时操作,两边各发各的。
|
||
const bool ok = !ctx()->estopTriggered;
|
||
for (QPushButton* b : m_moveButtons) b->setEnabled(ok);
|
||
for (QPushButton* b : m_endButtons) b->setEnabled(ok);
|
||
}
|
||
|
||
void ArmPage::onReturnToggled(bool on)
|
||
{
|
||
if (on) ctx()->comm()->sendArmReturn();
|
||
else ctx()->comm()->sendArmPause();
|
||
}
|
||
|
||
void ArmPage::refresh()
|
||
{
|
||
const Telemetry& t = ctx()->telemetry;
|
||
|
||
if (!t.commOk) {
|
||
m_banner->showAlarm("通信异常");
|
||
} else {
|
||
m_banner->clear();
|
||
}
|
||
|
||
// 未收到对应报文的数值统一显示 "--"(dashIf)
|
||
m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
|
||
m_chassisCard->setField(1, dashIf(t.hasBattery, QString("%1V").arg(t.volt, 0, 'f', 0)));
|
||
m_chassisCard->setFaults(t.chassisFaults);
|
||
|
||
m_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0)));
|
||
m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0)));
|
||
m_washCard->setAlarm(0, t.levelALow);
|
||
m_washCard->setAlarm(1, t.levelBLow);
|
||
m_washCard->setFaults(washCardFaults(t));
|
||
|
||
m_gasCard->setField(0, gasHostLabel(t));
|
||
m_gasCard->setField(1, t.gasType); // 有毒物种类,未收到报文时本身就是 "--"
|
||
m_gasCard->setField(2, t.gasLevel); // 有毒物等级
|
||
m_gasCard->setAlarm(0, t.gasHost == "fault");
|
||
m_gasCard->setFaults(t.detectorFaults);
|
||
|
||
// 本卡没有故障行:机械臂那四帧只有位置/姿态/关节角,协议里没有机械臂故障信号
|
||
m_armCard->setField(0, t.pos);
|
||
m_armCard->setField(1, t.pose);
|
||
m_armCard->setField(2, dashIf(t.hasArmPose, QString("%1").arg(t.vel, 0, 'f', 1)));
|
||
for (int i = 0; i < 6 && i < t.js.size(); ++i)
|
||
m_armCard->setField(3 + i, dashIf(t.hasJoints, QString("%1°").arg(t.js[i], 0, 'f', 0)));
|
||
|
||
// 回位状态机
|
||
if (t.returnState != m_prevReturnState) {
|
||
m_prevReturnState = t.returnState;
|
||
if (t.returnState == "done") {
|
||
m_returnBtn->setChecked(false);
|
||
m_returnStatus->setText("机械臂系统完成回位");
|
||
m_returnStatus->setStyleSheet(QString("color:%1;font-size:22px;font-weight:700;").arg(C::SUCCESS));
|
||
m_statusTimer->start();
|
||
} else if (t.returnState == "fault") {
|
||
m_returnBtn->setChecked(false);
|
||
m_returnStatus->setText("机械臂系统回位异常");
|
||
m_returnStatus->setStyleSheet(QString("color:%1;font-size:22px;font-weight:700;").arg(C::DANGER));
|
||
} else if (t.returnState == "returning") {
|
||
m_returnStatus->setText("回位中…");
|
||
m_returnStatus->setStyleSheet(QString("color:%1;font-size:22px;font-weight:700;").arg(C::WARNING));
|
||
} else {
|
||
m_returnStatus->setText("");
|
||
}
|
||
}
|
||
}
|