Files
QT_wuhan_YT/pages/chassis.cpp
T
2026-10-16 09:06:03 +08:00

294 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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, "车速已保存");
}