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

397 lines
19 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 "debugmaintain.h"
#include "titlebar.h"
#include "alarmbanner.h"
#include "datacard.h"
#include "ledindicator.h"
#include "paramcontrol.h"
#include "car.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QDateTime>
#include "global.h"
// 球阀“开启/关闭故障”的判据:下发指令后这么久还等不到对应到位反馈就判故障。
// 取 8s,与④自检里球阀四项的等待时长(kValveWaitTicks 18 拍 × 450ms ≈ 8s)一致。
static const qint64 kValveFaultMs = 8000;
DebugMaintainPage::DebugMaintainPage(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_warn = new AlarmBanner(this);
m_warn->showAlarm("警告:该模式下无保护功能,操作请谨慎!");
root->addWidget(m_warn);
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(20, 10, 10, 10);
leftLay->setSpacing(60);
// 故障提示作为球阀盒子的**上一条**:常占位(无故障时清空文字、留出这一条的高度),
// 出现故障不会把下面的盒子顶下去
auto addValve = [&](int valve) {
QWidget* box = new QWidget(left);
QVBoxLayout* boxLay = new QVBoxLayout(box);
boxLay->setContentsMargins(0, 0, 0, 0);
boxLay->setSpacing(8);
// 不能走 label():那个辅助函数会把 color 写进行内样式,而控件自身的样式表优先级高于
// 应用级样式表,会把 QSS 里 [alert] / [blink] 的红色和明暗闪烁一起盖掉(只剩常亮红字)。
// 这里只给字号字重,颜色交给应用级样式表的告警规则。
QLabel* fault = new QLabel(box);
fault->setAlignment(Qt::AlignCenter);
fault->setProperty("alert", true);
fault->setStyleSheet("font-size:28px;font-weight:700;");
fault->setFixedHeight(40); // 常占位:把这一条的高度钉死
boxLay->addWidget(fault);
boxLay->addWidget(makeValveGroup(valve));
leftLay->addWidget(box);
return fault;
};
m_valveFault1 = addValve(1);
m_valveFault2 = addValve(2);
// 三个泵的上电指示灯放在 D2 球阀下方的空位里,纵向居中排列。
// 指示规则不变(上电=绿、否则灭),仍由 refresh() 按车端反馈刷新。
auto addPumpLed = [&](const QString& text) {
LedIndicator* led = new LedIndicator(text, left);
led->setTextPixelSize(40);
// 圆点在前(LedIndicator 的默认):竖排下来像一张状态清单
leftLay->addWidget(led, 0, Qt::AlignHCenter);
return led;
};
m_ledPlunger = addPumpLed("柱塞泵");
m_ledAir = addPumpLed("空压机");
m_ledLeaf = addPumpLed("叶片泵");
leftLay->addStretch(1);
left->setFixedWidth(300);
main->addWidget(left);
// 中央卡片区(左右两列:左列 底盘/机械臂/气体检测仪 纵向排列,右列 洗消参数)
QWidget* center = new QWidget(this);
QHBoxLayout* centerLay = new QHBoxLayout(center);
centerLay->setContentsMargins(0, 0, 0, 0);
centerLay->setSpacing(28); // 与 main 的列间距一致
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("底盘电量SOC", "--");
m_chassisCard->addField("底盘电压U", "--");
m_armCard = new DataCard("机械臂", centerLeft);
// 这一列只有 474px,位置行在 42px 下约 530px,单独把数值字号收到 34(约 463px)
m_armCard->setValueFontPx(34);
m_armCard->addField("末端位置", "--");
m_armCard->addField("末端姿态", "--");
m_gasCard = new DataCard("气体检测仪", centerLeft);
m_gasCard->addField("主机状态", "--");
m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "--");
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("洗消模式", "--");
m_washCard->addField("A组分液位", "--");
m_washCard->addField("B组分液位", "--");
m_washCard->addField("A药剂流量", "--");
m_washCard->addField("B药剂流量", "--");
m_washCard->addField("气体流量", "--");
m_washCard->addField("管路压力", "--");
m_washCard->addField("空压机压力", "--");
m_washCard->addField("发泡倍数", "--");
centerLay->addWidget(m_washCard, 1);
main->addWidget(center, 1);
// 右泵控制区
QWidget* right = new QWidget(this);
QVBoxLayout* rightLay = new QVBoxLayout(right);
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);
root->addLayout(main, 1);
// 底部指示灯条
QWidget* bottom = new QWidget(this);
bottom->setObjectName("DebugBottom");
bottom->setFixedHeight(94);
// 用 #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->setSpacing(48);
auto makeLed = [&](const QString& text) {
LedIndicator* led = new LedIndicator(text, bottom);
led->setTextPixelSize(40); // 字号与卡片标题一致
led->setDotAfterText(true); // 圆点画在文字后面
bottomLay->addWidget(led);
return led;
};
// 三个泵的指示灯已经搬进各自的泵组里(放在按钮行左侧),底栏只留两路液位低
m_ledLevelA = makeLed("A组分/清水液位低");
m_ledLevelB = makeLed("B组分液位低");
// 这两盏灯居中于中央卡片区:卡片区 x ∈ [364, 1436](36 页边距 + 300 球阀列 + 28 列间距
// 起,宽 1072),中点 900。用 sizeHint 量出两盏灯连间距的实际宽度再定位,改字号也不会偏。
const int levelW = m_ledLevelA->sizeHint().width() + bottomLay->spacing()
+ m_ledLevelB->sizeHint().width();
bottomLay->setContentsMargins((364 + 1436) / 2 - levelW / 2, 0, 24, 0);
bottomLay->addStretch(1);
QPushButton* back = button("返回", "", bottom);
back->setFixedSize(160, 76);
back->setStyleSheet(cancelButtonStyle(40)); // 选中(悬停/按下/焦点)红底白字
connect(back, &QPushButton::clicked, this, [this]() { emit navigate(m_returnPage); });
bottomLay->addWidget(back);
root->addWidget(bottom);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, [this]() {
m_telemetrySeen = true; // 遥测一到,球阀到位灯改由 refresh() 接管
refresh();
});
// Tab 顺序(循环):返回 → D1控制 → D2控制 → 各泵(转速输入 → 启动 → 停止) → 回到返回。
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();
}
QWidget* DebugMaintainPage::makeValveGroup(int valve)
{
QWidget* grp = new QWidget(this);
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));
QVBoxLayout* lay = new QVBoxLayout(grp);
lay->setContentsMargins(12, 12, 12, 12);
lay->setSpacing(8);
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);
ctrl->setCheckable(true);
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::DISABLED).arg(C::SUCCESS));
connect(ctrl, &QPushButton::toggled, this, [this, valve](bool on) {
ctx()->comm()->sendBallValve(valve, on);
// 记下这一拍想要哪个阀位并开始等反馈;超时还没到位就在 refresh() 里判故障
if (valve == 1) { m_v1WantOpen = on ? 1 : 0; m_v1CmdMs = QDateTime::currentMSecsSinceEpoch(); m_v1Fault = false; }
else { m_v2WantOpen = on ? 1 : 0; m_v2CmdMs = QDateTime::currentMSecsSinceEpoch(); m_v2Fault = false; }
});
lay->addWidget(ctrl, 0, Qt::AlignHCenter);
// 开启到位 / 关闭到位(同一行,居中)。缺省状态按“阀体默认关着”给:
// 开启到位灭(灰)、关闭到位红。首次收到遥测后由 refresh() 按实际到位状态刷新
LedIndicator* open = new LedIndicator("开启", grp);
open->setState(LedIndicator::Off);
LedIndicator* close = new LedIndicator("关闭", grp);
close->setState(LedIndicator::Red);
QHBoxLayout* ioRow = new QHBoxLayout();
ioRow->setSpacing(10);
ioRow->addStretch(1);
ioRow->addWidget(open);
ioRow->addWidget(close);
ioRow->addStretch(1);
lay->addLayout(ioRow);
// 故障提示不在盒子里——它是盒子**上方**独立的一条,由构造函数的 addValve 建好
if (valve == 1) {
m_valve1Btn = ctrl;
m_v1Open = open;
m_v1Close = close;
} else {
m_valve2Btn = ctrl;
m_v2Open = open;
m_v2Close = close;
}
return grp;
}
QWidget* DebugMaintainPage::makePumpGroup(const QString& pump, const QString& name)
{
QWidget* grp = new QWidget(this);
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(30, 8, 20, 8);
lay->setSpacing(10);
// 上限按协议各泵量程:空压机 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);
// 启动 / 停止:互斥,选中态对应当前泵状态(绿=运行、红=停止)。
// 控件级样式表优先级高于全局 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);
});
connect(stop, &QPushButton::clicked, this, [this, pump, start, stop]() {
stop->setChecked(true);
start->setChecked(false);
ctx()->comm()->sendPumpPower(pump, false);
});
// 圆点+名称不在这里了——三个泵的上电指示灯已经搬到左列 D2 球阀下方(见构造函数)
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_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;
}
void DebugMaintainPage::refresh()
{
const Telemetry& t = ctx()->telemetry;
// 未收到对应报文的数值统一显示 "--"(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_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);
m_washCard->setField(0, t.washMode);
m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0)));
m_washCard->setField(2, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0)));
m_washCard->setField(3, dashIf(t.hasWash, QString("%1 L/min").arg(t.flowA, 0, 'f', 1)));
m_washCard->setField(4, dashIf(t.hasWash, QString("%1 L/min").arg(t.flowB, 0, 'f', 1)));
m_washCard->setField(5, dashIf(t.hasWash, QString("%1 L/min").arg(t.gasFlow, 0, 'f', 1)));
m_washCard->setField(6, dashIf(t.hasWashPress, QString("%1 MPa").arg(t.press, 0, 'f', 2)));
m_washCard->setField(7, dashIf(t.hasWashPress, QString("%1 MPa").arg(t.airPress, 0, 'f', 2)));
m_washCard->setField(8, dashIf(t.hasWash, QString("%1").arg(t.foam, 0, 'f', 1)));
m_washCard->setAlarm(1, t.levelALow);
m_washCard->setAlarm(2, t.levelBLow);
m_washCard->setFaults(washCardFaults(t));
// 球阀到位反馈:未收到过车端遥测时不动,保留构造时的缺省显示(开启灰 / 关闭红)
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);
}
// 球阀故障:协议里没有“D1开启故障/关闭故障”这种信号,只能自己推——
// 指令下发后 kValveFaultMs 内 MBV_Dn_Sta 还没变成想要的样子,就判该方向故障;
// 反馈一旦对上(到位)就解除。cmdMs 为 0 表示没有待确认的指令,不判。
const qint64 now = QDateTime::currentMSecsSinceEpoch();
auto judgeValve = [&](qint64& cmdMs, int wantOpen, bool fbOpen, bool& faulted,
QLabel* lab, int valve) {
if (cmdMs != 0) {
if (fbOpen == (wantOpen != 0)) { // 到位:解除等待
cmdMs = 0;
faulted = false;
} else if (now - cmdMs > kValveFaultMs) {
faulted = true;
}
}
// 这一条是**常占位**的:只切文字与闪烁,不动显隐,免得盒子上下跳。
// "当前有没有报故障"就看文字空不空——省掉再存一个状态。
if (faulted != !lab->text().isEmpty())
return;
if (faulted) {
lab->setText(QString("D%1球阀 %2 故障").arg(valve).arg(wantOpen ? "开启" : "关闭"));
startBlink(lab, "blink");
} else {
stopBlink(lab, "blink");
lab->clear();
}
};
judgeValve(m_v1CmdMs, m_v1WantOpen, t.d1open, m_v1Fault, m_valveFault1, 1);
judgeValve(m_v2CmdMs, m_v2WantOpen, t.d2open, m_v2Fault, m_valveFault2, 2);
// 泵启停状态同步(互斥组内先置“启动”再置“停止”,两者互补,结果总是一选中一未选)
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); }
// 底部指示灯:泵的三个已经挪进泵组,这里只剩两路液位低。
// 用协议位(A_LiquidLevel_Low / B_LiquidLevel_Low),不按 levelA<10 猜阈值
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_ledLevelA->setState(t.levelALow ? LedIndicator::RedBlink : LedIndicator::Off);
m_ledLevelB->setState(t.levelBLow ? LedIndicator::RedBlink : LedIndicator::Off);
}