增加通讯协议

This commit is contained in:
xu
2026-10-14 09:59:42 +08:00
parent 32878a9a80
commit 950e9e6c9e
102 changed files with 5538 additions and 2679 deletions
+74 -58
View File
@@ -1,13 +1,14 @@
#include "arm.h"
#include "titlebar.h"
#include "statusstrip.h"
#include "alarmbanner.h"
#include "datacard.h"
#include "udpcommunication.h"
#include "car.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
//#include <QGridLayout> // 末端按钮原为 2×3 网格,改为竖列后暂不需要
#include <QPushButton>
#include <QLabel>
#include <QTimer>
@@ -34,8 +35,8 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
// 左控制列(6 运动按钮 + 系统回位/暂停)
QWidget* left = new QWidget(this);
QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(0, 0, 0, 0);
leftLay->setSpacing(8);
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);
@@ -49,11 +50,14 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
addMove("前平移", "y", +1);
addMove("后平移", "y", -1);
m_returnBtn = button("系统回位/暂停", "", left);
m_returnBtn = button("系统回位/\n暂停", "", left);
m_returnBtn->setCheckable(true);
m_returnBtn->setFixedSize(180, 90);
m_returnBtn->setStyleSheet(QString("font-size:22px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;")
.arg(C::PANEL_HI, C::TEXT, C::BORDER));
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);
@@ -63,69 +67,72 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
leftLay->addWidget(m_returnStatus);
leftLay->addStretch(1);
left->setFixedWidth(180);
left->setFixedWidth(300); // 270 按钮 + 左右边距 30,避免按钮被裁
main->addWidget(left);
// 中央卡片区(底盘/洗消/气体检测仪)
// 中央区:左侧三张状态卡 + 机械臂参数卡,并排放在中间
QWidget* center = new QWidget(this);
QVBoxLayout* centerLay = new QVBoxLayout(center);
QHBoxLayout* centerLay = new QHBoxLayout(center);
centerLay->setContentsMargins(0, 0, 0, 0);
centerLay->setSpacing(12);
m_chassisCard = new DataCard("底盘", center);
m_chassisCard->addField("电量", "85%");
m_chassisCard->addField("电压", "360V");
m_chassisCard->addField("故障", "无");
QWidget* cardsCol = new QWidget(center);
QVBoxLayout* cardsLay = new QVBoxLayout(cardsCol);
cardsLay->setContentsMargins(0, 0, 0, 0);
cardsLay->setSpacing(12);
m_washCard = new DataCard("洗消", center);
m_washCard->addField("A液位", "100L");
m_washCard->addField("泡沫液位", "30L");
m_chassisCard = new DataCard("底盘", cardsCol);
m_chassisCard->addField("底盘电量SOC", "85%");
m_chassisCard->addField("底盘电压", "360V");
m_chassisCard->addField("底盘故障", "无");
m_gasCard = new DataCard("气体检测仪", center);
m_gasCard->addField("主机状态", "待机");
m_gasCard->addField("浓度", "0 ppb");
m_washCard = new DataCard("洗消", cardsCol);
m_washCard->addField("A组分/清水液位", "100L");
m_washCard->addField("泡沫箱液位", "30L");
centerLay->addWidget(m_chassisCard, 1);
centerLay->addWidget(m_washCard, 1);
centerLay->addWidget(m_gasCard, 1);
m_gasCard = new DataCard("气体检测仪", cardsCol);
m_gasCard->addField("主机状态", "点火状态");
m_gasCard->addField("有毒物种类", "S级");
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("末端位置", "XYZ");
m_armCard->addField("末端姿态", "RX/RY/RZ");
m_armCard->addField("末端速度", "0");
m_armCard->addField("Joint1关节角", "30°");
m_armCard->addField("Joint2关节角", "20°");
m_armCard->addField("Joint3关节角", "0°");
m_armCard->addField("Joint4关节角", "90°");
m_armCard->addField("Joint5关节角", "45°");
m_armCard->addField("Joint6关节角", "0°");
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(0, 0, 0, 0);
rightLay->setSpacing(12);
rightLay->setContentsMargins(10, 20, 20, 20);
rightLay->setSpacing(15);
m_armCard = new DataCard("机械臂参数", right);
m_armCard->addField("末端位置", "XYZ");
m_armCard->addField("姿态", "RX/RY/RZ");
m_armCard->addField("速度", "0");
m_armCard->addField("J1", "30°");
m_armCard->addField("J2", "20°");
m_armCard->addField("J3", "0°");
m_armCard->addField("J4", "90°");
m_armCard->addField("J5", "45°");
m_armCard->addField("J6", "0°");
rightLay->addWidget(m_armCard, 1);
// 末端 6 姿态按钮(2 列 × 3 行)
QGridLayout* endGrid = new QGridLayout();
endGrid->setSpacing(8);
auto addEnd = [&](const QString& t, const QString& axis, int dir, int r, int c) {
auto addEnd = [&](const QString& t, const QString& axis, int dir) {
QPushButton* b = makeMoveButton(t, axis, dir);
b->setFixedHeight(80);
m_endButtons << b;
endGrid->addWidget(b, r, c);
rightLay->addWidget(b);
};
addEnd("末端仰", "pitch", +1, 0, 0);
addEnd("末端俯", "pitch", -1, 0, 1);
addEnd("末端正偏航", "yaw", +1, 1, 0);
addEnd("末端负偏航", "yaw", -1, 1, 1);
addEnd("末端左横滚", "roll", -1, 2, 0);
addEnd("末端右横滚", "roll", +1, 2, 1);
rightLay->addLayout(endGrid);
addEnd("末端仰", "pitch", +1);
addEnd("末端俯", "pitch", -1);
addEnd("末端正偏航", "yaw", +1);
addEnd("末端负偏航", "yaw", -1);
addEnd("末端左横滚", "roll", -1);
addEnd("末端右横滚", "roll", +1);
right->setFixedWidth(430);
right->setFixedWidth(300); // 与左列同宽,按钮 270 不被裁
main->addWidget(right);
root->addLayout(main, 1);
@@ -141,9 +148,10 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
m_returnStatus->setText("");
});
connect(ctx->comm(), &UdpCommunication::telemetryUpdated, this, &ArmPage::refresh);
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(); });
updateModeGating();
refresh();
}
@@ -151,10 +159,17 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
QPushButton* ArmPage::makeMoveButton(const QString& text, const QString& axis, int dir)
{
QPushButton* b = button(text, "", this);
b->setFixedSize(180, 90);
b->setStyleSheet(QString("font-size:24px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;")
.arg(C::PANEL_HI, C::TEXT, C::BORDER));
b->setFixedSize(270, 76);
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]() {
if (ctx()->rcDriving()) {
ConfirmDialog::toast(this, "遥控器操作中");
return;
}
if (ctx()->mode != Mode::Arm) {
ConfirmDialog::toast(this, "请切换至机械臂模式");
return;
@@ -169,7 +184,8 @@ QPushButton* ArmPage::makeMoveButton(const QString& text, const QString& axis, i
void ArmPage::updateModeGating()
{
bool arm = (ctx()->mode == Mode::Arm) && !ctx()->estopTriggered;
// 非机械臂模式、急停触发或遥控器正在操作时禁用动作按钮(遥控器优先)
bool arm = (ctx()->mode == Mode::Arm) && !ctx()->estopTriggered && !ctx()->rcDriving();
for (QPushButton* b : m_moveButtons) b->setEnabled(arm);
for (QPushButton* b : m_endButtons) b->setEnabled(arm);
}
+81 -24
View File
@@ -4,7 +4,7 @@
#include "alarmbanner.h"
#include "datacard.h"
#include "paramcontrol.h"
#include "udpcommunication.h"
#include "car.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -29,13 +29,13 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
QHBoxLayout* main = new QHBoxLayout();
main->setContentsMargins(36, 16, 36, 16);
main->setSpacing(32);
main->setSpacing(0); // 各段间距改用显式 addSpacing,便于单独调整(底盘列位置见各段注释)
// 左控制列(垂直居中 + 平均分布)
QWidget* left = new QWidget(this);
QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(0, 0, 0, 0);
leftLay->setSpacing(8);
leftLay->setContentsMargins(20, 20, 0, 0);
leftLay->setSpacing(30);
QPushButton* fwd = makeDirButton("前进", "fwd");
QPushButton* back = makeDirButton("后退", "back");
@@ -54,6 +54,7 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
leftLay->addStretch(1);
left->setFixedWidth(180);
main->addWidget(left);
main->addSpacing(32);
// 中央卡片区(洗消/机械臂/气体检测仪)
QWidget* center = new QWidget(this);
@@ -76,36 +77,49 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
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* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(0, 0, 0, 0);
rightLay->setSpacing(12);
// 底盘状态列
QWidget* chassisCol = new QWidget(this);
QVBoxLayout* chassisLay = new QVBoxLayout(chassisCol);
chassisLay->setContentsMargins(0, 0, 0, 0);
chassisLay->setSpacing(12);
m_chassisCard = new DataCard("底盘状态", right);
m_chassisCard = new DataCard("底盘状态", chassisCol);
m_chassisCard->addField("电量SOC", "85%");
m_chassisCard->addField("电压U", "360V");
m_chassisCard->addField("车速V", "3.45 km/h");
m_chassisCard->addField("手刹", "有效");
m_chassisFieldFault = m_chassisCard->addField("故障", "无");
rightLay->addWidget(m_chassisCard, 1);
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);
m_speed = new ParamControl("车速设置", "km/h", 0.5, 0.0, 10.0, false, false, right);
// 协议 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("background:transparent;");
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);
m_statusStrip->setModeVisible(false); // 本页固定行车模式,底部不显示模式徽标
connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() { emit navigate(PageJob); });
root->addWidget(m_statusStrip);
@@ -115,38 +129,81 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
m_saveTimer->setInterval(1000);
connect(m_saveTimer, &QTimer::timeout, this, &ChassisPage::onSpeedSaved);
connect(ctx->comm(), &UdpCommunication::telemetryUpdated, this, &ChassisPage::refresh);
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(); });
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(180, 150);
b->setStyleSheet(QString("font-size:26px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;")
.arg(C::PANEL_HI, C::TEXT, C::BORDER));
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]() {
if (ctx()->rcDriving()) {
ConfirmDialog::toast(this, "遥控器操作中");
return;
}
if (ctx()->mode != Mode::Drive) {
ConfirmDialog::toast(this, "请切换至行车模式");
return;
}
ctx()->comm()->sendChassisMove(dir, ctx()->getDouble(ctx()->keySpeed(), 4.5));
m_heldDirs.insert(dir);
sendDirCommand();
});
connect(b, &QPushButton::released, this, [this]() {
ctx()->comm()->sendChassisMove("stop", 0);
connect(b, &QPushButton::released, this, [this, dir]() {
m_heldDirs.remove(dir);
sendDirCommand();
});
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()
{
// 非行车模式或急停触发时禁用方向按钮
bool drive = (ctx()->mode == Mode::Drive) && !ctx()->estopTriggered;
// 非行车模式、急停触发或遥控器正在操作时禁用方向按钮(遥控器优先)
const bool drive = (ctx()->mode == Mode::Drive) && !ctx()->estopTriggered && !ctx()->rcDriving();
for (QPushButton* b : m_dirButtons)
b->setEnabled(drive);
// 让位时清掉本地按住状态:按钮被禁用后不会再收到 released,否则方向会被一直挂住。
// 让位给遥控器时不补发 stop,否则会覆盖遥控器刚下发的那条指令。
if (!drive && !m_heldDirs.isEmpty()) {
m_heldDirs.clear();
if (!ctx()->rcDriving())
ctx()->comm()->sendChassisMove("stop", 0);
}
}
void ChassisPage::refresh()
+6
View File
@@ -3,6 +3,7 @@
#include "base.h"
#include <QVector>
#include <QSet>
class TitleBar;
class StatusStrip;
@@ -19,6 +20,9 @@ class ChassisPage : public BasePage
public:
explicit ChassisPage(AppContext* ctx, QWidget* parent = nullptr);
protected:
void showEvent(QShowEvent* ev) override; // 进入页面即切至行车模式
private slots:
void refresh();
void updateModeGating();
@@ -27,6 +31,7 @@ private slots:
private:
QPushButton* makeDirButton(const QString& text, const QString& dir);
void sendDirCommand(); // 按当前按住的方向组合下发(档位与转向可同时)
TitleBar* m_titleBar = nullptr;
StatusStrip* m_statusStrip = nullptr;
@@ -39,6 +44,7 @@ private:
QTimer* m_saveTimer = nullptr;
int m_chassisFieldFault = -1;
QVector<QPushButton*> m_dirButtons;
QSet<QString> m_heldDirs; // 当前按住的方向,可多键同时按
};
#endif // CHASSISPAGE_H
+142 -68
View File
@@ -4,7 +4,7 @@
#include "datacard.h"
#include "ledindicator.h"
#include "paramcontrol.h"
#include "udpcommunication.h"
#include "car.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
@@ -19,7 +19,7 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
root->setSpacing(0);
m_titleBar = new TitleBar(ctx, this);
m_titleBar->setTitle("洗消调试");
m_titleBar->setTitle("调试维护");
root->addWidget(m_titleBar);
// 警告横幅(常显红闪)
@@ -34,35 +34,38 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
// 左球阀控制区
QWidget* left = new QWidget(this);
QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(0, 0, 0, 0);
leftLay->setSpacing(16);
leftLay->setContentsMargins(20, 10, 10, 10);
leftLay->setSpacing(60);
leftLay->addWidget(makeValveGroup(1));
leftLay->addWidget(makeValveGroup(2));
leftLay->addStretch(1);
left->setFixedWidth(300);
main->addWidget(left);
// 中央卡片区
// 中央卡片区(左右两列:左列 底盘/机械臂/气体检测仪 纵向排列,右列 洗消参数)
QWidget* center = new QWidget(this);
QVBoxLayout* centerLay = new QVBoxLayout(center);
QHBoxLayout* centerLay = new QHBoxLayout(center);
centerLay->setContentsMargins(0, 0, 0, 0);
centerLay->setSpacing(10);
centerLay->setSpacing(28); // 与 main 的列间距一致
QHBoxLayout* miniRow = new QHBoxLayout();
miniRow->setSpacing(10);
m_chassisCard = new DataCard("底盘", center);
QWidget* centerLeft = new QWidget(center);
QVBoxLayout* centerLeftLay = new QVBoxLayout(centerLeft);
centerLeftLay->setContentsMargins(0, 0, 0, 0);
centerLeftLay->setSpacing(10);
m_chassisCard = new DataCard("底盘", centerLeft);
m_chassisCard->addField("电量", "85%");
m_chassisCard->addField("电压", "360V");
m_armCard = new DataCard("机械臂", center);
m_armCard = new DataCard("机械臂", centerLeft);
m_armCard->addField("末端位置", "XYZ");
m_armCard->addField("姿态", "RX/RY/RZ");
m_gasCard = new DataCard("气体检测仪", center);
m_gasCard = new DataCard("气体检测仪", centerLeft);
m_gasCard->addField("主机状态", "待机");
m_gasCard->addField("浓度", "0 ppb");
miniRow->addWidget(m_chassisCard, 1);
miniRow->addWidget(m_armCard, 1);
miniRow->addWidget(m_gasCard, 1);
centerLay->addLayout(miniRow, 1);
centerLeftLay->addWidget(m_chassisCard, 1);
centerLeftLay->addWidget(m_armCard, 1);
centerLeftLay->addWidget(m_gasCard, 1);
centerLay->addWidget(centerLeft, 1);
m_washCard = new DataCard("洗消参数", center);
m_washCard->addField("洗消模式", "--");
@@ -79,11 +82,11 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
// 右泵控制区
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(0, 0, 0, 0);
rightLay->setSpacing(8);
rightLay->addWidget(makePumpGroup("air", "空压机"));
rightLay->addWidget(makePumpGroup("plunger", "柱塞泵"));
rightLay->addWidget(makePumpGroup("leaf", "叶片泵"));
rightLay->setContentsMargins(10, 10,20,10);
rightLay->setSpacing(40);
rightLay->addWidget(makePumpGroup("air", "空压机(rpm)"), 0, Qt::AlignHCenter);
rightLay->addWidget(makePumpGroup("plunger", "柱塞泵(rpm)"), 0, Qt::AlignHCenter);
rightLay->addWidget(makePumpGroup("leaf", "叶片泵(rpm)"), 0, Qt::AlignHCenter);
rightLay->addStretch(1);
right->setFixedWidth(420);
main->addWidget(right);
@@ -92,28 +95,56 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
// 底部指示灯条
QWidget* bottom = new QWidget(this);
bottom->setObjectName("DebugBottom");
bottom->setFixedHeight(94);
bottom->setStyleSheet(QString("background-color:%1;border-top:1px solid %2;")
// 用 #DebugBottom 限定选择器:不带选择器的样式表会连子控件一起套上边框和底色
bottom->setStyleSheet(QString("QWidget#DebugBottom{background-color:%1;border-top:1px solid %2;}")
.arg(C::PANEL, C::BORDER));
QHBoxLayout* bottomLay = new QHBoxLayout(bottom);
bottomLay->setContentsMargins(36, 0, 24, 0);
// 左起 364 = 36(页面边距) + 300(球阀列) + 28(列间距),与中央“底盘”列左边缘对齐
bottomLay->setContentsMargins(364, 0, 24, 0);
bottomLay->setSpacing(48);
m_ledPlunger = new LedIndicator("柱塞泵", bottom);
m_ledAir = new LedIndicator("空压机", bottom);
m_ledLeaf = new LedIndicator("叶片泵", bottom);
m_ledLevel = new LedIndicator("液位低", bottom);
bottomLay->addWidget(m_ledPlunger);
bottomLay->addWidget(m_ledAir);
bottomLay->addWidget(m_ledLeaf);
bottomLay->addWidget(m_ledLevel);
auto makeLed = [&](const QString& text) {
LedIndicator* led = new LedIndicator(text, bottom);
led->setTextPixelSize(40); // 字号与卡片标题一致
led->setDotAfterText(true); // 圆点画在文字后面
bottomLay->addWidget(led);
return led;
};
m_ledPlunger = makeLed("柱塞泵");
m_ledAir = makeLed("空压机");
m_ledLeaf = makeLed("叶片泵");
m_ledLevel = makeLed("液位低");
bottomLay->addStretch(1);
QPushButton* back = button("返回", "", bottom);
back->setFixedSize(144, 76);
back->setFixedSize(160, 76);
back->setStyleSheet(cancelButtonStyle(40)); // 选中(悬停/按下/焦点)红底白字
connect(back, &QPushButton::clicked, this, [this]() { emit navigate(PageJob); });
bottomLay->addWidget(back);
root->addWidget(bottom);
connect(ctx->comm(), &UdpCommunication::telemetryUpdated, this, &DebugMaintainPage::refresh);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, [this]() {
m_telemetrySeen = true; // 遥测一到,球阀到位灯改由 refresh() 接管
refresh();
});
// Tab 顺序(循环):返回 → D1控制 → D2控制 → 各泵(转速输入 → 启动 → 停止) → 回到返回。
// 这些按钮在控件级样式表里写了 :checked,而控件级样式表的优先级高于全局
// QPushButton:focus,默认焦点链下容易看起来“停不上去”,故显式排一遍保证都能停住
QWidget* tabChain[] = {
m_valve1Btn, m_valve2Btn,
m_airRpm->editor(), m_airStart, m_airStop,
m_plungerRpm->editor(), m_plungerStart, m_plungerStop,
m_leafRpm->editor(), m_leafStart, m_leafStop,
};
QWidget* prev = back;
for (QWidget* w : tabChain) {
if (!w) continue;
QWidget::setTabOrder(prev, w);
prev = w;
}
QWidget::setTabOrder(prev, back); // 回到起点,形成循环
refresh();
}
@@ -127,26 +158,36 @@ QWidget* DebugMaintainPage::makeValveGroup(int valve)
lay->setContentsMargins(12, 12, 12, 12);
lay->setSpacing(8);
QLabel* title = label(QString("D%1 球阀").arg(valve), 20, C::TEXT, true, grp);
QLabel* title = label(QString("D%1 球阀").arg(valve), 30, C::TEXT, true, grp);
lay->addWidget(title);
// 控制开/关(toggle)
QPushButton* ctrl = button(QString("D%1 控制:关闭").arg(valve), "", grp);
// 控制开/关(toggle):文字固定,底色表示状态——未选中=关闭(红)、选中=开启(绿)
QPushButton* ctrl = button(QString("D%1 控制").arg(valve), "", grp);
ctrl->setCheckable(true);
ctrl->setFixedHeight(64);
connect(ctrl, &QPushButton::toggled, this, [this, valve, ctrl](bool on) {
ctrl->setText(QString("D%1 控制:%2").arg(valve).arg(on ? "开启" : "关闭"));
ctrl->setFixedSize(180, 64); // 窄按钮,与泵组的“上电”一致
// 控件级样式表优先级高于全局 QSS,故 :focus 也在这里补,否则 Tab 选中看不出变化
ctrl->setStyleSheet(QString("QPushButton{background-color:%1;color:#FFFFFF;border:none;}"
"QPushButton:checked{background-color:%2;color:#FFFFFF;border:none;}"
"QPushButton:focus{border:4px solid #8ABFFF;}")
.arg(C::DANGER).arg(C::SUCCESS));
connect(ctrl, &QPushButton::toggled, this, [this, valve](bool on) {
ctx()->comm()->sendBallValve(valve, on);
});
lay->addWidget(ctrl);
lay->addWidget(ctrl, 0, Qt::AlignHCenter);
// 开启到位 / 关闭到位
LedIndicator* open = new LedIndicator("开启到位", grp);
open->setState(LedIndicator::Off);
LedIndicator* close = new LedIndicator("关闭到位", grp);
// 开启到位 / 关闭到位(同一行,居中)。未收到车端遥测时保持缺省显示
// (开启绿 / 关闭红),首次收到遥测后由 refresh() 按实际到位状态刷新
LedIndicator* open = new LedIndicator("开启", grp);
open->setState(LedIndicator::Green);
LedIndicator* close = new LedIndicator("关闭", grp);
close->setState(LedIndicator::Red);
lay->addWidget(open);
lay->addWidget(close);
QHBoxLayout* ioRow = new QHBoxLayout();
ioRow->setSpacing(10);
ioRow->addStretch(1);
ioRow->addWidget(open);
ioRow->addWidget(close);
ioRow->addStretch(1);
lay->addLayout(ioRow);
// 故障提示
QLabel* fault = label("", 16, C::DANGER, true, grp);
@@ -175,29 +216,60 @@ QWidget* DebugMaintainPage::makePumpGroup(const QString& pump, const QString& na
grp->setProperty("card", true);
grp->setStyleSheet(QString("QWidget[card=\"true\"]{background-color:%1;border:2px solid %2;border-radius:12px;}")
.arg(C::PANEL, C::BORDER));
// 不设固定宽度:卡片按内容自适应(由最宽的“空压机(rpm)”标签决定)。
// 固定宽度一旦小于布局最小宽度,标签会被压到小于文字宽度而裁字
QVBoxLayout* lay = new QVBoxLayout(grp);
lay->setContentsMargins(12, 8, 12, 8);
lay->setSpacing(4);
lay->setContentsMargins(30, 8, 20, 8);
lay->setSpacing(10);
ParamControl* rpm = new ParamControl(name + "转速", "rpm", 10, 0, 2000, false, false, grp);
// 上限按协议各泵量程:空压机 AirComp_Spd 0-3000、柱塞泵 PlungerPump_Spd 0-1450、
// 叶片泵 VanePump__Spd 0-2000 rpm。每档 = 显示精度的最小单位,转速是整数显示故为 1 rpm
const int rpmMax = (pump == "air") ? 3000 : (pump == "plunger") ? 1450 : 2000;
ParamControl* rpm = new ParamControl(name, "", 1, 0, rpmMax, false, false, grp);
rpm->setCentered(true);
rpm->setValue(200);
connect(rpm, &ParamControl::valueChanged, this, [this, pump](double v) {
ctx()->comm()->sendPumpRpm(pump, static_cast<int>(v));
});
lay->addWidget(rpm);
QPushButton* power = button("上电", "", grp);
power->setCheckable(true);
power->setFixedHeight(56);
connect(power, &QPushButton::toggled, this, [this, pump, power](bool on) {
power->setText(on ? "上电中" : "上电");
ctx()->comm()->sendPumpPower(pump, on);
// 启动 / 停止:互斥,选中态对应当前泵状态(绿=运行、红=停止)。
// 控件级样式表优先级高于全局 QSS,故 :focus 也在这里补,否则 Tab 选中看不出变化
QPushButton* start = button("启动", "", grp);
start->setCheckable(true);
start->setFixedSize(120, 60);
start->setStyleSheet(QString("QPushButton:checked{background-color:%1;color:#FFFFFF;border:none;}"
"QPushButton:focus{border:4px solid #8ABFFF;}")
.arg(C::SUCCESS));
QPushButton* stop = button("停止", "", grp);
stop->setCheckable(true);
stop->setFixedSize(120, 60);
stop->setStyleSheet(QString("QPushButton:checked{background-color:%1;color:#FFFFFF;border:none;}"
"QPushButton:focus{border:4px solid #8ABFFF;}")
.arg(C::DANGER));
connect(start, &QPushButton::clicked, this, [this, pump, start, stop]() {
start->setChecked(true);
stop->setChecked(false);
ctx()->comm()->sendPumpPower(pump, true);
});
lay->addWidget(power);
connect(stop, &QPushButton::clicked, this, [this, pump, start, stop]() {
stop->setChecked(true);
start->setChecked(false);
ctx()->comm()->sendPumpPower(pump, false);
});
QHBoxLayout* pwrRow = new QHBoxLayout();
pwrRow->setSpacing(12);
pwrRow->addStretch(1);
pwrRow->addWidget(start);
pwrRow->addWidget(stop);
pwrRow->addStretch(1);
lay->addLayout(pwrRow);
if (pump == "air") { m_airRpm = rpm; m_airPower = power; }
else if (pump == "plunger") { m_plungerRpm = rpm; m_plungerPower = power; }
else { m_leafRpm = rpm; m_leafPower = power; }
if (pump == "air") { m_airRpm = rpm; m_airStart = start; m_airStop = stop; }
else if (pump == "plunger") { m_plungerRpm = rpm; m_plungerStart = start; m_plungerStop = stop; }
else { m_leafRpm = rpm; m_leafStart = start; m_leafStop = stop; }
return grp;
}
@@ -220,11 +292,13 @@ void DebugMaintainPage::refresh()
m_washCard->setField(5, t.pumpFaults.value(0));
m_washCard->setField(6, t.pumpFaults.value(0));
// 球阀到位反馈
m_v1Open->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off);
m_v1Close->setState(t.d1close ? LedIndicator::Red : LedIndicator::Off);
m_v2Open->setState(t.d2open ? LedIndicator::Green : LedIndicator::Off);
m_v2Close->setState(t.d2close ? LedIndicator::Red : LedIndicator::Off);
// 球阀到位反馈:未收到过车端遥测时不动,保留构造时的缺省显示(开启绿 / 关闭红)
if (m_telemetrySeen) {
m_v1Open->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off);
m_v1Close->setState(t.d1close ? LedIndicator::Red : LedIndicator::Off);
m_v2Open->setState(t.d2open ? LedIndicator::Green : LedIndicator::Off);
m_v2Close->setState(t.d2close ? LedIndicator::Red : LedIndicator::Off);
}
// 球阀故障提示
bool v1Fault = t.valveFault.contains("D1");
@@ -234,10 +308,10 @@ void DebugMaintainPage::refresh()
if (v2Fault) { m_valveFault2->setText(t.valveFault); m_valveFault2->show(); startBlink(m_valveFault2, "blink"); }
else { stopBlink(m_valveFault2, "blink"); m_valveFault2->hide(); }
// 泵上电状态同步
if (m_airPower) m_airPower->setChecked(t.air);
if (m_plungerPower) m_plungerPower->setChecked(t.plunger);
if (m_leafPower) m_leafPower->setChecked(t.leaf);
// 泵启停状态同步(互斥组内先置“启动”再置“停止”,两者互补,结果总是一选中一未选)
if (m_airStart) { m_airStart->setChecked(t.air); m_airStop->setChecked(!t.air); }
if (m_plungerStart){ m_plungerStart->setChecked(t.plunger); m_plungerStop->setChecked(!t.plunger); }
if (m_leafStart) { m_leafStart->setChecked(t.leaf); m_leafStop->setChecked(!t.leaf); }
// 底部指示灯
m_ledPlunger->setState(t.plunger ? LedIndicator::Green : LedIndicator::Off);
+11 -5
View File
@@ -12,7 +12,7 @@ class QPushButton;
class QLabel;
class QCheckBox;
// ===================== ⑪ 调试维护界面(洗消调试) =====================
// ===================== ⑪ 调试维护界面 =====================
class DebugMaintainPage : public BasePage
{
Q_OBJECT
@@ -25,7 +25,7 @@ private slots:
private:
// 单个球阀组:控制开/关 + 开启到位 + 关闭到位 + 故障提示
QWidget* makeValveGroup(int valve);
// 单个泵组:转速调节 + 上电启停
// 单个泵组:转速调节 + 启动/停止
QWidget* makePumpGroup(const QString& pump, const QString& name);
TitleBar* m_titleBar = nullptr;
@@ -49,15 +49,21 @@ private:
ParamControl* m_airRpm = nullptr;
ParamControl* m_plungerRpm = nullptr;
ParamControl* m_leafRpm = nullptr;
QPushButton* m_airPower = nullptr;
QPushButton* m_plungerPower = nullptr;
QPushButton* m_leafPower = nullptr;
QPushButton* m_airStart = nullptr;
QPushButton* m_airStop = nullptr;
QPushButton* m_plungerStart = nullptr;
QPushButton* m_plungerStop = nullptr;
QPushButton* m_leafStart = nullptr;
QPushButton* m_leafStop = nullptr;
// 底部指示灯
LedIndicator* m_ledPlunger = nullptr;
LedIndicator* m_ledAir = nullptr;
LedIndicator* m_ledLeaf = nullptr;
LedIndicator* m_ledLevel = nullptr;
// 是否收到过车端遥测:未收到时球阀到位灯保持“开启=绿、关闭=红”的缺省显示
bool m_telemetrySeen = false;
};
#endif // DEBUGMAINTAINPAGE_H
+62 -22
View File
@@ -4,7 +4,8 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QMessageBox>
#include <QTimer>
#include "confirmdialog.h"
#include "global.h"
FaultQueryPage::FaultQueryPage(AppContext* ctx, QWidget* parent)
@@ -13,58 +14,98 @@ FaultQueryPage::FaultQueryPage(AppContext* ctx, QWidget* parent)
m_categories << "机器人底盘" << "机械臂" << "洗消系统" << "气体检测仪";
QVBoxLayout* root = new QVBoxLayout(this);
root->setContentsMargins(36, 20, 36, 20);
root->setSpacing(16);
root->setContentsMargins(20, 20, 20, 20);
root->setSpacing(30);
// 顶条:标题 + 汇总数
m_title = label("历史故障", 28, C::TEXT, true, this);
m_title = label("历史故障", 50, C::TEXT, true, this);
root->addWidget(m_title);
QHBoxLayout* main = new QHBoxLayout();
main->setSpacing(28);
main->setSpacing(30);
// 表格
m_table = new FaultTable(this);
connect(m_table, &FaultTable::rowClicked, this, &FaultQueryPage::onRowClicked);
main->addWidget(m_table, 1);
// 分类按钮列(垂直居中 + 平均分布)
// 分类按钮列(左右各留 20px,按钮间距 40px,剩余高度由按钮等分)
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(0, 0, 0, 0);
rightLay->setSpacing(8);
rightLay->setContentsMargins(80, FaultTable::kHeaderHeight + 10, 30, 80); // 对齐表格内容区后再下移 10px
rightLay->setSpacing(60); // 按钮之间的间隔
for (const QString& cat : m_categories) {
QPushButton* b = button(cat + "故障", "", right);
b->setCheckable(true);
b->setFixedHeight(110);
b->setStyleSheet(QString("font-size:24px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;")
.arg(C::PANEL_HI, C::TEXT, C::BORDER));
b->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); // 纵向可拉伸 → 等分剩余空间
b->setStyleSheet(QString(
"QPushButton{font-size:32px;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(b, &QPushButton::toggled, this, &FaultQueryPage::onCategoryToggled);
m_categoryButtons << b;
rightLay->addStretch(1);
rightLay->addWidget(b);
rightLay->addWidget(b, 1);
}
rightLay->addStretch(1);
right->setFixedWidth(360);
main->addWidget(right);
root->addLayout(main, 1);
// 底部:返回
// 底部:返回(预留更大的行高空间)
QHBoxLayout* bottomLay = new QHBoxLayout();
bottomLay->setContentsMargins(0, 0, 40, 30);
bottomLay->addStretch(1);
QPushButton* back = button("返回", "", this);
back->setFixedSize(144, 76);
connect(back, &QPushButton::clicked, this, [this]() { emit navigate(m_returnPage); });
bottomLay->addWidget(back);
m_back = button("返回", "", this);
m_back->setFixedSize(250, 100);
m_back->setStyleSheet(cancelButtonStyle(60)); // 选中(悬停/按下/焦点)红底白字
connect(m_back, &QPushButton::clicked, this, [this]() { emit navigate(m_returnPage); });
bottomLay->addWidget(m_back);
root->addLayout(bottomLay);
// Tab 顺序(循环):表格 → 右侧分类按钮 → 返回 → 上一页 → 下一页 → 回到表格
QWidget* first = m_table->view();
QWidget* last = first;
for (QPushButton* b : m_categoryButtons) {
QWidget::setTabOrder(last, b);
last = b;
}
QWidget::setTabOrder(last, m_back);
QWidget::setTabOrder(m_back, m_table->prevButton());
QWidget::setTabOrder(m_table->prevButton(), m_table->nextButton());
QWidget::setTabOrder(m_table->nextButton(), first); // 回到表格,完成循环
refresh();
}
void FaultQueryPage::showEvent(QShowEvent* ev)
{
BasePage::showEvent(ev);
// 进入页面默认聚焦“分页”(上一页优先;首页禁用时退回下一页,再退回表格)
QTimer::singleShot(0, this, [this]() {
QWidget* prev = m_table->prevButton();
QWidget* next = m_table->nextButton();
if (prev->isEnabled()) prev->setFocus();
else if (next->isEnabled()) next->setFocus();
else m_table->view()->setFocus();
});
}
void FaultQueryPage::onCategoryToggled(bool on)
{
// 单选筛选:确定当前选中的分类
// 单选:选中一个分类时,取消其它分类(屏蔽信号,避免它们再触发本函数)
QPushButton* src = qobject_cast<QPushButton*>(sender());
if (on && src) {
for (QPushButton* b : m_categoryButtons) {
if (b != src && b->isChecked()) {
b->blockSignals(true);
b->setChecked(false);
b->blockSignals(false);
}
}
}
// 确定当前选中的分类(都没选 = 显示全部)
QString selected;
for (int i = 0; i < m_categoryButtons.size(); ++i) {
if (m_categoryButtons[i]->isChecked()) {
@@ -73,7 +114,6 @@ void FaultQueryPage::onCategoryToggled(bool on)
}
}
m_filter = selected;
Q_UNUSED(on);
refresh();
}
@@ -105,7 +145,7 @@ void FaultQueryPage::onRowClicked(int globalIndex)
if (globalIndex < 0 || globalIndex >= data.size())
return;
const FaultRecord& r = data[globalIndex];
QMessageBox::information(this, "故障详情",
ConfirmDialog::message(this, "故障详情",
QString("故障时间:%1\n分系统:%2\n故障代码:%3\n故障描述:%4")
.arg(r.time, r.system, r.code, r.desc));
}
+4
View File
@@ -17,6 +17,9 @@ public:
void setReturnPage(PageId id) { m_returnPage = id; }
protected:
void showEvent(QShowEvent* ev) override; // 进入页面时默认聚焦“分页”
private slots:
void refresh();
void onCategoryToggled(bool on);
@@ -25,6 +28,7 @@ private slots:
private:
FaultTable* m_table = nullptr;
QLabel* m_title = nullptr;
QPushButton* m_back = nullptr;
QVector<QPushButton*> m_categoryButtons;
QVector<QString> m_categories;
QString m_filter; // 空 = 全部
+8 -8
View File
@@ -2,7 +2,7 @@
#include "titlebar.h"
#include "statusstrip.h"
#include "datacard.h"
#include "udpcommunication.h"
#include "car.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -28,8 +28,8 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
// 中央卡片区(2×2)
QWidget* center = new QWidget(this);
QGridLayout* grid = new QGridLayout(center);
grid->setSpacing(16);
grid->setContentsMargins(0, 0, 0, 0);
grid->setSpacing(20);
grid->setContentsMargins(20, 0, 20, 0);
m_chassisCard = new DataCard("底盘", center);
m_chassisCard->addField("电量", "85%");
@@ -60,11 +60,11 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
// 右侧按钮列(垂直居中 + 平均分布)
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(0, 0, 0, 0);
rightLay->setContentsMargins(40, 40, 40, 40);
rightLay->setSpacing(8);
m_start = button("启动", "primary", right);
m_start->setFixedHeight(180);
m_start->setFixedHeight(100);
connect(m_start, &QPushButton::clicked, this, [this]() {
this->ctx()->comm()->sendGasStart();
this->ctx()->gasRunning = true;
@@ -72,7 +72,7 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
});
m_stop = button("停机", "", right);
m_stop->setFixedHeight(180);
m_stop->setFixedHeight(100);
connect(m_stop, &QPushButton::clicked, this, [this]() {
this->ctx()->comm()->sendGasStop();
this->ctx()->gasRunning = false;
@@ -90,9 +90,9 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1);
m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setModeVisible(false); // 本页不显示模式徽标
connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() {
if (this->ctx()->gasRunning) {
// 【设计建议】检测中返回弹提示
if (ConfirmDialog::confirm(this, "提示", "检测仍在进行,是否退出?"))
emit navigate(PageJob);
} else {
@@ -101,7 +101,7 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
});
root->addWidget(m_statusStrip);
connect(ctx->comm(), &UdpCommunication::telemetryUpdated, this, &GasDetectorPage::refresh);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &GasDetectorPage::refresh);
updateGating();
refresh();
}
+149
View File
@@ -0,0 +1,149 @@
#include "home.h"
#include "titlebar.h"
#include "statusstrip.h"
#include "datacard.h"
#include "confirmdialog.h"
#include "car.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QPushButton>
#include "global.h"
HomePage::HomePage(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);
QHBoxLayout* main = new QHBoxLayout();
main->setContentsMargins(24, 20, 24, 20);
main->setSpacing(24);
// ===== 按钮统一样式(改这里即可同时调整对应的按钮)=====
// 左侧 2 个菜单按钮(字号 50px;尺寸见 addMenu 里的 setFixedSize)
const QString menuBtnStyle =
QString("QPushButton{font-size:50px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;}"
"QPushButton:hover,QPushButton:focus{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}")
.arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY);
// 右侧动作按钮(字号 55px;尺寸见下方各自的 setFixedSize)
const QString actionBtnStyle = "QPushButton{font-size:55px;}";
// 左侧菜单(按钮在列内水平居中、垂直整体居中,选中发亮)
QWidget* left = new QWidget(this);
QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(10, 20, 10, 20);
leftLay->setSpacing(100);
auto addMenu = [&](const QString& t, PageId id) {
QPushButton* b = button(t, "", left);
b->setFixedSize(260, 100); // 固定尺寸(改这里同时调整 2 个菜单按钮)
b->setStyleSheet(menuBtnStyle);
connect(b, &QPushButton::clicked, this, [this, id]() { emit navigate(id); });
leftLay->addWidget(b, 0, Qt::AlignHCenter); // 不占拉伸因子,余量交给上下 stretch 均分
};
leftLay->addStretch(1);
addMenu("故障查询", PageFault);
addMenu("作业选择", PageJob);
leftLay->addStretch(1);
left->setFixedWidth(300);
main->addWidget(left);
// 中央卡片区(2×2,与作业选择页一致)
QWidget* center = new QWidget(this);
QGridLayout* grid = new QGridLayout(center);
grid->setSpacing(24);
grid->setContentsMargins(0, 0, 0, 0);
m_chassisCard = new DataCard("底盘", center);
m_chassisCard->addField("电量", "85%");
m_chassisCard->addField("电压", "360V");
m_chassisCard->addField("故障", "无");
m_armCard = new DataCard("机械臂", center);
m_armCard->addField("末端位置", "XYZ");
m_armCard->addField("姿态", "RX/RY/RZ");
m_washCard = new DataCard("洗消", center);
m_washCard->addField("A液位", "100L");
m_washCard->addField("泡沫液位", "30L");
m_gasCard = new DataCard("气体检测仪", center);
m_gasCard->addField("主机状态", "待机");
m_gasCard->addField("浓度", "0 ppb");
// 左列 底盘/机械臂,右列 洗消/气体检测仪
grid->addWidget(m_chassisCard, 0, 0);
grid->addWidget(m_washCard, 0, 1);
grid->addWidget(m_armCard, 1, 0);
grid->addWidget(m_gasCard, 1, 1);
main->addWidget(center, 1);
// 右侧按钮列(等分填满整列,选中发亮)
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(0, 0, 0, 0);
rightLay->setSpacing(24);
QPushButton* debug = button("调试维护", "", right);
debug->setFixedSize(240, 100);
debug->setStyleSheet(actionBtnStyle);
connect(debug, &QPushButton::clicked, this, [this]() { emit navigate(PageDebug); });
QPushButton* help = button("操作说明", "", right);
help->setFixedSize(240, 100);
help->setStyleSheet(actionBtnStyle);
connect(help, &QPushButton::clicked, this, &HomePage::onHelp);
QPushButton* spare = button("备用", "", right);
spare->setFixedSize(240, 100);
spare->setStyleSheet(actionBtnStyle);
spare->setEnabled(false); // 预留功能:置灰禁用,不接任何跳转
rightLay->addStretch(1);
rightLay->addWidget(debug, 0, Qt::AlignHCenter);
rightLay->addStretch(1);
rightLay->addWidget(help, 0, Qt::AlignHCenter);
rightLay->addStretch(1);
rightLay->addWidget(spare, 0, Qt::AlignHCenter);
rightLay->addStretch(1);
right->setFixedWidth(360);
main->addWidget(right);
root->addLayout(main, 1);
m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setBackVisible(false);
m_statusStrip->setModeVisible(false); // 本页不显示模式徽标(菜单已移到左右两列)
root->addWidget(m_statusStrip);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &HomePage::refresh);
refresh();
}
void HomePage::refresh()
{
const Telemetry& t = ctx()->telemetry;
m_chassisCard->setField(0, QString("%1%").arg(t.soc));
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0));
m_chassisCard->setField(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";"));
m_chassisCard->setAlarm(2, !t.chassisFaults.isEmpty());
m_armCard->setField(0, t.pos);
m_armCard->setField(1, t.pose);
m_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0));
m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 0, 'f', 0));
m_gasCard->setField(0, t.gasHost == "detecting" ? "检测中" : (t.gasHost == "fault" ? "故障" : "待机"));
m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0));
m_gasCard->setAlarm(1, t.gasConc > 500.0);
}
void HomePage::onHelp()
{
ConfirmDialog::message(this, "操作说明",
"系统自检将依次检测:通信系统、底盘通信、机械臂通信、洗消通信、"
"传感器信号、清水液位及电动球阀 D1/D2 的开启与关闭。\n"
"全部正常后方可进入作业选择界面。");
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef HOMEPAGE_H
#define HOMEPAGE_H
#include "base.h"
class TitleBar;
class StatusStrip;
class DataCard;
// ===================== ⑤ 系统首页面 =====================
class HomePage : public BasePage
{
Q_OBJECT
public:
explicit HomePage(AppContext* ctx, QWidget* parent = nullptr);
private slots:
void refresh();
void onHelp();
private:
TitleBar* m_titleBar = nullptr;
StatusStrip* m_statusStrip = nullptr;
DataCard* m_chassisCard = nullptr;
DataCard* m_armCard = nullptr;
DataCard* m_washCard = nullptr;
DataCard* m_gasCard = nullptr;
};
#endif // HOMEPAGE_H
+44 -25
View File
@@ -2,7 +2,7 @@
#include "titlebar.h"
#include "statusstrip.h"
#include "datacard.h"
#include "udpcommunication.h"
#include "car.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -21,36 +21,43 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent)
root->addWidget(m_titleBar);
QHBoxLayout* main = new QHBoxLayout();
main->setContentsMargins(36, 20, 36, 20);
main->setSpacing(32);
main->setContentsMargins(24, 20, 24, 20);
main->setSpacing(24);
// 左侧菜单(垂直居中 + 平均分布)
// ===== 按钮统一样式(改这里即可同时调整对应的按钮)=====
// 左侧 4 个菜单按钮(字号 50px;尺寸见 addMenu 里的 setFixedSize)
const QString menuBtnStyle =
QString("QPushButton{font-size:50px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;}"
"QPushButton:hover,QPushButton:focus{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}")
.arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY);
// 右侧动作按钮(字号 55px;尺寸见下方各自的 setFixedSize)
const QString actionBtnStyle = "QPushButton{font-size:55px;}";
// 左侧菜单(按钮在列内水平居中、垂直整体居中,选中发亮)
QWidget* left = new QWidget(this);
QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(0, 0, 0, 0);
leftLay->setSpacing(8);
leftLay->setContentsMargins(10, 20, 10, 20);
leftLay->setSpacing(100);
auto addMenu = [&](const QString& t, PageId id) {
QPushButton* b = button(t, "", left);
b->setFixedHeight(130);
b->setStyleSheet(QString("font-size:24px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;")
.arg(C::PANEL_HI, C::TEXT, C::BORDER));
b->setFixedSize(260, 100); // 固定尺寸(改这里同时调整 4 个菜单按钮)
b->setStyleSheet(menuBtnStyle);
connect(b, &QPushButton::clicked, this, [this, id]() { emit navigate(id); });
leftLay->addStretch(1);
leftLay->addWidget(b);
leftLay->addWidget(b, 0, Qt::AlignHCenter); // 不占拉伸因子,余量交给上下 stretch 均分
};
leftLay->addStretch(1);
addMenu("机器人底盘", PageChassis);
addMenu("机械臂", PageArm);
addMenu("气体检测仪", PageGas);
addMenu("调试维护", PageDebug);
addMenu("故障查询", PageFault);
leftLay->addStretch(1);
left->setFixedWidth(240);
left->setFixedWidth(300);
main->addWidget(left);
// 中央卡片区(2×2)
QWidget* center = new QWidget(this);
QGridLayout* grid = new QGridLayout(center);
grid->setSpacing(16);
grid->setSpacing(24);
grid->setContentsMargins(0, 0, 0, 0);
m_chassisCard = new DataCard("底盘", center);
@@ -70,27 +77,38 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent)
m_gasCard->addField("主机状态", "待机");
m_gasCard->addField("浓度", "0 ppb");
// 左列 底盘/机械臂,右列 洗消/气体检测仪
grid->addWidget(m_chassisCard, 0, 0);
grid->addWidget(m_armCard, 0, 1);
grid->addWidget(m_washCard, 1, 0);
grid->addWidget(m_washCard, 0, 1);
grid->addWidget(m_armCard, 1, 0);
grid->addWidget(m_gasCard, 1, 1);
main->addWidget(center, 1);
// 右侧按钮列(垂直居中 + 平均分布)
// 右侧按钮列(等分填满整列,选中发亮)
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(0, 0, 0, 0);
rightLay->setSpacing(8);
QPushButton* clean = button("手动消除", "primary", right);
clean->setFixedHeight(170);
rightLay->setSpacing(24);
QPushButton* clean = button("消除作业", "", right);
clean->setFixedSize(240, 100);
clean->setStyleSheet(actionBtnStyle);
connect(clean, &QPushButton::clicked, this, [this]() { emit navigate(PageClean); });
QPushButton* disinfect = button("手动消毒", "primary", right);
disinfect->setFixedHeight(170);
QPushButton* disinfect = button("消毒作业", "", right);
disinfect->setFixedSize(240, 100);
disinfect->setStyleSheet(actionBtnStyle);
connect(disinfect, &QPushButton::clicked, this, [this]() { emit navigate(PageDisinfect); });
// 返回:回系统首页面(⑤系统首页面 → ⑥作业选择)
QPushButton* back = button("返回", "", right);
back->setFixedSize(240, 100);
back->setStyleSheet(cancelButtonStyle(55)); // 选中(悬停/按下/焦点)红底白字
// connect(back, &QPushButton::clicked, this, [this]() { emit navigate(PageSelfCheck); }); // 原:回自检页
connect(back, &QPushButton::clicked, this, [this]() { emit navigate(PageHome); });
rightLay->addStretch(1);
rightLay->addWidget(clean);
rightLay->addWidget(clean, 0, Qt::AlignHCenter);
rightLay->addStretch(1);
rightLay->addWidget(disinfect);
rightLay->addWidget(disinfect, 0, Qt::AlignHCenter);
rightLay->addStretch(1);
rightLay->addWidget(back, 0, Qt::AlignHCenter);
rightLay->addStretch(1);
right->setFixedWidth(360);
main->addWidget(right);
@@ -99,9 +117,10 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent)
m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setBackVisible(false);
m_statusStrip->setModeVisible(false); // 本页不显示模式徽标(返回按钮已移到右侧列)
root->addWidget(m_statusStrip);
connect(ctx->comm(), &UdpCommunication::telemetryUpdated, this, &JobSelectPage::refresh);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &JobSelectPage::refresh);
refresh();
}
-28
View File
@@ -1,28 +0,0 @@
#include "manualclean.h"
#include "paramcontrol.h"
#include "udpcommunication.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include "global.h"
ManualCleanPage::ManualCleanPage(AppContext* ctx, QWidget* parent)
: WashBasePage(ctx, "手动消除", "洗消(手动消除)", parent)
{
double flow = ctx->getDouble(ctx->keyPumpFlow(), 20);
m_pumpFlow = new ParamControl("柱塞泵流量", "L/min", 0.5, 0.0, 100.0, true, false, this);
m_pumpFlow->setValue(flow);
connect(m_pumpFlow, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyPumpFlow(), m_pumpFlow->value());
this->ctx()->comm()->sendSetParam(this->ctx()->keyPumpFlow(), m_pumpFlow->value());
ConfirmDialog::toast(this, "已保存");
});
paramsLayout()->addWidget(m_pumpFlow);
}
void ManualCleanPage::resetFactory()
{
// 出厂值 20
m_pumpFlow->setValue(20);
ctx()->setDouble(ctx()->keyPumpFlow(), 20);
ConfirmDialog::toast(this, "已恢复出厂值");
}
-23
View File
@@ -1,23 +0,0 @@
#ifndef MANUALCLEANPAGE_H
#define MANUALCLEANPAGE_H
#include "washbase.h"
class ParamControl;
// ===================== ⑨ 手动消除界面 =====================
class ManualCleanPage : public WashBasePage
{
Q_OBJECT
public:
explicit ManualCleanPage(AppContext* ctx, QWidget* parent = nullptr);
protected:
void resetFactory() override;
QString washModeTag() const override { return "clean"; }
private:
ParamControl* m_pumpFlow = nullptr;
};
#endif // MANUALCLEANPAGE_H
-49
View File
@@ -1,49 +0,0 @@
#include "manualdisinfect.h"
#include "paramcontrol.h"
#include "udpcommunication.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include "global.h"
ManualDisinfectPage::ManualDisinfectPage(AppContext* ctx, QWidget* parent)
: WashBasePage(ctx, "手动消毒", "洗消(手动消毒)", parent)
{
// 发泡倍数(出厂 6)
m_foamRatio = new ParamControl("发泡倍数", "倍", 0.5, 0.0, 20.0, true, false, this);
m_foamRatio->setValue(ctx->getDouble(ctx->keyFoamRatio(), 6));
connect(m_foamRatio, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyFoamRatio(), m_foamRatio->value());
ConfirmDialog::toast(this, "已保存");
});
// 泡沫比例(出厂 3%)
m_foamPercent = new ParamControl("泡沫比例", "%", 0.5, 0.0, 20.0, true, false, this);
m_foamPercent->setValue(ctx->getDouble(ctx->keyFoamPercent(), 3));
connect(m_foamPercent, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyFoamPercent(), m_foamPercent->value());
ConfirmDialog::toast(this, "已保存");
});
// 空压机流量(出厂 100)
m_airFlow = new ParamControl("空压机流量", "L/min", 1, 0.0, 500.0, true, false, this);
m_airFlow->setValue(ctx->getDouble(ctx->keyAirFlow(), 100));
connect(m_airFlow, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyAirFlow(), m_airFlow->value());
ConfirmDialog::toast(this, "已保存");
});
paramsLayout()->addWidget(m_foamRatio);
paramsLayout()->addWidget(m_foamPercent);
paramsLayout()->addWidget(m_airFlow);
}
void ManualDisinfectPage::resetFactory()
{
m_foamRatio->setValue(6);
m_foamPercent->setValue(3);
m_airFlow->setValue(100);
ctx()->setDouble(ctx()->keyFoamRatio(), 6);
ctx()->setDouble(ctx()->keyFoamPercent(), 3);
ctx()->setDouble(ctx()->keyAirFlow(), 100);
ConfirmDialog::toast(this, "已恢复出厂值");
}
-25
View File
@@ -1,25 +0,0 @@
#ifndef MANUALDISINFECTPAGE_H
#define MANUALDISINFECTPAGE_H
#include "washbase.h"
class ParamControl;
// ===================== ⑩ 手动消毒界面 =====================
class ManualDisinfectPage : public WashBasePage
{
Q_OBJECT
public:
explicit ManualDisinfectPage(AppContext* ctx, QWidget* parent = nullptr);
protected:
void resetFactory() override;
QString washModeTag() const override { return "disinfect"; }
private:
ParamControl* m_foamRatio = nullptr; // 发泡倍数
ParamControl* m_foamPercent = nullptr; // 泡沫比例
ParamControl* m_airFlow = nullptr; // 空压机流量
};
#endif // MANUALDISINFECTPAGE_H
+13 -9
View File
@@ -1,7 +1,7 @@
#include "selfcheck.h"
#include "titlebar.h"
#include "ledindicator.h"
#include "udpcommunication.h"
#include "car.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -9,7 +9,7 @@
#include <QPushButton>
#include <QProgressBar>
#include <QTimer>
#include <QMessageBox>
#include "confirmdialog.h"
#include "global.h"
SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent)
@@ -29,7 +29,7 @@ SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent)
main->setSpacing(40);
// 两侧按钮统一样式:字号在此统一调整;按钮会拉伸填满整列(sideBtnH 为最小高度)
const int sideBtnH = 200;
const int sideBtnH = 130;
const QString sideBtnStyle = "font-size:60px;";
auto makeSideBtn = [&](const QString& text, const QString& variant, QWidget* parent) {
QPushButton* b = button(text, variant, parent);
@@ -127,8 +127,8 @@ SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1);
// 连接通信信号
connect(ctx->comm(), &UdpCommunication::selfCheckItem, this, &SelfCheckPage::onSelfCheckItem);
connect(ctx->comm(), &UdpCommunication::selfCheckDone, this, &SelfCheckPage::onSelfCheckDone);
connect(ctx->comm(), &CarCommunication::selfCheckItem, this, &SelfCheckPage::onSelfCheckItem);
connect(ctx->comm(), &CarCommunication::selfCheckDone, this, &SelfCheckPage::onSelfCheckDone);
}
void SelfCheckPage::showEvent(QShowEvent* ev)
@@ -200,12 +200,15 @@ void SelfCheckPage::onSelfCheckDone(bool allOk)
m_status->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::SUCCESS));
m_job->setEnabled(true);
m_job->setFocus(); // 自检完成后聚焦到“作业选择”
// 自检完成后停留在本界面,不自动跳转(需进入作业选择请手动点击)
// 【设计建议】自检完成 2s 后自动进入作业选择
// 自检完成 2s 后自动进入系统首页面(原目标为作业选择页)
// QTimer::singleShot(2000, this, [this]() {
// if (m_done && isVisible())
// emit navigate(PageJob);
// });
QTimer::singleShot(2000, this, [this]() {
if (m_done && isVisible())
emit navigate(PageHome);
});
} else {
m_status->setText("无法完成自检 请检查设备");
m_status->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::DANGER));
@@ -218,7 +221,8 @@ void SelfCheckPage::onSelfCheckDone(bool allOk)
void SelfCheckPage::onIgnoreFault()
{
emit navigate(PageJob);
// emit navigate(PageJob); // 原:直接进作业选择页
emit navigate(PageHome); // 忽略故障后进入系统首页面
}
void SelfCheckPage::onJobSelect()
@@ -228,7 +232,7 @@ void SelfCheckPage::onJobSelect()
void SelfCheckPage::onHelp()
{
QMessageBox::information(this, "操作说明",
ConfirmDialog::message(this, "操作说明",
"系统自检将依次检测:通信系统、底盘通信、机械臂通信、洗消通信、"
"传感器信号、清水液位及电动球阀 D1/D2 的开启与关闭。\n"
"全部正常后方可进入作业选择界面。");
+5 -2
View File
@@ -3,7 +3,7 @@
#include "alarmbanner.h"
#include "datacard.h"
#include "ledindicator.h"
#include "udpcommunication.h"
#include "car.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -68,6 +68,7 @@ WashBasePage::WashBasePage(AppContext* ctx, const QString& title, const QString&
m_fFoam = m_washCard->addField("发泡倍数", "0");
m_fAirFault = m_washCard->addField("空压机故障代码", "");
m_fPlungerFault = m_washCard->addField("柱塞泵故障代码", "");
m_washCard->indentRows(); // 行文字缩进到中间偏左(卡片很宽,贴左缘会显得空)
main->addWidget(m_washCard, 1);
// 右侧操作列
@@ -109,6 +110,7 @@ WashBasePage::WashBasePage(AppContext* ctx, const QString& title, const QString&
m_factory = button("恢复出厂值", "", bottom);
m_factory->setFixedSize(220, 64);
m_factory->setStyleSheet(cancelButtonStyle(40)); // 选中(悬停/按下/焦点)红底白字
connect(m_factory, &QPushButton::clicked, this, &WashBasePage::onFactory);
bottomLay->addWidget(m_factory);
bottomLay->addSpacing(20);
@@ -134,6 +136,7 @@ WashBasePage::WashBasePage(AppContext* ctx, const QString& title, const QString&
m_back = button("返回", "", bottom);
m_back->setFixedSize(144, 76);
m_back->setStyleSheet(cancelButtonStyle(40)); // 选中(悬停/按下/焦点)红底白字
connect(m_back, &QPushButton::clicked, this, &WashBasePage::onBack);
bottomLay->addWidget(m_back);
@@ -145,7 +148,7 @@ WashBasePage::WashBasePage(AppContext* ctx, const QString& title, const QString&
connect(m_faultTimer, &QTimer::timeout, this, &WashBasePage::rotateFaults);
m_faultTimer->start();
connect(ctx->comm(), &UdpCommunication::telemetryUpdated, this, &WashBasePage::refresh);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &WashBasePage::refresh);
updateGating();
refresh();
}
+1 -1
View File
@@ -12,7 +12,7 @@ class QPushButton;
class QVBoxLayout;
class QTimer;
// ===================== 洗消页面基类(⑨手动消除 / ⑩手动消毒 共用) =====================
// ===================== 洗消页面基类(⑨消除作业 / ⑩消毒作业 共用) =====================
// 左侧状态卡 + 中央洗消参数卡 + 右侧启动/停止与参数 + 底部指示灯条
class WashBasePage : public BasePage
{
+369
View File
@@ -0,0 +1,369 @@
#include "washpage.h"
#include "titlebar.h"
#include "alarmbanner.h"
#include "datacard.h"
#include "ledindicator.h"
#include "paramcontrol.h"
#include "car.h"
#include "confirmdialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QTimer>
#include "global.h"
WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
: BasePage(ctx, parent), m_mode(mode)
{
const bool clean = (mode == Clean);
QVBoxLayout* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
m_titleBar = new TitleBar(ctx, this);
m_titleBar->setTitle(clean ? "消除作业" : "消毒作业");
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(28);
// 左侧状态卡(底盘/机械臂/气体检测仪)
QWidget* left = new QWidget(this);
QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(0, 0, 0, 0);
leftLay->setSpacing(10);
m_chassisCard = new DataCard("底盘", left);
m_chassisCard->addField("电量", "85%");
m_chassisCard->addField("电压", "360V");
m_chassisCard->addField("故障", "无");
m_armCard = new DataCard("机械臂", left);
m_armCard->addField("末端位置", "XYZ");
m_armCard->addField("姿态", "RX/RY/RZ");
m_gasCard = new DataCard("气体检测仪", left);
m_gasCard->addField("主机状态", "待机");
m_gasCard->addField("浓度", "0 ppb");
leftLay->addWidget(m_chassisCard, 1);
leftLay->addWidget(m_armCard, 1);
leftLay->addWidget(m_gasCard, 1);
left->setFixedWidth(600); // 需容纳最长的“末端位置:X:1200 Y:300 Z:800”(42px 字号约 523px)
main->addSpacing(60); // 左列整体右移的量;空白由中间拉伸列让出,卡片宽度不变
main->addWidget(left);
// 中央洗消卡
m_washCard = new DataCard(clean ? "洗消(消除作业)" : "洗消(消毒作业)", this);
m_fMode = m_washCard->addField("洗消模式", "--");
m_fLevelA = m_washCard->addField("A组分液位", "100L");
m_fLevelFoam = m_washCard->addField("泡沫箱液位", "30L");
m_fFlowA = m_washCard->addField("A药剂流量", "0 L/min");
m_fFlowB = m_washCard->addField("B药剂流量", "0 L/min");
m_fGasFlow = m_washCard->addField("气体流量", "0 L/min");
m_fPress = m_washCard->addField("管路压力", "0 MPa");
m_fAirPress = m_washCard->addField("空压机压力", "0 MPa");
m_fFoam = m_washCard->addField("发泡倍数", "0");
m_fAirFault = m_washCard->addField("空压机故障代码", "");
m_fPlungerFault = m_washCard->addField("柱塞泵故障代码", "");
main->addSpacing(60);
main->addWidget(m_washCard, 1);
// 右侧操作列
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
rightLay->setContentsMargins(20, 20, 20, 10);
rightLay->setSpacing(60);
m_start = button("启动", "primary", right);
m_start->setFixedSize(180, 100);
connect(m_start, &QPushButton::clicked, this, &WashPage::onStart);
m_stop = button("停止", "danger", right);
m_stop->setFixedSize(180, 100);
connect(m_stop, &QPushButton::clicked, this, &WashPage::onStop);
rightLay->addWidget(m_start, 0, Qt::AlignHCenter);
rightLay->addWidget(m_stop, 0, Qt::AlignHCenter);
// 参数区:按模式不同
m_paramsLayout = new QVBoxLayout();
m_paramsLayout->setSpacing(8);
rightLay->addLayout(m_paramsLayout);
rightLay->addStretch(1);
if (clean) {
// 柱塞泵流量(出厂 20;协议 PlungerPump_flow 0-24 L/min)
m_pumpFlow = new ParamControl("柱塞泵流量", "L/min", 0.1, 0.0, 24.0, true, false, this);
m_pumpFlow->setValue(ctx->getDouble(ctx->keyPumpFlow(), 20));
connect(m_pumpFlow, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyPumpFlow(), m_pumpFlow->value());
this->ctx()->comm()->sendSetParam(this->ctx()->keyPumpFlow(), m_pumpFlow->value());
refreshPumpValues(); // 底部数值跟着更新
ConfirmDialog::toast(this, "已保存");
});
m_paramsLayout->addWidget(m_pumpFlow);
} else {
// 发泡倍数(出厂 6;协议 Expansion_ratioCmd 6-12 倍)
m_foamRatio = new ParamControl("发泡倍数", "倍", 0.1, 6.0, 12.0, true, false, this);
m_foamRatio->setValue(ctx->getDouble(ctx->keyFoamRatio(), 6));
connect(m_foamRatio, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyFoamRatio(), m_foamRatio->value());
this->ctx()->comm()->sendSetParam(this->ctx()->keyFoamRatio(), m_foamRatio->value());
ConfirmDialog::toast(this, "已保存");
});
m_paramsLayout->addWidget(m_foamRatio);
// 泡沫比例(出厂 3%;协议 Foam_ratio 0-10%)
m_foamPercent = new ParamControl("泡沫比例", "%", 0.1, 0.0, 10.0, true, false, this);
m_foamPercent->setValue(ctx->getDouble(ctx->keyFoamPercent(), 3));
connect(m_foamPercent, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyFoamPercent(), m_foamPercent->value());
this->ctx()->comm()->sendSetParam(this->ctx()->keyFoamPercent(), m_foamPercent->value());
ConfirmDialog::toast(this, "已保存");
});
m_paramsLayout->addWidget(m_foamPercent);
// 空压机流量(出厂 100;协议 AirComp_flow 0-340 L/min)
m_airFlow = new ParamControl("空压机流量", "L/min", 1, 0.0, 340.0, true, false, this);
m_airFlow->setValue(ctx->getDouble(ctx->keyAirFlow(), 100));
connect(m_airFlow, &ParamControl::saveClicked, this, [this]() {
this->ctx()->setDouble(this->ctx()->keyAirFlow(), m_airFlow->value());
this->ctx()->comm()->sendSetParam(this->ctx()->keyAirFlow(), m_airFlow->value());
refreshPumpValues(); // 底部数值跟着更新
ConfirmDialog::toast(this, "已保存");
});
m_paramsLayout->addWidget(m_airFlow);
}
right->setFixedWidth(500); // 参数行需容纳 输入框150 + 单位约101 + 保存140 + 两个间距24 ≈ 415px
main->addSpacing(60); // 洗消卡 ↔ 右列的间隙;空白由中间拉伸列让出,卡片文字不受影响
main->addWidget(right);
root->addLayout(main, 1);
// 底部条:左右两个按钮纵向占满两行,中间上下两排(泵数值 / 指示灯)居中
QWidget* bottom = new QWidget(this);
bottom->setObjectName("WashBottom");
bottom->setFixedHeight(160);
// 用 #WashBottom 限定选择器:不带选择器的样式表会连子控件一起套上边框和底色
bottom->setStyleSheet(QString("QWidget#WashBottom{background-color:%1;border-top:1px solid %2;}")
.arg(C::PANEL, C::BORDER));
QHBoxLayout* bottomRoot = new QHBoxLayout(bottom);
bottomRoot->setContentsMargins(20, 8, 20, 8);
bottomRoot->setSpacing(12);
// 左:恢复出厂值(占满两行高度)
m_factory = button("恢复出厂值", "", bottom);
m_factory->setFixedSize(220, 100);
m_factory->setStyleSheet(cancelButtonStyle(40)); // 选中(悬停/按下/焦点)红底白字
connect(m_factory, &QPushButton::clicked, this, &WashPage::onFactory);
bottomRoot->addWidget(m_factory, 0, Qt::AlignVCenter);
// 中:上排三泵数值、下排指示灯,两排都水平居中
QVBoxLayout* midLay = new QVBoxLayout();
midLay->setSpacing(6);
QHBoxLayout* valueLay = new QHBoxLayout();
valueLay->setSpacing(40);
valueLay->addStretch(1);
auto addFlow = [&](const QString& name) {
valueLay->addWidget(label(name + ":", 40, C::TEXT_SUB, false, bottom));
QLabel* v = label("--", 40, C::TEXT, true, bottom);
valueLay->addWidget(v);
return v;
};
m_valPlunger = addFlow("柱塞泵");
m_valAir = addFlow("空压机");
m_valLeaf = addFlow("叶片泵");
refreshPumpValues(); // 开机先显示参数的出厂默认值
valueLay->addStretch(1);
QHBoxLayout* ledLay = new QHBoxLayout();
ledLay->setSpacing(10); // 8 个灯 40px 字号后总宽约 1300px
ledLay->addStretch(1);
auto makeLed = [&](const QString& text) {
LedIndicator* led = new LedIndicator(text, bottom);
led->setTextPixelSize(40); // 字号与卡片标题一致
ledLay->addWidget(led);
return led;
};
m_ledRun = makeLed("启停");
m_ledPlunger = makeLed("柱塞泵");
m_ledAir = makeLed("空压机");
m_ledLeaf = makeLed("叶片泵");
m_ledLevel = makeLed("液位低");
m_ledD1 = makeLed("D1");
m_ledD2 = makeLed("D2");
m_ledFault = makeLed("系统故障");
ledLay->addStretch(1);
midLay->addStretch(1);
midLay->addLayout(valueLay);
midLay->addLayout(ledLay);
midLay->addStretch(1);
bottomRoot->addLayout(midLay, 1);
// 右:返回(占满两行高度)
m_back = button("返回", "", bottom);
m_back->setFixedSize(170, 100);
m_back->setStyleSheet(cancelButtonStyle(40)); // 选中(悬停/按下/焦点)红底白字
connect(m_back, &QPushButton::clicked, this, &WashPage::onBack);
bottomRoot->addWidget(m_back, 0, Qt::AlignVCenter);
root->addWidget(bottom);
// 故障代码轮流循环显示(2s/条)
m_faultTimer = new QTimer(this);
m_faultTimer->setInterval(2000);
connect(m_faultTimer, &QTimer::timeout, this, &WashPage::rotateFaults);
m_faultTimer->start();
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &WashPage::refresh);
updateGating();
refresh();
}
QString WashPage::washModeTag() const
{
return m_mode == Clean ? "clean" : "disinfect";
}
// 底部三个泵的数值:柱塞泵/空压机跟右侧参数的设定值(没改过就是出厂默认值),
// 叶片泵本页没有对应参数,先取遥测的实时流量
void WashPage::refreshPumpValues()
{
// 这里读的是配置值,一并按协议量程夹取(柱塞泵 0-24、空压机 0-340 L/min),
// 否则旧配置里超范围的历史值会与控件显示、实际上报的数值对不上
m_valPlunger->setText(QString("%1 L/min").arg(qBound(0.0, ctx()->getDouble(ctx()->keyPumpFlow(), 20), 24.0), 0, 'f', 1));
m_valAir->setText(QString("%1 L/min").arg(qBound(0.0, ctx()->getDouble(ctx()->keyAirFlow(), 100), 340.0), 0, 'f', 1));
m_valLeaf->setText(QString("%1 L/min").arg(ctx()->telemetry.flowB, 0, 'f', 1));
}
void WashPage::onStart()
{
ctx()->comm()->sendWashStart(washModeTag());
ctx()->washRunning = true;
updateGating();
}
void WashPage::onStop()
{
ctx()->comm()->sendWashStop();
ctx()->washRunning = false;
updateGating();
}
void WashPage::onBack()
{
if (ctx()->washRunning) {
ConfirmDialog::toast(this, "请先停止");
return;
}
emit navigate(PageJob);
}
void WashPage::onFactory()
{
if (ConfirmDialog::confirm(this, "恢复出厂值", "确定将参数恢复为出厂值?"))
resetFactory();
}
void WashPage::resetFactory()
{
if (m_mode == Clean) {
m_pumpFlow->setValue(20); // 出厂 20
ctx()->setDouble(ctx()->keyPumpFlow(), 20);
ctx()->comm()->sendSetParam(ctx()->keyPumpFlow(), 20);
} else {
m_foamRatio->setValue(6); // 出厂 6
m_foamPercent->setValue(3); // 出厂 3%
m_airFlow->setValue(100); // 出厂 100
ctx()->setDouble(ctx()->keyFoamRatio(), 6);
ctx()->setDouble(ctx()->keyFoamPercent(), 3);
ctx()->setDouble(ctx()->keyAirFlow(), 100);
ctx()->comm()->sendSetParam(ctx()->keyFoamRatio(), 6);
ctx()->comm()->sendSetParam(ctx()->keyFoamPercent(), 3);
ctx()->comm()->sendSetParam(ctx()->keyAirFlow(), 100);
}
refreshPumpValues(); // 底部数值跟着回到出厂值
ConfirmDialog::toast(this, "已恢复出厂值");
}
void WashPage::updateGating()
{
bool running = ctx()->washRunning;
m_start->setEnabled(!running);
m_stop->setEnabled(running);
m_back->setEnabled(!running);
}
void WashPage::refresh()
{
const Telemetry& t = ctx()->telemetry;
if (!t.commOk) m_banner->showAlarm("通信异常");
else m_banner->clear();
// 以泵状态同步运行状态
if (t.plunger || t.air || t.leaf) ctx()->washRunning = true;
else if (t.washMode == "--") ctx()->washRunning = false;
updateGating();
m_chassisCard->setField(0, QString("%1%").arg(t.soc));
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0));
m_chassisCard->setField(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";"));
m_armCard->setField(0, t.pos);
m_armCard->setField(1, t.pose);
m_gasCard->setField(0, t.gasHost == "detecting" ? "检测中" : (t.gasHost == "fault" ? "故障" : "待机"));
m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0));
m_washCard->setField(m_fMode, t.washMode);
m_washCard->setField(m_fLevelA, QString("%1L").arg(t.levelA, 0, 'f', 0));
m_washCard->setField(m_fLevelFoam, QString("%1L").arg(t.levelFoam, 0, 'f', 0));
m_washCard->setField(m_fFlowA, QString("%1 L/min").arg(t.flowA, 0, 'f', 1));
m_washCard->setField(m_fFlowB, QString("%1 L/min").arg(t.flowB, 0, 'f', 1));
m_washCard->setField(m_fGasFlow, QString("%1 L/min").arg(t.gasFlow, 0, 'f', 1));
m_washCard->setField(m_fPress, QString("%1 MPa").arg(t.press, 0, 'f', 2));
m_washCard->setField(m_fAirPress, QString("%1 MPa").arg(t.airPress, 0, 'f', 2));
m_washCard->setField(m_fFoam, QString("%1").arg(t.foam, 0, 'f', 1));
// 指示灯
m_ledRun->setState(ctx()->washRunning ? LedIndicator::Green : LedIndicator::Off);
m_ledPlunger->setState(t.plunger ? LedIndicator::Green : LedIndicator::Off);
m_ledAir->setState(t.air ? LedIndicator::Green : LedIndicator::Off);
m_ledLeaf->setState(t.leaf ? LedIndicator::Green : LedIndicator::Off);
m_ledLevel->setState(t.levelA < 10 ? LedIndicator::RedBlink : LedIndicator::Off);
m_ledD1->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off);
m_ledD2->setState(t.d2open ? LedIndicator::Green : LedIndicator::Off);
m_ledFault->setState((!t.washFaults.isEmpty() || !t.pumpFaults.isEmpty())
? LedIndicator::RedBlink : LedIndicator::Off);
// 底部三个泵的数值
refreshPumpValues();
// 故障代码显示(静态更新,循环由 rotateFaults 驱动)
rotateFaults();
}
void WashPage::rotateFaults()
{
const Telemetry& t = ctx()->telemetry;
QStringList all = t.washFaults + t.pumpFaults;
if (all.isEmpty()) {
m_washCard->setField(m_fAirFault, "");
m_washCard->setField(m_fPlungerFault, "");
return;
}
m_faultIndex = m_faultIndex % all.size();
QString code = all[m_faultIndex];
m_washCard->setField(m_fAirFault, code);
m_washCard->setField(m_fPlungerFault, code);
m_faultIndex = (m_faultIndex + 1) % all.size();
}
+86
View File
@@ -0,0 +1,86 @@
#ifndef WASHPAGE_H
#define WASHPAGE_H
#include "base.h"
class TitleBar;
class AlarmBanner;
class DataCard;
class LedIndicator;
class ParamControl;
class QPushButton;
class QLabel;
class QVBoxLayout;
class QTimer;
// ===================== 洗消页(⑨消除作业 / ⑩消毒作业 共用) =====================
// 自包含页面:三列骨架、中央洗消卡、右侧启停与参数区、底部指示灯条都在本文件里。
// 两个页面只差 mode(标题、洗消标识、参数项及出厂值),调整布局改这一处即可。
class WashPage : public BasePage
{
Q_OBJECT
public:
enum Mode { Clean, Disinfect };
explicit WashPage(AppContext* ctx, Mode mode, QWidget* parent = nullptr);
private slots:
void onStart();
void onStop();
void onBack();
void onFactory();
void refresh();
void rotateFaults();
private:
void updateGating();
void resetFactory();
void refreshPumpValues(); // 刷新底部三个泵的数值
QString washModeTag() const; // "clean" / "disinfect"
Mode m_mode;
TitleBar* m_titleBar = nullptr;
AlarmBanner* m_banner = nullptr;
DataCard* m_chassisCard = nullptr;
DataCard* m_armCard = nullptr;
DataCard* m_gasCard = nullptr;
DataCard* m_washCard = nullptr;
QPushButton* m_start = nullptr;
QPushButton* m_stop = nullptr;
QPushButton* m_back = nullptr;
QPushButton* m_factory = nullptr;
QVBoxLayout* m_paramsLayout = nullptr;
// 参数:消除用柱塞泵流量;消毒用发泡倍数 / 泡沫比例 / 空压机流量
ParamControl* m_pumpFlow = nullptr;
ParamControl* m_foamRatio = nullptr;
ParamControl* m_foamPercent = nullptr;
ParamControl* m_airFlow = nullptr;
// 底部指示灯
LedIndicator* m_ledRun = nullptr;
LedIndicator* m_ledPlunger = nullptr;
LedIndicator* m_ledAir = nullptr;
LedIndicator* m_ledLeaf = nullptr;
LedIndicator* m_ledLevel = nullptr;
LedIndicator* m_ledD1 = nullptr;
LedIndicator* m_ledD2 = nullptr;
LedIndicator* m_ledFault = nullptr;
// 底部第二行:三个泵的当前流量值
QLabel* m_valPlunger = nullptr;
QLabel* m_valAir = nullptr;
QLabel* m_valLeaf = nullptr;
// 中央洗消卡字段索引
int m_fMode = -1, m_fLevelA = -1, m_fLevelFoam = -1;
int m_fFlowA = -1, m_fFlowB = -1, m_fGasFlow = -1;
int m_fPress = -1, m_fAirPress = -1, m_fFoam = -1;
int m_fAirFault = -1, m_fPlungerFault = -1;
QTimer* m_faultTimer = nullptr;
int m_faultIndex = 0;
};
#endif // WASHPAGE_H