diff --git a/.claude/settings.json b/.claude/settings.json index 54c08d2..012916c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -9,7 +9,136 @@ "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== onHeartbeatTimeout / setCommOk ===\" && grep -n \"onHeartbeatTimeout\" -A 8 comm/car.cpp && echo && echo \"=== rcLinkOk / linkChanged / linkOk 的消费方 ===\" && grep -rn \"rcLinkOk\\\\|linkChanged\\\\|linkOk\\(\\)\" --include=*.cpp --include=*.h . | grep -v \"comm/uart\")", "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"onRcLinkChanged\" -A 10 app/appcontext.cpp | head -20 && echo \"=== commOk 消费方 ===\" && grep -rn \"commOk\" --include=*.cpp pages/ widgets/ | head -20)", "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"setFixedHeight\\\\|setMinimumHeight\\\\|kHeight\\\\|setFixedSize\" widgets/titlebar.cpp widgets/alarmbanner.cpp | head -20)", - "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== washpage.cpp:63 ===\" && sed -n '63p' pages/washpage.cpp && echo \"=== car.cpp:499-503 ===\" && sed -n '499,503p' comm/car.cpp && echo \"=== global.h:85 ===\" && sed -n '85p' app/global.h && echo \"=== passwordmgmt.cpp 关键行 ===\" && sed -n '11,12p;76,79p;88,92p' pages/passwordmgmt.cpp && echo && echo \"=== mtime ===\" && ls -l --time-style=full-iso pages/washpage.cpp comm/car.cpp app/global.h pages/passwordmgmt.cpp && echo && echo \"=== date ===\" && date)" + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== washpage.cpp:63 ===\" && sed -n '63p' pages/washpage.cpp && echo \"=== car.cpp:499-503 ===\" && sed -n '499,503p' comm/car.cpp && echo \"=== global.h:85 ===\" && sed -n '85p' app/global.h && echo \"=== passwordmgmt.cpp 关键行 ===\" && sed -n '11,12p;76,79p;88,92p' pages/passwordmgmt.cpp && echo && echo \"=== mtime ===\" && ls -l --time-style=full-iso pages/washpage.cpp comm/car.cpp app/global.h pages/passwordmgmt.cpp && echo && echo \"=== date ===\" && date)", + "Bash(ls -la \"//192.168.1.188/Ui_wuhan/comm/\" 2>/dev/null || ls -la \"/192.168.1.188/Ui_wuhan/comm/\")", + "Bash(ls -la \"//192.168.1.188/Ui_wuhan/\")", + "Bash(iconv -f GB18030 -t UTF-8)", + "Bash(iconv -f GB18030 -t UTF-8 \"洗消机器人遥控器通信协议20260810.csv\")", + "Bash(ls -la \"//192.168.1.188/Ui_wuhan/tools/\" \"//192.168.1.188/Ui_wuhan/app/\" \"//192.168.1.188/Ui_wuhan/pages/\" \"//192.168.1.188/Ui_wuhan/widgets/\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -rn \"rc_port\\\\|RcCommunication\\\\|frameReceived\\\\|9002\" --include=*.cpp --include=*.h --include=*.md --include=*.ini . | head -30)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"rc_sender\\\\|联调\\\\|虚拟串口\\\\|socat\\\\|ttyS7\" README.md cfg_test.ini | head -20)", + "Bash(sed -n '140,150p' \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\洗消机器人遥控器通信协议20260810.csv\")", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"resize\\\\|showFullScreen\\\\|setFixedSize\\\\|setGeometry\" mainwindow.cpp main.cpp 2>/dev/null | head -30)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && sed -n '1,120p' comm/car.cpp)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"onSendMid\\\\|kIdDecontamDbg\\\\|Decontam_DebugCmd\" comm/car.cpp)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && echo \"=== cfg_test.ini ===\" && cat cfg_test.ini && echo \"=== Telemetry struct ===\" && grep -n \"struct Telemetry\" -A 60 app/global.h | head -70)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && find . -iname \"config.ini\" -o -iname \"*.ini\" | head && echo \"=== sendBallValve callers ===\" && grep -rn \"sendBallValve\\\\|sendPumpPower\\\\|m_debugMode\" pages/ app/ comm/ | grep -v \"^comm/car.cpp:68\" )", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"d1open\\\\|d1close\\\\|d2open\\\\|d2close\" pages/*.cpp)", + "Bash(sed -n '/void CarCommunication::onDatagram/,/^}/p' comm/car.cpp)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"底盘电机故障\\\\|BMS系统故障\\\\|转向制动系统故障\\\\|故障:\" --include=*.cpp --include=*.h | head -40)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"chassisFaults\\\\|washFaults\\\\|pumpFaults\\\\|detectorFaults\\\\|kSysChassis\\\\|kSysWash\\\\|kSysArm\\\\|kSysDetector\" --include=*.cpp --include=*.h | head -40)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"chassisFaults\\\\|washFaults\\\\|pumpFaults\\\\|detectorFaults\\\\|valveFault\" pages/ app/ | grep -v global.h)", + "Bash(sed -n '/^void addFault\\\\|^void addLevelFault\\\\|^void addFaultByCode\\\\|^static void addFault\\\\|^static void addLevelFault\\\\|^static void addFaultByCode/,/^}/p' comm/car.cpp)", + "Bash(sed -n '/^void CarCommunication::cacheFaults/,/^}/p' comm/car.cpp)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && ls pages/ app/ && echo \"=== faultAppended consumers ===\" && grep -rn \"faultAppended\\\\|FaultRecord\\\\|faultHistory\\\\|m_faults\" app/*.h app/*.cpp | head -30)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"电机过温警告\\\\|SOC过低\" 洗消机器人遥控器通信协议20260810.csv | head -20)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && sed -n '95,145p' 洗消机器人遥控器通信协议20260810.csv)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && echo \"=== warning.cpp ===\" && sed -n '1,60p' pages/warning.cpp && echo \"=== mainwindow estop banner ===\" && sed -n '30,70p' mainwindow.cpp)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"轮换\\\\|轮流\\\\|滚动\\\\|marquee\\\\|Marquee\" --include=*.cpp --include=*.h --include=*.md . | head -20)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && sed -n '135,155p' pages/washbase.cpp && echo \"=== showFault def ===\" && grep -n \"m_faultIdx\\\\|m_faultTimer\" pages/washbase.cpp pages/washbase.h)", + "Bash(awk -F, '{print $NF}' 洗消机器人遥控器通信协议20260810.csv)", + "Bash(grep -E \"故障$\")", + "Bash(sed 's/^\\\\\\([0-9]*\\\\\\):.*,\\\\\\([^,]*\\\\\\)$/\\\\1 -> \\\\2/')", + "Bash(awk 'NR>=104 && NR<=135 {n=NF; last=\"\"; for\\(i=NF;i>=1;i--\\){ if\\($i!=\"\"\\){last=$i; break} } printf \"%d fields=%d last=%s\\\\n\", NR, n, last}' 洗消机器人遥控器通信协议20260810.csv)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -o '[^,\"]*\\\\\\(系统故障\\\\|电机故障\\\\\\)' 洗消机器人遥控器通信协议20260810.csv | sort | uniq -c | sort -rn)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"转向制动系统故障\\\\|底盘电机故障\\\\|BMS系统故障\" 洗消机器人遥控器通信协议20260810.csv | cut -c1-260)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && echo \"addFault: $\\(grep -c 'addFault\\(' comm/car.cpp\\)\" && echo \"addFaultByCode: $\\(grep -c 'addFaultByCode\\(' comm/car.cpp\\)\" && echo \"addLevelFault: $\\(grep -c 'addLevelFault\\(' comm/car.cpp\\)\" && echo \"--- CanFault users ---\" && grep -rn \"CanFault\" --include=*.cpp --include=*.h . | grep -v \"comm/car.cpp\")", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"EPS\" 洗消机器人遥控器通信协议20260810.csv | sed -n '1,10p' | cut -c1-200 && echo \"=== steering group context \\(line 235-250\\) ===\" && sed -n '235,250p' 洗消机器人遥控器通信协议20260810.csv | cut -c1-200)", + "Bash(awk 'NR>=280 && NR<=340' 洗消机器人遥控器通信协议20260810.csv)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"addFault\\(\\\\|addFaultByCode\\(\\\\|addLevelFault\\(\\\\|addNoCodeFault\\(\" comm/car.cpp)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"setTitle\\(\" pages/*.cpp | sed 's/.*setTitle\\(//' | sort -u)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"commOkChanged\\\\|telemetry.commOk\\\\|->commOk\\\\|\\\\.commOk\" --include=*.cpp --include=*.h . | grep -v \"commOkChanged\\(bool\")", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -n \"顶条\\\\|通信正常\\\\|通信异常\\\\|TitleBar\" README.md)", + "Bash(cd \"\\\\\\\\\\\\\\\\192.168.1.188\\\\\\\\Ui_wuhan\\\\\\\\\" && grep -rn \"kCanTxHost\\\\|kCanTxPort\\\\|kCanRxPort\\\\|9001\\\\|0\\\\.0\\\\.0\\\\.0\" --include=*.cpp --include=*.h --include=*.py --include=*.md . | grep -v \"^./tools/\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && ls -la && ls tools/ 2>/dev/null)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && sed -n '120,155p' \"洗消机器人遥控器通信协议20260810.csv\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && cat cfg_test.ini && ls -la *.ini 2>/dev/null; grep -rn \"recv_port\\\\|rcv_port\" --include=*.cpp --include=*.h --include=*.ini . | head -20)", + "Bash(python -c ' *)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && sed -n '224,245p' \"洗消机器人遥控器通信协议20260810.csv\" && echo \"===== 0x183B8180 =====\" && sed -n '316,345p' \"洗消机器人遥控器通信协议20260810.csv\")", + "mcp__claude-vscode__getDiagnostics", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && head -30 pages/washpage.h && echo \"---- grep in build files ----\" && ls *.pro CMakeLists.txt 2>/dev/null)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && cat wuhan.pro)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && head -12 pages/home.cpp pages/chassis.cpp pages/washpage.cpp pages/gasdetector.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && head -15 comm/car.h && echo \"=== arm.cpp includes ===\" && head -14 pages/arm.cpp && echo \"=== debugmaintain includes ===\" && head -12 pages/debugmaintain.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"5AA5\\\\|HDR\\\\|pack\\\\|header\\\\|18\" tools/rc_sender.py | head -20; echo \"===== README =====\"; sed -n '34,50p' README.md)", + "Bash(python -I -)", + "Bash(python -I -c ' *)", + "Bash(python -)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -rn \"->start\\(\\)\\\\|comm\\(\\)->start\\\\|\\\\.start\\(\\)\" --include=*.cpp --include=*.h . | grep -v \"Timer\\\\|timer\\\\|Blink\\\\|blink\" | head -20)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && sed -n '30,36p' comm/car.cpp && echo \"--- m_lastCommOk ---\" && grep -n \"m_lastCommOk\" comm/car.h comm/car.cpp && echo \"--- onHeartbeatTimeout ---\" && grep -n -A6 \"void CarCommunication::onHeartbeatTimeout\" comm/car.cpp && echo \"--- cfgPath ---\" && grep -rn \"cfgPath\" comm/car.cpp app/appcontext.cpp | head)", + "Bash(cd \"//192.168.1.188/Ui_wuhan/tools\" *)", + "Read(//c//**)", + "Read(//c/Qt/5.15.2/Src/qtbase/src/widgets/widgets/**)", + "Read(//c/Users/Administrator/**)", + "Bash(qmake -v)", + "Read(//f/QT/**)", + "Read(//f/QT/5.15.2/Src/**)", + "Read(//f/QT/5.15.2//**)", + "Bash(sed -n '/^QSize QLabelPrivate::sizeForWidth/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/widgets/qlabel.cpp)", + "Bash(sed -n '/^void QLabel::setWordWrap/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/widgets/qlabel.cpp)", + "Bash(sed -n '/int QBoxLayoutPrivate::calcHfw/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/layouts/qboxlayout.cpp)", + "Read(//f/QT/5.15.2/Src/qtbase/src/widgets/widgets/**)", + "Read(//f/QT/5.15.2/Src/qtbase/src/widgets/**)", + "Bash(sed -n '/int QBoxLayoutPrivate::calcHfw/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/kernel/qboxlayout.cpp)", + "Bash(grep -n \"void QStyleSheetStyle::polish\\(QWidget\" -A 40 /f/QT/5.15.2/Src/qtbase/src/widgets/styles/qstylesheetstyle.cpp | grep -n \"font\\\\|Font\" | head -20)", + "Read(//f/QT/5.15.2/Src/qtbase/src/widgets/styles/**)", + "Bash(grep -n \"updateStyleSheetFont\\\\|resolveFont\\\\|setFont\" /f/QT/5.15.2/Src/qtbase/src/widgets/styles/qstylesheetstyle.cpp | head -20)", + "Bash(sed -n '2645,2670p;6082,6110p' /f/QT/5.15.2/Src/qtbase/src/widgets/styles/qstylesheetstyle.cpp)", + "Bash(sed -n '/^bool QWidget::hasHeightForWidth\\(\\) const/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/kernel/qwidget.cpp)", + "Read(//f/QT/5.15.2/Src/qtbase/src/widgets/kernel/**)", + "Bash(sed -n '/^int QWidget::heightForWidth/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/kernel/qwidget.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n -A8 \"kMcuFaults\\\\[\\\\] =\\\\|kMcuFaults\\\\[\" comm/car.cpp | head -20 && echo \"--- 制动电机 ---\" && grep -n -A6 \"kBrakeMotFaults\\\\[8\\\\]\" comm/car.cpp && echo \"--- comm ---\" && grep -n -A12 \"kCommFaults\\\\[\\\\] =\" comm/car.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && sed -n '1,40p' pages/washpage.h && echo \"===== mainwindow =====\" && grep -n \"WashPage\\\\|WashBasePage\\\\|PageClean\\\\|PageDisinfect\" mainwindow.cpp && echo \"===== wuhan.pro =====\" && grep -n \"wash\" wuhan.pro)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"===== Arm 反馈里的信号 =====\" && sed -n '153,170p' \"洗消机器人遥控器通信协议20260810.csv\" | cut -d, -f8,9 && echo && echo \"===== 各页 底盘卡 字段 =====\" && grep -n \"DataCard\\(\\\\\"底盘\\\\|addField\\(\\\\\"电量\\\\|addField\\(\\\\\"电压\\\\|addField\\(\\\\\"车速\\\\|addField\\(\\\\\"手刹\\\\|m_chassisCard->addField\" pages/*.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"setFixedWidth\\\\|addWidget\\(left\\\\|left->set\" pages/washpage.cpp pages/arm.cpp pages/gasdetector.cpp pages/debugmaintain.cpp | head -30)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== wuhan.pro washbase ===\" && grep -n \"washbase\" wuhan.pro || echo \"\\(不在 .pro 里\\)\" ; echo \"=== 谁 include 了 washbase.h ===\" && grep -rn \"washbase.h\" --include=*.cpp --include=*.h . || echo \"\\(无人引用\\)\" ; echo \"=== CanFault::text 用法 ===\" && grep -rn \"\\\\.text\\(\\)\" --include=*.cpp --include=*.h . ; echo \"=== 残留 faultLines ===\" && grep -rn \"faultLines\\\\|buildFaultLines\\\\|commOkChanged\" --include=*.cpp --include=*.h . || echo \"\\(干净\\)\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n -B3 -A14 \"struct CanFault\" comm/car.h && echo \"=== .cpp ===\" && grep -n -B4 -A12 \"QString CanFault::text\" comm/car.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== setFaults 调用点 ===\" && grep -rn \"setFaults\" --include=*.cpp --include=*.h . && echo && echo \"=== 卡片只闪 残留注释 ===\" && grep -rn \"卡片只闪\" --include=*.cpp . || echo \"\\(无\\)\" ; echo && echo \"=== commText/commCode ===\" && grep -rn \"commText\\\\|commCode\" --include=*.cpp --include=*.h . && echo && echo \"=== washCardFaults ===\" && grep -rn \"washCardFaults\" --include=*.cpp --include=*.h .)", + "Bash(echo \"--- 退出码 $? (1=全仓已无引用)---\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 各页标题 ===\" && grep -rn \"setTitle\\(\" pages/*.cpp | grep -v titleBar->setTitle.*QString ; echo && echo \"=== 各页主区左边距 / 左列宽 / 间距 ===\" && grep -n \"setContentsMargins\\\\|setFixedWidth\\\\|setSpacing\\\\|addSpacing\" pages/home.cpp pages/jobselect.cpp pages/chassis.cpp pages/selfcheck.cpp | head -40)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 标题 ===\" && grep -rhn \"setTitle\\(\" pages/*.cpp | sed 's/^ *//' ; echo && echo \"=== selfcheck 布局 ===\" && grep -n \"setContentsMargins\\\\|setFixedWidth\\\\|setSpacing\\\\|addSpacing\\\\|addWidget\\(left\\\\|addWidget\\(right\" pages/selfcheck.cpp | head -20)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"setFixedHeight\" widgets/statusstrip.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== telemetryUpdated 的连接 ===\" && grep -rn \"telemetryUpdated\" --include=*.cpp --include=*.h . && echo && echo \"=== 收发路径 ===\" && grep -n \"emit telemetryUpdated\\\\|void CarCommunication::onDatagram\\\\|handleFrame\\(\" comm/car.cpp && echo && echo \"=== 页面里有没有用定时器刷新 ===\" && grep -rn \"QTimer\" pages/*.cpp | grep -i \"refresh\\\\|update\" )", + "Bash(grep -v \"//\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== startBlink 调用点 ===\" && grep -rn \"startBlink\" --include=*.cpp --include=*.h . && echo && echo \"=== stopBlink 调用点 ===\" && grep -rn \"stopBlink\" --include=*.cpp --include=*.h . && echo && echo \"=== setState\\(RedBlink/YellowBlink\\) ===\" && grep -rn \"RedBlink\\\\|YellowBlink\" --include=*.cpp pages/ widgets/ | grep -v ledindicator)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 残留 kFaultMaxH / 28px ===\" && grep -rn \"kFaultMaxH\\\\|font-size:28px\" --include=*.cpp --include=*.h . || echo \"\\(干净\\)\" ; echo && echo \"=== 故障行相关 ===\" && grep -n \"kRowMaxH\\\\|kLabelFontPx\\\\|kValueFontPx\\\\|m_faultText\\\\|applyFaultText\\\\|eventFilter\" widgets/datacard.cpp widgets/datacard.h)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 残留 m_faultLabel ===\" && grep -rn \"m_faultLabel\" --include=*.cpp --include=*.h . || echo \"\\(干净\\)\" ; echo && echo \"=== 故障行相关常量/用法 ===\" && grep -n \"kRowMaxH\\\\|kLabelFontPx\\\\|kValueFontPx\\\\|kFaultFontPx\\\\|m_faultText\\\\|applyFaultText\" widgets/datacard.cpp)", + "Bash(sort -t: -k1,1 -k2,2n)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 这些信号现在有没有别处已经格式化 ===\" && grep -rn \"gearPos\\\\|parking\\\\|awdMode\\\\|rdyStatus\\\\|steerAngle\\\\|handbrake\" --include=*.cpp pages/ comm/ app/ | grep -v \"global.h\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"hasSpeed\\\\|hasBattery\\\\|hasChassis\" comm/car.cpp app/global.h && echo && echo \"=== vsf4 case ===\" && sed -n '508,522p' comm/car.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 当前 chassis.cpp 的卡片定义 ===\" && sed -n '75,105p' pages/chassis.cpp && echo && echo \"=== refresh 里的卡片刷新 ===\" && sed -n '250,275p' pages/chassis.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" *)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 两处「操作说明」现状 ===\" && grep -n -A9 \"ConfirmDialog::message\\(this, \\\\\"操作说明\\\\\"\" pages/*.cpp && echo && echo \"=== 别处还有没有提到自检流程的提示文本 ===\" && grep -rn \"忽略故障\\\\|自检过程\\\\|自检正常\\\\|自检将依次\" --include=*.cpp --include=*.md . | grep -v \"^./README.md\")", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && sed -n '170,198p' \"洗消机器人遥控器通信协议20260810.csv\" | cut -d, -f8,9,10,11)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== handleFrame 的调用点 ===\" && grep -rn \"handleFrame\" --include=*.cpp --include=*.h . && echo && echo \"=== handleFrame 结尾 ===\" && sed -n '660,680p' comm/car.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== car.h 公开接口 ===\" && sed -n '60,110p' comm/car.h && echo && echo \"=== Arm_ControlCmd2 组帧 ===\" && grep -n -B4 -A22 \"kIdArmCmd2\" comm/car.cpp | head -60)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== sendArmMove / sendArmReturn / sendArmPause ===\" && grep -n -A16 \"void CarCommunication::sendArmMove\" comm/car.cpp && echo && echo \"=== 机械臂相关成员 ===\" && grep -n \"m_roll\\\\|m_pitch\\\\|m_yaw\\\\|m_homingPause\\\\|m_armMode\\\\|m_decontamMode\\\\|m_decontamStart\" comm/car.h)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 机械臂页的轴按钮 ===\" && grep -n \"sendArmMove\\\\|pitch\\\\|roll\\\\|yaw\\\\|俯仰\" pages/arm.cpp | head -20 && echo && echo \"=== 洗消页 启动/停止 ===\" && grep -n -A10 \"void WashPage::onStart\" pages/washpage.cpp && echo && echo \"=== 遥控器轴量程 ===\" && grep -n \"kRcAxisMid\\\\|4096\" comm/car.h comm/car.cpp | head -12)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n -A12 \"void CarCommunication::sendWashStart\\\\|void CarCommunication::sendWashStop\" comm/car.cpp && echo \"=== setArmMode 调用点 ===\" && grep -rn \"setArmMode\" --include=*.cpp . && echo \"=== m_armMode 用法 ===\" && grep -n \"m_armMode\" comm/car.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== setMode 调用点(谁会开机械臂使能)===\" && grep -rn \"setMode\\(\" --include=*.cpp . && echo && echo \"=== onSendFast 里的摆动 ===\" && sed -n '808,828p' comm/car.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -rn \"washRunning\" --include=*.cpp --include=*.h .)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && echo \"=== 协议里和球阀/阀门相关的信号 ===\" && grep -n \"MBV\\\\|阀\\\\|Valve\" \"洗消机器人遥控器通信协议20260810.csv\" | cut -d, -f8,9,10,11,19 && echo && echo \"=== valveFault 有没有被写过 ===\" && grep -rn \"valveFault\" --include=*.cpp --include=*.h . && echo && echo \"=== 泵的故障码字段 ===\" && grep -n \"Pump_FaultCode\\\\|FaultCode\" \"洗消机器人遥控器通信协议20260810.csv\" | cut -d, -f8,9,10,11)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"setFixedWidth\\\\|setFixedSize\\\\|setMinimumWidth\\\\|font-size\" widgets/paramcontrol.cpp | head -20)", + "Bash(sed -n '/QPushButton\\\\* ChassisPage::makeDirButton/,/^}/p' pages/chassis.cpp)", + "Bash(sed -n '/void ChassisPage::updateModeGating/,/^}/p' pages/chassis.cpp)", + "Bash(sed -n '/void AppContext::updateRcDriving/,/^}/p' app/appcontext.cpp)", + "Bash(sed -n '/void ChassisPage::sendDirCommand/,/^}/p' pages/chassis.cpp)", + "Bash(sed -n '/auto addMove/,/^ };/p' pages/arm.cpp)", + "Bash(sed -n '/QPushButton\\\\* ArmPage::makeMoveButton/,/^}/p' pages/arm.cpp)", + "Bash(sed -n '/void QAbstractButton::setDown/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/widgets/qabstractbutton.cpp)", + "Bash(sed -n '/bool QAbstractButton::event\\\\|void QAbstractButton::changeEvent/,/^}/p' /f/QT/5.15.2/Src/qtbase/src/widgets/widgets/qabstractbutton.cpp)", + "Bash(cd \"//192.168.1.188/Ui_wuhan\" && grep -n \"#include\" pages/arm.cpp)", + "Bash(sed -n '/void AppContext::sendChassisCommand/,/^}/p' app/appcontext.cpp)", + "Bash(sed -n '/int axisDir/,/^}/p' app/appcontext.cpp)", + "Bash(sed -n '/ButtonFocusGlow::eventFilter/,/^}/p' app/global.cpp)", + "Bash(sed -n '/void installButtonFocusGlow/,/^}/p' app/global.cpp)", + "Bash(sed -n '/makeSideBtn/,/^ };/p' pages/selfcheck.cpp)", + "Bash(sed -n '/void clearStaleHover\\\\|static void clearStaleHover/,/^}/p' app/global.cpp)", + "Bash(sed -n '/sideBtnStyle/,/;/p' pages/selfcheck.cpp)" + ], + "additionalDirectories": [ + "F:\\QT\\5.15.2\\Src\\qtbase\\src\\widgets\\widgets", + "F:\\QT\\5.15.2\\Src\\qtbase\\src\\widgets\\kernel" ] } } diff --git a/README.md b/README.md index 7adcbfe..1f414e6 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,18 @@ make -j4 **19 条**报文(分类四标准帧 `DIDO/PWM/AI/BMS/MCU` 未实现)。所有报文均为 **29 位扩展帧、 信号一律 Intel(小端)位序**。 -串口是字节流,故线上帧统一为 **18 字节**,收发同一格式: +一个 UDP 数据报里可能塞多条帧,故线上帧统一为 **16 字节**,收发同一格式: | 偏移 | 长度 | 含义 | | --- | --- | --- | -| 0 | 4 | 帧头 `0x5AA5FF00`(线上字节序 `00 FF A5 5A`,用于在字节流中定位报文起始) | -| 4 | 4 | CAN ID(29 位扩展帧),直接用于分流 | -| 8 | 1 | DLC,固定 `8` | -| 9 | 8 | CAN 数据段 | -| 17 | 1 | 校验和:canId 四字节 + DLC + 数据八字节求和取低 8 位 | +| 0 | 1 | 帧头 `0xAA`(用于在数据报中定位报文起始) | +| 1 | 4 | CAN ID(29 位扩展帧),直接用于分流 | +| 5 | 1 | DLC,固定 `8` | +| 6 | 8 | CAN 数据段 | +| 14 | 1 | 校验和:帧头 + canId 四字节 + DLC + 数据八字节 + 帧尾求和取低 8 位 | +| 15 | 1 | 帧尾 `0xBB` | -收包时逐字节找帧头、校验不过右移一字节继续找,容忍粘包、拆包与杂包。 +收包时逐字节找帧头,帧头/帧尾/校验任一不过就右移一字节继续找,容忍粘包、拆包与杂包。 **上行 7 条**(遥控器 → 车端)按协议「报文周期时间」列分三组重发:**50ms** 底盘 / 机械臂1 / 机械臂2 / 急停,**100ms** 洗消指令 / 洗消调试,**1000ms** 气体检测仪。界面上层只更新控制状态, @@ -77,12 +78,26 @@ make -j4 车速 0~5.00 km/h、发泡倍数 6~12 倍、泡沫比例 0~10%、空压机流量 0~340 L/min、柱塞泵流量 0~24 L/min;调试维护页三泵转速上限各按协议(空压机 3000 / 柱塞泵 1450 / 叶片泵 2000 rpm)。 -顶部「通信正常/异常」按收帧判定:连续 **2s** 未收到任何合法帧即判为异常(最慢的下行周期 -1000ms)。UDP 端口绑定失败(如 9000 已被占用)时不启动定时器、不崩溃,界面显示「通信异常」。 +顶部左区**固定**显示一行通信状态,不再轮播:`CarCommunication::updateCommText()` 按 +`All_Comm_FaultCode` 与链路状态渲染进 `Telemetry::commText`,`TitleBar` 只决定配色—— +正常为绿字「系统通信正常」,有码时用该码的文案(如「机械臂通信异常」),链路断且无码时为 +「通信异常」;异常时整条指示器红色闪烁。 -车端 CAN 走 UDP,端口在 `comm/car.cpp` 中以常量固定:本机 **9000** 收、上行命令发往 **127.0.0.1:9001**。 +各项子系统故障的明细不在顶部,而是进对应卡片的**「故障」行**(`DataCard::setFaults`): +底盘、洗消、气体检测仪三张卡各一行,**无故障时整行隐藏**,有故障时显示并红色闪烁, +多条按 **1s** 轮流显示。机械臂卡没有这一行——协议里机械臂那四帧只有位置/姿态/关节角, +没有故障信号。洗消卡那一行是「洗消系统 + 泵」;气体检测仪的告警单独一份 +(`Telemetry::detectorFaults`),不会再去点亮洗消页底栏的「系统故障」灯。 + +链路判定:连续 **2s** 未收到任何合法帧即判为异常(最慢的下行周期 1000ms),此时顶部那行 +显示不带故障码的「通信异常」。UDP 端口绑定失败(如 9000 已被占用)时不启动定时器、不崩溃, +同样显示「通信异常」。 + +车端 CAN 走 UDP,地址在 `comm/car.cpp` 中以常量固定:本机绑 **AnyIPv4:9000** 收、上行命令发往 +车端 **192.168.1.224:3001**(`kCanTxHost`,改车端 IP 只动这一处)。 遥控器仍走串口:`comm/uart.cpp` 打开 `rc/device`(默认 `/dev/ttyS7`)、`rc/baud`(默认 `115200`)。 -`config.ini`(程序目录)中与通信相关的配置还有 `net/selfcheck_fault`(自检故障演示)。 +`config.ini`(程序目录)中与通信相关的配置还有 `net/selfcheck_fault`(自检故障演示,作用于 +`清水液位` 这一项,默认开)。 参数(车速、柱塞泵流量、发泡倍数、泡沫比例、空压机流量)与密码经 `QSettings` 持久化,上电自动读取。 `tools/can_sender.py` 是车端 CAN 的 UDP 联调工具(给 UI 发反馈时目标端口用 **9000**); @@ -90,8 +105,13 @@ make -j4 ### 已知缺口 -- **自检 10 项无对应 CAN 报文**,`sendSelfCheckStart()` 暂由本端跑完序列(见 `onSelfCheckTick` - 的 TODO),仅为维持 ④自检 → ⑤系统首页面 流程不中断;拿到真实报文后替换该本地驱动。 +- **自检 14 项里 0~9 项无对应 CAN 报文**(通信系统/各分系统通信取自 `All_Comm_FaultCode`,其余为 + 清水液位),`sendSelfCheckStart()` 暂由本端计时直接判过(见 `onSelfCheckTick` 的 TODO), + 仅为维持 ④自检 → ⑤系统首页面 流程不中断;拿到真实报文后替换该本地驱动。 +- **自检的球阀 4 项(D1/D2 开启、关闭)是真检查**:`onSelfCheckTick` 会置 `Decontam_DebugCmd` + 的调试模式位并写目标阀位(命令帧由 100ms 周期定时器持续重发),再等 `MBV_D1_Sta` / + `MBV_D2_Sta` 反馈与指令一致才算通过;`kValveWaitTicks`(默认 18 拍 ≈ 8s)内未到位、 + 或车端根本没有反馈(通信异常)都判故障。自检结束后恢复原有的调试模式位。 - **行车/机械臂模式**没有对应的下行信号:协议 12 条反馈里没有该字段,`telemetry.driveMode` 无处可填,故模式只由遥控器报文决定(遥控器离线时保持上次值)。 - **`Vehi_spd` 按协议取 8 位**(bit24、精度 0.01 → 0~2.55 km/h)。协议表里该信号与其后的 diff --git a/app/appcontext.cpp b/app/appcontext.cpp index f9172df..73b54f1 100644 --- a/app/appcontext.cpp +++ b/app/appcontext.cpp @@ -172,12 +172,30 @@ void AppContext::triggerEstop() void AppContext::setMode(Mode m) { + // 洗消作业期间强制机械臂模式:作业时末端要自动俯仰摆动,而 Arm_ControlCmd1 的 + // 使能位就挂在这个模式上。遥控器**每帧**都会调进来,所以这个覆盖只能放在这里—— + // 放到 sendWashStart 之类的地方,会被下一帧遥控器报文冲掉。 + if (washRunning) m = Mode::Arm; + if (mode == m) return; mode = m; emit modeChanged(m); if (m_comm) m_comm->setArmMode(m == Mode::Arm); // 同步到 Arm_ControlCmd1 的 Arm_modeCmd/Arm_enable } +void AppContext::setWashRunning(bool on) +{ + if (on == washRunning) return; + washRunning = on; + + if (on) { + m_modeBeforeWash = mode; + setMode(Mode::Arm); // 覆盖生效,进入洗消状态 + } else { + setMode(m_modeBeforeWash); // 作业结束,还原作业前的模式 + } +} + // ===================== 遥控器(24 字节二进制帧) ===================== void AppContext::onRcFrame(const RcFrame& f) { @@ -236,7 +254,7 @@ void AppContext::applyRcArm(const RcFrame& f) const int d = allowed ? axisDir(vals[i]) : 0; if (d == m_rcAxisDir[i]) continue; // 方向未变,不重复发包 m_rcAxisDir[i] = d; - m_comm->sendArmMove(QString::fromLatin1(kArmAxes[i]), d); + m_comm->sendArmMoveRc(QString::fromLatin1(kArmAxes[i]), d); } } @@ -253,11 +271,11 @@ void AppContext::sendChassisCommand() else if (m_rcSteerDir < 0) turn = "left"; if (dir == "stop" && turn.isEmpty()) { - m_comm->sendChassisMove("stop", 0); + m_comm->sendChassisMoveRc("stop", 0); return; } const double spd = (dir == "stop") ? 0.0 : getDouble(keySpeed(), 4.5); - m_comm->sendChassisMove(dir, spd, turn); + m_comm->sendChassisMoveRc(dir, spd, turn); } void AppContext::updateRcDriving() @@ -270,6 +288,9 @@ void AppContext::updateRcDriving() if (driving == m_rcDriving) return; m_rcDriving = driving; + // 先告诉通信层再发信号:页面收到 rcDrivingChanged(false) 会重发按住的方向, + // 那一下必须畅通(让位标志得已经落下) + if (m_comm) m_comm->setRcActive(driving); emit rcDrivingChanged(driving); } @@ -280,15 +301,16 @@ void AppContext::onRcLinkChanged(bool ok) if (!ok) { // 链路中断:撤销所有遥控器动作,否则最后一条指令会被一直挂住。 // 不动急停状态——丢包导致的 bit4 读为 0 不应被当成遥控器下发的复位。 + // 这里必须走 Rc 入口:此刻 rcActive 还是真,走面板入口会被让位挡掉。 if (m_rcGearDir != 0 || m_rcSteerDir != 0) { m_rcGearDir = 0; m_rcSteerDir = 0; - m_comm->sendChassisMove("stop", 0); + m_comm->sendChassisMoveRc("stop", 0); } for (int i = 0; i < 6; ++i) { if (m_rcAxisDir[i] == 0) continue; m_rcAxisDir[i] = 0; - m_comm->sendArmMove(QString::fromLatin1(kArmAxes[i]), 0); + m_comm->sendArmMoveRc(QString::fromLatin1(kArmAxes[i]), 0); } } updateRcDriving(); diff --git a/app/appcontext.h b/app/appcontext.h index 562882e..2c1635c 100644 --- a/app/appcontext.h +++ b/app/appcontext.h @@ -34,9 +34,14 @@ public: // 全局状态 Mode mode = Mode::Drive; // 行车/机械臂模式(机械旋钮) bool estopTriggered = false; // 急停是否触发(需人工复位) - bool washRunning = false; // 洗消运行中(消除/消毒共用) bool gasRunning = false; // 气体检测仪检测中 + // 洗消作业开关(消除/消毒共用)。作业期间**强制机械臂模式**——末端要自动俯仰摆动, + // 而 Arm_ControlCmd1 的使能位就挂在这个模式上;作业结束后回到作业前的模式。 + // 页面不要直接改成员,走这个入口。 + void setWashRunning(bool on); + bool isWashRunning() const { return washRunning; } + // ===================== 存储区参数(QSettings 持久化) ===================== double getDouble(const QString& key, double def) const; void setDouble(const QString& key, double val); @@ -100,6 +105,9 @@ private: int m_rcAxisDir[6] = {0, 0, 0, 0, 0, 0}; // 顺序见 kArmAxes bool m_rcEstopPrev = false; bool m_rcDriving = false; + + bool washRunning = false; + Mode m_modeBeforeWash = Mode::Drive; // 洗消作业前的工作模式,作业结束后还原 }; // 便捷访问宏 diff --git a/app/global.cpp b/app/global.cpp index 7b16f55..305255f 100644 --- a/app/global.cpp +++ b/app/global.cpp @@ -8,33 +8,62 @@ #include #include -// 启动 500ms 闪烁:每 500ms 切换一次动态属性,QSS 据此在 40%↔100% 透明度间切换 -QTimer* startBlink(QWidget* w, const char* prop) +// ===================== 全局闪烁节拍 ===================== +BlinkBeat::BlinkBeat(QObject* parent) + : QObject(parent) { - // 复用已存在的定时器 - if (auto* t = w->findChild(QString("blink_") + prop)) - return t; - QTimer* t = new QTimer(w); - t->setObjectName(QString("blink_") + prop); - t->setInterval(500); - QObject::connect(t, &QTimer::timeout, w, [w, prop]() { - bool cur = w->property(prop).toBool(); - w->setProperty(prop, !cur); + m_timer = new QTimer(this); + m_timer->setInterval(500); // 闪烁半周期:亮 500ms、灭 500ms + connect(m_timer, &QTimer::timeout, this, [this]() { + m_on = !m_on; + emit toggled(m_on); + }); + m_timer->start(); +} + +BlinkBeat* BlinkBeat::instance() +{ + // 首次用到才建(此时 QApplication 已就绪),进程结束自然销毁。 + // 不挂在某个控件下:它要跨页面、跨控件地给所有闪烁源提供同一个相位。 + static BlinkBeat beat; + return &beat; +} + +namespace { +// 订阅句柄挂在被闪的控件下、名字固定,stopBlink 靠它找回并删掉。 +// QObject 析构时会自动断开它身上所有的连接,所以“删除句柄”就等于“退订”。 +QString blinkKey(const char* prop) { return QString("blinkconn_") + prop; } +} + +// 挂到全局节拍上:每拍翻转一次动态属性,QSS 据此在 40%↔100% 透明度间切换。 +// 用公共节拍而不是各起一个定时器——各自起的话相位各异,同屏几处会各闪各的。 +void startBlink(QWidget* w, const char* prop) +{ + if (w->findChild(blinkKey(prop))) + return; // 已订阅 + + QObject* holder = new QObject(w); + holder->setObjectName(blinkKey(prop)); + // 以 holder 作接收者:它随控件一起销毁,连接也就一并断开 + QObject::connect(BlinkBeat::instance(), &BlinkBeat::toggled, holder, [w, prop](bool on) { + w->setProperty(prop, on); w->style()->unpolish(w); w->style()->polish(w); w->update(); }); - w->setProperty(prop, true); - t->start(); - return t; + + // 立刻按当前相位显示,不然要等到下一拍 + w->setProperty(prop, BlinkBeat::instance()->on()); + w->style()->unpolish(w); + w->style()->polish(w); + w->update(); } void stopBlink(QWidget* w, const char* prop) { - if (auto* t = w->findChild(QString("blink_") + prop)) { - t->stop(); - delete t; - } + if (QObject* holder = w->findChild(blinkKey(prop))) + delete holder; // 删除句柄 = 退订 + w->setProperty(prop, false); w->style()->unpolish(w); w->style()->polish(w); @@ -101,6 +130,13 @@ bool ButtonFocusGlow::eventFilter(QObject* obj, QEvent* ev) parent->update(parent->rect()); } break; + case QEvent::EnabledChange: + // 光标一直停在按钮上时,禁用期间 Qt 仍给它留着 WA_UnderMouse;重新启用的一瞬间 + // :hover 规则就生效了(:disabled 排在 :hover 之后才压得住它),看着像"被选中"。 + // 这里把这层残影清掉,和 FocusIn 那条是同一件事:保证同时只有一个按钮呈选中样式。 + if (w->isEnabled() && w->underMouse()) + clearStaleHover(QApplication::focusWidget()); + break; case QEvent::MouseButtonPress: // 触摸/点击与 Tab 归一到同一个焦点:谁最后操作,谁就是唯一的选中 if (static_cast(ev)->button() == Qt::LeftButton @@ -264,6 +300,12 @@ QPushButton[numpad="true"]:focus { border: 2px solid #8ABFFF; } +/* ===== 数据卡片数值 ===== */ +/* 数值标签的颜色必须由这里给:卡片的行内样式只写字号/字重,不写 color。 + 行内样式优先级高于应用级样式表,写上 color 就会把下面的告警红 / 暗红闪一起盖掉。 + 本规则与 [alert] 同为单属性选择器,靠位置分胜负,故必须排在告警规则之前。 */ +QLabel[cardValue="true"] { color: TXT; } + /* ===== 告警文字(红色闪烁由 blink 属性驱动) ===== */ QLabel[alert="true"] { color: DAN; font-weight: 700; } QLabel[alert="true"][blink="false"] { color: rgba(229,72,77,102); } diff --git a/app/global.h b/app/global.h index 39f7604..6c28157 100644 --- a/app/global.h +++ b/app/global.h @@ -58,23 +58,26 @@ enum PageId { }; // ===================== 遥测数据(车载端 → UI) ===================== -// 由通信层解析车端反馈报文后填充,页面刷新时读取 +// 由通信层解析车端反馈报文后填充,页面刷新时读取。 +// 这里不留任何演示初值:数值字段只由报文写入,配套的 has* 标志在收到对应报文时置位, +// 页面按 dashIf() 判断“还没有值”,未收到时显示 "--"。 struct Telemetry { // 底盘 - int soc = 85; // 电量 % - double volt = 360.0; // 电压 V - double speed = 3.45; // 车速 km/h - bool handbrake = true; // 手刹 + int soc = 0; // 电量 % + double volt = 0.0; // 电压 V + double speed = 0.0; // 车速 km/h + bool handbrake = false; // 手刹 int driveMode = 0; // 行车/机械臂模式:0=行车 1=机械臂(遥控器拨钮→串口→底盘报文 mode) int rdyStatus = 0; // 底盘 ready:0=初始 1=高压上电完成 2=准备就绪 int gearPos = 0; // 挡位:0=N 1=D 2=R int parking = 0; // 驻车:0=未驻车 1=左电机 2=右电机 3=完全驻车 int awdMode = 0; // 四驱模式:0=二驱 1=四驱 2=四驱锁定 double steerAngle = 0.0; // 方向盘角度 + int alignStatus = 0; // 角度对中状态:0x11=对中失败、0xEE/0x55=成功、其它=未对中 QStringList chassisFaults; // 底盘故障(多条 2s 轮换) // 机械臂 - QVector js{30, 20, 0, 90, 45, 0}; // 关节角 J1~J6(°) + QVector js{0, 0, 0, 0, 0, 0}; // 关节角 J1~J6(°) QString pos = "--"; // 末端位置 XYZ QString pose = "--"; // 姿态 RX/RY/RZ double vel = 0.0; // 末端速度 @@ -96,11 +99,13 @@ struct Telemetry { QStringList washFaults; // 洗消故障代码 // 液位 - double levelA = 100.0; // 清水/药剂液位 L - double levelFoam = 30.0; // 泡沫液位 L + double levelA = 0.0; // 清水/药剂液位 L + double levelFoam = 0.0; // 泡沫液位 L + bool levelALow = false; // A组分/清水液位低(A_LiquidLevel_Low) + bool levelBLow = false; // B组分/泡沫液位低(B_LiquidLevel_Low) // 气体检测仪 - QString gasHost = "standby"; // 主机状态 standby/detecting/fault + QString gasHost; // 主机状态 standby/detecting/fault,空=未收到 QString gasType = "--"; // 有毒物种类 QString gasLevel = "--"; // 有毒物等级 double gasConc = 0.0; // 有毒物浓度 ppb @@ -110,7 +115,6 @@ struct Telemetry { bool d1close = true; // D1 关闭到位 bool d2open = false; // D2 开启到位 bool d2close = true; // D2 关闭到位 - QString valveFault; // 球阀故障码 // 泵 bool plunger = false; // 柱塞泵上电 @@ -118,6 +122,10 @@ struct Telemetry { bool leaf = false; // 叶片泵上电 QStringList pumpFaults; // 泵故障代码 + // 气体检测仪故障。单列一份、不并进 washFaults:洗消页底栏的“系统故障”灯 + // 只看洗消系统与泵,气体检测仪的告警要归到它自己的卡片上。 + QStringList detectorFaults; + // 遥控器(24 字节二进制帧,见 comm/uart.h) bool rcLinkOk = false; // 遥控器链路是否在线 int rcBattery = 0; // 遥控器电量 % @@ -138,13 +146,122 @@ struct Telemetry { // 全局 bool commOk = true; // 通信状态 bool estop = false; // 急停触发 + + // 顶部左区固定显示的一行通信状态,由 CarCommunication 渲染: + // 链路正常且 All_Comm_FaultCode=0 → “系统通信正常”;有码 → 该码的文案 + // (“机械臂通信异常”…);链路断开、没有码 → “通信异常”。 + // 各项子系统故障明细不在这里,而是各自进对应卡片的“故障”行。 + QString commText = QString::fromUtf8("系统通信正常"); + int commCode = 0; // All_Comm_FaultCode 原值,0 = 通信正常 + + // ---- 各报文是否已收到 ---- + // CarCommunication 每解析到一条报文就置位对应的 has*,页面按它决定“还没有值”时显示 "--"。 + // 按“一条报文覆盖哪些显示项”分组,而不是每个字段一个标志。 + bool hasBattery = false; // 0x18308180 底盘状态1:电量 / 电压 + bool hasSpeed = false; // 0x18318180 底盘状态2:车速 / 手刹 / Ready状态 + bool hasChassis4 = false; // 0x183B8180 底盘状态4:挡位 / 驻车 / 四驱 / 方向盘角度 + bool hasArmPose = false; // 0x18338180 机械臂状态2:末端速度 + bool hasJoints = false; // 0x18348180 / 0x18358180:关节角 J1~J6 + bool hasWash = false; // 0x18368180 洗消状态1:液位 / 流量 / 发泡倍数 + bool hasWashPress= false; // 0x18378180 洗消状态2:管路压力 / 空压机压力 + bool hasGasHost = false; // 0x18388180 检测仪状态1:主机状态 + bool hasGasConc = false; // 0x18398180 检测仪状态2:有毒物浓度 +}; + +// 遥测未收到(has* 为 false)时统一显示 "--" +inline QString dashIf(bool has, const QString& text) +{ + return has ? text : QStringLiteral("--"); +} + +// 气体检测仪主机状态 → 显示文案 +inline QString gasHostLabel(const Telemetry& t) +{ + if (!t.hasGasHost) return QStringLiteral("--"); + return t.gasHost == "detecting" ? QString::fromUtf8("检测中") + : t.gasHost == "fault" ? QString::fromUtf8("故障") + : QString::fromUtf8("待机"); +} + +// 洗消卡片“故障”行的内容:洗消系统与泵都算它,气体检测仪另走一份 detectorFaults。 +inline QStringList washCardFaults(const Telemetry& t) +{ + return t.washFaults + t.pumpFaults; +} + +// ---- 底盘的几个枚举信号 → 显示文案(取值见协议「信号值描述」列) ---- +inline QString gearLabel(int v) +{ + return v == 1 ? QString::fromUtf8("D挡") + : v == 2 ? QString::fromUtf8("R挡") + : QString::fromUtf8("N挡"); +} + +inline QString parkingLabel(int v) +{ + switch (v) { + case 1: return QString::fromUtf8("左电机驻车"); + case 2: return QString::fromUtf8("右电机驻车"); + case 3: return QString::fromUtf8("完全驻车"); + default: return QString::fromUtf8("未驻车"); + } +} + +inline QString awdLabel(int v) +{ + return v == 1 ? QString::fromUtf8("四驱") + : v == 2 ? QString::fromUtf8("四驱锁定") + : QString::fromUtf8("二驱"); +} + +inline QString rdyLabel(int v) +{ + return v == 1 ? QString::fromUtf8("高压上电完成") + : v == 2 ? QString::fromUtf8("准备就绪") + : QString::fromUtf8("初始状态"); +} + +// Angle_alignment_status:0 是"还没做过",与失败要分清楚。 +// 文案比协议描述短一点("未进行过角度对中"→"未进行过对中"):标签已经写了"角度对中状态", +// 值里再重复一遍就溢出行宽了(底盘卡正文区 548px)。 +inline QString alignLabel(int v) +{ + switch (v) { + case 0x11: return QString::fromUtf8("角度对中失败"); + case 0xEE: return QString::fromUtf8("角度对中成功"); + case 0x55: return QString::fromUtf8("已对中成功"); + default: return QString::fromUtf8("未进行过对中"); + } +} + +// ===================== 全局闪烁节拍 ===================== +// 界面上有好几处在“异常闪烁”:顶部通信状态、卡片故障行、告警横幅、底栏指示灯、 +// 自检结果……各自起定时器的话相位互不相同,同一屏上会各闪各的。 +// 这里做成**一个**公共节拍,所有闪烁都挂在它上面:整屏同时亮、同时灭。 +// 要改闪烁快慢只改这一处的 interval(500ms)。 +class BlinkBeat : public QObject +{ + Q_OBJECT +public: + static BlinkBeat* instance(); + + bool on() const { return m_on; } // 当前是“亮”还是“灭”,供刚订阅上来的对齐相位 + +signals: + void toggled(bool on); + +private: + explicit BlinkBeat(QObject* parent = nullptr); + + QTimer* m_timer = nullptr; + bool m_on = true; }; // ===================== 工具函数 ===================== -// 启动 500ms 闪烁:每 500ms 切换一次 QSS 动态属性(40% ↔ 100% 透明度由 QSS 规则体现) -QTimer* startBlink(QWidget* w, const char* prop = "blink"); +// 启动闪烁:挂到全局节拍上,每拍翻转一次动态属性(40% ↔ 100% 透明度由 QSS 规则体现) +void startBlink(QWidget* w, const char* prop = "blink"); -// 关闭闪烁定时器并复位属性 +// 退订全局节拍并复位属性 void stopBlink(QWidget* w, const char* prop = "blink"); // 退出/取消按钮预设样式:被选中(悬停/按下/焦点)时红底白字 diff --git a/comm/car.cpp b/comm/car.cpp index b47f982..6b519ad 100644 --- a/comm/car.cpp +++ b/comm/car.cpp @@ -6,38 +6,57 @@ #include #include #include +#include #include namespace { // ===================== 线格式 ===================== -// 一个 UDP 数据报承载 = 32 位帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和,共 18 字节。 -// 多字节字段按主机小端排列,与车载端网关一致;帧头与校验和用于确认数据报内容合法。 +// 一个 UDP 数据报承载 = 1 字节帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和 + 1 字节帧尾, +// 共 16 字节。多字节字段按主机小端排列,与车载端网关一致;帧头/帧尾与校验和用于确认数据报内容合法。 #pragma pack(push, 1) struct CanFrame { - quint32 header; // 固定帧头 0x5AA5FF00(线上字节序 00 FF A5 5A) + quint8 header; // 固定帧头 0xAA quint32 canId; // 29 位扩展帧 ID,直接用于分流 quint8 dlc; // 数据长度码,固定 8 quint8 data[8]; // 8 字节 CAN 原生数据段 - quint8 checksum; // canId 4 字节 + dlc + data[8],共 13 字节求和取低 8 位 + quint8 checksum; // 帧头 + canId 4 字节 + dlc + data[8] + 帧尾,共 15 字节求和取低 8 位 + quint8 tail; // 固定帧尾 0xBB }; #pragma pack(pop) -const quint32 kFrameHeader = 0x5AA5FF00u; +const quint8 kFrameHeader = 0xAA; +const quint8 kFrameTail = 0xBB; const int kFrameLen = int(sizeof(CanFrame)); const int kDataLen = 8; -// 车端 CAN 的 UDP 端口:本机 9000 收(车端反馈),上行命令发往 9001 +// 洗消作业期间机械臂末端的自动俯仰摆动:中位 ±1024(轴量程 0~4096)、4 秒一个往返。 +// 摆动在 50ms 那一组里推进(Arm_ControlCmd2 就在这组),所以一拍 = 50ms、 +// 一个往返 = 4s ÷ 50ms = 80 拍。kWashSwingAmp 别超过 kRcAxisMid,不然会顶到量程两端。 +const int kWashSwingAmp = 1024; +const int kWashSwingTicks = 80; +const int kSwingRampTicks = 10; // 回中 / 还原的过渡时长:10 拍 = 0.5s +const double kPi = 3.14159265358979323846; + +// 轴值线性插值并夹到量程 0~4096。回中与还原都走斜坡,别一步跳变把机械臂顿一下。 +quint16 lerpAxis(int from, int to, double t) +{ + return quint16(qBound(0, int(from + (to - from) * qBound(0.0, t, 1.0)), 4096)); +} + +// 车端 CAN 的 UDP 端口:本机 9000 收(车端反馈),上行命令发往车端 192.168.1.224:3001。 +// 收端绑 AnyIPv4(各网卡都收),发端必须写明车端 IP —— "0.0.0.0" 不是合法目的地址。 const quint16 kCanRxPort = 9000; -const quint16 kCanTxPort = 9001; -const char* kCanTxHost = "0.0.0.0"; +const quint16 kCanTxPort = 3001; +const char* kCanTxHost = "192.168.1.224"; quint8 canChecksum(quint32 canId, quint8 dlc, const quint8* data) { - quint8 sum = 0; + quint8 sum = kFrameHeader; // 帧头/帧尾也算进去(帧尾虽在校验和之后,双方按定长解析无妨) for (int i = 0; i < 4; ++i) sum += quint8((canId >> (8 * i)) & 0xFF); sum += dlc; for (int i = 0; i < kDataLen; ++i) sum += data[i]; + sum += kFrameTail; return sum; } @@ -182,42 +201,65 @@ const char* const kBrakeMotFaults[8] = { "驱动电路故障", "供电电压过低", "供电电压过高", }; -void addFault(QVector& out, const QString& system, const char* codePrefix, - quint8 code, const char* text) +void addFault(QVector& out, const QString& system, const QString& group, + const char* codePrefix, quint8 code, const char* text, int level = 0) { if (!text) return; CanFault f; - f.system = system; - f.code = QString::fromLatin1(codePrefix) + QString::number(code); - f.desc = QString::fromUtf8(text); + f.system = system; + f.group = group; + f.code = QString::fromLatin1(codePrefix) + QString::number(code); + f.codeNum = QString::number(code); + f.desc = QString::fromUtf8(text); + f.level = level; out.append(f); } // 未在码表中登记的码也上报,避免“有故障但界面无提示” -void addFaultByCode(QVector& out, const QString& system, const char* codePrefix, - const CodeText* tbl, int n, quint8 code) +void addFaultByCode(QVector& out, const QString& system, const QString& group, + const char* codePrefix, const CodeText* tbl, int n, quint8 code, int level = 0) { if (code == 0) return; const char* t = lookup(tbl, n, code); - if (t) addFault(out, system, codePrefix, code, t); - else { - CanFault f; - f.system = system; - f.code = QString::fromLatin1(codePrefix) + QString::number(code); - f.desc = QString::fromLatin1(codePrefix) + QString::number(code); - out.append(f); + if (t) { + addFault(out, system, group, codePrefix, code, t, level); + return; } + CanFault f; + f.system = system; + f.group = group; + f.code = QString::fromLatin1(codePrefix) + QString::number(code); + f.codeNum = QString::number(code); + f.desc = QString::fromLatin1(codePrefix) + QString::number(code); + f.level = level; + out.append(f); } -// 故障等级:>0 才上报,描述形如“单体电压过低二级故障” -void addLevelFault(QVector& out, const QString& system, const char* codePrefix, - const char* name, quint8 level) +// 故障等级:>0 才上报。等级型信号没有“码”,顶部显示成“<组名>:<描述>,N级” +void addLevelFault(QVector& out, const QString& system, const QString& group, + const char* codePrefix, const char* name, quint8 level) { if (level == 0 || level > 3) return; CanFault f; f.system = system; - f.code = QString::fromLatin1(codePrefix) + QString::number(level); - f.desc = QString::fromUtf8(name) + QString::fromUtf8(kLevels[level]); + f.group = group; + f.code = QString::fromLatin1(codePrefix) + QString::number(level); // 追溯用,顶部不显示 + f.desc = QString::fromUtf8(name); + f.level = level; + out.append(f); +} + +// 没有“故障码”的故障(单个故障位、方向盘对中状态这类无编码信号): +// 顶部只显示描述,tracePrefix+traceSuffix 仅作故障历史追溯用。 +void addNoCodeFault(QVector& out, const QString& system, const QString& group, + const char* tracePrefix, int traceSuffix, const char* text) +{ + if (!text) return; + CanFault f; + f.system = system; + f.group = group; + f.code = QString::fromLatin1(tracePrefix) + QString::number(traceSuffix); + f.desc = QString::fromUtf8(text); out.append(f); } @@ -225,8 +267,38 @@ const char* const kSysChassis = "机器人底盘"; const char* const kSysWash = "洗消系统"; const char* const kSysDetector = "气体检测仪"; +// 顶部轮播的显示组名。前三个取自协议 CSV 的故障分组列(底盘状态 1~4 各自归属), +// 洗消/检测仪协议里没给分组列,按 system 命名。某组没有故障时顶部就不出现该组名。 +const char* const kGroupChassisMotor = "底盘电机故障"; +const char* const kGroupBms = "BMS系统故障"; +const char* const kGroupSteerBrake = "转向制动系统故障"; +const char* const kGroupWash = "洗消系统故障"; +const char* const kGroupDetector = "气体检测仪故障"; +// All_Comm_FaultCode 本身也是“一组”故障,顶部按“通信异常:3-机械臂通信异常”显示 +const char* const kGroupComm = "通信异常"; + +// All_Comm_FaultCode → 顶部那行的文案。表里写的就是“机械臂通信异常”这种, +// 不再加“xx系统”前缀。码不认识时退回笼统的“通信异常”。 +QString commFaultText(quint8 code) +{ + for (const CodeText& c : kCommFaults) + if (c.code == code) return QString::fromUtf8(c.text); + return QString::fromUtf8(kGroupComm); +} + } // namespace +// 完整一行:<组名>:[<码>-]<描述>[,级];desc 为空则只显示组名(链路超时用) +QString CanFault::text() const +{ + if (desc.isEmpty()) return group; + QString s = group + QString::fromUtf8(":"); + if (!codeNum.isEmpty()) s += codeNum + "-"; + s += desc; + if (level > 0) s += QString::fromUtf8(",%1级").arg(level); + return s; +} + // ===================== 构造与生命周期 ===================== CarCommunication::CarCommunication(Telemetry* tel, QObject* parent) : QObject(parent), m_tel(tel) @@ -287,6 +359,7 @@ bool CarCommunication::start() // 端口被占用时走这里:不启动定时器,界面显示通信异常 qWarning() << "车端 UDP 端口绑定失败:" << kCanRxPort << m_sock->errorString(); setCommOk(false); + updateCommText(); // 收不到帧也不会有 cacheFaults,顶部得靠这里才显示“通信异常” return false; } m_hbTimer->start(); @@ -311,7 +384,6 @@ void CarCommunication::setCommOk(bool ok) if (ok == m_lastCommOk) return; m_lastCommOk = ok; if (m_tel) m_tel->commOk = ok; - emit commOkChanged(ok); } // ===================== 收包与分流 ===================== @@ -319,81 +391,97 @@ void CarCommunication::setCommOk(bool ok) // 不再需要串口时代那套跨读字节流的残帧缓冲与滑窗重同步。 void CarCommunication::onDatagram() { + bool got = false; while (m_sock->hasPendingDatagrams()) { const QByteArray d = m_sock->receiveDatagram().data(); int off = 0; while (off + kFrameLen <= d.size()) { CanFrame f; std::memcpy(&f, d.constData() + off, kFrameLen); - if (f.header == kFrameHeader && f.dlc == kDataLen + if (f.header == kFrameHeader && f.tail == kFrameTail && f.dlc == kDataLen && f.checksum == canChecksum(f.canId, f.dlc, f.data)) { handleFrame(f.canId, f.data); + got = true; off += kFrameLen; } else { ++off; // 数据报内可能有杂字节,向后找下一个帧头 } } } + + // 一批数据报**全部解完**再统一刷一次界面。 + // 积压时 Telemetry 里留下的本来就是各字段的最新值,中间每帧都推一遍纯属浪费—— + // 一批 100 帧会变成 100×(7 个页面 + 8 个底条) 次刷新,而界面只需要最后那一次。 + // 只在"同一批"里合并,不额外等定时器,所以不引入延迟。 + if (!got) + return; + m_hbTimer->start(); // 收到任意合法帧即刷新链路(CAN 无独立心跳报文) + setCommOk(true); + cacheFaults(); + emit telemetryUpdated(); } void CarCommunication::handleFrame(quint32 canId, const quint8* d) { if (!m_tel) return; - // 收到任意合法帧即刷新链路(CAN 无独立心跳报文) - m_hbTimer->start(); - setCommOk(true); - + // 这里只把数据写进 Telemetry,不动链路状态、也不刷界面——那两件事统一在 + // onDatagram() 排空一批之后做一次,这样一大批积压也只会重绘一次。 switch (canId) { // ---------- 0x18308180 底盘状态1(BMS):电池总况与故障等级 ---------- case kIdVehicleFb1: { m_tel->volt = phys(intelBits(d, 0, 16), 0.1); m_tel->soc = int(phys(intelBits(d, 16, 16), 0.1)); + m_tel->hasBattery = true; m_vsf1Faults.clear(); - addFaultByCode(m_vsf1Faults, QString::fromUtf8(kSysChassis), "MCU", + // 底盘电机:故障码(8位)与故障等级(2位)是配套下发的两个信号,协议注明“不会出现 + // 等级为 0 却有故障码”的情况,故合并成一条“底盘电机故障:18-电机过温警告,1级”。 + addFaultByCode(m_vsf1Faults, QString::fromUtf8(kSysChassis), + QString::fromUtf8(kGroupChassisMotor), "MCU", kMcuFaults, int(sizeof(kMcuFaults) / sizeof(CodeText)), - quint8(intelBits(d, 32, 8))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "MCUL", - "底盘电机", quint8(intelBits(d, 40, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CELLL", - "单体电压过低", quint8(intelBits(d, 45, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CELLHL", - "单体电压过高", quint8(intelBits(d, 47, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "VOLTL", - "总电压过低", quint8(intelBits(d, 49, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "VOLTHL", - "总电压过高", quint8(intelBits(d, 51, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "DLTPL", - "放电低温", quint8(intelBits(d, 53, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "DHTPL", - "放电高温", quint8(intelBits(d, 55, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CLTPL", - "充电低温", quint8(intelBits(d, 57, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CHTPL", - "充电高温", quint8(intelBits(d, 59, 2))); - addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "TIL", - "温度不均衡", quint8(intelBits(d, 61, 2))); + quint8(intelBits(d, 32, 8)), int(intelBits(d, 40, 2))); + // 以下 *FaultLevel 都归 BMS 系统 + const QString bms = QString::fromUtf8(kGroupBms); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "CELLL", "单体电压过低", quint8(intelBits(d, 45, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "CELLHL", "单体电压过高", quint8(intelBits(d, 47, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "VOLTL", "总电压过低", quint8(intelBits(d, 49, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "VOLTHL", "总电压过高", quint8(intelBits(d, 51, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "DLTPL", "放电低温", quint8(intelBits(d, 53, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "DHTPL", "放电高温", quint8(intelBits(d, 55, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "CLTPL", "充电低温", quint8(intelBits(d, 57, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "CHTPL", "充电高温", quint8(intelBits(d, 59, 2))); + addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms, + "TIL", "温度不均衡", quint8(intelBits(d, 61, 2))); break; } // ---------- 0x18318180 底盘状态2(BMS):手刹 / 车速 / 继电器 / 通信 ---------- case kIdVehicleFb2: { m_vsf2Faults.clear(); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "VIL", - "压差不均衡", quint8(intelBits(d, 0, 2))); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "DOCL", - "放电过流", quint8(intelBits(d, 2, 2))); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "COCL", - "充电过流", quint8(intelBits(d, 4, 2))); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "INCL", - "内部通讯", quint8(intelBits(d, 6, 2))); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "EXCL", - "外部通讯", quint8(intelBits(d, 8, 2))); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "SOCL", - "SOC过低", quint8(intelBits(d, 10, 2))); - addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "INSL", - "绝缘", quint8(intelBits(d, 12, 2))); + const QString bms = QString::fromUtf8(kGroupBms); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "VIL", "压差不均衡", quint8(intelBits(d, 0, 2))); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "DOCL", "放电过流", quint8(intelBits(d, 2, 2))); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "COCL", "充电过流", quint8(intelBits(d, 4, 2))); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "INCL", "内部通讯", quint8(intelBits(d, 6, 2))); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "EXCL", "外部通讯", quint8(intelBits(d, 8, 2))); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "SOCL", "SOC过低", quint8(intelBits(d, 10, 2))); + addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, + "INSL", "绝缘", quint8(intelBits(d, 12, 2))); // 继电器粘连:0=正常 非 0=故障 struct { int bit; const char* name; const char* code; } relays[] = { @@ -404,7 +492,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) }; for (const auto& r : relays) { if (intelBits(d, r.bit, 2) != 0) - addFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), r.code, 1, r.name); + addFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms, r.code, 1, r.name); } m_tel->handbrake = intelBits(d, 22, 2) != 0; @@ -412,9 +500,13 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) // Reserve(34)、All_Comm_FaultCode(40) 的起始位与 8 位布局自洽,故按协议取值。 m_tel->speed = phys(intelBits(d, 24, 8), 0.01); m_tel->rdyStatus = int(intelBits(d, 32, 2)); + m_tel->hasSpeed = true; const quint8 comm = quint8(intelBits(d, 40, 8)); // All_Comm_FaultCode - addFaultByCode(m_vsf2Faults, QString::fromUtf8(kSysChassis), "COMM", + m_commFaultCode = comm; + m_commFaults.clear(); + addFaultByCode(m_commFaults, QString::fromUtf8(kSysChassis), + QString::fromUtf8(kGroupComm), "COMM", kCommFaults, int(sizeof(kCommFaults) / sizeof(CodeText)), comm); break; } @@ -422,17 +514,20 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) // ---------- 0x183A8180 底盘状态3:EPS / 制动故障位 ---------- case kIdVehicleFb3: { m_vsf3Faults.clear(); + const QString sys = QString::fromUtf8(kSysChassis); + const QString brake = QString::fromUtf8(kGroupSteerBrake); for (int i = 0; i < 46; ++i) { if (intelBits(d, i, 1) != 0) - addFault(m_vsf3Faults, QString::fromUtf8(kSysChassis), "EPS", quint8(i), - kVsb3Bits[i]); + addNoCodeFault(m_vsf3Faults, sys, brake, "EPS", i, kVsb3Bits[i]); } const quint8 eps = quint8(intelBits(d, 48, 8)); if (eps != 0) { CanFault f; - f.system = QString::fromUtf8(kSysChassis); - f.code = QString("EPSC") + QString::number(eps); - f.desc = QString("EPS故障码") + QString::number(eps); + f.system = sys; + f.group = brake; + f.code = QString("EPSC") + QString::number(eps); + f.codeNum = QString::number(eps); + f.desc = QString::fromUtf8("EPS故障码"); m_vsf3Faults.append(f); } break; @@ -445,19 +540,25 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) m_tel->parking = int(intelBits(d, 2, 2)); // 0=未驻车 1=左电机 2=右电机 3=完全驻车 m_tel->awdMode = int(intelBits(d, 4, 2)); // 0=二驱 1=四驱 2=四驱锁定 m_tel->steerAngle = double(intelBits(d, 8, 16)); + m_tel->hasChassis4 = true; + // ALIGN 是状态字节(0x11=对中失败、0xEE/0x55=正常),不是故障码,顶部只显示描述 + const QString sys = QString::fromUtf8(kSysChassis); + const QString brake = QString::fromUtf8(kGroupSteerBrake); const quint8 align = quint8(intelBits(d, 24, 8)); - if (align == 0x11) addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "ALIGN", 1, "方向盘对中失败"); + m_tel->alignStatus = align; // 底盘页「角度对中状态」行 + if (align == 0x11) + addNoCodeFault(m_vsf4Faults, sys, brake, "ALIGN", 1, "方向盘对中失败"); else if (align != 0xEE && align != 0x55) - addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "ALIGN", 0, "方向盘未对中"); + addNoCodeFault(m_vsf4Faults, sys, brake, "ALIGN", 0, "方向盘未对中"); // 注:协议表把两个信号的“信号值描述”写反了(Brake_Right 描述成左电机、 // Brake_Left 描述成右电机),此处以信号名为准。 const quint8 br = quint8(intelBits(d, 32, 8)); - if (br != 0) addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "BRKR", br, + if (br != 0) addFault(m_vsf4Faults, sys, brake, "BRKR", br, br < 8 ? kBrakeMotFaults[br] : "制动电机故障"); const quint8 bl = quint8(intelBits(d, 40, 8)); - if (bl != 0) addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "BRKL", bl, + if (bl != 0) addFault(m_vsf4Faults, sys, brake, "BRKL", bl, bl < 8 ? kBrakeMotFaults[bl] : "制动电机故障"); break; } @@ -477,6 +578,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) .arg(intelSigned(d, 16, 16)) .arg(intelSigned(d, 32, 16)); m_tel->vel = double(intelSigned(d, 48, 8)); + m_tel->hasArmPose = true; break; } // ---------- 0x18348180 机械臂状态3:关节角 J1~J4 ---------- @@ -484,6 +586,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) if (m_tel->js.size() < 6) m_tel->js.resize(6); for (int i = 0; i < 4; ++i) m_tel->js[i] = double(intelSigned(d, i * 16, 16)); + m_tel->hasJoints = true; break; } // ---------- 0x18358180 机械臂状态4:关节角 J5~J6 ---------- @@ -491,6 +594,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) if (m_tel->js.size() < 6) m_tel->js.resize(6); m_tel->js[4] = double(intelSigned(d, 0, 16)); m_tel->js[5] = double(intelSigned(d, 16, 16)); + m_tel->hasJoints = true; break; } @@ -511,12 +615,18 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) m_tel->d2close = !m_tel->d2open; m_washFaults.clear(); + const QString sys = QString::fromUtf8(kSysWash); + const QString grp = QString::fromUtf8(kGroupWash); if (intelBits(d, 10, 1) != 0) // Sys_fault:无分路码,只报总故障 - addFault(m_washFaults, QString::fromUtf8(kSysWash), "SYS", 1, "洗消系统故障"); - if (intelBits(d, 6, 1) != 0) - addFault(m_washFaults, QString::fromUtf8(kSysWash), "LVL", 1, "A组分/清水液位低"); - if (intelBits(d, 7, 1) != 0) - addFault(m_washFaults, QString::fromUtf8(kSysWash), "LVL", 2, "B组分液位低"); + addNoCodeFault(m_washFaults, sys, grp, "SYS", 1, "洗消系统故障"); + const bool lowA = intelBits(d, 6, 1) != 0; + const bool lowB = intelBits(d, 7, 1) != 0; + if (lowA) + addNoCodeFault(m_washFaults, sys, grp, "LVL", 1, "A组分/清水液位低"); + if (lowB) + addNoCodeFault(m_washFaults, sys, grp, "LVL", 2, "B组分液位低"); + m_tel->levelALow = lowA; // 首页/作业选择中间区域的液位数值据此红闪 + m_tel->levelBLow = lowB; m_tel->levelA = double(intelBits(d, 16, 8)); m_tel->levelFoam = double(intelBits(d, 24, 8)); @@ -524,6 +634,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) m_tel->flowB = double(intelBits(d, 40, 8)); m_tel->gasFlow = double(intelBits(d, 48, 8)); m_tel->foam = double(intelBits(d, 56, 8)); + m_tel->hasWash = true; break; } // ---------- 0x18378180 洗消工作状态2:压力 / 流量反馈 / 泵故障码 ---------- @@ -533,12 +644,14 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) m_tel->flowPlungerFb = double(intelBits(d, 16, 8)); m_tel->flowAirFb = double(intelBits(d, 32, 8)); m_tel->flowVaneFb = double(intelBits(d, 40, 8)); + m_tel->hasWashPress = true; m_pumpFaults.clear(); - addFaultByCode(m_pumpFaults, QString::fromUtf8(kSysWash), "AIR", + const QString washGrp = QString::fromUtf8(kGroupWash); + addFaultByCode(m_pumpFaults, QString::fromUtf8(kSysWash), washGrp, "AIR", kAirCompFaults, int(sizeof(kAirCompFaults) / sizeof(CodeText)), quint8(intelBits(d, 48, 8))); - addFaultByCode(m_pumpFaults, QString::fromUtf8(kSysWash), "PLG", + addFaultByCode(m_pumpFaults, QString::fromUtf8(kSysWash), washGrp, "PLG", kPlungerFaults, int(sizeof(kPlungerFaults) / sizeof(CodeText)), quint8(intelBits(d, 56, 8))); break; @@ -552,6 +665,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) m_tel->gasHost = (warn & 0x04) ? QString("fault") : (sta == 4) ? QString("detecting") : QString("standby"); + m_tel->hasGasHost = true; // Toxic_types 是位组合:1=P类 2=S类 4=HNO类 8=CH类 16=AS类 const quint8 types = quint8(intelBits(d, 8, 8)); @@ -567,25 +681,24 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d) m_tel->gasLevel = (lvl >= 1 && lvl <= 5) ? QString("%1级").arg(lvl) : QString("--"); m_detectorFaults.clear(); + // 告警是位组合(1/2/4/8),没有故障码,顶部只显示描述 for (const CodeText& w : kDetectorWarns) if (warn & w.code) - m_detectorFaults.append(CanFault{QString::fromUtf8(kSysDetector), - QString::fromLatin1("W") + QString::number(w.code), - QString::fromUtf8(w.text)}); + addNoCodeFault(m_detectorFaults, QString::fromUtf8(kSysDetector), + QString::fromUtf8(kGroupDetector), "W", w.code, + w.text); break; } // ---------- 0x18398180 检测仪结果状态2:40 位浓度(ppb) ---------- case kIdDetectorFb2: { m_tel->gasConc = double(intelBits(d, 0, 40)); + m_tel->hasGasConc = true; break; } default: - return; // 未知 canId 不触发刷新(分类四本次未实现) + break; // 未知 canId(分类四本次未实现):这一帧不写任何东西 } - - cacheFaults(); - emit telemetryUpdated(); } // 把各帧的故障合并进 Telemetry,并把“新出现”的故障写入故障历史 @@ -596,35 +709,70 @@ void CarCommunication::cacheFaults() const QVector*> groups = { &m_vsf1Faults, &m_vsf2Faults, &m_vsf3Faults, &m_vsf4Faults, }; + + // 页面按 desc 显示:等级型故障补回“一级故障/二级故障/三级故障”后缀, + // 与顶部轮播的“,N级”写法不同,保持页面既有文案不变。 + auto pageText = [](const CanFault& f) { + return f.level > 0 ? f.desc + QString::fromUtf8(kLevels[f.level]) : f.desc; + }; + QStringList chassis; for (const QVector* g : groups) - for (const CanFault& f : *g) chassis << f.desc; + for (const CanFault& f : *g) chassis << pageText(f); m_tel->chassisFaults = chassis; QStringList wash; - for (const CanFault& f : m_washFaults) wash << f.desc; - for (const CanFault& f : m_detectorFaults) wash << f.desc; + for (const CanFault& f : m_washFaults) wash << pageText(f); m_tel->washFaults = wash; QStringList pump; - for (const CanFault& f : m_pumpFaults) pump << f.desc; + for (const CanFault& f : m_pumpFaults) pump << pageText(f); m_tel->pumpFaults = pump; + // 气体检测仪单列一份:洗消页底栏的“系统故障”灯只认洗消与泵, + // 检测仪的告警归到它自己的卡片,不能顺带点亮洗消的故障灯。 + QStringList det; + for (const CanFault& f : m_detectorFaults) det << pageText(f); + m_tel->detectorFaults = det; + + updateCommText(); + // 周期报文会反复报到同一故障,只在首次出现时写故障历史 const QVector*> all = { &m_vsf1Faults, &m_vsf2Faults, &m_vsf3Faults, &m_vsf4Faults, - &m_washFaults, &m_pumpFaults, &m_detectorFaults, + &m_washFaults, &m_pumpFaults, &m_detectorFaults, &m_commFaults, }; for (const QVector* g : all) { for (const CanFault& f : *g) { const QString key = f.system + "|" + f.code; if (m_reportedFaults.contains(key)) continue; m_reportedFaults.insert(key); - emit faultAppended(f.system, f.code, f.desc); + emit faultAppended(f.system, f.code, pageText(f)); } } } +// 顶部左区固定显示的那一行:只反映通信状态,不再轮播各项故障明细 +// (底盘/洗消/气体检测仪的明细各自进对应卡片的“故障”行)。 +void CarCommunication::updateCommText() +{ + if (!m_tel) return; + + QString text; + if (!m_lastCommOk) + text = QString::fromUtf8(kGroupComm); // 链路断时没有码,只能笼统报“通信异常” + else if (m_commFaultCode != 0) + text = commFaultText(quint8(m_commFaultCode)); + else + text = QString::fromUtf8("系统通信正常"); + + // 周期报文会反复刷同样的内容,只有真变了才通知顶部 + if (text == m_tel->commText && m_commFaultCode == m_tel->commCode) return; + m_tel->commText = text; + m_tel->commCode = m_commFaultCode; + emit commTextChanged(); +} + // ===================== 发包 ===================== void CarCommunication::sendFrame(quint32 canId, const quint8* data) { @@ -636,10 +784,51 @@ void CarCommunication::sendFrame(quint32 canId, const quint8* data) f.dlc = kDataLen; std::memcpy(f.data, data, kDataLen); f.checksum = canChecksum(canId, f.dlc, f.data); + f.tail = kFrameTail; m_sock->writeDatagram(reinterpret_cast(&f), kFrameLen, QHostAddress(kCanTxHost), kCanTxPort); } +// 洗消作业期间的末端俯仰动作,每 50ms 一拍,共三段: +// 启动 → SwingCentering:先把俯仰轴斜坡放回中位,到位后转 SwingSine +// 作业 → SwingSine: 从中位起按正弦来回,一个往返 kWashSwingTicks 拍 +// 停止 → SwingRestoring:从当前的摆动值斜坡还原成作业前的轴值,到位后收手 +// 这三轴在协议里叫「末端姿态俯仰动作」,是像摇杆偏摆那样的量(中位 = 不动),不是绝对位置: +// 所以"回中心"就是把轴值放回 kRcAxisMid,"回到洗消前的位置"就是放回 m_pitchBeforeWash。 +// 停的时候**必须**把轴值收回到一个不动值——Arm_ControlCmd2 是周期报文, +// 停在哪个值,车端就认为还在给哪个俯仰指令、末端会一直走下去。 +void CarCommunication::updateWashSwing() +{ + switch (m_swingState) { + case SwingOff: + return; + + case SwingCentering: + m_pitch = lerpAxis(m_pitchBeforeWash, kRcAxisMid, double(++m_swingTick) / kSwingRampTicks); + if (m_swingTick >= kSwingRampTicks) { + m_swingState = SwingSine; + m_swingPhase = 0; // 起摆点固定是中位,作业过程可复现 + } + return; + + case SwingSine: { + const double ph = 2.0 * kPi * m_swingPhase / kWashSwingTicks; + // 夹一下:万一以后把 kWashSwingAmp 调过 kRcAxisMid,负数转 quint16 会翻成极值 + m_pitch = quint16(qBound(0, int(kRcAxisMid + kWashSwingAmp * std::sin(ph)), 4096)); + m_swingPhase = (m_swingPhase + 1) % kWashSwingTicks; + return; + } + + case SwingRestoring: + m_pitch = lerpAxis(m_swingFrom, m_pitchBeforeWash, double(++m_swingTick) / kSwingRampTicks); + if (m_swingTick >= kSwingRampTicks) { + m_pitch = m_pitchBeforeWash; // 收尾对齐,别停在插值的截断值上 + m_swingState = SwingOff; + } + return; + } +} + // 50ms 组:Chassis_ControlCmd / Arm_ControlCmd1 / Arm_ControlCmd2 / Emergency_Cmd void CarCommunication::onSendFast() { @@ -664,6 +853,8 @@ void CarCommunication::onSendFast() setIntelBits(d, 40, 16, m_frontrear); sendFrame(kIdArmCmd1, d); } + // 洗消作业期间的末端俯仰摆动:这一拍该发什么轴值由它决定 + updateWashSwing(); { quint8 d[8] = {0}; setIntelBits(d, 0, 16, m_roll); @@ -727,23 +918,34 @@ void CarCommunication::onSendSlow() } // ===================== 上行命令 → 控制状态 ===================== +// 方向名 → 轴量:档位 后 0 / 中 2048 / 前 4096;转向 左 0 / 中 2048 / 右 4096 +static void chassisAxis(const QString& dir, const QString& turn, quint16& gear, quint16& turning) +{ + gear = (dir == "fwd") ? 4096 : (dir == "back") ? 0 : kRcAxisMid; + turning = (turn == "left") ? 0 : (turn == "right") ? 4096 : kRcAxisMid; +} + void CarCommunication::sendChassisMove(const QString& dir, double speed, const QString& turn) { - // 档位推杆量:后 0 / 中 2048 / 前 4096 - if (dir == "fwd") m_gear = 4096; - else if (dir == "back") m_gear = 0; - else m_gear = kRcAxisMid; + if (m_rcActive) return; // 遥控器正在操作:面板这一路让位(遥控器松手后页面会重发) + sendChassisMoveRc(dir, speed, turn); +} - // 转向推杆量:左 0 / 中 2048 / 右 4096 - if (turn == "left") m_turning = 0; - else if (turn == "right") m_turning = 4096; - else m_turning = kRcAxisMid; +void CarCommunication::sendChassisMoveRc(const QString& dir, double speed, const QString& turn) +{ + chassisAxis(dir, turn, m_gear, m_turning); // Vehi_TargetSpd 精度 0.01 km/h,量程 0~500(即 0~5.00 km/h) m_targetSpd = quint16(qBound(0, int(qRound(speed * 100.0)), 500)); } void CarCommunication::sendArmMove(const QString& axis, int dir) +{ + if (m_rcActive) return; // 遥控器正在操作:面板这一路让位(遥控器松手后页面会重发) + sendArmMoveRc(axis, dir); +} + +void CarCommunication::sendArmMoveRc(const QString& axis, int dir) { // 轴位推杆量:dir +1 / -1 / 0 → 4096 / 0 / 中位 const quint16 v = (dir > 0) ? 4096 : (dir < 0) ? 0 : kRcAxisMid; @@ -780,11 +982,32 @@ void CarCommunication::sendWashStart(const QString& mode) { m_decontamMode = (mode == "disinfect") ? 3 : 2; // 3=手动消毒 2=手动消除 m_decontamStart = true; + + // 末端俯仰摆动:记下作业前的轴值,先回中心再起摆(见 updateWashSwing) + m_pitchBeforeWash = m_pitch; + m_swingTick = 0; + if (m_pitch == kRcAxisMid) { + m_swingState = SwingSine; // 本来就在中位,不用空走一段回中 + m_swingPhase = 0; + } else { + m_swingState = SwingCentering; + } } void CarCommunication::sendWashStop() { m_decontamStart = false; + + // 摆动收尾:从当前值斜坡还原成作业前的轴值。 + // 不直接把状态清掉,是因为清掉等于把最后一拍的轴值一直重发下去——末端会接着走。 + if (m_swingState != SwingOff) { + m_swingFrom = int(m_pitch); + m_swingTick = 0; + // 已经就在作业前的位置(常见:作业前本来就是中位),不必多走一段 + m_swingState = (m_swingFrom == int(m_pitchBeforeWash)) ? SwingOff : SwingRestoring; + if (m_swingState == SwingOff) + m_pitch = m_pitchBeforeWash; + } } void CarCommunication::sendBallValve(int valve, bool open) @@ -829,6 +1052,9 @@ void CarCommunication::sendSetParam(const QString& key, double value) void CarCommunication::sendSelfCheckStart() { m_selfCheckIndex = 0; + m_selfCheckWait = 0; + m_selfCheckAllOk = true; + m_debugModeBeforeCheck = m_debugMode; m_selfCheckTimer->start(); } @@ -842,23 +1068,64 @@ void CarCommunication::sendEstopReset() m_emergStop = 0; } -// TODO:CAN 矩阵(sheet RC)没有自检报文,故暂由本端跑完 10 项序列,仅为维持 +// TODO:CAN 矩阵(sheet RC)没有自检报文,故 0~9 项暂由本端计时直接判过,仅为维持 // ④自检 → ⑤作业选择 的流程不中断(onSelfCheckDone 是唯一点亮“作业选择”的地方)。 -// 拿到真实自检报文后,改为解析该报文并删除此本地驱动。 +// 拿到真实自检报文后,改为解析该报文并删除这段本地驱动。 +// 14 项里只有球阀那 4 项(kSelfCheckValveFirst 起)是真检查:要下发指令并等反馈。 void CarCommunication::onSelfCheckTick() { - bool ok = true; - if (m_selfCheckFault && m_selfCheckIndex == 6) ok = false; // 演示:球阀D1关闭故障 + // ---- 球阀 D1/D2 开启、关闭:下发 Decontam_DebugCmd,等 MBV_D1_Sta/MBV_D2_Sta 反馈到位 ---- + if (m_selfCheckIndex >= kSelfCheckValveFirst) { + const int k = m_selfCheckIndex - kSelfCheckValveFirst; // 0..3 + const bool isD1 = (k < 2); + const bool wantOpen = (k % 2 == 0); + + if (m_selfCheckWait == 0) { + // 项首拍:置调试模式 + 写目标阀位。命令帧由 100ms 周期定时器持续重发, + // 所以只需在这里改状态,不必每拍重发。 + m_debugMode = true; + if (isD1) m_d1Open = wantOpen; + else m_d2Open = wantOpen; + emit selfCheckItemBusy(m_selfCheckIndex); + } + + // 车端完全没有反馈时不能算“回复正常”:否则 d1open/d2open 的缺省值 false + // 会让“关闭”两项在遥测全无的情况下被误判通过。 + const bool fbOpen = m_tel && (isD1 ? m_tel->d1open : m_tel->d2open); + const bool pass = m_lastCommOk && (fbOpen == wantOpen); + + if (!pass && m_selfCheckWait < kValveWaitTicks) { + ++m_selfCheckWait; // 反馈还没到位:继续等,本轮不推进序列 + return; + } + m_selfCheckWait = 0; + finishSelfCheckItem(pass); + return; + } + + // ---- 0~9:无对应报文,本地计时判过。演示开关只在此段生效 ---- + finishSelfCheckItem(!(m_selfCheckFault && m_selfCheckIndex == kSelfCheckDemoFaultIndex)); +} + +// 记录单项结果并推进序列;跑完最后一项时收尾(恢复调试模式、发 selfCheckDone)。 +// allOk 由本端累计,不能再用演示开关推——球阀那 4 项真的可能失败。 +void CarCommunication::finishSelfCheckItem(bool ok) +{ + m_selfCheckAllOk = m_selfCheckAllOk && ok; emit selfCheckItem(m_selfCheckIndex, ok); - ++m_selfCheckIndex; - if (m_selfCheckIndex >= 10) { - m_selfCheckTimer->stop(); - emit selfCheckDone(!m_selfCheckFault); - } + if (++m_selfCheckIndex < kSelfCheckItemCount) + return; + + m_selfCheckTimer->stop(); + m_debugMode = m_debugModeBeforeCheck; // 退出调试模式,别把整车留在自检状态 + emit selfCheckDone(m_selfCheckAllOk); } void CarCommunication::onHeartbeatTimeout() { setCommOk(false); + // 超时后不再有收帧,cacheFaults 不会跑:这里补刷一次顶部, + // 否则顶部会一直停在“系统通信正常”上、看不出链路已经断了。 + updateCommText(); } diff --git a/comm/car.h b/comm/car.h index 74af0b9..ba70c33 100644 --- a/comm/car.h +++ b/comm/car.h @@ -14,12 +14,34 @@ class QUdpSocket; // 摇杆量协议范围 0~4096,中位 2048(分类一各轴、Vehi_gear/Vehi_turning 通用) static const int kRcAxisMid = 2048; -// CAN 反馈只给故障码,这里带上系统与中文描述: -// system 取界面故障历史既有的四个分组名,desc 供页面直接显示,code 供故障查询追溯。 +// 自检项数目(④自检界面与本地自检序列共用,顺序即界面显示顺序): +// 0 通信系统(All_Comm_FaultCode=0) → 1~8 各分系统通信(码1~8) → 9 清水液位 +// (A_LiquidLevel_Low) → 10~13 球阀 D1/D2 开启、关闭(MBV_D1_Sta / MBV_D2_Sta) +static const int kSelfCheckItemCount = 14; + +// 球阀 4 项的自检起始下标:10=D1开启 11=D1关闭 12=D2开启 13=D2关闭。 +// 这 4 项不能只靠本端计时通过——要先下发 Decontam_DebugCmd,再等 +// MBV_D1_Sta / MBV_D2_Sta 反馈与指令一致;不一致即超时判故障。 +static const int kSelfCheckValveFirst = 10; + +// 球阀等待反馈的节拍数(自检定时器 450ms/拍):18 拍 ≈ 8s 未到位即判故障。 +static const int kValveWaitTicks = 18; + +// 演示故障所在的自检项(config.ini 的 net/selfcheck_fault),只作用于 0~9 段, +// 免得覆盖球阀 4 项的真实反馈结果。9 = 清水液位。 +static const int kSelfCheckDemoFaultIndex = 9; + +// CAN 反馈只给故障码与故障等级,这里带上一个故障所需的全部要素。 +// text() 渲染的是完整一行:<组名>:[<码>-]<描述>[,级], +// “底盘电机故障:18-电机过温警告,1级”;各卡片那一行太窄,只用 desc(+等级后缀)。 struct CanFault { - QString system; - QString code; - QString desc; + QString system; // 故障历史分组(机器人底盘/洗消系统/气体检测仪) + QString group; // 组名(底盘电机故障/BMS系统故障/…),完整一行才用得到 + QString code; // 追溯码,如 MCU18 / SOCL1 / COMM3,供故障历史去重与故障查询 + QString codeNum; // 完整一行里的码,如 "18";空表示该故障没有码(等级型故障) + QString desc; // 描述,不含等级,如“电机过温警告” + int level = 0; // 0=不显示等级;1~3 → 尾部追加“,N级” + QString text() const; // 完整一行,如“底盘电机故障:18-电机过温警告,1级” }; // ===================== 车端通信层(CAN 报文经 /dev/ttyS7 串口收发) ===================== @@ -30,8 +52,8 @@ struct CanFault { // 下行 分类二/三 12 条(车端 → 遥控器):底盘 4 条、机械臂 4 条、洗消 2 条、气体检测仪 2 条, // 按 canId 分流转 handleFrame() 写入 Telemetry。 // 全部为 29 位扩展帧、信号一律 Intel(小端)位序。 -// 传输走 UDP:bind 本机 9000 端口收下行反馈,上行命令发往 0.0.0.0:9001。 -// UDP 载荷统一为 18 字节:帧头 0x5AA5FF00 + canId + DLC + 8 字节数据 + 校验和。 +// 传输走 UDP:bind 本机 9000 端口收下行反馈,上行命令发往车端 192.168.1.224:3001。 +// UDP 载荷统一为 16 字节:帧头 0xAA + canId + DLC + 8 字节数据 + 校验和 + 帧尾 0xBB。 // 通信超时判定:连续 2s 未收到任何合法帧则判为通信异常(最慢的下行周期 1000ms)。 class CarCommunication : public QObject { @@ -46,11 +68,19 @@ public: // 行车/机械臂模式(面板旋钮或遥控器报文)→ Arm_ControlCmd1 的 Arm_modeCmd / Arm_enable void setArmMode(bool arm) { m_armMode = arm; } + // 遥控器是否正在操作(由 AppContext 维护)。面板那两路入口据此让位: + // 两端都能操作,但遥控器一动手就以遥控器为准;遥控器松手后由页面重发按住的方向/轴。 + void setRcActive(bool on) { m_rcActive = on; } + // ---------- 上行命令(分类一)---------- // 命令只更新本端控制状态,实际报文由周期定时器编码发送(CAN 控制帧本就是周期报文)。 + // 底盘/机械臂各有**两个入口**:面板一个、遥控器一个(名字带 Rc)。两路写的是同一份 + // 轴值缓存,差别只在“遥控器在操作时面板那一路直接忽略”。 // turn 非空("left"/"right")时与 dir 同时下发,支持边前进/后退边转向 void sendChassisMove(const QString& dir, double speed, const QString& turn = QString()); + void sendChassisMoveRc(const QString& dir, double speed, const QString& turn = QString()); void sendArmMove(const QString& axis, int dir); // dir: +1 / -1 / 0(停止) + void sendArmMoveRc(const QString& axis, int dir); void sendArmReturn(); void sendArmPause(); void sendGasStart(); @@ -66,9 +96,11 @@ public: void sendEstopReset(); signals: - void telemetryUpdated(); // 任意遥测包到达(页面据此刷新) - void commOkChanged(bool ok); // 通信状态变化 - void selfCheckItem(int index, bool ok); // 自检单项结果(0~9) + void telemetryUpdated(); // 一批数据报解析完(页面据此刷新,同批多帧只发一次) + // 顶部左区那行通信状态变化(Telemetry::commText)。周期报文内容不变时不发。 + void commTextChanged(); + void selfCheckItem(int index, bool ok); // 自检单项结果(0~kSelfCheckItemCount-1) + void selfCheckItemBusy(int index); // 自检单项开始执行(已下发指令、正等反馈) void selfCheckDone(bool allOk); // 自检完成 void faultAppended(const QString& system, const QString& code, const QString& desc); @@ -81,11 +113,14 @@ private slots: void onSelfCheckTick(); private: - // 收包:按 canId 分流;发包:18 字节封帧后写串口 + // 收包:按 canId 分流;发包:16 字节封帧后写 UDP void handleFrame(quint32 canId, const quint8* d); void sendFrame(quint32 canId, const quint8* data); void cacheFaults(); + void updateCommText(); // 重建顶部那行通信状态(Telemetry::commText) + void updateWashSwing(); // 洗消作业期间的末端俯仰摆动(每 50ms 一拍) void setCommOk(bool ok); + void finishSelfCheckItem(bool ok); // 记录单项结果并推进到下一项 Telemetry* m_tel = nullptr; QUdpSocket* m_sock = nullptr; @@ -96,12 +131,18 @@ private: QTimer* m_selfCheckTimer = nullptr; bool m_lastCommOk = true; + int m_commFaultCode = 0; // 最近的 All_Comm_FaultCode,0 = 通信正常 bool m_armMode = false; + bool m_rcActive = false; // 遥控器正在操作:面板那两路入口让位(见 setRcActive) // 本地自检序列(CAN 矩阵无自检报文,见 .cpp TODO) int m_selfCheckIndex = 0; bool m_selfCheckFault = true; + int m_selfCheckWait = 0; // 当前项已等待的节拍数:0 = 尚未下发指令 + bool m_selfCheckAllOk = true; // 已跑完各项的累计结果,决定 selfCheckDone 的入参 + // 自检要临时置调试模式才能操作球阀,结束后恢复原值,免得把整车留在调试模式 + bool m_debugModeBeforeCheck = false; // ---- 分类一 上行控制状态 ---- // Chassis_ControlCmd 0x18208081 @@ -116,6 +157,13 @@ private: quint16 m_roll = kRcAxisMid; quint16 m_pitch = kRcAxisMid; quint16 m_yaw = kRcAxisMid; + // 洗消作业期间的末端俯仰摆动:回中 → 正弦摆动 → 还原(见 updateWashSwing,50ms/拍) + enum SwingState { SwingOff, SwingCentering, SwingSine, SwingRestoring }; + SwingState m_swingState = SwingOff; + int m_swingPhase = 0; // 正弦相位,0..kWashSwingTicks + int m_swingTick = 0; // 过渡段已走的拍数 + int m_swingFrom = kRcAxisMid; // 还原过渡的起始轴值 + quint16 m_pitchBeforeWash = kRcAxisMid; // 洗消前的俯仰轴值,作业结束后还原 quint8 m_homingPause = 0x55; // System_homing_pause:0xAA=系统回位 0x55=暂停回位 // Decontam_ControlCmd 0x18238081 @@ -143,6 +191,9 @@ private: // 气体检测仪告警周期 1000ms、洗消故障 200ms,分开存放,否则后到的会把先到的清掉。 QVector m_vsf1Faults, m_vsf2Faults, m_vsf3Faults, m_vsf4Faults; QVector m_washFaults, m_pumpFaults, m_detectorFaults; + // 整系统通信故障码单独一份:它是全系统级的,不属于任何一张卡片, + // 只进顶部那行通信状态与故障历史,不并进 chassisFaults。 + QVector m_commFaults; QSet m_reportedFaults; // 已记入故障历史的“系统|码”,周期性报文只上报新增故障 }; diff --git a/pages/arm.cpp b/pages/arm.cpp index 15e7f36..3abcbae 100644 --- a/pages/arm.cpp +++ b/pages/arm.cpp @@ -5,7 +5,6 @@ #include "alarmbanner.h" #include "datacard.h" #include "car.h" -#include "confirmdialog.h" #include #include //#include // 末端按钮原为 2×3 网格,改为竖列后暂不需要 @@ -82,33 +81,32 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent) cardsLay->setSpacing(12); m_chassisCard = new DataCard("底盘", cardsCol); - m_chassisCard->addField("底盘电量SOC", "85%"); - m_chassisCard->addField("底盘电压", "360V"); - m_chassisCard->addField("底盘故障", "无"); + m_chassisCard->addField("底盘电量SOC", "--"); + m_chassisCard->addField("底盘电压", "--"); m_washCard = new DataCard("洗消", cardsCol); - m_washCard->addField("A组分/清水液位", "100L"); - m_washCard->addField("泡沫箱液位", "30L"); + m_washCard->addField("A组分/清水液位", "--"); + m_washCard->addField("泡沫箱液位", "--"); m_gasCard = new DataCard("气体检测仪", cardsCol); - m_gasCard->addField("主机状态", "点火状态"); - m_gasCard->addField("有毒物种类", "S级"); - m_gasCard->addField("有毒物等级", "二级"); + m_gasCard->addField("主机状态", "--"); + m_gasCard->addField("有毒物种类", "--"); + 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°"); + m_armCard->addField("末端位置", "--"); + m_armCard->addField("末端姿态", "--"); + m_armCard->addField("末端速度", "--"); + m_armCard->addField("Joint1关节角", "--"); + m_armCard->addField("Joint2关节角", "--"); + m_armCard->addField("Joint3关节角", "--"); + m_armCard->addField("Joint4关节角", "--"); + m_armCard->addField("Joint5关节角", "--"); + m_armCard->addField("Joint6关节角", "--"); centerLay->addWidget(cardsCol, 1); centerLay->addWidget(m_armCard, 1); @@ -151,29 +149,43 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent) 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(); }); + connect(ctx, &AppContext::rcDrivingChanged, this, [this](bool) { + updateModeGating(); + // 同底盘页:让位给遥控器期间按下的轴被忽略,遥控器松手后把按着的键重发一遍。 + // 注意写 this->ctx():构造函数形参也叫 ctx,直接写 ctx() 会被它遮住 + if (this->ctx()->rcDriving()) return; + for (QPushButton* b : m_moveButtons) + if (b->isDown()) + this->ctx()->comm()->sendArmMove(b->property("armAxis").toString(), + b->property("armDir").toInt()); + }); updateModeGating(); refresh(); } +void ArmPage::showEvent(QShowEvent* ev) +{ + BasePage::showEvent(ev); + // 每次进入机械臂页自动切到机械臂模式:Arm_ControlCmd1 的 Arm_modeCmd/Arm_enable + // 挂在它上面,不切的话车端会忽略这里发出的动作指令。遥控器在线时模式由遥控器报文 + // 决定,不抢——和底盘页进页面切行车模式是同一套处理。 + if (!ctx()->rc()->linkOk()) + ctx()->setMode(Mode::Arm); +} + QPushButton* ArmPage::makeMoveButton(const QString& text, const QString& axis, int dir) { QPushButton* b = button(text, "", this); b->setFixedSize(270, 76); + // 记下这根按钮管哪个轴/往哪边,好让"遥控器松手后重发按住的键"找得到它 + b->setProperty("armAxis", axis); + b->setProperty("armDir", dir); 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; - } ctx()->comm()->sendArmMove(axis, dir); }); connect(b, &QPushButton::released, this, [this, axis]() { @@ -184,10 +196,11 @@ QPushButton* ArmPage::makeMoveButton(const QString& text, const QString& axis, i void ArmPage::updateModeGating() { - // 非机械臂模式、急停触发或遥控器正在操作时禁用动作按钮(遥控器优先) - 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); + // 和底盘页一样:现在只有**急停**会禁用动作按钮。模式不再限制(进页面即用), + // 遥控器也不再让位——面板和遥控器可以同时操作,两边各发各的。 + const bool ok = !ctx()->estopTriggered; + for (QPushButton* b : m_moveButtons) b->setEnabled(ok); + for (QPushButton* b : m_endButtons) b->setEnabled(ok); } void ArmPage::onReturnToggled(bool on) @@ -206,20 +219,29 @@ void ArmPage::refresh() m_banner->clear(); } - 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(";")); + // 未收到对应报文的数值统一显示 "--"(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_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_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0))); + m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0))); + m_washCard->setAlarm(0, t.levelALow); + m_washCard->setAlarm(1, t.levelBLow); + m_washCard->setFaults(washCardFaults(t)); + m_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_armCard->setField(0, t.pos); m_armCard->setField(1, t.pose); - m_armCard->setField(2, QString("%1").arg(t.vel, 0, 'f', 1)); + m_armCard->setField(2, dashIf(t.hasArmPose, QString("%1").arg(t.vel, 0, 'f', 1))); for (int i = 0; i < 6 && i < t.js.size(); ++i) - m_armCard->setField(3 + i, QString("%1°").arg(t.js[i], 0, 'f', 0)); + m_armCard->setField(3 + i, dashIf(t.hasJoints, QString("%1°").arg(t.js[i], 0, 'f', 0))); // 回位状态机 if (t.returnState != m_prevReturnState) { diff --git a/pages/arm.h b/pages/arm.h index 62f22ce..3a5221c 100644 --- a/pages/arm.h +++ b/pages/arm.h @@ -19,6 +19,9 @@ class ArmPage : public BasePage public: explicit ArmPage(AppContext* ctx, QWidget* parent = nullptr); +protected: + void showEvent(QShowEvent* ev) override; + private slots: void refresh(); void updateModeGating(); diff --git a/pages/chassis.cpp b/pages/chassis.cpp index 8cecdac..5833c41 100644 --- a/pages/chassis.cpp +++ b/pages/chassis.cpp @@ -12,6 +12,12 @@ #include #include "global.h" +namespace { +// 收起姿态取全零:任一关节角绝对值超过这个角度即认为机械臂已展开。 +// 单位与 CAN 下发的关节角一致(car.cpp 直接写入 t.js,未经缩放)。 +const double kArmStowedTolDeg = 5.0; +} + ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent) : BasePage(ctx, parent) { @@ -63,16 +69,17 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent) centerLay->setSpacing(12); m_washCard = new DataCard("洗消", center); - m_washCard->addField("A液位", "100L"); - m_washCard->addField("泡沫液位", "30L"); + m_washCard->addField("A组分/清水液位", "--"); + m_washCard->addField("泡沫箱液位", "--"); m_armCard = new DataCard("机械臂", center); - m_armCard->addField("末端位置", "XYZ"); - m_armCard->addField("姿态", "RX/RY/RZ"); + m_armCard->addField("末端位置", "--"); + m_armCard->addField("末端姿态", "--"); m_gasCard = new DataCard("气体检测仪", center); - m_gasCard->addField("主机状态", "待机"); - m_gasCard->addField("浓度", "0 ppb"); + m_gasCard->addField("主机状态", "--"); + m_gasCard->addField("有毒物种类", "--"); + m_gasCard->addField("有毒物等级", "--"); centerLay->addWidget(m_washCard, 1); centerLay->addWidget(m_armCard, 1); @@ -88,11 +95,15 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent) chassisLay->setSpacing(12); 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("故障", "无"); + m_chassisCard->addField("底盘电量SOC", "--"); + m_chassisCard->addField("底盘电压U", "--"); + m_chassisCard->addField("底盘车速V", "--"); + m_chassisCard->addField("档位状态", "--"); + m_chassisCard->addField("驻车状态", "--"); + m_chassisCard->addField("四驱模式", "--"); + m_chassisCard->addField("底盘Ready状态", "--"); + m_chassisCard->addField("方向盘角度", "--"); + m_chassisCard->addField("角度对中状态", "--"); chassisLay->addWidget(m_chassisCard, 1); main->addWidget(chassisCol, 1); // 与中间卡片列平均分配中间区域 main->addSpacing(17); // 底盘列 ↔ 车速设置列:由 32 减小到 17(底盘列整体右移 15px) @@ -119,7 +130,6 @@ ChassisPage::ChassisPage(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]() { emit navigate(PageJob); }); root->addWidget(m_statusStrip); @@ -132,7 +142,14 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent) 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(); }); + connect(ctx, &AppContext::rcDrivingChanged, this, [this](bool) { + updateModeGating(); + // 让位给遥控器时按下的方向是被忽略掉的,遥控器一松手就重发一遍—— + // 否则手一直按着也看不出恢复,车端停在遥控器最后给的那个值上。 + // 注意写 this->ctx():构造函数形参也叫 ctx,直接写 ctx() 会被它遮住 + if (!this->ctx()->rcDriving() && !m_heldDirs.isEmpty()) + sendDirCommand(); + }); updateModeGating(); refresh(); } @@ -155,20 +172,14 @@ QPushButton* ChassisPage::makeDirButton(const QString& text, const QString& dir) "{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; - } m_heldDirs.insert(dir); sendDirCommand(); + updateAlarm(); }); connect(b, &QPushButton::released, this, [this, dir]() { m_heldDirs.remove(dir); sendDirCommand(); + updateAlarm(); }); return b; } @@ -192,46 +203,80 @@ void ChassisPage::sendDirCommand() void ChassisPage::updateModeGating() { - // 非行车模式、急停触发或遥控器正在操作时禁用方向按钮(遥控器优先) - const bool drive = (ctx()->mode == Mode::Drive) && !ctx()->estopTriggered && !ctx()->rcDriving(); + // 现在只有**急停**会禁用方向按钮:模式不再限制(进页面即用),遥控器也不再让位—— + // 面板和遥控器可以同时操作,两边各发各的,由车端处理。 + const bool ok = !ctx()->estopTriggered; for (QPushButton* b : m_dirButtons) - b->setEnabled(drive); + b->setEnabled(ok); - // 让位时清掉本地按住状态:按钮被禁用后不会再收到 released,否则方向会被一直挂住。 - // 让位给遥控器时不补发 stop,否则会覆盖遥控器刚下发的那条指令。 - if (!drive && !m_heldDirs.isEmpty()) { + // 急停把按钮禁掉时清掉按住状态并补一条 stop。(Qt 禁用时会补发 released, + // 各按钮自己那一遍已经发过了,这里只是兜底。) + if (!ok && !m_heldDirs.isEmpty()) { m_heldDirs.clear(); - if (!ctx()->rcDriving()) - ctx()->comm()->sendChassisMove("stop", 0); + ctx()->comm()->sendChassisMove("stop", 0); } + + updateAlarm(); +} + +// 任一关节角偏离收起姿态(全零)即认为机械臂已展开 +bool ChassisPage::armNotStowed() const +{ + const QVector& js = ctx()->telemetry.js; + for (double a : js) + if (a > kArmStowedTolDeg || a < -kArmStowedTolDeg) + return true; + return false; +} + +// 面板方向键按住,或遥控器正在推杆,都算正在驱动底盘 +bool ChassisPage::chassisDriving() const +{ + return !m_heldDirs.isEmpty() || ctx()->rcDriving(); +} + +// 通信异常优先;其次是“机械臂已展开且正在驱动底盘” +void ChassisPage::updateAlarm() +{ + if (!ctx()->telemetry.commOk) + m_banner->showAlarm("通信异常"); + else if (armNotStowed() && chassisDriving()) + m_banner->showAlarm("机械臂未回位,请谨慎操作"); + else + m_banner->clear(); } void ChassisPage::refresh() { const Telemetry& t = ctx()->telemetry; - // 机械臂未回位 / 通信异常告警 - if (!t.commOk) { - m_banner->showAlarm("通信异常"); - } else if (!t.armHomed && t.returnState != "idle") { - m_banner->showAlarm("机械臂未回位,请谨慎操作"); - } else { - m_banner->clear(); - } + updateAlarm(); - 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, QString("%1 km/h").arg(t.speed, 0, 'f', 2)); - m_chassisCard->setField(3, t.handbrake ? "有效" : "无效"); - m_chassisCard->setField(m_chassisFieldFault, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); - m_chassisCard->setAlarm(m_chassisFieldFault, !t.chassisFaults.isEmpty()); + // 未收到对应报文的数值统一显示 "--"(dashIf) + m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc))); + m_chassisCard->setField(1, dashIf(t.hasBattery, QString("%1V").arg(t.volt, 0, 'f', 0))); + m_chassisCard->setField(2, dashIf(t.hasSpeed, QString("%1 km/h").arg(t.speed, 0, 'f', 2))); + m_chassisCard->setField(3, dashIf(t.hasChassis4, gearLabel(t.gearPos))); + m_chassisCard->setField(4, dashIf(t.hasChassis4, parkingLabel(t.parking))); + m_chassisCard->setField(5, dashIf(t.hasChassis4, awdLabel(t.awdMode))); + m_chassisCard->setField(6, dashIf(t.hasSpeed, rdyLabel(t.rdyStatus))); + m_chassisCard->setField(7, dashIf(t.hasChassis4, QString("%1°").arg(t.steerAngle, 0, 'f', 0))); + m_chassisCard->setField(8, dashIf(t.hasChassis4, alignLabel(t.alignStatus))); + m_chassisCard->setFaults(t.chassisFaults); + + m_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0))); + m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0))); + m_washCard->setAlarm(0, t.levelALow); + m_washCard->setAlarm(1, t.levelBLow); + m_washCard->setFaults(washCardFaults(t)); - m_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0)); - m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 0, 'f', 0)); 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_gasCard->setField(0, gasHostLabel(t)); + m_gasCard->setField(1, t.gasType); // 有毒物种类,未收到报文时本身就是 "--" + m_gasCard->setField(2, t.gasLevel); // 有毒物等级 + m_gasCard->setAlarm(0, t.gasHost == "fault"); + m_gasCard->setFaults(t.detectorFaults); } void ChassisPage::onSpeedChanged(double v) diff --git a/pages/chassis.h b/pages/chassis.h index 305c023..96d1634 100644 --- a/pages/chassis.h +++ b/pages/chassis.h @@ -32,6 +32,9 @@ private slots: private: QPushButton* makeDirButton(const QString& text, const QString& dir); void sendDirCommand(); // 按当前按住的方向组合下发(档位与转向可同时) + void updateAlarm(); // 横幅:通信异常 / 机械臂未回位时驱动底盘 + bool armNotStowed() const; // 任一关节角偏离收起姿态(全零) + bool chassisDriving() const; // 面板方向键按住,或遥控器正在推杆 TitleBar* m_titleBar = nullptr; StatusStrip* m_statusStrip = nullptr; @@ -42,7 +45,6 @@ private: DataCard* m_chassisCard = nullptr; ParamControl* m_speed = nullptr; QTimer* m_saveTimer = nullptr; - int m_chassisFieldFault = -1; QVector m_dirButtons; QSet m_heldDirs; // 当前按住的方向,可多键同时按 }; diff --git a/pages/debugmaintain.cpp b/pages/debugmaintain.cpp index 8f3a54e..cdba125 100644 --- a/pages/debugmaintain.cpp +++ b/pages/debugmaintain.cpp @@ -9,8 +9,13 @@ #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) { @@ -36,8 +41,42 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent) QVBoxLayout* leftLay = new QVBoxLayout(left); leftLay->setContentsMargins(20, 10, 10, 10); leftLay->setSpacing(60); - leftLay->addWidget(makeValveGroup(1)); - leftLay->addWidget(makeValveGroup(2)); + // 故障提示作为球阀盒子的**上一条**:常占位(无故障时清空文字、留出这一条的高度), + // 出现故障不会把下面的盒子顶下去 + 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); @@ -54,27 +93,33 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent) centerLeftLay->setSpacing(10); m_chassisCard = new DataCard("底盘", centerLeft); - m_chassisCard->addField("电量", "85%"); - m_chassisCard->addField("电压", "360V"); + m_chassisCard->addField("底盘电量SOC", "--"); + m_chassisCard->addField("底盘电压U", "--"); m_armCard = new DataCard("机械臂", centerLeft); - m_armCard->addField("末端位置", "XYZ"); - m_armCard->addField("姿态", "RX/RY/RZ"); + // 这一列只有 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("浓度", "0 ppb"); + 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组分液位", "100L"); - m_washCard->addField("A药剂流量", "0 L/min"); - m_washCard->addField("管路压力", "0 MPa"); - m_washCard->addField("发泡倍数", "0"); - m_washCard->addField("空压机故障代码", ""); - 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); @@ -101,8 +146,6 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent) bottom->setStyleSheet(QString("QWidget#DebugBottom{background-color:%1;border-top:1px solid %2;}") .arg(C::PANEL, C::BORDER)); QHBoxLayout* bottomLay = new QHBoxLayout(bottom); - // 左起 364 = 36(页面边距) + 300(球阀列) + 28(列间距),与中央“底盘”列左边缘对齐 - bottomLay->setContentsMargins(364, 0, 24, 0); bottomLay->setSpacing(48); auto makeLed = [&](const QString& text) { LedIndicator* led = new LedIndicator(text, bottom); @@ -111,10 +154,14 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent) bottomLay->addWidget(led); return led; }; - m_ledPlunger = makeLed("柱塞泵"); - m_ledAir = makeLed("空压机"); - m_ledLeaf = makeLed("叶片泵"); - m_ledLevel = makeLed("液位低"); + // 三个泵的指示灯已经搬进各自的泵组里(放在按钮行左侧),底栏只留两路液位低 + 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); @@ -159,7 +206,7 @@ QWidget* DebugMaintainPage::makeValveGroup(int valve) QLabel* title = label(QString("D%1 球阀").arg(valve), 30, C::TEXT, true, grp); lay->addWidget(title); - // 控制开/关(toggle):文字固定,底色表示状态——未选中=关闭(红)、选中=开启(绿) + // 控制开/关(toggle):文字固定,底色表示状态——未选中=关闭(灰)、选中=开启(绿) QPushButton* ctrl = button(QString("D%1 控制").arg(valve), "", grp); ctrl->setCheckable(true); ctrl->setFixedSize(180, 64); // 窄按钮,与泵组的“上电”一致 @@ -167,16 +214,19 @@ QWidget* DebugMaintainPage::makeValveGroup(int valve) 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)); + .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() 按实际到位状态刷新 + // 开启到位 / 关闭到位(同一行,居中)。缺省状态按“阀体默认关着”给: + // 开启到位灭(灰)、关闭到位红。首次收到遥测后由 refresh() 按实际到位状态刷新 LedIndicator* open = new LedIndicator("开启", grp); - open->setState(LedIndicator::Green); + open->setState(LedIndicator::Off); LedIndicator* close = new LedIndicator("关闭", grp); close->setState(LedIndicator::Red); QHBoxLayout* ioRow = new QHBoxLayout(); @@ -187,23 +237,16 @@ QWidget* DebugMaintainPage::makeValveGroup(int valve) ioRow->addStretch(1); lay->addLayout(ioRow); - // 故障提示 - QLabel* fault = label("", 16, C::DANGER, true, grp); - fault->setAlignment(Qt::AlignCenter); - fault->setProperty("alert", true); - fault->hide(); - lay->addWidget(fault); + // 故障提示不在盒子里——它是盒子**上方**独立的一条,由构造函数的 addValve 建好 if (valve == 1) { m_valve1Btn = ctrl; m_v1Open = open; m_v1Close = close; - m_valveFault1 = fault; } else { m_valve2Btn = ctrl; m_v2Open = open; m_v2Close = close; - m_valveFault2 = fault; } return grp; } @@ -257,6 +300,7 @@ QWidget* DebugMaintainPage::makePumpGroup(const QString& pump, const QString& na start->setChecked(false); ctx()->comm()->sendPumpPower(pump, false); }); + // 圆点+名称不在这里了——三个泵的上电指示灯已经搬到左列 D2 球阀下方(见构造函数) QHBoxLayout* pwrRow = new QHBoxLayout(); pwrRow->setSpacing(12); pwrRow->addStretch(1); @@ -265,9 +309,9 @@ QWidget* DebugMaintainPage::makePumpGroup(const QString& pump, const QString& na pwrRow->addStretch(1); lay->addLayout(pwrRow); - if (pump == "air") { m_airRpm = rpm; m_airStart = start; m_airStop = stop; } + 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; } + else { m_leafRpm = rpm; m_leafStart = start; m_leafStop = stop; } return grp; } @@ -275,22 +319,32 @@ void DebugMaintainPage::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)); + // 未收到对应报文的数值统一显示 "--"(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, t.gasHost == "detecting" ? "检测中" : (t.gasHost == "fault" ? "故障" : "待机")); - m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); + 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, QString("%1L").arg(t.levelA, 0, 'f', 0)); - m_washCard->setField(2, QString("%1 L/min").arg(t.flowA, 0, 'f', 1)); - m_washCard->setField(3, QString("%1 MPa").arg(t.press, 0, 'f', 2)); - m_washCard->setField(4, QString("%1").arg(t.foam, 0, 'f', 1)); - m_washCard->setField(5, t.pumpFaults.value(0)); - m_washCard->setField(6, t.pumpFaults.value(0)); + 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); @@ -298,22 +352,45 @@ void DebugMaintainPage::refresh() m_v2Close->setState(t.d2close ? LedIndicator::Red : LedIndicator::Off); } - // 球阀故障提示 - bool v1Fault = t.valveFault.contains("D1"); - bool v2Fault = t.valveFault.contains("D2"); - if (v1Fault) { m_valveFault1->setText(t.valveFault); m_valveFault1->show(); startBlink(m_valveFault1, "blink"); } - else { stopBlink(m_valveFault1, "blink"); m_valveFault1->hide(); } - if (v2Fault) { m_valveFault2->setText(t.valveFault); m_valveFault2->show(); startBlink(m_valveFault2, "blink"); } - else { stopBlink(m_valveFault2, "blink"); m_valveFault2->hide(); } + // 球阀故障:协议里没有“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_ledLevel->setState(t.levelA < 10 ? LedIndicator::RedBlink : LedIndicator::Off); + m_ledLevelA->setState(t.levelALow ? LedIndicator::RedBlink : LedIndicator::Off); + m_ledLevelB->setState(t.levelBLow ? LedIndicator::RedBlink : LedIndicator::Off); } diff --git a/pages/debugmaintain.h b/pages/debugmaintain.h index a862023..0f91286 100644 --- a/pages/debugmaintain.h +++ b/pages/debugmaintain.h @@ -58,15 +58,27 @@ private: QPushButton* m_leafStart = nullptr; QPushButton* m_leafStop = nullptr; - // 底部指示灯 + // 泵上电指示灯(在各自泵组的按钮行左侧,由 makePumpGroup 填)+ 底部的两路液位低 LedIndicator* m_ledPlunger = nullptr; LedIndicator* m_ledAir = nullptr; LedIndicator* m_ledLeaf = nullptr; - LedIndicator* m_ledLevel = nullptr; + LedIndicator* m_ledLevelA = nullptr; // A组分/清水液位低 + LedIndicator* m_ledLevelB = nullptr; // B组分液位低 - // 是否收到过车端遥测:未收到时球阀到位灯保持“开启=绿、关闭=红”的缺省显示 + // 是否收到过车端遥测:未收到时球阀到位灯保持“开启=灰、关闭=红”的缺省显示 bool m_telemetrySeen = false; + // 球阀“开启/关闭故障”是**超时未到位**推出来的——协议里没有这两个故障信号, + // 球阀下行只有 MBV_Dn_Sta 一个到位位(关闭到位由它取反得到)。 + // 下发指令时记下时刻与想要的阀位;kValveFaultMs 内反馈还没变成想要的样子就判故障, + // 反馈对上(到位)或重新下发指令则解除。cmdMs 为 0 表示当前没有待确认的指令。 + qint64 m_v1CmdMs = 0; + qint64 m_v2CmdMs = 0; + int m_v1WantOpen = 0; // 最后一次指令想要的阀位:0=关闭 1=开启 + int m_v2WantOpen = 0; + bool m_v1Fault = false; + bool m_v2Fault = false; + // “返回”去向:本页有两个入口(系统首页 / 作业选择),由 MainWindow 记录来源后原路返回 PageId m_returnPage = PageJob; }; diff --git a/pages/gasdetector.cpp b/pages/gasdetector.cpp index 1c68307..07edad3 100644 --- a/pages/gasdetector.cpp +++ b/pages/gasdetector.cpp @@ -25,36 +25,36 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent) main->setContentsMargins(36, 16, 36, 16); main->setSpacing(32); - // 中央卡片区(2×2) + // 中央卡片区:左列 底盘/洗消/机械臂 纵向排列,右列由气体检测仪独占并跨满三行 QWidget* center = new QWidget(this); QGridLayout* grid = new QGridLayout(center); grid->setSpacing(20); grid->setContentsMargins(20, 0, 20, 0); m_chassisCard = new DataCard("底盘", center); - m_chassisCard->addField("电量", "85%"); - m_chassisCard->addField("电压", "360V"); - m_chassisCard->addField("故障", "无"); + m_chassisCard->addField("电量", "--"); + m_chassisCard->addField("电压", "--"); m_washCard = new DataCard("洗消", center); - m_washCard->addField("A液位", "100L"); - m_washCard->addField("泡沫液位", "30L"); + m_washCard->addField("A液位", "--"); + m_washCard->addField("泡沫液位", "--"); m_armCard = new DataCard("机械臂", center); - m_armCard->addField("末端位置", "XYZ"); - m_armCard->addField("姿态", "RX/RY/RZ"); + m_armCard->addField("末端位置", "--"); + m_armCard->addField("姿态", "--"); m_gasCard = new DataCard("气体检测仪", center); - m_gasCard->addField("主机状态", "待机"); + m_gasCard->addField("主机状态", "--"); m_gasCard->addField("有毒物种类", "--"); m_gasCard->addField("有毒物等级", "--"); - m_gasConcField = m_gasCard->addField("浓度", "0 ppb", true); + m_gasConcField = m_gasCard->addField("浓度", "--", true); m_gasCard->setHighlight(true); // 主色描边突出 grid->addWidget(m_chassisCard, 0, 0); - grid->addWidget(m_washCard, 0, 1); - grid->addWidget(m_gasCard, 1, 0); - grid->addWidget(m_armCard, 1, 1); + grid->addWidget(m_washCard, 1, 0); + grid->addWidget(m_armCard, 2, 0); + // 本页主角,独占右列:跨 3 行填满 + grid->addWidget(m_gasCard, 0, 1, 3, 1); main->addWidget(center, 1); // 右侧按钮列(垂直居中 + 平均分布) @@ -90,7 +90,6 @@ 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, "提示", "检测仍在进行,是否退出?")) @@ -117,24 +116,30 @@ void GasDetectorPage::refresh() { const Telemetry& t = ctx()->telemetry; - // 同步运行状态(以遥测为准) - if (t.gasHost == "detecting") ctx()->gasRunning = true; - else if (t.gasHost == "standby") ctx()->gasRunning = false; + // 同步运行状态(以遥测为准):未收到报文(gasHost 为空)时按未运行处理 + ctx()->gasRunning = (t.gasHost == "detecting"); 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(";")); + // 未收到对应报文的数值统一显示 "--"(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_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0))); + m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0))); + m_washCard->setAlarm(0, t.levelALow); + m_washCard->setAlarm(1, t.levelBLow); + m_washCard->setFaults(washCardFaults(t)); - m_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0)); - m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 0, 'f', 0)); 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(0, gasHostLabel(t)); m_gasCard->setField(1, t.gasType); m_gasCard->setField(2, t.gasLevel); - m_gasCard->setField(m_gasConcField, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); - // 超阈值红闪 + m_gasCard->setField(m_gasConcField, dashIf(t.hasGasConc, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0))); + // 主机状态为“故障”与浓度超限是两回事,各自独立闪 + m_gasCard->setAlarm(0, t.gasHost == "fault"); m_gasCard->setAlarm(m_gasConcField, t.gasConc > 500.0); + m_gasCard->setFaults(t.detectorFaults); } diff --git a/pages/home.cpp b/pages/home.cpp index 63bb07b..98e03f5 100644 --- a/pages/home.cpp +++ b/pages/home.cpp @@ -60,21 +60,23 @@ HomePage::HomePage(AppContext* ctx, QWidget* parent) grid->setContentsMargins(0, 0, 0, 0); m_chassisCard = new DataCard("底盘", center); - m_chassisCard->addField("电量", "85%"); - m_chassisCard->addField("电压", "360V"); - m_chassisCard->addField("故障", "无"); + m_chassisCard->addField("底盘电量SOC", "--"); + m_chassisCard->addField("底盘电压U", "--"); m_armCard = new DataCard("机械臂", center); - m_armCard->addField("末端位置", "XYZ"); - m_armCard->addField("姿态", "RX/RY/RZ"); + // 位置/姿态两行的数值最长约 554px,超出 2×2 卡片的内容区 522px,单独把字号收小一号 + m_armCard->setValueFontPx(38); + m_armCard->addField("末端位置", "--"); + m_armCard->addField("末端姿态", "--"); m_washCard = new DataCard("洗消", center); - m_washCard->addField("A液位", "100L"); - m_washCard->addField("泡沫液位", "30L"); + m_washCard->addField("A组分/清水液位", "--"); + m_washCard->addField("泡沫箱液位", "--"); m_gasCard = new DataCard("气体检测仪", center); - m_gasCard->addField("主机状态", "待机"); - m_gasCard->addField("浓度", "0 ppb"); + m_gasCard->addField("主机状态", "--"); + m_gasCard->addField("有毒物种类", "--"); + m_gasCard->addField("有毒物等级", "--"); // 左列 底盘/机械臂,右列 洗消/气体检测仪 grid->addWidget(m_chassisCard, 0, 0); @@ -113,8 +115,7 @@ HomePage::HomePage(AppContext* ctx, QWidget* parent) root->addLayout(main, 1); m_statusStrip = new StatusStrip(ctx, this); - m_statusStrip->setBackVisible(false); - m_statusStrip->setModeVisible(false); // 本页不显示模式徽标(菜单已移到左右两列) + m_statusStrip->setBackVisible(false); // 本页「返回」在右侧按钮列,底条不重复放 root->addWidget(m_statusStrip); connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &HomePage::refresh); @@ -124,26 +125,35 @@ HomePage::HomePage(AppContext* ctx, QWidget* parent) 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()); + + // 未收到对应报文的数值统一显示 "--"(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_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0)); - m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 0, 'f', 0)); + m_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0))); + m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0))); + m_washCard->setAlarm(0, t.levelALow); + m_washCard->setAlarm(1, t.levelBLow); + m_washCard->setFaults(washCardFaults(t)); - m_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); + m_gasCard->setField(0, gasHostLabel(t)); + m_gasCard->setField(1, t.gasType); // 有毒物种类,未收到报文时本身就是 "--" + m_gasCard->setField(2, t.gasLevel); // 有毒物等级 + m_gasCard->setAlarm(0, t.gasHost == "fault"); + m_gasCard->setFaults(t.detectorFaults); } void HomePage::onHelp() { + // 正文按 2+1 断句。字号 34 时最长一行 918px,正文区 944px,不会被 wordWrap 二次折行 ConfirmDialog::message(this, "操作说明", - "系统自检将依次检测:通信系统、底盘通信、机械臂通信、洗消通信、" - "传感器信号、清水液位及电动球阀 D1/D2 的开启与关闭。\n" - "全部正常后方可进入作业选择界面。"); + "自检过程主要检测各分系统通信、传感器信号数据是否正常、\n" + "电动球阀D1~D2是否能正常开启和关闭。\n" + "自检正常或人为点击“忽略故障”,则进入“系统首页面”。", + 1000, 34); } diff --git a/pages/jobselect.cpp b/pages/jobselect.cpp index 8a5ebfe..816fdca 100644 --- a/pages/jobselect.cpp +++ b/pages/jobselect.cpp @@ -61,21 +61,23 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent) grid->setContentsMargins(0, 0, 0, 0); m_chassisCard = new DataCard("底盘", center); - m_chassisCard->addField("电量", "85%"); - m_chassisCard->addField("电压", "360V"); - m_chassisCard->addField("故障", "无"); + m_chassisCard->addField("底盘电量SOC", "--"); + m_chassisCard->addField("底盘电压U", "--"); m_armCard = new DataCard("机械臂", center); - m_armCard->addField("末端位置", "XYZ"); - m_armCard->addField("姿态", "RX/RY/RZ"); + // 位置/姿态两行的数值最长约 554px,超出 2×2 卡片的内容区 522px,单独把字号收小一号 + m_armCard->setValueFontPx(38); + m_armCard->addField("末端位置", "--"); + m_armCard->addField("末端姿态", "--"); m_washCard = new DataCard("洗消", center); - m_washCard->addField("A液位", "100L"); - m_washCard->addField("泡沫液位", "30L"); + m_washCard->addField("A组分/清水液位", "--"); + m_washCard->addField("泡沫箱液位", "--"); m_gasCard = new DataCard("气体检测仪", center); - m_gasCard->addField("主机状态", "待机"); - m_gasCard->addField("浓度", "0 ppb"); + m_gasCard->addField("主机状态", "--"); + m_gasCard->addField("有毒物种类", "--"); + m_gasCard->addField("有毒物等级", "--"); // 左列 底盘/机械臂,右列 洗消/气体检测仪 grid->addWidget(m_chassisCard, 0, 0); @@ -116,8 +118,7 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent) root->addLayout(main, 1); m_statusStrip = new StatusStrip(ctx, this); - m_statusStrip->setBackVisible(false); - m_statusStrip->setModeVisible(false); // 本页不显示模式徽标(返回按钮已移到右侧列) + m_statusStrip->setBackVisible(false); // 本页「返回」在右侧按钮列,底条不重复放 root->addWidget(m_statusStrip); connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &JobSelectPage::refresh); @@ -127,18 +128,25 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent) void JobSelectPage::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()); + + // 未收到对应报文的数值统一显示 "--"(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_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0)); - m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 0, 'f', 0)); + m_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0))); + m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0))); + m_washCard->setAlarm(0, t.levelALow); + m_washCard->setAlarm(1, t.levelBLow); + m_washCard->setFaults(washCardFaults(t)); - m_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); + 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); } diff --git a/pages/selfcheck.cpp b/pages/selfcheck.cpp index cf079b2..acfd055 100644 --- a/pages/selfcheck.cpp +++ b/pages/selfcheck.cpp @@ -68,39 +68,45 @@ SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent) centerLay->addWidget(m_status); centerLay->addSpacing(16); - // 10 个自检项,两列 + // 14 个自检项,两列各 7 行 + const int rows = kSelfCheckItemCount / 2; QGridLayout* grid = new QGridLayout(); grid->setSpacing(8); - const QString names[10] = { - "通信系统", "底盘通信", "机械臂通信", "洗消通信", "传感器信号", - "清水液位", "球阀D1开启", "球阀D1关闭", "球阀D2开启", "球阀D2关闭" + const QString names[kSelfCheckItemCount] = { + // All_Comm_FaultCode:0=通信正常,1~8=各分系统通信异常 + "通信系统", "底盘电机通信", "底盘BMS通信", "机械臂通信", "空压机通信", + "柱塞泵通信", "叶片泵通信", "气体检测仪通信", "遥控器通信", + // A_LiquidLevel_Low + B_LiquidLevel_Low + "清水液位", + // MBV_D1_Sta / MBV_D2_Sta + "球阀D1开启", "球阀D1关闭", "球阀D2开启", "球阀D2关闭" }; - for (int i = 0; i < 10; ++i) { + for (int i = 0; i < kSelfCheckItemCount; ++i) { LedIndicator* led = new LedIndicator(names[i], center); - led->setFixedHeight(80); - led->setRadius(26); - led->setTextPixelSize(60); - QLabel* res = label("", 50, C::TEXT_SUB, false, center); - res->setFixedWidth(200); + led->setFixedHeight(68); + led->setRadius(22); + led->setTextPixelSize(44); // 两列各 7 行,字号收窄以保证最长项“气体检测仪通信”不被裁切 + QLabel* res = label("", 44, C::TEXT_SUB, false, center); + res->setFixedWidth(160); QWidget* row = new QWidget(center); QHBoxLayout* rowLay = new QHBoxLayout(row); rowLay->setContentsMargins(0, 0, 0, 0); rowLay->addWidget(led, 1); rowLay->addWidget(res); - grid->addWidget(row, i % 5, i / 5); + grid->addWidget(row, i % rows, i / rows); m_leds.append(led); m_results.append(res); } - resetItems(); // 初始化 10 个自检项 + resetItems(); // 初始化 14 个自检项 centerLay->addLayout(grid); - centerLay->addSpacing(50); // 文字与进度条之间的间隙 + centerLay->addSpacing(40); // 文字与进度条之间的间隙 // 进度条(位于屏幕约 3/4 高度处,文字全部在其上方) m_progress = new QProgressBar(center); m_progress->setRange(0, 100); m_progress->setValue(0); - m_progress->setFixedHeight(60); + m_progress->setFixedHeight(56); centerLay->addWidget(m_progress); centerLay->addStretch(3); // 下方留白(更大)→ 进度条落在约 3/4 处 @@ -130,6 +136,7 @@ SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent) root->addLayout(main, 1); // 连接通信信号 + connect(ctx->comm(), &CarCommunication::selfCheckItemBusy, this, &SelfCheckPage::onSelfCheckItemBusy); connect(ctx->comm(), &CarCommunication::selfCheckItem, this, &SelfCheckPage::onSelfCheckItem); connect(ctx->comm(), &CarCommunication::selfCheckDone, this, &SelfCheckPage::onSelfCheckDone); } @@ -150,13 +157,13 @@ void SelfCheckPage::showEvent(QShowEvent* ev) }); } -// 复位 10 个自检项:指示灯置灰、结果“待检测” +// 复位 14 个自检项:指示灯置灰、结果“待检测” void SelfCheckPage::resetItems() { - for (int i = 0; i < 10; ++i) { + for (int i = 0; i < kSelfCheckItemCount; ++i) { m_leds[i]->setState(LedIndicator::Off); m_results[i]->setText("待检测"); - m_results[i]->setStyleSheet(QString("color:%1;font-size:60px;").arg(C::TEXT_SUB)); + m_results[i]->setStyleSheet(QString("color:%1;font-size:44px;").arg(C::TEXT_SUB)); } } @@ -177,22 +184,31 @@ void SelfCheckPage::resetAll() m_autoStarted = false; } +// 单项开始执行(球阀项要下发指令并等车端反馈,可能持续数秒):先亮黄灯、文字置“检测中” +void SelfCheckPage::onSelfCheckItemBusy(int index) +{ + if (index < 0 || index >= kSelfCheckItemCount) + return; + m_leds[index]->setState(LedIndicator::Yellow); + m_results[index]->setText("检测中"); + m_results[index]->setStyleSheet(QString("color:%1;font-size:44px;").arg(C::TEXT_SUB)); +} + void SelfCheckPage::onSelfCheckItem(int index, bool ok) { - if (index < 0 || index >= 10) + if (index < 0 || index >= kSelfCheckItemCount) return; - m_leds[index]->setState(LedIndicator::Yellow); // 检测中黄闪 → 结果 // 结果态 if (ok) { m_leds[index]->setState(LedIndicator::Green); m_results[index]->setText("正常"); - m_results[index]->setStyleSheet(QString("color:%1;font-size:60px;").arg(C::SUCCESS)); + m_results[index]->setStyleSheet(QString("color:%1;font-size:44px;").arg(C::SUCCESS)); } else { m_leds[index]->setState(LedIndicator::RedBlink); - m_results[index]->setText("异常"); - m_results[index]->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::DANGER)); + m_results[index]->setText("故障"); + m_results[index]->setStyleSheet(QString("color:%1;font-size:44px;font-weight:700;").arg(C::DANGER)); } - m_progress->setValue((index + 1) * 10); + m_progress->setValue((index + 1) * 100 / kSelfCheckItemCount); } void SelfCheckPage::onSelfCheckDone(bool allOk) @@ -224,7 +240,6 @@ void SelfCheckPage::onSelfCheckDone(bool allOk) void SelfCheckPage::onIgnoreFault() { - // emit navigate(PageJob); // 原:直接进作业选择页 emit navigate(PageHome); // 忽略故障后进入系统首页面 } @@ -235,8 +250,10 @@ void SelfCheckPage::onJobSelect() void SelfCheckPage::onHelp() { + // 与系统首页面的「操作说明」同一份文案(2+1 行) ConfirmDialog::message(this, "操作说明", - "系统自检将依次检测:通信系统、底盘通信、机械臂通信、洗消通信、" - "传感器信号、清水液位及电动球阀 D1/D2 的开启与关闭。\n" - "全部正常后方可进入作业选择界面。"); + "自检过程主要检测各分系统通信、传感器信号数据是否正常、\n" + "电动球阀D1~D2是否能正常开启和关闭。\n" + "自检正常或人为点击“忽略故障”,则进入“系统首页面”。", + 1000, 34); } diff --git a/pages/selfcheck.h b/pages/selfcheck.h index 96fde65..e898053 100644 --- a/pages/selfcheck.h +++ b/pages/selfcheck.h @@ -24,6 +24,7 @@ protected: void showEvent(QShowEvent* ev) override; private slots: + void onSelfCheckItemBusy(int index); void onSelfCheckItem(int index, bool ok); void onSelfCheckDone(bool allOk); void onIgnoreFault(); @@ -31,7 +32,7 @@ private slots: void onHelp(); private: - void resetItems(); // 复位 10 个自检项(指示灯置灰、结果“待检测”) + void resetItems(); // 复位 14 个自检项(指示灯置灰、结果“待检测”) TitleBar* m_titleBar = nullptr; QLabel* m_status = nullptr; diff --git a/pages/washbase.cpp b/pages/washbase.cpp deleted file mode 100644 index ed56c11..0000000 --- a/pages/washbase.cpp +++ /dev/null @@ -1,254 +0,0 @@ -#include "washbase.h" -#include "titlebar.h" -#include "alarmbanner.h" -#include "datacard.h" -#include "ledindicator.h" -#include "car.h" -#include "confirmdialog.h" -#include -#include -#include -#include -#include "global.h" - -WashBasePage::WashBasePage(AppContext* ctx, const QString& title, const QString& washTitle, - 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(title); - 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(280); - main->addWidget(left); - - // 中央洗消卡 - m_washCard = new DataCard(washTitle, 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("柱塞泵故障代码", ""); - m_washCard->indentRows(); // 行文字缩进到中间偏左(卡片很宽,贴左缘会显得空) - main->addWidget(m_washCard, 1); - - // 右侧操作列 - QWidget* right = new QWidget(this); - QVBoxLayout* rightLay = new QVBoxLayout(right); - rightLay->setContentsMargins(0, 0, 0, 0); - rightLay->setSpacing(10); - - m_start = button("启动", "primary", right); - m_start->setFixedSize(354, 112); - connect(m_start, &QPushButton::clicked, this, &WashBasePage::onStart); - - m_stop = button("停止", "danger", right); - m_stop->setFixedSize(354, 112); - connect(m_stop, &QPushButton::clicked, this, &WashBasePage::onStop); - - rightLay->addWidget(m_start); - rightLay->addWidget(m_stop); - - // 参数区(子类填充) - m_paramsLayout = new QVBoxLayout(); - m_paramsLayout->setSpacing(8); - rightLay->addLayout(m_paramsLayout); - rightLay->addStretch(1); - - right->setFixedWidth(380); - main->addWidget(right); - - root->addLayout(main, 1); - - // 底部指示灯条 - QWidget* bottom = new QWidget(this); - bottom->setFixedHeight(94); - bottom->setStyleSheet(QString("background-color:%1;border-top:1px solid %2;") - .arg(C::PANEL, C::BORDER)); - QHBoxLayout* bottomLay = new QHBoxLayout(bottom); - bottomLay->setContentsMargins(24, 0, 24, 0); - bottomLay->setSpacing(18); - - 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); - - m_ledRun = new LedIndicator("启停", bottom); - m_ledPlunger = new LedIndicator("柱塞泵", bottom); - m_ledAir = new LedIndicator("空压机", bottom); - m_ledLeaf = new LedIndicator("叶片泵", bottom); - m_ledLevel = new LedIndicator("液位低", bottom); - m_ledD1 = new LedIndicator("D1", bottom); - m_ledD2 = new LedIndicator("D2", bottom); - m_ledFault = new LedIndicator("系统故障", bottom); - - bottomLay->addWidget(m_ledRun); - bottomLay->addWidget(m_ledPlunger); - bottomLay->addWidget(m_ledAir); - bottomLay->addWidget(m_ledLeaf); - bottomLay->addWidget(m_ledLevel); - bottomLay->addWidget(m_ledD1); - bottomLay->addWidget(m_ledD2); - bottomLay->addWidget(m_ledFault); - bottomLay->addStretch(1); - - 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); - - root->addWidget(bottom); - - // 故障代码轮流循环显示(2s/条) - m_faultTimer = new QTimer(this); - m_faultTimer->setInterval(2000); - connect(m_faultTimer, &QTimer::timeout, this, &WashBasePage::rotateFaults); - m_faultTimer->start(); - - connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &WashBasePage::refresh); - updateGating(); - refresh(); -} - -void WashBasePage::onStart() -{ - ctx()->comm()->sendWashStart(washModeTag()); - ctx()->washRunning = true; - updateGating(); - onWashStateChanged(true); -} - -void WashBasePage::onStop() -{ - ctx()->comm()->sendWashStop(); - ctx()->washRunning = false; - updateGating(); - onWashStateChanged(false); -} - -void WashBasePage::onBack() -{ - if (ctx()->washRunning) { - ConfirmDialog::toast(this, "请先停止"); - return; - } - emit navigate(PageJob); -} - -void WashBasePage::onFactory() -{ - if (ConfirmDialog::confirm(this, "恢复出厂值", "确定将参数恢复为出厂值?")) - resetFactory(); -} - -void WashBasePage::updateGating() -{ - bool running = ctx()->washRunning; - m_start->setEnabled(!running); - m_stop->setEnabled(running); - m_back->setEnabled(!running); -} - -void WashBasePage::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); - - // 故障代码显示(静态更新,循环由 rotateFaults 驱动) - rotateFaults(); -} - -void WashBasePage::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(); -} diff --git a/pages/washbase.h b/pages/washbase.h deleted file mode 100644 index 8ffae63..0000000 --- a/pages/washbase.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef WASHBASEPAGE_H -#define WASHBASEPAGE_H - -#include "base.h" -#include - -class TitleBar; -class AlarmBanner; -class DataCard; -class LedIndicator; -class QPushButton; -class QVBoxLayout; -class QTimer; - -// ===================== 洗消页面基类(⑨消除作业 / ⑩消毒作业 共用) ===================== -// 左侧状态卡 + 中央洗消参数卡 + 右侧启动/停止与参数 + 底部指示灯条 -class WashBasePage : public BasePage -{ - Q_OBJECT -public: - // title:页面标题;washTitle:中央洗消卡标题 - WashBasePage(AppContext* ctx, const QString& title, const QString& washTitle, - QWidget* parent = nullptr); - -protected: - // 子类向右侧参数区添加参数控件 - QVBoxLayout* paramsLayout() const { return m_paramsLayout; } - - // 启动/停止状态变化通知 - virtual void onWashStateChanged(bool running) { Q_UNUSED(running); } - - // 恢复出厂值(子类实现) - virtual void resetFactory() = 0; - - // 洗消模式标识:"clean" / "disinfect" - virtual QString washModeTag() const = 0; - -private slots: - void onStart(); - void onStop(); - void onBack(); - void onFactory(); - void refresh(); - void rotateFaults(); - -private: - void updateGating(); - - 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; - - // 底部指示灯 - 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; - - // 中央洗消卡字段索引 - 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 // WASHBASEPAGE_H diff --git a/pages/washpage.cpp b/pages/washpage.cpp index 547b9cf..2858ed1 100644 --- a/pages/washpage.cpp +++ b/pages/washpage.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include "global.h" WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) @@ -40,17 +39,17 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) leftLay->setSpacing(10); m_chassisCard = new DataCard("底盘", left); - m_chassisCard->addField("电量", "85%"); - m_chassisCard->addField("电压", "360V"); - m_chassisCard->addField("故障", "无"); + m_chassisCard->addField("底盘电量SOC", "--"); + m_chassisCard->addField("底盘电压U", "--"); m_armCard = new DataCard("机械臂", left); - m_armCard->addField("末端位置", "XYZ"); - m_armCard->addField("姿态", "RX/RY/RZ"); + m_armCard->addField("末端位置", "--"); + m_armCard->addField("末端姿态", "--"); m_gasCard = new DataCard("气体检测仪", left); - m_gasCard->addField("主机状态", "待机"); - m_gasCard->addField("浓度", "0 ppb"); + m_gasCard->addField("主机状态", "--"); + m_gasCard->addField("有毒物种类", "--"); + m_gasCard->addField("有毒物等级", "--"); leftLay->addWidget(m_chassisCard, 1); leftLay->addWidget(m_armCard, 1); @@ -62,16 +61,14 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) // 中央洗消卡 m_washCard = new DataCard("洗消", 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("柱塞泵故障代码", ""); + 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); @@ -148,10 +145,11 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) root->addLayout(main, 1); - // 底部条:左右两个按钮纵向占满两行,中间上下两排(泵数值 / 指示灯)居中 + // 底部条:左右两个按钮纵向占满,中间三排(泵数值 / 指示灯 / 两路液位低)居中。 + // 高度由 160 加到 188:中间那列三排约 164px 再加 8+8 的上下边距,160 装不下第三排 QWidget* bottom = new QWidget(this); bottom->setObjectName("WashBottom"); - bottom->setFixedHeight(160); + bottom->setFixedHeight(188); // 用 #WashBottom 限定选择器:不带选择器的样式表会连子控件一起套上边框和底色 bottom->setStyleSheet(QString("QWidget#WashBottom{background-color:%1;border-top:1px solid %2;}") .arg(C::PANEL, C::BORDER)); @@ -166,7 +164,7 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) connect(m_factory, &QPushButton::clicked, this, &WashPage::onFactory); bottomRoot->addWidget(m_factory, 0, Qt::AlignVCenter); - // 中:上排三泵数值、下排指示灯,两排都水平居中 + // 中:上排三泵数值、中排指示灯、下排两路液位低,三排都水平居中 QVBoxLayout* midLay = new QVBoxLayout(); midLay->setSpacing(6); @@ -186,27 +184,36 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) valueLay->addStretch(1); QHBoxLayout* ledLay = new QHBoxLayout(); - ledLay->setSpacing(10); // 8 个灯 40px 字号后总宽约 1300px + ledLay->setSpacing(10); ledLay->addStretch(1); - auto makeLed = [&](const QString& text) { + // 灯要分两行放,故把目标行当参数传进来 + auto makeLed = [&](QHBoxLayout* row, const QString& text) { LedIndicator* led = new LedIndicator(text, bottom); led->setTextPixelSize(40); // 字号与卡片标题一致 - ledLay->addWidget(led); + row->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("系统故障"); + 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); @@ -219,12 +226,6 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) 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(); @@ -243,26 +244,30 @@ void WashPage::refreshPumpValues() // 否则旧配置里超范围的历史值会与控件显示、实际上报的数值对不上 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)); + 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()); - ctx()->washRunning = true; + // 同时把机器强制到机械臂模式(见 AppContext::setWashRunning),否则 Arm_enable=0, + // 末端那条俯仰摆动指令车端不会理 updateGating(); + // 启停灯立刻跟着变,不等下一帧遥测——否则车端不回话时按钮看着像没反应 + updateLeds(); } void WashPage::onStop() { ctx()->comm()->sendWashStop(); - ctx()->washRunning = false; + ctx()->setWashRunning(false); updateGating(); + updateLeds(); } void WashPage::onBack() { - if (ctx()->washRunning) { + if (ctx()->isWashRunning()) { ConfirmDialog::toast(this, "请先停止"); return; } @@ -298,7 +303,7 @@ void WashPage::resetFactory() void WashPage::updateGating() { - bool running = ctx()->washRunning; + bool running = ctx()->isWashRunning(); m_start->setEnabled(!running); m_stop->setEnabled(running); m_back->setEnabled(!running); @@ -312,58 +317,56 @@ void WashPage::refresh() else m_banner->clear(); // 以泵状态同步运行状态 - if (t.plunger || t.air || t.leaf) ctx()->washRunning = true; - else if (t.washMode == "--") ctx()->washRunning = false; + if (t.plunger || t.air || t.leaf) ctx()->setWashRunning(true); + else if (t.washMode == "--") ctx()->setWashRunning(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(";")); + // 未收到对应报文的数值统一显示 "--"(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, t.gasHost == "detecting" ? "检测中" : (t.gasHost == "fault" ? "故障" : "待机")); - m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); + 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, 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_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)); - // 指示灯 - 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); + updateLeds(); // 底部三个泵的数值 refreshPumpValues(); - - // 故障代码显示(静态更新,循环由 rotateFaults 驱动) - rotateFaults(); } -void WashPage::rotateFaults() +// 底栏指示灯。单独拆出来是为了让 onStart/onStop 能直接调它: +// 遥测还没回来时 refresh() 里的“以泵状态同步运行状态”会把刚置上的 washRunning 又抹掉, +// 所以启停的即时反馈不能靠整个 refresh()。 +void WashPage::updateLeds() { 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(); + + 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); } diff --git a/pages/washpage.h b/pages/washpage.h index 31800ff..6d38925 100644 --- a/pages/washpage.h +++ b/pages/washpage.h @@ -11,7 +11,6 @@ class ParamControl; class QPushButton; class QLabel; class QVBoxLayout; -class QTimer; // ===================== 洗消页(⑨消除作业 / ⑩消毒作业 共用) ===================== // 自包含页面:三列骨架、中央洗消卡、右侧启停与参数区、底部指示灯条都在本文件里。 @@ -30,10 +29,10 @@ private slots: void onBack(); void onFactory(); void refresh(); - void rotateFaults(); private: void updateGating(); + void updateLeds(); // 底栏指示灯(启停 / 泵 / 液位低 / 球阀 / 系统故障) void resetFactory(); void refreshPumpValues(); // 刷新底部三个泵的数值 QString washModeTag() const; // "clean" / "disinfect" @@ -58,15 +57,17 @@ private: 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; + + // 底部第二行:两路液位低(协议里 A_LiquidLevel_Low / B_LiquidLevel_Low 本就是两个位) + LedIndicator* m_ledLevelA = nullptr; + LedIndicator* m_ledLevelB = nullptr; // 底部第二行:三个泵的当前流量值 QLabel* m_valPlunger = nullptr; @@ -77,10 +78,6 @@ private: 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 diff --git a/setTitle.QString b/setTitle.QString new file mode 100644 index 0000000..e69de29 diff --git a/test.txt b/test.txt deleted file mode 100644 index e61ef7b..0000000 --- a/test.txt +++ /dev/null @@ -1 +0,0 @@ -aa diff --git a/tools/__pycache__/can_sender.cpython-311.pyc b/tools/__pycache__/can_sender.cpython-311.pyc index 4fe0a33..da9f110 100644 Binary files a/tools/__pycache__/can_sender.cpython-311.pyc and b/tools/__pycache__/can_sender.cpython-311.pyc differ diff --git a/tools/can_sender.py b/tools/can_sender.py index 3a6c903..951891f 100644 --- a/tools/can_sender.py +++ b/tools/can_sender.py @@ -2,12 +2,12 @@ """车端 CAN 报文模拟器 / 上行抓包器(联调用,非工程代码)。 依据《洗消机器人遥控器通信协议20260810.xlsx》sheet RC(分类一/二/三,全部扩展帧、Intel 小端位序)。 -线格式:32 位帧头 0x5AA5FF00 + 32 位 canId + DLC(8) + 8 字节数据 + 校验和,共 18 字节, -多字节字段按小端排列,与 comm/udpcommunication.cpp 的 UdpCanFrame 一致。 +线格式:帧头 0xAA + 32 位 canId + DLC(8) + 8 字节数据 + 校验和 + 帧尾 0xBB,共 16 字节, +多字节字段按小端排列,与 comm/car.cpp 的 CanFrame 一致。 用法示例: # 向 UI 的 9001 端口周期发 VCU 反馈,看底盘页电量/电压/车速刷新 - python can_sender.py --soc 85 --volt 360 --speed 4.5 + python can_sender.py --soc 85 --volt 360 --speed 2.0 # 制造故障:MCU 码 61(电机缺相故障)+ 整系统通信异常(底盘BMS) python can_sender.py --mcu-fault 61 --comm-fault 2 @@ -24,7 +24,8 @@ import socket import struct import time -HDR = 0x5AA5FF00 +HDR = 0xAA # 帧头 +TAIL = 0xBB # 帧尾 # 分类一 遥控器控制指令(上行,UI → 车端) ID_CHASSIS_CMD = 0x18208081 @@ -91,11 +92,14 @@ def get_signed(d, start, ln): def checksum(can_id, dlc, d): - return (sum((can_id >> (8 * i)) & 0xFF for i in range(4)) + dlc + sum(d)) & 0xFF + # 帧头 + canId 四字节 + DLC + 数据八字节 + 帧尾 求和取低 8 位 + return (HDR + sum((can_id >> (8 * i)) & 0xFF for i in range(4)) + + dlc + sum(d) + TAIL) & 0xFF def pack(can_id, d): - return struct.pack("setFixedSize(900, 600); // 正文行数少 → 面板收窄 dlg.m_cancel->hide(); // 只保留“确认” dlg.m_title->setStyleSheet(QString("background:transparent;color:%1;font-size:50px;font-weight:700;").arg(C::TEXT)); - dlg.m_body->setStyleSheet(QString("background:transparent;color:%1;font-size:50px;").arg(C::TEXT)); + // 正文单独给字号:文案长的调用点可以调小它,让固定的字数在更窄的面板里排成预期行数 + dlg.m_body->setStyleSheet(QString("background:transparent;color:%1;font-size:%2px;") + .arg(C::TEXT).arg(fontPx)); dlg.m_title->setWordWrap(true); dlg.m_title->setContentsMargins(0, 20, 0, 0); // 标题文字距该行顶部 10px dlg.m_body->setContentsMargins(0, 0, 0, 0); @@ -97,6 +99,14 @@ void ConfirmDialog::message(QWidget* parent, const QString& title, const QString } dlg.setTitle(title); dlg.setText(text); + + // 面板宽度由调用点给定,高度按内容算:标题/正文都开了 wordWrap,宽度先定下来, + // layout 才能用 heightForWidth 量出准确高度(QLabel 的 sizeHint 对换行文本不准, + // 所以不能靠 adjustSize)。宽度以前是写死的常量、三个调用点共用,一处的长文案 + // 会把别处的弹窗一起撑宽,故改成逐调用点传入。 + const int kW = width; + dlg.m_panel->ensurePolished(); // 先让 QSS 的 50px 字号落到控件上,量出来的高度才对 + dlg.m_panel->setFixedSize(kW, dlg.m_panel->heightForWidth(kW)); dlg.exec(); } diff --git a/widgets/confirmdialog.h b/widgets/confirmdialog.h index df75595..e9707b7 100644 --- a/widgets/confirmdialog.h +++ b/widgets/confirmdialog.h @@ -20,8 +20,12 @@ public: // 模态确认,返回是否“确认” static bool confirm(QWidget* parent, const QString& title, const QString& text); - // 独立信息弹窗(仅“确认”按钮,替代系统 QMessageBox;正文 50px) - static void message(QWidget* parent, const QString& title, const QString& text); + // 独立信息弹窗(仅“确认”按钮,替代系统 QMessageBox)。 + // width = 面板宽度,fontPx = **正文字号**(标题固定 50px);正文按宽度换行、高度按内容算。 + // 两个参数都是**各调用点各给各的**——以前写死共用,一处的长文案会把别处的弹窗一起撑宽, + // 想让它窄一点就只能靠截断,现在可以靠缩字号。 + static void message(QWidget* parent, const QString& title, const QString& text, + int width = 1200, int fontPx = 50); // 轻提示:显示 text,ms 毫秒后自动消失(用于“修改成功”等) static void toast(QWidget* parent, const QString& text, int ms = 2000); diff --git a/widgets/datacard.cpp b/widgets/datacard.cpp index c702b48..8638f4e 100644 --- a/widgets/datacard.cpp +++ b/widgets/datacard.cpp @@ -2,9 +2,40 @@ #include #include #include +#include +#include +#include #include "global.h" #include +namespace { +// 行高的上限。行按 stretch 长到这个高度就停,多出来的空间还给卡片末尾的弹性—— +// 否则卡片一高(首页 2×2 那种高卡)就把两三行拉成稀稀拉拉一大片。 +// 数值行的最大字号是 50px(行高约 60px),72 给足了余量,想调松紧改这个数。 +const int kRowMaxH = 72; + +// 字号:数值行标签 32(数值字号随卡片走,见 DataCard::setValueFontPx) +const int kLabelFontPx = 32; + +// 故障行单独一号,且**全应用统一**。放不下时不再截断,而是整条从右往左自动滑动 +// (见 tickFaultScroll),所以字号可以比数值行小一点也依然读得全。 +const int kFaultFontPx = 40; +const int kFaultScrollMs = 30; // 滑动节拍 +const int kFaultScrollStep = 3; // 每拍左移像素 -> 约 100px/s + +// 故障文字的裁剪容器至少要有一行高,否则没有布局时 QWidget::sizeHint() 是 (-1,-1), +// 这一行会被压成 0 高 +const int kFaultBoxMinH = kFaultFontPx + 8; + +// 给一行的所有控件设最大高度,从而把整行的高度卡住(QHBoxLayout 的高度取子项最大值) +void capRow(QHBoxLayout* row, int h) +{ + for (int i = 0; i < row->count(); ++i) + if (QWidget* w = row->itemAt(i)->widget()) + w->setMaximumHeight(h); +} +} // namespace + DataCard::DataCard(const QString& title, QWidget* parent) : QWidget(parent) { @@ -23,7 +54,11 @@ DataCard::DataCard(const QString& title, QWidget* parent) .arg(C::TEXT, C::BORDER)); lay->addWidget(m_title); - // 字段行容器 + // 故障行:构造时就建好,永远占着最后一行——有无故障都不改变卡片布局。 + ensureFaultRow(); + + // 末尾弹性:各行长到自己的上限后,剩下的高度全归它。 + // 顺序因此固定为 [标题][数值行…][故障行][弹性],addField 插在倒数第二项之前。 lay->addStretch(1); } @@ -41,18 +76,23 @@ int DataCard::addField(const QString& label, const QString& value, bool big) QLabel* lab = new QLabel(label + ":", this); lab->setProperty("sub", true); - lab->setStyleSheet(QString("color:%1;font-size:32px;").arg(C::TEXT_SUB)); + lab->setStyleSheet(QString("color:%1;font-size:%2px;").arg(C::TEXT_SUB).arg(kLabelFontPx)); + // 数值标签只在行内样式里给字号/字重,颜色交给应用级样式表的 [cardValue] / [alert] 规则: + // 控件自身的样式表优先级高于应用级样式表,行内一写 color,告警的红字与暗红闪烁就全被盖掉, + // 属性照样翻转、界面上却看不出任何变化。AlarmBanner 就是这么写的,这里保持一致。 QLabel* val = new QLabel(value, this); - val->setStyleSheet(QString("color:%1;%2").arg(C::TEXT, - big ? "font-size:50px;font-weight:700;" : "font-size:42px;font-weight:700;")); + val->setProperty("cardValue", true); + val->setStyleSheet(QString("font-size:%1px;font-weight:700;").arg(big ? 50 : m_valueFontPx)); row->addWidget(lab); row->addWidget(val, 1); - // 插入到标题栏之后(stretch 之前) + // 插到故障行之前(末尾两项固定是 [故障行][弹性]); + // stretch = 1 让行在有富余高度时长到 kRowMaxH 为止,再高就不长了 + capRow(row, kRowMaxH); QVBoxLayout* lay = qobject_cast(layout()); - lay->insertLayout(lay->count() - 1, row); + lay->insertLayout(lay->count() - 2, row, 1); m_values.append(val); m_labels.append(label); @@ -71,17 +111,159 @@ void DataCard::setAlarm(int index, bool on) if (index < 0 || index >= m_values.size()) return; QLabel* val = m_values[index]; - if (on) { - val->setProperty("alert", true); + if (val->property("alert").toBool() == on) + return; // 每次遥测刷新都会调进来,状态没变就别重算样式 + val->setProperty("alert", on); + if (on) startBlink(val, "blink"); - } else { + else stopBlink(val, "blink"); - val->setProperty("alert", false); - val->style()->unpolish(val); - val->style()->polish(val); + // 置属性不会自动重算样式,不补这一次重算,红字要等下一次 blink 翻转(最多 500ms)才出现 + val->style()->unpolish(val); + val->style()->polish(val); + val->update(); +} + +// 故障行:构造时就建好,永远占着最后一行,高度与位置都不随有无故障变化。 +// 不放“故障:”标签——那要吃掉一百多像素,卡片本来就窄;整行宽度都给文字。 +// 文字比整行还宽时不截断,改成在裁剪容器里从右往左自动滑动。 +void DataCard::ensureFaultRow() +{ + if (m_faultRow) + return; + + m_faultRow = new QHBoxLayout(); + m_faultRow->setSpacing(0); + + // 裁剪容器:子控件超出它的范围会被裁掉,正好当滑动窗口用。 + // 里面不放布局,标签的宽度和位置由 positionFaultLabel() 手动给。 + m_faultBox = new QWidget(this); + m_faultBox->setMinimumHeight(kFaultBoxMinH); + m_faultBox->installEventFilter(this); // 宽度定了才知道要不要滑 + + // 与字段行同规矩:行内只写字号/字重,颜色交给应用级样式表的 [cardValue] / [alert] + m_faultValue = new QLabel(m_faultBox); + m_faultValue->setProperty("cardValue", true); + m_faultValue->setStyleSheet(QString("font-size:%1px;font-weight:700;").arg(kFaultFontPx)); + + m_faultRow->addWidget(m_faultBox, 1); + capRow(m_faultRow, kRowMaxH); // 与数值行同一个上限 + + // 无故障时用**空文字**占位,而不是 hide():隐藏的子控件会让这一项被判成“空”, + // QBoxLayout 连它那份高度一起跳过,卡片照样会跳一下。 + + QVBoxLayout* lay = qobject_cast(layout()); + if (lay) + lay->addLayout(m_faultRow, 1); // 固定在数值行之后,高度同样受 kRowMaxH 限制 + + m_faultTimer = new QTimer(this); + m_faultTimer->setInterval(1000); // 多条故障按 1s 轮流显示 + connect(m_faultTimer, &QTimer::timeout, this, &DataCard::rotateFault); + + m_scrollTimer = new QTimer(this); + m_scrollTimer->setInterval(kFaultScrollMs); + connect(m_scrollTimer, &QTimer::timeout, this, &DataCard::tickFaultScroll); +} + +// 设文字、按字体量出像素宽,再决定停着显示还是滑动 +void DataCard::applyFaultText() +{ + QFont f = m_faultValue->font(); + f.setPixelSize(kFaultFontPx); // QSS 的字号未必已经反映到 QLabel::font() 上 + const QFontMetrics fm(f); + + m_faultValue->setText(m_faultText); + m_textW = m_faultText.isEmpty() ? 0 : fm.horizontalAdvance(m_faultText) + 2; + m_faultValue->setFixedWidth(m_textW > 0 ? m_textW : 1); + m_offset = 0; + + updateFaultScroll(); + positionFaultLabel(); +} + +// 放得下就停着显示;放不下才滑动。首次布局前 m_faultBox->width() 还是默认值, +// 这里会先当成"放得下",等拿到真宽度后由 eventFilter 再判一次。 +void DataCard::updateFaultScroll() +{ + const bool need = m_textW > 0 && m_faultBox->width() > 0 && m_textW > m_faultBox->width(); + if (need) { + if (!m_scrollTimer->isActive()) m_scrollTimer->start(); + } else { + m_scrollTimer->stop(); + m_offset = 0; } } +void DataCard::positionFaultLabel() +{ + m_faultValue->resize(m_textW > 0 ? m_textW : 1, m_faultBox->height()); + m_faultValue->move(-m_offset, 0); // 负偏移 = 往左移出裁剪容器 +} + +// 左移一拍;整条完全移出左边界后从头再来 +void DataCard::tickFaultScroll() +{ + if (m_textW <= 0) + return; + m_offset += kFaultScrollStep; + if (m_offset > m_textW) + m_offset = 0; + positionFaultLabel(); +} + +bool DataCard::eventFilter(QObject* obj, QEvent* ev) +{ + if (obj == m_faultBox && ev->type() == QEvent::Resize) { + // 真宽度下来了,这一刻才判得准要不要滑 + updateFaultScroll(); + positionFaultLabel(); + } + return QWidget::eventFilter(obj, ev); +} + +void DataCard::setFaults(const QStringList& faults) +{ + // 每次遥测刷新都会调进来。内容没变就直接返回:既省掉重复重算样式, + // 也避免把 m_faultIdx 清零——否则多条故障永远停在第一条上。 + if (faults == m_faults) + return; + m_faults = faults; + m_faultIdx = 0; + + // 行本身一直占着位(见 ensureFaultRow),这里只切内容: + // 无故障清空文字、有故障填内容并红闪——显隐规则与以前一致。 + if (m_faults.isEmpty()) { + m_faultTimer->stop(); + stopBlink(m_faultValue, "blink"); + m_faultValue->setProperty("alert", false); + m_faultText.clear(); + applyFaultText(); // 清空文字:停滑、宽度归 1,这一行仍然占位 + return; + } + + m_faultText = m_faults.at(0); + applyFaultText(); + m_faultValue->setProperty("alert", true); + startBlink(m_faultValue, "blink"); + m_faultValue->style()->unpolish(m_faultValue); + m_faultValue->style()->polish(m_faultValue); + m_faultValue->update(); + + if (m_faults.size() > 1) + m_faultTimer->start(); + else + m_faultTimer->stop(); +} + +void DataCard::rotateFault() +{ + if (m_faults.size() < 2) + return; + m_faultIdx = (m_faultIdx + 1) % m_faults.size(); + m_faultText = m_faults.at(m_faultIdx); + applyFaultText(); +} + void DataCard::setHighlight(bool on) { if (on) diff --git a/widgets/datacard.h b/widgets/datacard.h index dcd5d9a..b0ee310 100644 --- a/widgets/datacard.h +++ b/widgets/datacard.h @@ -1,13 +1,21 @@ #ifndef DATACARD_H #define DATACARD_H -#include +#include +#include #include +#include class QLabel; +class QHBoxLayout; +class QTimer; +class QEvent; // ===================== 数据卡片 ===================== -// 标题栏 + 若干“标签:值”行;支持指定字段红色闪烁告警 +// 标题栏 + 若干“标签:值”行 + 一条常驻的“故障”行。 +// 每行有最大高度、长到上限就停,多出来的高度归末尾弹性,所以有无故障都不改变布局; +// 字段行支持红色闪烁告警;故障行没有标签、字号 kFaultFontPx,有故障时红闪、 +// 多条按 1s 轮流显示,文字比整行宽时自动滑动,无故障时内容为空(行仍占位)。 class DataCard : public QWidget { Q_OBJECT @@ -18,13 +26,41 @@ public: int addField(const QString& label, const QString& value = "--", bool big = false); void setField(int index, const QString& value); void setAlarm(int index, bool on); + void setFaults(const QStringList& faults); // 故障行内容(空 = 清空文字,行仍占位) + // 数值行字号。要在 addField 之前调——字号是建标签时就写进行内样式表的,改不了已建的。 + // big 字段不走这个值(固定 50)。 + void setValueFontPx(int px) { m_valueFontPx = px; } void setHighlight(bool on); // 主色描边突出 void indentRows(); // 字段行整体缩进到「中间偏左」(行首 : 行尾 = 2 : 3),只需调用一次 +protected: + bool eventFilter(QObject* obj, QEvent* ev) override; // 盯故障数值标签的宽度变化 + +private slots: + void rotateFault(); + void tickFaultScroll(); // 故障文字滑动一拍 + private: + void ensureFaultRow(); + void applyFaultText(); // 设文字、量宽度、决定要不要滑动 + void updateFaultScroll(); // 放得下就停着显示,放不下才开滑动 + void positionFaultLabel(); // 按当前偏移摆放故障标签 + QLabel* m_title = nullptr; + QHBoxLayout* m_faultRow = nullptr; // 构造函数里就建好,永远占着最后一行 + QWidget* m_faultBox = nullptr; // 裁剪容器:文字超宽时标签在这里面左移 + QLabel* m_faultValue = nullptr; // 无“故障:”标签;宽度按文字自然宽度,位置手动给 + QTimer* m_faultTimer = nullptr; // 多条故障 1s 轮换 + QTimer* m_scrollTimer = nullptr; // 滑动节拍 + QStringList m_faults; // 当前故障列表,空 = 无故障 + QString m_faultText; // 当前该显示的完整文字 + int m_faultIdx = 0; + int m_textW = 0; // 当前故障文字的像素宽 + int m_offset = 0; // 已左移的像素 + QVector m_values; QVector m_labels; + int m_valueFontPx = 42; // 数值行字号;big 字段固定 50,不受它影响 }; #endif // DATACARD_H diff --git a/widgets/ledindicator.cpp b/widgets/ledindicator.cpp index 85cc0c1..bf63a6b 100644 --- a/widgets/ledindicator.cpp +++ b/widgets/ledindicator.cpp @@ -26,21 +26,17 @@ LedIndicator::LedIndicator(const QString& text, QWidget* parent) void LedIndicator::setState(State s) { m_state = s; - bool needBlink = (s == RedBlink || s == YellowBlink); - if (needBlink && !m_blink) { - m_blink = new QTimer(this); - m_blink->setInterval(500); - connect(m_blink, &QTimer::timeout, this, [this]() { - m_on = !m_on; - update(); - }); - m_blink->start(); - m_on = true; - } else if (!needBlink && m_blink) { - m_blink->stop(); - delete m_blink; - m_blink = nullptr; - m_on = true; + const bool needBlink = (s == RedBlink || s == YellowBlink); + if (needBlink && !m_blinkConn) { + // 挂全局节拍,而不是本控件自己起定时器:各自起的话相位互不相同, + // 同一屏上的几个指示灯(顶部通信状态、底栏、自检结果…)会各闪各的。 + m_blinkConn = connect(BlinkBeat::instance(), &BlinkBeat::toggled, this, + [this](bool on) { m_on = on; update(); }); + m_on = BlinkBeat::instance()->on(); // 立刻对齐当前相位,别等下一拍 + } else if (!needBlink && m_blinkConn) { + disconnect(m_blinkConn); + m_blinkConn = QMetaObject::Connection(); + m_on = true; // 停闪=常亮 } update(); } @@ -103,7 +99,11 @@ void LedIndicator::paintEvent(QPaintEvent*) p.drawEllipse(c, m_radius, m_radius); // 文字(圆点在文字后时,文字从控件左边缘开始) - p.setPen(QColor(C::TEXT)); + QColor textColor = m_textColor.isValid() ? m_textColor : QColor(C::TEXT); + // 顶部状态条这类要“整条闪烁”的场合:文字与圆点同步降到 40% 透明度 + if (m_blinkText && (m_state == RedBlink || m_state == YellowBlink) && !m_on) + textColor.setAlpha(102); + p.setPen(textColor); const QRectF textRect(m_dotAfter ? 0 : textLeft(), 0, m_dotAfter ? textW : width() - textLeft() - 10, height()); p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, m_text); diff --git a/widgets/ledindicator.h b/widgets/ledindicator.h index 8e7568e..8dbc353 100644 --- a/widgets/ledindicator.h +++ b/widgets/ledindicator.h @@ -1,8 +1,9 @@ #ifndef LEDINDICATOR_H #define LEDINDICATOR_H +#include +#include #include -#include // ===================== 指示灯(自绘圆形渐变 + 外发光) ===================== // 状态:灰=未运行/关闭/正常,绿=运行/开启/到位,红=故障/液位低, @@ -20,6 +21,9 @@ public: void setTextPixelSize(int px) { m_textPx = px; updateGeometry(); update(); } // 文字字号 void setRadius(int r) { m_radius = r; updateGeometry(); update(); } // 圆形指示点半径 void setDotAfterText(bool after) { m_dotAfter = after; updateGeometry(); update(); } // 圆点画在文字后面 + void setTextColor(const QColor& c) { m_textColor = c; update(); } // 文字颜色,默认 C::TEXT + // 文字是否随闪烁状态一起明暗(默认只闪圆点,文字常亮) + void setBlinkText(bool on) { m_blinkText = on; update(); } State state() const { return m_state; } QSize sizeHint() const override; // 宽度按文字实际宽度计算,避免文字被裁 @@ -32,11 +36,13 @@ private: State m_state = Off; QString m_text; - QTimer* m_blink = nullptr; + QMetaObject::Connection m_blinkConn; // 订阅全局闪烁节拍(见 BlinkBeat) bool m_on = true; int m_radius = 16; // 圆形指示点 32×32 int m_textPx = 24; // 文字字号 bool m_dotAfter = false; // false=圆点在文字前,true=圆点在文字后 + QColor m_textColor; // 无效色 = 用默认 C::TEXT + bool m_blinkText = false; // true=文字随闪烁明暗,false=文字常亮 }; #endif // LEDINDICATOR_H diff --git a/widgets/statusstrip.cpp b/widgets/statusstrip.cpp index 6fd6880..e722fa8 100644 --- a/widgets/statusstrip.cpp +++ b/widgets/statusstrip.cpp @@ -18,32 +18,8 @@ StatusStrip::StatusStrip(AppContext* ctx, QWidget* parent) lay->setContentsMargins(36, 0, 24, 0); lay->setSpacing(40); - // 底部不再显示底盘/液位数据(改由各页面卡片展示),只保留模式徽标 + 返回按钮 - //auto makeVal = [this](const QString& label) { - // QLabel* l = new QLabel(this); - // l->setStyleSheet(QString("color:%1;font-size:40px;").arg(C::TEXT_SUB)); - // l->setProperty("label", label); - // return l; - //}; - //m_battery = makeVal("底盘电量"); - //m_voltage = makeVal("底盘电压"); - //m_levelA = makeVal("清水液位"); - //m_levelFoam = makeVal("泡沫液位"); - //lay->addWidget(m_battery); - //lay->addWidget(m_voltage); - //lay->addWidget(m_levelA); - //lay->addWidget(m_levelFoam); - lay->addStretch(1); - // 模式徽标(仅显示:行车/机械臂由遥控器拨钮经串口回传,界面不参与切换) - m_mode = new QLabel("行车模式", this); - m_mode->setFixedSize(260, 80); - m_mode->setAlignment(Qt::AlignCenter); - m_mode->setStyleSheet(QString("background-color:%1;color:#FFFFFF;border-radius:8px;font-size:40px;font-weight:700;") - .arg(C::SUCCESS)); - lay->addWidget(m_mode); - // 返回按钮 m_back = new QPushButton("返回", this); m_back->setFixedSize(200, 80); @@ -52,11 +28,6 @@ StatusStrip::StatusStrip(AppContext* ctx, QWidget* parent) lay->addWidget(m_back); connect(m_ctx->comm(), &CarCommunication::telemetryUpdated, this, &StatusStrip::refresh); - connect(m_ctx, &AppContext::modeChanged, this, [this](Mode m) { - m_mode->setText(m == Mode::Drive ? "行车模式" : "机械臂模式"); - m_mode->setStyleSheet(QString("background-color:%1;color:#FFFFFF;border-radius:8px;font-size:40px;font-weight:700;") - .arg(m == Mode::Drive ? C::SUCCESS : C::WARNING)); - }); refresh(); } @@ -70,17 +41,7 @@ void StatusStrip::setBackEnabled(bool enabled) m_back->setEnabled(enabled); } -void StatusStrip::setModeVisible(bool visible) -{ - m_mode->setVisible(visible); -} - void StatusStrip::refresh() { // 底部已不显示遥测数据,保留空实现(仍连着 telemetryUpdated,方便以后加显示项) - //const Telemetry& t = m_ctx->telemetry; - //m_battery->setText(QString("底盘电量:%1%").arg(t.soc)); - //m_voltage->setText(QString("底盘电压:%1V").arg(t.volt, 0, 'f', 0)); - //m_levelA->setText(QString("清水液位:%1L").arg(t.levelA, 0, 'f', 0)); - //m_levelFoam->setText(QString("泡沫液位:%1L").arg(t.levelFoam, 0, 'f', 0)); } diff --git a/widgets/statusstrip.h b/widgets/statusstrip.h index 0de7e76..c815e76 100644 --- a/widgets/statusstrip.h +++ b/widgets/statusstrip.h @@ -8,7 +8,7 @@ class QPushButton; class AppContext; // ===================== 底部状态条 ===================== -// 只保留:模式徽标(仅显示)+ 返回按钮。行车/机械臂由遥控器拨钮决定,界面不参与切换。 +// 只保留「返回」按钮。行车/机械臂模式徽标已撤——底部不再展示模式。 class StatusStrip : public QWidget { Q_OBJECT @@ -17,7 +17,6 @@ public: void setBackVisible(bool visible); void setBackEnabled(bool enabled); - void setModeVisible(bool visible); // 固定行车模式的页面(如底盘页)可隐藏模式徽标 signals: void backClicked(); @@ -27,11 +26,6 @@ private slots: private: AppContext* m_ctx; - //QLabel* m_battery = nullptr; // 底部不再显示数据 - //QLabel* m_voltage = nullptr; - //QLabel* m_levelA = nullptr; - //QLabel* m_levelFoam = nullptr; - QLabel* m_mode = nullptr; // 模式徽标(只显示,不参与切换) QPushButton* m_back = nullptr; }; diff --git a/widgets/titlebar.cpp b/widgets/titlebar.cpp index 946c6a3..8b6e557 100644 --- a/widgets/titlebar.cpp +++ b/widgets/titlebar.cpp @@ -2,10 +2,22 @@ #include "ledindicator.h" #include "appcontext.h" #include "car.h" +#include #include #include #include "global.h" +namespace { +// 首页/作业选择“中央卡片区”的左边缘:主区左内边距 24 + 左列 300 + 间距 24 +const int kCardLeft = 300; +// LedIndicator 的文字起点(圆点区宽度):radius(16)×2 + 左侧留白 18,见 LedIndicator::textLeft() +const int kCommTextLeft = 50; +// 顶栏左右区宽度。左区要容下最长的一条异常“气体检测仪通信异常” +// (9 个全角字 × 43px = 387):274(左侧留白) + 50 + 387 + 10 = 721; +// 上限 726——居中标题的左边缘在 750,再宽就压上去了。 +const int kSideW = 726; +} // namespace + TitleBar::TitleBar(AppContext* ctx, QWidget* parent) : QWidget(parent), m_ctx(ctx) { @@ -14,16 +26,18 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent) setStyleSheet(QString("QWidget#TitleBar{background-color:%1;border-bottom:1px solid %2;}") .arg(C::PANEL, C::BORDER)); - // 左侧区(车载端通信状态) + // 左侧区(车载端通信状态,固定一行,不轮播) QWidget* leftBox = new QWidget(this); QHBoxLayout* leftLay = new QHBoxLayout(leftBox); - leftLay->setContentsMargins(260, 0, 0, 0); - leftLay->setSpacing(20); - m_comm = new LedIndicator("通信正常", leftBox); - m_comm->setFixedHeight(80); // 容纳放大后的文字,避免上下裁切 - m_comm->setTextPixelSize(45); + leftLay->setSpacing(0); // 只有指示器 + 弹簧,留间距只会白白吃掉文字宽度 + m_comm = new LedIndicator("系统通信正常", leftBox); + // 高度沿用 LedIndicator 默认的 52:43px 字与 48px 直径的圆点外发光刚好容得下 + m_comm->setTextPixelSize(45); // 与右侧“遥控器正常”同字号 + m_comm->setBlinkText(true); // 异常时文字跟着圆点一起闪 m_comm->setState(LedIndicator::Green); - leftLay->addWidget(m_comm); + leftLay->setContentsMargins(kCardLeft - 24 - kCommTextLeft, 0, 0, 0); + + leftLay->addWidget(m_comm, 0, Qt::AlignLeft | Qt::AlignVCenter); leftLay->addStretch(1); // 右侧区(遥控器状态:链路 + 电量) @@ -37,35 +51,37 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent) m_rc->setTextPixelSize(45); m_rc->setState(LedIndicator::RedBlink); rightLay->addWidget(m_rc); - m_rcBat = new QLabel("--%", rightBox); - m_rcBat->setStyleSheet(QString("color:%1;font-size:60px;").arg(C::TEXT_SUB)); + m_rcBat = new QLabel(rightBox); m_rcBat->setAlignment(Qt::AlignCenter); rightLay->addWidget(m_rcBat); + setRcBattery(-1); - // 左右两侧等宽 → 中间标题恰好居中于屏幕 - // 左区需 260(左边距)+ 240(圆点区与留白 60 + 文字 4×45)= 500 - // 右区需 285(圆点区与留白 60 + 文字 5×45)+ 30 + 130(电量)+ 20 = 465 - const int sideW = 520; - leftBox->setFixedWidth(sideW); - rightBox->setFixedWidth(sideW); + // 左右两侧区等宽(宽度见 kSideW:左区要放得下最长的一条异常) + leftBox->setFixedWidth(kSideW); + rightBox->setFixedWidth(kSideW); // 标题(居中) m_title = new QLabel("", this); m_title->setAlignment(Qt::AlignCenter); m_title->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::TEXT)); - QHBoxLayout* lay = new QHBoxLayout(this); + // 用 QGridLayout 让标题横跨整条顶栏居中,而不是夹在左右两区之间拉伸: + // 后者下标题的可用宽度 = 1920 - 24×2 - 两侧区宽×2,左区为了放下通信文字一加宽, + // 标题就被挤窄甚至裁字;横跨三列后标题始终居中于屏幕,两侧区多宽都不影响它。 + QGridLayout* lay = new QGridLayout(this); lay->setContentsMargins(24, 6, 24, 6); lay->setSpacing(0); - lay->addWidget(leftBox); - lay->addWidget(m_title, 1); - lay->addWidget(rightBox); + lay->addWidget(leftBox, 0, 0, Qt::AlignLeft | Qt::AlignVCenter); + lay->addWidget(rightBox, 0, 2, Qt::AlignRight | Qt::AlignVCenter); + lay->addWidget(m_title, 0, 0, 1, 3, Qt::AlignCenter); - // 车载端通信状态刷新 - connect(m_ctx->comm(), &CarCommunication::commOkChanged, this, [this](bool ok) { - if (ok) { m_comm->setText("通信正常"); m_comm->setState(LedIndicator::Green); } - else { m_comm->setText("通信异常"); m_comm->setState(LedIndicator::RedBlink); } - }); + // 车载端通信状态:只在文案变化时由通信层通知,这里不做任何轮播。 + connect(m_ctx->comm(), &CarCommunication::commTextChanged, + this, &TitleBar::onCommTextChanged); + + // 每个页面各有自己的顶条实例,构造时通信层可能早就报过状态了;而 commTextChanged + // 只在内容变化时才发,所以这里必须先取一次当前值,否则新页面的顶条会一直停在构造时的初值。 + updateComm(); // 遥控器链路与电量刷新 connect(m_ctx->rc(), &RcCommunication::linkChanged, this, &TitleBar::onRcLink); @@ -73,6 +89,22 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent) onRcLink(m_ctx->rc()->linkOk()); } +void TitleBar::onCommTextChanged() +{ + updateComm(); +} + +// 左区固定显示通信状态:链路断或带 All_Comm_FaultCode 就红色闪烁,其余绿灯。 +// 文案由通信层渲染(Telemetry::commText),这里只决定配色。 +void TitleBar::updateComm() +{ + const Telemetry& t = m_ctx->telemetry; + const bool warn = !t.commOk || t.commCode != 0; + m_comm->setText(t.commText); + m_comm->setState(warn ? LedIndicator::RedBlink : LedIndicator::Green); + m_comm->setTextColor(QColor(warn ? C::DANGER : C::TEXT)); // 只有异常才用红色警示 +} + void TitleBar::setTitle(const QString& title) { m_title->setText(title); @@ -86,11 +118,21 @@ void TitleBar::onRcLink(bool ok) } else { m_rc->setText("遥控器断开"); m_rc->setState(LedIndicator::RedBlink); - m_rcBat->setText("--%"); + setRcBattery(-1); } } +void TitleBar::setRcBattery(int battery) +{ + // battery < 0 表示链路断开,无电量可显示 + const bool valid = battery >= 0; + const QString color = !valid ? C::TEXT_SUB + : (battery <= 20 ? C::DANGER : C::SUCCESS); + m_rcBat->setText(valid ? QString("%1%").arg(battery) : QString("--%")); + m_rcBat->setStyleSheet(QString("color:%1;font-size:50px;").arg(color)); +} + void TitleBar::onRcFrame(const RcFrame& f) { - m_rcBat->setText(QString("%1%").arg(f.battery)); + setRcBattery(f.battery); } diff --git a/widgets/titlebar.h b/widgets/titlebar.h index bef1aff..ccf0159 100644 --- a/widgets/titlebar.h +++ b/widgets/titlebar.h @@ -9,7 +9,9 @@ class AppContext; class LedIndicator; // ===================== 顶条组件(高 98px) ===================== -// 左侧“车载端通信:●正常/异常”,中央页面标题,右侧“遥控器:●正常/断开 + 电量” +// 左侧“车载端通信:●系统通信正常/xx通信异常”,中央页面标题,右侧“遥控器:●正常/断开 + 电量”。 +// 左区**固定**显示通信状态一行,不再轮播故障明细——各项子系统的故障明细分别进对应卡片的 +// “故障”行(见 DataCard::setFaults)。文案由 CarCommunication 渲染进 Telemetry::commText。 class TitleBar : public QWidget { Q_OBJECT @@ -21,8 +23,15 @@ public: private slots: void onRcLink(bool ok); void onRcFrame(const RcFrame& f); + void onCommTextChanged(); private: + // 按 Telemetry 的通信状态刷新左区指示灯 + void updateComm(); + + // 电量文字与配色:>20% 绿色,≤20% 红色,无数据时用次文字色 + void setRcBattery(int battery); + AppContext* m_ctx; LedIndicator* m_comm = nullptr; LedIndicator* m_rc = nullptr;