#include "debugmaintain.h" #include "titlebar.h" #include "alarmbanner.h" #include "datacard.h" #include "ledindicator.h" #include "paramcontrol.h" #include "car.h" #include #include #include #include #include #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(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); }