#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 #include #include #include #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("底盘电量SOC", "--"); m_chassisCard->addField("底盘电压U", "--"); m_armCard = new DataCard("机械臂", left); m_armCard->addField("末端位置", "--"); m_armCard->addField("末端姿态", "--"); m_gasCard = new DataCard("气体检测仪", left); m_gasCard->addField("主机状态", "--"); m_gasCard->addField("有毒物种类", "--"); m_gasCard->addField("有毒物等级", "--"); 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("洗消", this); m_fMode = m_washCard->addField("洗消模式", "--"); m_fLevelA = m_washCard->addField("A组分液位", "--"); m_fLevelFoam = m_washCard->addField("B组分液位", "--"); m_fFlowA = m_washCard->addField("A药剂流量", "--"); m_fFlowB = m_washCard->addField("B药剂流量", "--"); m_fGasFlow = m_washCard->addField("气体流量", "--"); m_fPress = m_washCard->addField("管路压力", "--"); m_fAirPress = m_washCard->addField("空压机压力", "--"); m_fFoam = 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); // 底部条:左右两个按钮纵向占满,中间三排(泵数值 / 指示灯 / 两路液位低)居中。 // 高度由 160 加到 188:中间那列三排约 164px 再加 8+8 的上下边距,160 装不下第三排 QWidget* bottom = new QWidget(this); bottom->setObjectName("WashBottom"); bottom->setFixedHeight(188); // 用 #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); ledLay->addStretch(1); // 灯要分两行放,故把目标行当参数传进来 auto makeLed = [&](QHBoxLayout* row, const QString& text) { LedIndicator* led = new LedIndicator(text, bottom); led->setTextPixelSize(40); // 字号与卡片标题一致 row->addWidget(led); return led; }; m_ledRun = makeLed(ledLay, "启停"); m_ledPlunger = makeLed(ledLay, "柱塞泵"); m_ledAir = makeLed(ledLay, "空压机"); m_ledLeaf = makeLed(ledLay, "叶片泵"); m_ledD1 = makeLed(ledLay, "D1"); m_ledD2 = makeLed(ledLay, "D2"); ledLay->addStretch(1); // 下一行:液位低拆成 A / B 两路。原来那颗笼统的「液位低」和「系统故障」都撤了—— // 后者在洗消卡片的故障行里已经有了,底部再挂一颗是重复的 QHBoxLayout* levelLay = new QHBoxLayout(); levelLay->setSpacing(40); levelLay->addStretch(1); m_ledLevelA = makeLed(levelLay, "A组分/清水液位低"); m_ledLevelB = makeLed(levelLay, "B组分液位低"); levelLay->addStretch(1); midLay->addStretch(1); midLay->addLayout(valueLay); midLay->addLayout(ledLay); midLay->addLayout(levelLay); 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); 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(dashIf(ctx()->telemetry.hasWash, QString("%1 L/min").arg(ctx()->telemetry.flowB, 0, 'f', 1))); } void WashPage::onStart() { ctx()->comm()->sendWashStart(washModeTag()); // 同时把机器强制到机械臂模式(见 AppContext::setWashRunning),否则 Arm_enable=0, // 末端那条俯仰摆动指令车端不会理 updateGating(); // 启停灯立刻跟着变,不等下一帧遥测——否则车端不回话时按钮看着像没反应 updateLeds(); } void WashPage::onStop() { ctx()->comm()->sendWashStop(); ctx()->setWashRunning(false); updateGating(); updateLeds(); } void WashPage::onBack() { if (ctx()->isWashRunning()) { 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()->isWashRunning(); 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()->setWashRunning(true); else if (t.washMode == "--") ctx()->setWashRunning(false); updateGating(); // 未收到对应报文的数值统一显示 "--"(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(m_fMode, t.washMode); m_washCard->setField(m_fLevelA, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0))); m_washCard->setField(m_fLevelFoam, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0))); m_washCard->setField(m_fFlowA, dashIf(t.hasWash, QString("%1 L/min").arg(t.flowA, 0, 'f', 1))); m_washCard->setField(m_fFlowB, dashIf(t.hasWash, QString("%1 L/min").arg(t.flowB, 0, 'f', 1))); m_washCard->setField(m_fGasFlow, dashIf(t.hasWash, QString("%1 L/min").arg(t.gasFlow, 0, 'f', 1))); m_washCard->setField(m_fPress, dashIf(t.hasWashPress, QString("%1 MPa").arg(t.press, 0, 'f', 2))); m_washCard->setField(m_fAirPress, dashIf(t.hasWashPress, QString("%1 MPa").arg(t.airPress, 0, 'f', 2))); m_washCard->setField(m_fFoam, dashIf(t.hasWash, QString("%1").arg(t.foam, 0, 'f', 1))); m_washCard->setAlarm(m_fLevelA, t.levelALow); m_washCard->setAlarm(m_fLevelFoam, t.levelBLow); m_washCard->setFaults(washCardFaults(t)); updateLeds(); // 底部三个泵的数值 refreshPumpValues(); } // 底栏指示灯。单独拆出来是为了让 onStart/onStop 能直接调它: // 遥测还没回来时 refresh() 里的“以泵状态同步运行状态”会把刚置上的 washRunning 又抹掉, // 所以启停的即时反馈不能靠整个 refresh()。 void WashPage::updateLeds() { const Telemetry& t = ctx()->telemetry; m_ledRun->setState(ctx()->isWashRunning() ? 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_ledD1->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off); m_ledD2->setState(t.d2open ? LedIndicator::Green : LedIndicator::Off); // 液位低拆成 A / B 两路,各用各自的协议位(A_LiquidLevel_Low / B_LiquidLevel_Low); // 不再按 levelA<10 猜阈值,免得卡片液位行红闪、这颗灯却不亮 m_ledLevelA->setState(t.levelALow ? LedIndicator::RedBlink : LedIndicator::Off); m_ledLevelB->setState(t.levelBLow ? LedIndicator::RedBlink : LedIndicator::Off); }