294 lines
12 KiB
C++
294 lines
12 KiB
C++
#include "chassis.h"
|
||
#include "titlebar.h"
|
||
#include "statusstrip.h"
|
||
#include "alarmbanner.h"
|
||
#include "datacard.h"
|
||
#include "paramcontrol.h"
|
||
#include "car.h"
|
||
#include "confirmdialog.h"
|
||
#include <QVBoxLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QPushButton>
|
||
#include <QTimer>
|
||
#include "global.h"
|
||
|
||
namespace {
|
||
// 收起姿态取全零:任一关节角绝对值超过这个角度即认为机械臂已展开。
|
||
// 单位与 CAN 下发的关节角一致(car.cpp 直接写入 t.js,未经缩放)。
|
||
const double kArmStowedTolDeg = 5.0;
|
||
}
|
||
|
||
ChassisPage::ChassisPage(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(0); // 各段间距改用显式 addSpacing,便于单独调整(底盘列位置见各段注释)
|
||
|
||
// 左控制列(垂直居中 + 平均分布)
|
||
QWidget* left = new QWidget(this);
|
||
QVBoxLayout* leftLay = new QVBoxLayout(left);
|
||
leftLay->setContentsMargins(20, 20, 0, 0);
|
||
leftLay->setSpacing(30);
|
||
|
||
QPushButton* fwd = makeDirButton("前进", "fwd");
|
||
QPushButton* back = makeDirButton("后退", "back");
|
||
QPushButton* leftT = makeDirButton("左转向", "left");
|
||
QPushButton* rightT = makeDirButton("右转向", "right");
|
||
m_dirButtons << fwd << back << leftT << rightT;
|
||
|
||
leftLay->addStretch(1);
|
||
leftLay->addWidget(fwd);
|
||
leftLay->addStretch(1);
|
||
leftLay->addWidget(back);
|
||
leftLay->addStretch(1);
|
||
leftLay->addWidget(leftT);
|
||
leftLay->addStretch(1);
|
||
leftLay->addWidget(rightT);
|
||
leftLay->addStretch(1);
|
||
left->setFixedWidth(180);
|
||
main->addWidget(left);
|
||
main->addSpacing(32);
|
||
|
||
// 中央卡片区(洗消/机械臂/气体检测仪)
|
||
QWidget* center = new QWidget(this);
|
||
QVBoxLayout* centerLay = new QVBoxLayout(center);
|
||
centerLay->setContentsMargins(0, 0, 0, 0);
|
||
centerLay->setSpacing(12);
|
||
|
||
m_washCard = new DataCard("洗消", center);
|
||
m_washCard->addField("A组分/清水液位", "--");
|
||
m_washCard->addField("泡沫箱液位", "--");
|
||
|
||
m_armCard = new DataCard("机械臂", center);
|
||
m_armCard->addField("末端位置", "--");
|
||
m_armCard->addField("末端姿态", "--");
|
||
|
||
m_gasCard = new DataCard("气体检测仪", center);
|
||
m_gasCard->addField("主机状态", "--");
|
||
m_gasCard->addField("有毒物种类", "--");
|
||
m_gasCard->addField("有毒物等级", "--");
|
||
|
||
centerLay->addWidget(m_washCard, 1);
|
||
centerLay->addWidget(m_armCard, 1);
|
||
centerLay->addWidget(m_gasCard, 1);
|
||
// 中间区域由「卡片列」与「底盘状态列」平均分配(两列拉伸因子都为 1)
|
||
main->addWidget(center, 1);
|
||
main->addSpacing(47); // 中间卡片列 ↔ 底盘列:由 32 加大到 47
|
||
|
||
// 底盘状态列
|
||
QWidget* chassisCol = new QWidget(this);
|
||
QVBoxLayout* chassisLay = new QVBoxLayout(chassisCol);
|
||
chassisLay->setContentsMargins(0, 0, 0, 0);
|
||
chassisLay->setSpacing(12);
|
||
|
||
m_chassisCard = new DataCard("底盘状态", chassisCol);
|
||
m_chassisCard->addField("底盘电量SOC", "--");
|
||
m_chassisCard->addField("底盘电压U", "--");
|
||
m_chassisCard->addField("底盘车速V", "--");
|
||
m_chassisCard->addField("档位状态", "--");
|
||
m_chassisCard->addField("驻车状态", "--");
|
||
m_chassisCard->addField("四驱模式", "--");
|
||
m_chassisCard->addField("底盘Ready状态", "--");
|
||
m_chassisCard->addField("方向盘角度", "--");
|
||
m_chassisCard->addField("角度对中状态", "--");
|
||
chassisLay->addWidget(m_chassisCard, 1);
|
||
main->addWidget(chassisCol, 1); // 与中间卡片列平均分配中间区域
|
||
main->addSpacing(17); // 底盘列 ↔ 车速设置列:由 32 减小到 17(底盘列整体右移 15px)
|
||
|
||
// 车速设置列(垂直居中)
|
||
QWidget* right = new QWidget(this);
|
||
QVBoxLayout* rightLay = new QVBoxLayout(right);
|
||
rightLay->setContentsMargins(0, 0, 0, 0);
|
||
rightLay->setSpacing(12);
|
||
|
||
double speed = ctx->getDouble(ctx->keySpeed(), 4.5);
|
||
// 协议 Vehi_TargetSpd:精度 0.01、0~500 → 物理范围 0~5.00 km/h
|
||
// 每档 = 显示精度的最小单位(1 位小数 → 0.1),比协议的 0.01 粗,够用
|
||
m_speed = new ParamControl("车速设置", "km/h", 0.1, 0.0, 5.0, false, false, right);
|
||
m_speed->setValue(speed);
|
||
m_speed->setStyleSheet("ParamControl{background:transparent;}"); // 只作用于本控件,避免影响子控件
|
||
connect(m_speed, &ParamControl::valueChanged, this, &ChassisPage::onSpeedChanged);
|
||
rightLay->addStretch(1);
|
||
rightLay->addWidget(m_speed);
|
||
rightLay->addStretch(1);
|
||
right->setFixedWidth(380);
|
||
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);
|
||
|
||
// 车速自动保存(1s 防抖)
|
||
m_saveTimer = new QTimer(this);
|
||
m_saveTimer->setSingleShot(true);
|
||
m_saveTimer->setInterval(1000);
|
||
connect(m_saveTimer, &QTimer::timeout, this, &ChassisPage::onSpeedSaved);
|
||
|
||
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &ChassisPage::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() && !m_heldDirs.isEmpty())
|
||
sendDirCommand();
|
||
});
|
||
updateModeGating();
|
||
refresh();
|
||
}
|
||
|
||
void ChassisPage::showEvent(QShowEvent* ev)
|
||
{
|
||
BasePage::showEvent(ev);
|
||
// 每次进入底盘页自动切换到行车模式;遥控器在线时模式由遥控器报文决定,不抢
|
||
if (!ctx()->rc()->linkOk())
|
||
ctx()->setMode(Mode::Drive);
|
||
}
|
||
|
||
QPushButton* ChassisPage::makeDirButton(const QString& text, const QString& dir)
|
||
{
|
||
QPushButton* b = button(text, "", this);
|
||
b->setFixedSize(140, 100);
|
||
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, dir]() {
|
||
m_heldDirs.insert(dir);
|
||
sendDirCommand();
|
||
updateAlarm();
|
||
});
|
||
connect(b, &QPushButton::released, this, [this, dir]() {
|
||
m_heldDirs.remove(dir);
|
||
sendDirCommand();
|
||
updateAlarm();
|
||
});
|
||
return b;
|
||
}
|
||
|
||
// 同时按住前进/后退与左/右转向时一并下发;两者都松开才停车
|
||
void ChassisPage::sendDirCommand()
|
||
{
|
||
const QString dir = m_heldDirs.contains("fwd") ? "fwd"
|
||
: m_heldDirs.contains("back") ? "back" : QString("stop");
|
||
const QString turn = m_heldDirs.contains("right") ? "right"
|
||
: m_heldDirs.contains("left") ? "left" : QString();
|
||
|
||
if (dir == "stop" && turn.isEmpty()) {
|
||
ctx()->comm()->sendChassisMove("stop", 0);
|
||
return;
|
||
}
|
||
// 仅转向(档位回中)时发 stop + turn,由车载端决定是否原地转向
|
||
const double spd = (dir == "stop") ? 0.0 : ctx()->getDouble(ctx()->keySpeed(), 4.5);
|
||
ctx()->comm()->sendChassisMove(dir, spd, turn);
|
||
}
|
||
|
||
void ChassisPage::updateModeGating()
|
||
{
|
||
// 现在只有**急停**会禁用方向按钮:模式不再限制(进页面即用),遥控器也不再让位——
|
||
// 面板和遥控器可以同时操作,两边各发各的,由车端处理。
|
||
const bool ok = !ctx()->estopTriggered;
|
||
for (QPushButton* b : m_dirButtons)
|
||
b->setEnabled(ok);
|
||
|
||
// 急停把按钮禁掉时清掉按住状态并补一条 stop。(Qt 禁用时会补发 released,
|
||
// 各按钮自己那一遍已经发过了,这里只是兜底。)
|
||
if (!ok && !m_heldDirs.isEmpty()) {
|
||
m_heldDirs.clear();
|
||
ctx()->comm()->sendChassisMove("stop", 0);
|
||
}
|
||
|
||
updateAlarm();
|
||
}
|
||
|
||
// 任一关节角偏离收起姿态(全零)即认为机械臂已展开
|
||
bool ChassisPage::armNotStowed() const
|
||
{
|
||
const QVector<double>& js = ctx()->telemetry.js;
|
||
for (double a : js)
|
||
if (a > kArmStowedTolDeg || a < -kArmStowedTolDeg)
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
// 面板方向键按住,或遥控器正在推杆,都算正在驱动底盘
|
||
bool ChassisPage::chassisDriving() const
|
||
{
|
||
return !m_heldDirs.isEmpty() || ctx()->rcDriving();
|
||
}
|
||
|
||
// 通信异常优先;其次是“机械臂已展开且正在驱动底盘”
|
||
void ChassisPage::updateAlarm()
|
||
{
|
||
if (!ctx()->telemetry.commOk)
|
||
m_banner->showAlarm("通信异常");
|
||
else if (armNotStowed() && chassisDriving())
|
||
m_banner->showAlarm("机械臂未回位,请谨慎操作");
|
||
else
|
||
m_banner->clear();
|
||
}
|
||
|
||
void ChassisPage::refresh()
|
||
{
|
||
const Telemetry& t = ctx()->telemetry;
|
||
|
||
updateAlarm();
|
||
|
||
// 未收到对应报文的数值统一显示 "--"(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->setField(2, dashIf(t.hasSpeed, QString("%1 km/h").arg(t.speed, 0, 'f', 2)));
|
||
m_chassisCard->setField(3, dashIf(t.hasChassis4, gearLabel(t.gearPos)));
|
||
m_chassisCard->setField(4, dashIf(t.hasChassis4, parkingLabel(t.parking)));
|
||
m_chassisCard->setField(5, dashIf(t.hasChassis4, awdLabel(t.awdMode)));
|
||
m_chassisCard->setField(6, dashIf(t.hasSpeed, rdyLabel(t.rdyStatus)));
|
||
m_chassisCard->setField(7, dashIf(t.hasChassis4, QString("%1°").arg(t.steerAngle, 0, 'f', 0)));
|
||
m_chassisCard->setField(8, dashIf(t.hasChassis4, alignLabel(t.alignStatus)));
|
||
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_armCard->setField(0, t.pos);
|
||
m_armCard->setField(1, t.pose);
|
||
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);
|
||
}
|
||
|
||
void ChassisPage::onSpeedChanged(double v)
|
||
{
|
||
// 1s 防抖后自动保存
|
||
m_saveTimer->start();
|
||
ctx()->comm()->sendSetParam(ctx()->keySpeed(), v);
|
||
}
|
||
|
||
void ChassisPage::onSpeedSaved()
|
||
{
|
||
ctx()->setDouble(ctx()->keySpeed(), m_speed->value());
|
||
ConfirmDialog::toast(this, "车速已保存");
|
||
}
|