新版布局

This commit is contained in:
xu
2026-10-16 09:06:03 +08:00
parent e5efdea06e
commit 1ec93e42cb
37 changed files with 1785 additions and 977 deletions
+130 -1
View File
@@ -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\" && 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 \"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\" && 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"
] ]
} }
} }
+33 -13
View File
@@ -32,17 +32,18 @@ make -j4
**19 条**报文(分类四标准帧 `DIDO/PWM/AI/BMS/MCU` 未实现)。所有报文均为 **29 位扩展帧、 **19 条**报文(分类四标准帧 `DIDO/PWM/AI/BMS/MCU` 未实现)。所有报文均为 **29 位扩展帧、
信号一律 Intel(小端)位序**。 信号一律 Intel(小端)位序**。
串口是字节流,故线上帧统一为 **18 字节**,收发同一格式: 一个 UDP 数据报里可能塞多条帧,故线上帧统一为 **16 字节**,收发同一格式:
| 偏移 | 长度 | 含义 | | 偏移 | 长度 | 含义 |
| --- | --- | --- | | --- | --- | --- |
| 0 | 4 | 帧头 `0x5AA5FF00`(线上字节序 `00 FF A5 5A`,用于在字节流中定位报文起始) | | 0 | 1 | 帧头 `0xAA`(用于在数据报中定位报文起始) |
| 4 | 4 | CAN ID(29 位扩展帧),直接用于分流 | | 1 | 4 | CAN ID(29 位扩展帧),直接用于分流 |
| 8 | 1 | DLC,固定 `8` | | 5 | 1 | DLC,固定 `8` |
| 9 | 8 | CAN 数据段 | | 6 | 8 | CAN 数据段 |
| 17 | 1 | 校验和:canId 四字节 + DLC + 数据八字节求和取低 8 位 | | 14 | 1 | 校验和:帧头 + canId 四字节 + DLC + 数据八字节 + 帧尾求和取低 8 位 |
| 15 | 1 | 帧尾 `0xBB` |
收包时逐字节找帧头、校验不过右移一字节继续找,容忍粘包、拆包与杂包。 收包时逐字节找帧头,帧头/帧尾/校验任一不过就右移一字节继续找,容忍粘包、拆包与杂包。
**上行 7 条**(遥控器 → 车端)按协议「报文周期时间」列分三组重发:**50ms** 底盘 / 机械臂1 / **上行 7 条**(遥控器 → 车端)按协议「报文周期时间」列分三组重发:**50ms** 底盘 / 机械臂1 /
机械臂2 / 急停,**100ms** 洗消指令 / 洗消调试,**1000ms** 气体检测仪。界面上层只更新控制状态, 机械臂2 / 急停,**100ms** 洗消指令 / 洗消调试,**1000ms** 气体检测仪。界面上层只更新控制状态,
@@ -77,12 +78,26 @@ make -j4
车速 0~5.00 km/h、发泡倍数 6~12 倍、泡沫比例 0~10%、空压机流量 0~340 L/min、柱塞泵流量 车速 0~5.00 km/h、发泡倍数 6~12 倍、泡沫比例 0~10%、空压机流量 0~340 L/min、柱塞泵流量
0~24 L/min;调试维护页三泵转速上限各按协议(空压机 3000 / 柱塞泵 1450 / 叶片泵 2000 rpm)。 0~24 L/min;调试维护页三泵转速上限各按协议(空压机 3000 / 柱塞泵 1450 / 叶片泵 2000 rpm)。
顶部「通信正常/异常」按收帧判定:连续 **2s** 未收到任何合法帧即判为异常(最慢的下行周期 顶部左区**固定**显示一行通信状态,不再轮播:`CarCommunication::updateCommText()` 按
1000ms)。UDP 端口绑定失败(如 9000 已被占用)时不启动定时器、不崩溃,界面显示「通信异常」。 `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`)。 遥控器仍走串口:`comm/uart.cpp` 打开 `rc/device`(默认 `/dev/ttyS7`)、`rc/baud`(默认 `115200`)。
`config.ini`(程序目录)中与通信相关的配置还有 `net/selfcheck_fault`(自检故障演示)。 `config.ini`(程序目录)中与通信相关的配置还有 `net/selfcheck_fault`(自检故障演示,作用于
`清水液位` 这一项,默认开)。
参数(车速、柱塞泵流量、发泡倍数、泡沫比例、空压机流量)与密码经 `QSettings` 持久化,上电自动读取。 参数(车速、柱塞泵流量、发泡倍数、泡沫比例、空压机流量)与密码经 `QSettings` 持久化,上电自动读取。
`tools/can_sender.py` 是车端 CAN 的 UDP 联调工具(给 UI 发反馈时目标端口用 **9000**); `tools/can_sender.py` 是车端 CAN 的 UDP 联调工具(给 UI 发反馈时目标端口用 **9000**);
@@ -90,8 +105,13 @@ make -j4
### 已知缺口 ### 已知缺口
- **自检 10 项无对应 CAN 报文**,`sendSelfCheckStart()` 暂由本端跑完序列(见 `onSelfCheckTick` - **自检 14 项里 0~9 项无对应 CAN 报文**(通信系统/各分系统通信取自 `All_Comm_FaultCode`,其余为
的 TODO),仅为维持 ④自检 → ⑤系统首页面 流程不中断;拿到真实报文后替换该本地驱动。 清水液位),`sendSelfCheckStart()` 暂由本端计时直接判过(见 `onSelfCheckTick` 的 TODO),
仅为维持 ④自检 → ⑤系统首页面 流程不中断;拿到真实报文后替换该本地驱动。
- **自检的球阀 4 项(D1/D2 开启、关闭)是真检查**:`onSelfCheckTick` 会置 `Decontam_DebugCmd`
的调试模式位并写目标阀位(命令帧由 100ms 周期定时器持续重发),再等 `MBV_D1_Sta` /
`MBV_D2_Sta` 反馈与指令一致才算通过;`kValveWaitTicks`(默认 18 拍 ≈ 8s)内未到位、
或车端根本没有反馈(通信异常)都判故障。自检结束后恢复原有的调试模式位。
- **行车/机械臂模式**没有对应的下行信号:协议 12 条反馈里没有该字段,`telemetry.driveMode` - **行车/机械臂模式**没有对应的下行信号:协议 12 条反馈里没有该字段,`telemetry.driveMode`
无处可填,故模式只由遥控器报文决定(遥控器离线时保持上次值)。 无处可填,故模式只由遥控器报文决定(遥控器离线时保持上次值)。
- **`Vehi_spd` 按协议取 8 位**(bit24、精度 0.01 → 0~2.55 km/h)。协议表里该信号与其后的 - **`Vehi_spd` 按协议取 8 位**(bit24、精度 0.01 → 0~2.55 km/h)。协议表里该信号与其后的
+27 -5
View File
@@ -172,12 +172,30 @@ void AppContext::triggerEstop()
void AppContext::setMode(Mode m) void AppContext::setMode(Mode m)
{ {
// 洗消作业期间强制机械臂模式:作业时末端要自动俯仰摆动,而 Arm_ControlCmd1 的
// 使能位就挂在这个模式上。遥控器**每帧**都会调进来,所以这个覆盖只能放在这里——
// 放到 sendWashStart 之类的地方,会被下一帧遥控器报文冲掉。
if (washRunning) m = Mode::Arm;
if (mode == m) return; if (mode == m) return;
mode = m; mode = m;
emit modeChanged(m); emit modeChanged(m);
if (m_comm) m_comm->setArmMode(m == Mode::Arm); // 同步到 Arm_ControlCmd1 的 Arm_modeCmd/Arm_enable 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 字节二进制帧) ===================== // ===================== 遥控器(24 字节二进制帧) =====================
void AppContext::onRcFrame(const RcFrame& f) void AppContext::onRcFrame(const RcFrame& f)
{ {
@@ -236,7 +254,7 @@ void AppContext::applyRcArm(const RcFrame& f)
const int d = allowed ? axisDir(vals[i]) : 0; const int d = allowed ? axisDir(vals[i]) : 0;
if (d == m_rcAxisDir[i]) continue; // 方向未变,不重复发包 if (d == m_rcAxisDir[i]) continue; // 方向未变,不重复发包
m_rcAxisDir[i] = d; 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"; else if (m_rcSteerDir < 0) turn = "left";
if (dir == "stop" && turn.isEmpty()) { if (dir == "stop" && turn.isEmpty()) {
m_comm->sendChassisMove("stop", 0); m_comm->sendChassisMoveRc("stop", 0);
return; return;
} }
const double spd = (dir == "stop") ? 0.0 : getDouble(keySpeed(), 4.5); 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() void AppContext::updateRcDriving()
@@ -270,6 +288,9 @@ void AppContext::updateRcDriving()
if (driving == m_rcDriving) return; if (driving == m_rcDriving) return;
m_rcDriving = driving; m_rcDriving = driving;
// 先告诉通信层再发信号:页面收到 rcDrivingChanged(false) 会重发按住的方向,
// 那一下必须畅通(让位标志得已经落下)
if (m_comm) m_comm->setRcActive(driving);
emit rcDrivingChanged(driving); emit rcDrivingChanged(driving);
} }
@@ -280,15 +301,16 @@ void AppContext::onRcLinkChanged(bool ok)
if (!ok) { if (!ok) {
// 链路中断:撤销所有遥控器动作,否则最后一条指令会被一直挂住。 // 链路中断:撤销所有遥控器动作,否则最后一条指令会被一直挂住。
// 不动急停状态——丢包导致的 bit4 读为 0 不应被当成遥控器下发的复位。 // 不动急停状态——丢包导致的 bit4 读为 0 不应被当成遥控器下发的复位。
// 这里必须走 Rc 入口:此刻 rcActive 还是真,走面板入口会被让位挡掉。
if (m_rcGearDir != 0 || m_rcSteerDir != 0) { if (m_rcGearDir != 0 || m_rcSteerDir != 0) {
m_rcGearDir = 0; m_rcGearDir = 0;
m_rcSteerDir = 0; m_rcSteerDir = 0;
m_comm->sendChassisMove("stop", 0); m_comm->sendChassisMoveRc("stop", 0);
} }
for (int i = 0; i < 6; ++i) { for (int i = 0; i < 6; ++i) {
if (m_rcAxisDir[i] == 0) continue; if (m_rcAxisDir[i] == 0) continue;
m_rcAxisDir[i] = 0; m_rcAxisDir[i] = 0;
m_comm->sendArmMove(QString::fromLatin1(kArmAxes[i]), 0); m_comm->sendArmMoveRc(QString::fromLatin1(kArmAxes[i]), 0);
} }
} }
updateRcDriving(); updateRcDriving();
+9 -1
View File
@@ -34,9 +34,14 @@ public:
// 全局状态 // 全局状态
Mode mode = Mode::Drive; // 行车/机械臂模式(机械旋钮) Mode mode = Mode::Drive; // 行车/机械臂模式(机械旋钮)
bool estopTriggered = false; // 急停是否触发(需人工复位) bool estopTriggered = false; // 急停是否触发(需人工复位)
bool washRunning = false; // 洗消运行中(消除/消毒共用)
bool gasRunning = false; // 气体检测仪检测中 bool gasRunning = false; // 气体检测仪检测中
// 洗消作业开关(消除/消毒共用)。作业期间**强制机械臂模式**——末端要自动俯仰摆动,
// 而 Arm_ControlCmd1 的使能位就挂在这个模式上;作业结束后回到作业前的模式。
// 页面不要直接改成员,走这个入口。
void setWashRunning(bool on);
bool isWashRunning() const { return washRunning; }
// ===================== 存储区参数(QSettings 持久化) ===================== // ===================== 存储区参数(QSettings 持久化) =====================
double getDouble(const QString& key, double def) const; double getDouble(const QString& key, double def) const;
void setDouble(const QString& key, double val); void setDouble(const QString& key, double val);
@@ -100,6 +105,9 @@ private:
int m_rcAxisDir[6] = {0, 0, 0, 0, 0, 0}; // 顺序见 kArmAxes int m_rcAxisDir[6] = {0, 0, 0, 0, 0, 0}; // 顺序见 kArmAxes
bool m_rcEstopPrev = false; bool m_rcEstopPrev = false;
bool m_rcDriving = false; bool m_rcDriving = false;
bool washRunning = false;
Mode m_modeBeforeWash = Mode::Drive; // 洗消作业前的工作模式,作业结束后还原
}; };
// 便捷访问宏 // 便捷访问宏
+60 -18
View File
@@ -8,33 +8,62 @@
#include <QMouseEvent> #include <QMouseEvent>
#include <QGraphicsDropShadowEffect> #include <QGraphicsDropShadowEffect>
// 启动 500ms 闪烁:每 500ms 切换一次动态属性,QSS 据此在 40%↔100% 透明度间切换 // ===================== 全局闪烁节拍 =====================
QTimer* startBlink(QWidget* w, const char* prop) BlinkBeat::BlinkBeat(QObject* parent)
: QObject(parent)
{ {
// 复用已存在的定时器 m_timer = new QTimer(this);
if (auto* t = w->findChild<QTimer*>(QString("blink_") + prop)) m_timer->setInterval(500); // 闪烁半周期:亮 500ms、灭 500ms
return t; connect(m_timer, &QTimer::timeout, this, [this]() {
QTimer* t = new QTimer(w); m_on = !m_on;
t->setObjectName(QString("blink_") + prop); emit toggled(m_on);
t->setInterval(500); });
QObject::connect(t, &QTimer::timeout, w, [w, prop]() { m_timer->start();
bool cur = w->property(prop).toBool(); }
w->setProperty(prop, !cur);
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<QObject*>(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()->unpolish(w);
w->style()->polish(w); w->style()->polish(w);
w->update(); 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) void stopBlink(QWidget* w, const char* prop)
{ {
if (auto* t = w->findChild<QTimer*>(QString("blink_") + prop)) { if (QObject* holder = w->findChild<QObject*>(blinkKey(prop)))
t->stop(); delete holder; // 删除句柄 = 退订
delete t;
}
w->setProperty(prop, false); w->setProperty(prop, false);
w->style()->unpolish(w); w->style()->unpolish(w);
w->style()->polish(w); w->style()->polish(w);
@@ -101,6 +130,13 @@ bool ButtonFocusGlow::eventFilter(QObject* obj, QEvent* ev)
parent->update(parent->rect()); parent->update(parent->rect());
} }
break; break;
case QEvent::EnabledChange:
// 光标一直停在按钮上时,禁用期间 Qt 仍给它留着 WA_UnderMouse;重新启用的一瞬间
// :hover 规则就生效了(:disabled 排在 :hover 之后才压得住它),看着像"被选中"。
// 这里把这层残影清掉,和 FocusIn 那条是同一件事:保证同时只有一个按钮呈选中样式。
if (w->isEnabled() && w->underMouse())
clearStaleHover(QApplication::focusWidget());
break;
case QEvent::MouseButtonPress: case QEvent::MouseButtonPress:
// 触摸/点击与 Tab 归一到同一个焦点:谁最后操作,谁就是唯一的选中 // 触摸/点击与 Tab 归一到同一个焦点:谁最后操作,谁就是唯一的选中
if (static_cast<QMouseEvent*>(ev)->button() == Qt::LeftButton if (static_cast<QMouseEvent*>(ev)->button() == Qt::LeftButton
@@ -264,6 +300,12 @@ QPushButton[numpad="true"]:focus {
border: 2px solid #8ABFFF; border: 2px solid #8ABFFF;
} }
/* ===== 数据卡片数值 ===== */
/* 数值标签的颜色必须由这里给:卡片的行内样式只写字号/字重,不写 color。
行内样式优先级高于应用级样式表,写上 color 就会把下面的告警红 / 暗红闪一起盖掉。
本规则与 [alert] 同为单属性选择器,靠位置分胜负,故必须排在告警规则之前。 */
QLabel[cardValue="true"] { color: TXT; }
/* ===== 告警文字(红色闪烁由 blink 属性驱动) ===== */ /* ===== 告警文字(红色闪烁由 blink 属性驱动) ===== */
QLabel[alert="true"] { color: DAN; font-weight: 700; } QLabel[alert="true"] { color: DAN; font-weight: 700; }
QLabel[alert="true"][blink="false"] { color: rgba(229,72,77,102); } QLabel[alert="true"][blink="false"] { color: rgba(229,72,77,102); }
+130 -13
View File
@@ -58,23 +58,26 @@ enum PageId {
}; };
// ===================== 遥测数据(车载端 → UI) ===================== // ===================== 遥测数据(车载端 → UI) =====================
// 由通信层解析车端反馈报文后填充,页面刷新时读取 // 由通信层解析车端反馈报文后填充,页面刷新时读取。
// 这里不留任何演示初值:数值字段只由报文写入,配套的 has* 标志在收到对应报文时置位,
// 页面按 dashIf() 判断“还没有值”,未收到时显示 "--"。
struct Telemetry { struct Telemetry {
// 底盘 // 底盘
int soc = 85; // 电量 % int soc = 0; // 电量 %
double volt = 360.0; // 电压 V double volt = 0.0; // 电压 V
double speed = 3.45; // 车速 km/h double speed = 0.0; // 车速 km/h
bool handbrake = true; // 手刹 bool handbrake = false; // 手刹
int driveMode = 0; // 行车/机械臂模式:0=行车 1=机械臂(遥控器拨钮→串口→底盘报文 mode) int driveMode = 0; // 行车/机械臂模式:0=行车 1=机械臂(遥控器拨钮→串口→底盘报文 mode)
int rdyStatus = 0; // 底盘 ready:0=初始 1=高压上电完成 2=准备就绪 int rdyStatus = 0; // 底盘 ready:0=初始 1=高压上电完成 2=准备就绪
int gearPos = 0; // 挡位:0=N 1=D 2=R int gearPos = 0; // 挡位:0=N 1=D 2=R
int parking = 0; // 驻车:0=未驻车 1=左电机 2=右电机 3=完全驻车 int parking = 0; // 驻车:0=未驻车 1=左电机 2=右电机 3=完全驻车
int awdMode = 0; // 四驱模式:0=二驱 1=四驱 2=四驱锁定 int awdMode = 0; // 四驱模式:0=二驱 1=四驱 2=四驱锁定
double steerAngle = 0.0; // 方向盘角度 double steerAngle = 0.0; // 方向盘角度
int alignStatus = 0; // 角度对中状态:0x11=对中失败、0xEE/0x55=成功、其它=未对中
QStringList chassisFaults; // 底盘故障(多条 2s 轮换) QStringList chassisFaults; // 底盘故障(多条 2s 轮换)
// 机械臂 // 机械臂
QVector<double> js{30, 20, 0, 90, 45, 0}; // 关节角 J1~J6(°) QVector<double> js{0, 0, 0, 0, 0, 0}; // 关节角 J1~J6(°)
QString pos = "--"; // 末端位置 XYZ QString pos = "--"; // 末端位置 XYZ
QString pose = "--"; // 姿态 RX/RY/RZ QString pose = "--"; // 姿态 RX/RY/RZ
double vel = 0.0; // 末端速度 double vel = 0.0; // 末端速度
@@ -96,11 +99,13 @@ struct Telemetry {
QStringList washFaults; // 洗消故障代码 QStringList washFaults; // 洗消故障代码
// 液位 // 液位
double levelA = 100.0; // 清水/药剂液位 L double levelA = 0.0; // 清水/药剂液位 L
double levelFoam = 30.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 gasType = "--"; // 有毒物种类
QString gasLevel = "--"; // 有毒物等级 QString gasLevel = "--"; // 有毒物等级
double gasConc = 0.0; // 有毒物浓度 ppb double gasConc = 0.0; // 有毒物浓度 ppb
@@ -110,7 +115,6 @@ struct Telemetry {
bool d1close = true; // D1 关闭到位 bool d1close = true; // D1 关闭到位
bool d2open = false; // D2 开启到位 bool d2open = false; // D2 开启到位
bool d2close = true; // D2 关闭到位 bool d2close = true; // D2 关闭到位
QString valveFault; // 球阀故障码
// 泵 // 泵
bool plunger = false; // 柱塞泵上电 bool plunger = false; // 柱塞泵上电
@@ -118,6 +122,10 @@ struct Telemetry {
bool leaf = false; // 叶片泵上电 bool leaf = false; // 叶片泵上电
QStringList pumpFaults; // 泵故障代码 QStringList pumpFaults; // 泵故障代码
// 气体检测仪故障。单列一份、不并进 washFaults:洗消页底栏的“系统故障”灯
// 只看洗消系统与泵,气体检测仪的告警要归到它自己的卡片上。
QStringList detectorFaults;
// 遥控器(24 字节二进制帧,见 comm/uart.h) // 遥控器(24 字节二进制帧,见 comm/uart.h)
bool rcLinkOk = false; // 遥控器链路是否在线 bool rcLinkOk = false; // 遥控器链路是否在线
int rcBattery = 0; // 遥控器电量 % int rcBattery = 0; // 遥控器电量 %
@@ -138,13 +146,122 @@ struct Telemetry {
// 全局 // 全局
bool commOk = true; // 通信状态 bool commOk = true; // 通信状态
bool estop = false; // 急停触发 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 规则体现) // 启动闪烁:挂到全局节拍上,每拍翻转一次动态属性(40% ↔ 100% 透明度由 QSS 规则体现)
QTimer* startBlink(QWidget* w, const char* prop = "blink"); void startBlink(QWidget* w, const char* prop = "blink");
// 关闭闪烁定时器并复位属性 // 退订全局节拍并复位属性
void stopBlink(QWidget* w, const char* prop = "blink"); void stopBlink(QWidget* w, const char* prop = "blink");
// 退出/取消按钮预设样式:被选中(悬停/按下/焦点)时红底白字 // 退出/取消按钮预设样式:被选中(悬停/按下/焦点)时红底白字
+385 -118
View File
@@ -6,38 +6,57 @@
#include <QSettings> #include <QSettings>
#include <QUdpSocket> #include <QUdpSocket>
#include <QtGlobal> #include <QtGlobal>
#include <cmath>
#include <cstring> #include <cstring>
namespace { namespace {
// ===================== 线格式 ===================== // ===================== 线格式 =====================
// 一个 UDP 数据报承载 = 32 位帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和,共 18 字节。 // 一个 UDP 数据报承载 = 1 字节帧头 + 32 位 CAN ID + DLC + 8 字节数据 + 8 位校验和 + 1 字节帧尾,
// 多字节字段按主机小端排列,与车载端网关一致;帧头与校验和用于确认数据报内容合法。 // 共 16 字节。多字节字段按主机小端排列,与车载端网关一致;帧头/帧尾与校验和用于确认数据报内容合法。
#pragma pack(push, 1) #pragma pack(push, 1)
struct CanFrame { struct CanFrame {
quint32 header; // 固定帧头 0x5AA5FF00(线上字节序 00 FF A5 5A) quint8 header; // 固定帧头 0xAA
quint32 canId; // 29 位扩展帧 ID,直接用于分流 quint32 canId; // 29 位扩展帧 ID,直接用于分流
quint8 dlc; // 数据长度码,固定 8 quint8 dlc; // 数据长度码,固定 8
quint8 data[8]; // 8 字节 CAN 原生数据段 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) #pragma pack(pop)
const quint32 kFrameHeader = 0x5AA5FF00u; const quint8 kFrameHeader = 0xAA;
const quint8 kFrameTail = 0xBB;
const int kFrameLen = int(sizeof(CanFrame)); const int kFrameLen = int(sizeof(CanFrame));
const int kDataLen = 8; 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 kCanRxPort = 9000;
const quint16 kCanTxPort = 9001; const quint16 kCanTxPort = 3001;
const char* kCanTxHost = "0.0.0.0"; const char* kCanTxHost = "192.168.1.224";
quint8 canChecksum(quint32 canId, quint8 dlc, const quint8* data) 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); for (int i = 0; i < 4; ++i) sum += quint8((canId >> (8 * i)) & 0xFF);
sum += dlc; sum += dlc;
for (int i = 0; i < kDataLen; ++i) sum += data[i]; for (int i = 0; i < kDataLen; ++i) sum += data[i];
sum += kFrameTail;
return sum; return sum;
} }
@@ -182,42 +201,65 @@ const char* const kBrakeMotFaults[8] = {
"驱动电路故障", "供电电压过低", "供电电压过高", "驱动电路故障", "供电电压过低", "供电电压过高",
}; };
void addFault(QVector<CanFault>& out, const QString& system, const char* codePrefix, void addFault(QVector<CanFault>& out, const QString& system, const QString& group,
quint8 code, const char* text) const char* codePrefix, quint8 code, const char* text, int level = 0)
{ {
if (!text) return; if (!text) return;
CanFault f; CanFault f;
f.system = system; f.system = system;
f.code = QString::fromLatin1(codePrefix) + QString::number(code); f.group = group;
f.desc = QString::fromUtf8(text); f.code = QString::fromLatin1(codePrefix) + QString::number(code);
f.codeNum = QString::number(code);
f.desc = QString::fromUtf8(text);
f.level = level;
out.append(f); out.append(f);
} }
// 未在码表中登记的码也上报,避免“有故障但界面无提示” // 未在码表中登记的码也上报,避免“有故障但界面无提示”
void addFaultByCode(QVector<CanFault>& out, const QString& system, const char* codePrefix, void addFaultByCode(QVector<CanFault>& out, const QString& system, const QString& group,
const CodeText* tbl, int n, quint8 code) const char* codePrefix, const CodeText* tbl, int n, quint8 code, int level = 0)
{ {
if (code == 0) return; if (code == 0) return;
const char* t = lookup(tbl, n, code); const char* t = lookup(tbl, n, code);
if (t) addFault(out, system, codePrefix, code, t); if (t) {
else { addFault(out, system, group, codePrefix, code, t, level);
CanFault f; return;
f.system = system;
f.code = QString::fromLatin1(codePrefix) + QString::number(code);
f.desc = QString::fromLatin1(codePrefix) + QString::number(code);
out.append(f);
} }
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 才上报,描述形如“单体电压过低二级故障” // 故障等级:>0 才上报。等级型信号没有“码”,顶部显示成“<组名>:<描述>,N级”
void addLevelFault(QVector<CanFault>& out, const QString& system, const char* codePrefix, void addLevelFault(QVector<CanFault>& out, const QString& system, const QString& group,
const char* name, quint8 level) const char* codePrefix, const char* name, quint8 level)
{ {
if (level == 0 || level > 3) return; if (level == 0 || level > 3) return;
CanFault f; CanFault f;
f.system = system; f.system = system;
f.code = QString::fromLatin1(codePrefix) + QString::number(level); f.group = group;
f.desc = QString::fromUtf8(name) + QString::fromUtf8(kLevels[level]); f.code = QString::fromLatin1(codePrefix) + QString::number(level); // 追溯用,顶部不显示
f.desc = QString::fromUtf8(name);
f.level = level;
out.append(f);
}
// 没有“故障码”的故障(单个故障位、方向盘对中状态这类无编码信号):
// 顶部只显示描述,tracePrefix+traceSuffix 仅作故障历史追溯用。
void addNoCodeFault(QVector<CanFault>& 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); out.append(f);
} }
@@ -225,8 +267,38 @@ const char* const kSysChassis = "机器人底盘";
const char* const kSysWash = "洗消系统"; const char* const kSysWash = "洗消系统";
const char* const kSysDetector = "气体检测仪"; 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 } // namespace
// 完整一行:<组名>:[<码>-]<描述>[,<N>级];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) CarCommunication::CarCommunication(Telemetry* tel, QObject* parent)
: QObject(parent), m_tel(tel) : QObject(parent), m_tel(tel)
@@ -287,6 +359,7 @@ bool CarCommunication::start()
// 端口被占用时走这里:不启动定时器,界面显示通信异常 // 端口被占用时走这里:不启动定时器,界面显示通信异常
qWarning() << "车端 UDP 端口绑定失败:" << kCanRxPort << m_sock->errorString(); qWarning() << "车端 UDP 端口绑定失败:" << kCanRxPort << m_sock->errorString();
setCommOk(false); setCommOk(false);
updateCommText(); // 收不到帧也不会有 cacheFaults,顶部得靠这里才显示“通信异常”
return false; return false;
} }
m_hbTimer->start(); m_hbTimer->start();
@@ -311,7 +384,6 @@ void CarCommunication::setCommOk(bool ok)
if (ok == m_lastCommOk) return; if (ok == m_lastCommOk) return;
m_lastCommOk = ok; m_lastCommOk = ok;
if (m_tel) m_tel->commOk = ok; if (m_tel) m_tel->commOk = ok;
emit commOkChanged(ok);
} }
// ===================== 收包与分流 ===================== // ===================== 收包与分流 =====================
@@ -319,81 +391,97 @@ void CarCommunication::setCommOk(bool ok)
// 不再需要串口时代那套跨读字节流的残帧缓冲与滑窗重同步。 // 不再需要串口时代那套跨读字节流的残帧缓冲与滑窗重同步。
void CarCommunication::onDatagram() void CarCommunication::onDatagram()
{ {
bool got = false;
while (m_sock->hasPendingDatagrams()) { while (m_sock->hasPendingDatagrams()) {
const QByteArray d = m_sock->receiveDatagram().data(); const QByteArray d = m_sock->receiveDatagram().data();
int off = 0; int off = 0;
while (off + kFrameLen <= d.size()) { while (off + kFrameLen <= d.size()) {
CanFrame f; CanFrame f;
std::memcpy(&f, d.constData() + off, kFrameLen); 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)) { && f.checksum == canChecksum(f.canId, f.dlc, f.data)) {
handleFrame(f.canId, f.data); handleFrame(f.canId, f.data);
got = true;
off += kFrameLen; off += kFrameLen;
} else { } else {
++off; // 数据报内可能有杂字节,向后找下一个帧头 ++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) void CarCommunication::handleFrame(quint32 canId, const quint8* d)
{ {
if (!m_tel) return; if (!m_tel) return;
// 收到任意合法帧即刷新链路(CAN 无独立心跳报文) // 这里只把数据写进 Telemetry,不动链路状态、也不刷界面——那两件事统一在
m_hbTimer->start(); // onDatagram() 排空一批之后做一次,这样一大批积压也只会重绘一次。
setCommOk(true);
switch (canId) { switch (canId) {
// ---------- 0x18308180 底盘状态1(BMS):电池总况与故障等级 ---------- // ---------- 0x18308180 底盘状态1(BMS):电池总况与故障等级 ----------
case kIdVehicleFb1: { case kIdVehicleFb1: {
m_tel->volt = phys(intelBits(d, 0, 16), 0.1); m_tel->volt = phys(intelBits(d, 0, 16), 0.1);
m_tel->soc = int(phys(intelBits(d, 16, 16), 0.1)); m_tel->soc = int(phys(intelBits(d, 16, 16), 0.1));
m_tel->hasBattery = true;
m_vsf1Faults.clear(); 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)), kMcuFaults, int(sizeof(kMcuFaults) / sizeof(CodeText)),
quint8(intelBits(d, 32, 8))); quint8(intelBits(d, 32, 8)), int(intelBits(d, 40, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "MCUL", // 以下 *FaultLevel 都归 BMS 系统
"底盘电机", quint8(intelBits(d, 40, 2))); const QString bms = QString::fromUtf8(kGroupBms);
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CELLL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"单体电压过低", quint8(intelBits(d, 45, 2))); "CELLL", "单体电压过低", quint8(intelBits(d, 45, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CELLHL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"单体电压过高", quint8(intelBits(d, 47, 2))); "CELLHL", "单体电压过高", quint8(intelBits(d, 47, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "VOLTL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"总电压过低", quint8(intelBits(d, 49, 2))); "VOLTL", "总电压过低", quint8(intelBits(d, 49, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "VOLTHL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"总电压过高", quint8(intelBits(d, 51, 2))); "VOLTHL", "总电压过高", quint8(intelBits(d, 51, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "DLTPL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"放电低温", quint8(intelBits(d, 53, 2))); "DLTPL", "放电低温", quint8(intelBits(d, 53, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "DHTPL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"放电高温", quint8(intelBits(d, 55, 2))); "DHTPL", "放电高温", quint8(intelBits(d, 55, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CLTPL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"充电低温", quint8(intelBits(d, 57, 2))); "CLTPL", "充电低温", quint8(intelBits(d, 57, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "CHTPL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"充电高温", quint8(intelBits(d, 59, 2))); "CHTPL", "充电高温", quint8(intelBits(d, 59, 2)));
addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), "TIL", addLevelFault(m_vsf1Faults, QString::fromUtf8(kSysChassis), bms,
"温度不均衡", quint8(intelBits(d, 61, 2))); "TIL", "温度不均衡", quint8(intelBits(d, 61, 2)));
break; break;
} }
// ---------- 0x18318180 底盘状态2(BMS):手刹 / 车速 / 继电器 / 通信 ---------- // ---------- 0x18318180 底盘状态2(BMS):手刹 / 车速 / 继电器 / 通信 ----------
case kIdVehicleFb2: { case kIdVehicleFb2: {
m_vsf2Faults.clear(); m_vsf2Faults.clear();
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "VIL", const QString bms = QString::fromUtf8(kGroupBms);
"压差不均衡", quint8(intelBits(d, 0, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "DOCL", "VIL", "压差不均衡", quint8(intelBits(d, 0, 2)));
"放电过流", quint8(intelBits(d, 2, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "COCL", "DOCL", "放电过流", quint8(intelBits(d, 2, 2)));
"充电过流", quint8(intelBits(d, 4, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "INCL", "COCL", "充电过流", quint8(intelBits(d, 4, 2)));
"内部通讯", quint8(intelBits(d, 6, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "EXCL", "INCL", "内部通讯", quint8(intelBits(d, 6, 2)));
"外部通讯", quint8(intelBits(d, 8, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "SOCL", "EXCL", "外部通讯", quint8(intelBits(d, 8, 2)));
"SOC过低", quint8(intelBits(d, 10, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), "INSL", "SOCL", "SOC过低", quint8(intelBits(d, 10, 2)));
"绝缘", quint8(intelBits(d, 12, 2))); addLevelFault(m_vsf2Faults, QString::fromUtf8(kSysChassis), bms,
"INSL", "绝缘", quint8(intelBits(d, 12, 2)));
// 继电器粘连:0=正常 非 0=故障 // 继电器粘连:0=正常 非 0=故障
struct { int bit; const char* name; const char* code; } relays[] = { 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) { for (const auto& r : relays) {
if (intelBits(d, r.bit, 2) != 0) 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; 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 位布局自洽,故按协议取值。 // Reserve(34)、All_Comm_FaultCode(40) 的起始位与 8 位布局自洽,故按协议取值。
m_tel->speed = phys(intelBits(d, 24, 8), 0.01); m_tel->speed = phys(intelBits(d, 24, 8), 0.01);
m_tel->rdyStatus = int(intelBits(d, 32, 2)); m_tel->rdyStatus = int(intelBits(d, 32, 2));
m_tel->hasSpeed = true;
const quint8 comm = quint8(intelBits(d, 40, 8)); // All_Comm_FaultCode 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); kCommFaults, int(sizeof(kCommFaults) / sizeof(CodeText)), comm);
break; break;
} }
@@ -422,17 +514,20 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d)
// ---------- 0x183A8180 底盘状态3:EPS / 制动故障位 ---------- // ---------- 0x183A8180 底盘状态3:EPS / 制动故障位 ----------
case kIdVehicleFb3: { case kIdVehicleFb3: {
m_vsf3Faults.clear(); m_vsf3Faults.clear();
const QString sys = QString::fromUtf8(kSysChassis);
const QString brake = QString::fromUtf8(kGroupSteerBrake);
for (int i = 0; i < 46; ++i) { for (int i = 0; i < 46; ++i) {
if (intelBits(d, i, 1) != 0) if (intelBits(d, i, 1) != 0)
addFault(m_vsf3Faults, QString::fromUtf8(kSysChassis), "EPS", quint8(i), addNoCodeFault(m_vsf3Faults, sys, brake, "EPS", i, kVsb3Bits[i]);
kVsb3Bits[i]);
} }
const quint8 eps = quint8(intelBits(d, 48, 8)); const quint8 eps = quint8(intelBits(d, 48, 8));
if (eps != 0) { if (eps != 0) {
CanFault f; CanFault f;
f.system = QString::fromUtf8(kSysChassis); f.system = sys;
f.code = QString("EPSC") + QString::number(eps); f.group = brake;
f.desc = QString("EPS故障码") + QString::number(eps); f.code = QString("EPSC") + QString::number(eps);
f.codeNum = QString::number(eps);
f.desc = QString::fromUtf8("EPS故障码");
m_vsf3Faults.append(f); m_vsf3Faults.append(f);
} }
break; 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->parking = int(intelBits(d, 2, 2)); // 0=未驻车 1=左电机 2=右电机 3=完全驻车
m_tel->awdMode = int(intelBits(d, 4, 2)); // 0=二驱 1=四驱 2=四驱锁定 m_tel->awdMode = int(intelBits(d, 4, 2)); // 0=二驱 1=四驱 2=四驱锁定
m_tel->steerAngle = double(intelBits(d, 8, 16)); 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)); 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) else if (align != 0xEE && align != 0x55)
addFault(m_vsf4Faults, QString::fromUtf8(kSysChassis), "ALIGN", 0, "方向盘未对中"); addNoCodeFault(m_vsf4Faults, sys, brake, "ALIGN", 0, "方向盘未对中");
// 注:协议表把两个信号的“信号值描述”写反了(Brake_Right 描述成左电机、 // 注:协议表把两个信号的“信号值描述”写反了(Brake_Right 描述成左电机、
// Brake_Left 描述成右电机),此处以信号名为准。 // Brake_Left 描述成右电机),此处以信号名为准。
const quint8 br = quint8(intelBits(d, 32, 8)); 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] : "制动电机故障"); br < 8 ? kBrakeMotFaults[br] : "制动电机故障");
const quint8 bl = quint8(intelBits(d, 40, 8)); 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] : "制动电机故障"); bl < 8 ? kBrakeMotFaults[bl] : "制动电机故障");
break; break;
} }
@@ -477,6 +578,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d)
.arg(intelSigned(d, 16, 16)) .arg(intelSigned(d, 16, 16))
.arg(intelSigned(d, 32, 16)); .arg(intelSigned(d, 32, 16));
m_tel->vel = double(intelSigned(d, 48, 8)); m_tel->vel = double(intelSigned(d, 48, 8));
m_tel->hasArmPose = true;
break; break;
} }
// ---------- 0x18348180 机械臂状态3:关节角 J1~J4 ---------- // ---------- 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); if (m_tel->js.size() < 6) m_tel->js.resize(6);
for (int i = 0; i < 4; ++i) for (int i = 0; i < 4; ++i)
m_tel->js[i] = double(intelSigned(d, i * 16, 16)); m_tel->js[i] = double(intelSigned(d, i * 16, 16));
m_tel->hasJoints = true;
break; break;
} }
// ---------- 0x18358180 机械臂状态4:关节角 J5~J6 ---------- // ---------- 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); if (m_tel->js.size() < 6) m_tel->js.resize(6);
m_tel->js[4] = double(intelSigned(d, 0, 16)); m_tel->js[4] = double(intelSigned(d, 0, 16));
m_tel->js[5] = double(intelSigned(d, 16, 16)); m_tel->js[5] = double(intelSigned(d, 16, 16));
m_tel->hasJoints = true;
break; break;
} }
@@ -511,12 +615,18 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d)
m_tel->d2close = !m_tel->d2open; m_tel->d2close = !m_tel->d2open;
m_washFaults.clear(); m_washFaults.clear();
const QString sys = QString::fromUtf8(kSysWash);
const QString grp = QString::fromUtf8(kGroupWash);
if (intelBits(d, 10, 1) != 0) // Sys_fault:无分路码,只报总故障 if (intelBits(d, 10, 1) != 0) // Sys_fault:无分路码,只报总故障
addFault(m_washFaults, QString::fromUtf8(kSysWash), "SYS", 1, "洗消系统故障"); addNoCodeFault(m_washFaults, sys, grp, "SYS", 1, "洗消系统故障");
if (intelBits(d, 6, 1) != 0) const bool lowA = intelBits(d, 6, 1) != 0;
addFault(m_washFaults, QString::fromUtf8(kSysWash), "LVL", 1, "A组分/清水液位低"); const bool lowB = intelBits(d, 7, 1) != 0;
if (intelBits(d, 7, 1) != 0) if (lowA)
addFault(m_washFaults, QString::fromUtf8(kSysWash), "LVL", 2, "B组分液位低"); 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->levelA = double(intelBits(d, 16, 8));
m_tel->levelFoam = double(intelBits(d, 24, 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->flowB = double(intelBits(d, 40, 8));
m_tel->gasFlow = double(intelBits(d, 48, 8)); m_tel->gasFlow = double(intelBits(d, 48, 8));
m_tel->foam = double(intelBits(d, 56, 8)); m_tel->foam = double(intelBits(d, 56, 8));
m_tel->hasWash = true;
break; break;
} }
// ---------- 0x18378180 洗消工作状态2:压力 / 流量反馈 / 泵故障码 ---------- // ---------- 0x18378180 洗消工作状态2:压力 / 流量反馈 / 泵故障码 ----------
@@ -533,12 +644,14 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d)
m_tel->flowPlungerFb = double(intelBits(d, 16, 8)); m_tel->flowPlungerFb = double(intelBits(d, 16, 8));
m_tel->flowAirFb = double(intelBits(d, 32, 8)); m_tel->flowAirFb = double(intelBits(d, 32, 8));
m_tel->flowVaneFb = double(intelBits(d, 40, 8)); m_tel->flowVaneFb = double(intelBits(d, 40, 8));
m_tel->hasWashPress = true;
m_pumpFaults.clear(); 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)), kAirCompFaults, int(sizeof(kAirCompFaults) / sizeof(CodeText)),
quint8(intelBits(d, 48, 8))); 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)), kPlungerFaults, int(sizeof(kPlungerFaults) / sizeof(CodeText)),
quint8(intelBits(d, 56, 8))); quint8(intelBits(d, 56, 8)));
break; break;
@@ -552,6 +665,7 @@ void CarCommunication::handleFrame(quint32 canId, const quint8* d)
m_tel->gasHost = (warn & 0x04) ? QString("fault") m_tel->gasHost = (warn & 0x04) ? QString("fault")
: (sta == 4) ? QString("detecting") : (sta == 4) ? QString("detecting")
: QString("standby"); : QString("standby");
m_tel->hasGasHost = true;
// Toxic_types 是位组合:1=P类 2=S类 4=HNO类 8=CH类 16=AS类 // Toxic_types 是位组合:1=P类 2=S类 4=HNO类 8=CH类 16=AS类
const quint8 types = quint8(intelBits(d, 8, 8)); 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_tel->gasLevel = (lvl >= 1 && lvl <= 5) ? QString("%1级").arg(lvl) : QString("--");
m_detectorFaults.clear(); m_detectorFaults.clear();
// 告警是位组合(1/2/4/8),没有故障码,顶部只显示描述
for (const CodeText& w : kDetectorWarns) for (const CodeText& w : kDetectorWarns)
if (warn & w.code) if (warn & w.code)
m_detectorFaults.append(CanFault{QString::fromUtf8(kSysDetector), addNoCodeFault(m_detectorFaults, QString::fromUtf8(kSysDetector),
QString::fromLatin1("W") + QString::number(w.code), QString::fromUtf8(kGroupDetector), "W", w.code,
QString::fromUtf8(w.text)}); w.text);
break; break;
} }
// ---------- 0x18398180 检测仪结果状态2:40 位浓度(ppb) ---------- // ---------- 0x18398180 检测仪结果状态2:40 位浓度(ppb) ----------
case kIdDetectorFb2: { case kIdDetectorFb2: {
m_tel->gasConc = double(intelBits(d, 0, 40)); m_tel->gasConc = double(intelBits(d, 0, 40));
m_tel->hasGasConc = true;
break; break;
} }
default: default:
return; // 未知 canId 不触发刷新(分类四本次未实现) break; // 未知 canId(分类四本次未实现):这一帧不写任何东西
} }
cacheFaults();
emit telemetryUpdated();
} }
// 把各帧的故障合并进 Telemetry,并把“新出现”的故障写入故障历史 // 把各帧的故障合并进 Telemetry,并把“新出现”的故障写入故障历史
@@ -596,35 +709,70 @@ void CarCommunication::cacheFaults()
const QVector<QVector<CanFault>*> groups = { const QVector<QVector<CanFault>*> groups = {
&m_vsf1Faults, &m_vsf2Faults, &m_vsf3Faults, &m_vsf4Faults, &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; QStringList chassis;
for (const QVector<CanFault>* g : groups) for (const QVector<CanFault>* g : groups)
for (const CanFault& f : *g) chassis << f.desc; for (const CanFault& f : *g) chassis << pageText(f);
m_tel->chassisFaults = chassis; m_tel->chassisFaults = chassis;
QStringList wash; QStringList wash;
for (const CanFault& f : m_washFaults) wash << f.desc; for (const CanFault& f : m_washFaults) wash << pageText(f);
for (const CanFault& f : m_detectorFaults) wash << f.desc;
m_tel->washFaults = wash; m_tel->washFaults = wash;
QStringList pump; QStringList pump;
for (const CanFault& f : m_pumpFaults) pump << f.desc; for (const CanFault& f : m_pumpFaults) pump << pageText(f);
m_tel->pumpFaults = pump; m_tel->pumpFaults = pump;
// 气体检测仪单列一份:洗消页底栏的“系统故障”灯只认洗消与泵,
// 检测仪的告警归到它自己的卡片,不能顺带点亮洗消的故障灯。
QStringList det;
for (const CanFault& f : m_detectorFaults) det << pageText(f);
m_tel->detectorFaults = det;
updateCommText();
// 周期报文会反复报到同一故障,只在首次出现时写故障历史 // 周期报文会反复报到同一故障,只在首次出现时写故障历史
const QVector<QVector<CanFault>*> all = { const QVector<QVector<CanFault>*> all = {
&m_vsf1Faults, &m_vsf2Faults, &m_vsf3Faults, &m_vsf4Faults, &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<CanFault>* g : all) { for (const QVector<CanFault>* g : all) {
for (const CanFault& f : *g) { for (const CanFault& f : *g) {
const QString key = f.system + "|" + f.code; const QString key = f.system + "|" + f.code;
if (m_reportedFaults.contains(key)) continue; if (m_reportedFaults.contains(key)) continue;
m_reportedFaults.insert(key); 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) void CarCommunication::sendFrame(quint32 canId, const quint8* data)
{ {
@@ -636,10 +784,51 @@ void CarCommunication::sendFrame(quint32 canId, const quint8* data)
f.dlc = kDataLen; f.dlc = kDataLen;
std::memcpy(f.data, data, kDataLen); std::memcpy(f.data, data, kDataLen);
f.checksum = canChecksum(canId, f.dlc, f.data); f.checksum = canChecksum(canId, f.dlc, f.data);
f.tail = kFrameTail;
m_sock->writeDatagram(reinterpret_cast<const char*>(&f), kFrameLen, m_sock->writeDatagram(reinterpret_cast<const char*>(&f), kFrameLen,
QHostAddress(kCanTxHost), kCanTxPort); 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 // 50ms 组:Chassis_ControlCmd / Arm_ControlCmd1 / Arm_ControlCmd2 / Emergency_Cmd
void CarCommunication::onSendFast() void CarCommunication::onSendFast()
{ {
@@ -664,6 +853,8 @@ void CarCommunication::onSendFast()
setIntelBits(d, 40, 16, m_frontrear); setIntelBits(d, 40, 16, m_frontrear);
sendFrame(kIdArmCmd1, d); sendFrame(kIdArmCmd1, d);
} }
// 洗消作业期间的末端俯仰摆动:这一拍该发什么轴值由它决定
updateWashSwing();
{ {
quint8 d[8] = {0}; quint8 d[8] = {0};
setIntelBits(d, 0, 16, m_roll); 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) void CarCommunication::sendChassisMove(const QString& dir, double speed, const QString& turn)
{ {
// 档位推杆量:后 0 / 中 2048 / 前 4096 if (m_rcActive) return; // 遥控器正在操作:面板这一路让位(遥控器松手后页面会重发)
if (dir == "fwd") m_gear = 4096; sendChassisMoveRc(dir, speed, turn);
else if (dir == "back") m_gear = 0; }
else m_gear = kRcAxisMid;
// 转向推杆量:左 0 / 中 2048 / 右 4096 void CarCommunication::sendChassisMoveRc(const QString& dir, double speed, const QString& turn)
if (turn == "left") m_turning = 0; {
else if (turn == "right") m_turning = 4096; chassisAxis(dir, turn, m_gear, m_turning);
else m_turning = kRcAxisMid;
// Vehi_TargetSpd 精度 0.01 km/h,量程 0~500(即 0~5.00 km/h) // Vehi_TargetSpd 精度 0.01 km/h,量程 0~500(即 0~5.00 km/h)
m_targetSpd = quint16(qBound(0, int(qRound(speed * 100.0)), 500)); m_targetSpd = quint16(qBound(0, int(qRound(speed * 100.0)), 500));
} }
void CarCommunication::sendArmMove(const QString& axis, int dir) 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 / 中位 // 轴位推杆量:dir +1 / -1 / 0 → 4096 / 0 / 中位
const quint16 v = (dir > 0) ? 4096 : (dir < 0) ? 0 : kRcAxisMid; 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_decontamMode = (mode == "disinfect") ? 3 : 2; // 3=手动消毒 2=手动消除
m_decontamStart = true; 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() void CarCommunication::sendWashStop()
{ {
m_decontamStart = false; 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) void CarCommunication::sendBallValve(int valve, bool open)
@@ -829,6 +1052,9 @@ void CarCommunication::sendSetParam(const QString& key, double value)
void CarCommunication::sendSelfCheckStart() void CarCommunication::sendSelfCheckStart()
{ {
m_selfCheckIndex = 0; m_selfCheckIndex = 0;
m_selfCheckWait = 0;
m_selfCheckAllOk = true;
m_debugModeBeforeCheck = m_debugMode;
m_selfCheckTimer->start(); m_selfCheckTimer->start();
} }
@@ -842,23 +1068,64 @@ void CarCommunication::sendEstopReset()
m_emergStop = 0; m_emergStop = 0;
} }
// TODO:CAN 矩阵(sheet RC)没有自检报文,故暂由本端跑完 10 项序列,仅为维持 // TODO:CAN 矩阵(sheet RC)没有自检报文,故 0~9 项暂由本端计时直接判过,仅为维持
// ④自检 → ⑤作业选择 的流程不中断(onSelfCheckDone 是唯一点亮“作业选择”的地方)。 // ④自检 → ⑤作业选择 的流程不中断(onSelfCheckDone 是唯一点亮“作业选择”的地方)。
// 拿到真实自检报文后,改为解析该报文并删除此本地驱动。 // 拿到真实自检报文后,改为解析该报文并删除这段本地驱动。
// 14 项里只有球阀那 4 项(kSelfCheckValveFirst 起)是真检查:要下发指令并等反馈。
void CarCommunication::onSelfCheckTick() void CarCommunication::onSelfCheckTick()
{ {
bool ok = true; // ---- 球阀 D1/D2 开启、关闭:下发 Decontam_DebugCmd,等 MBV_D1_Sta/MBV_D2_Sta 反馈到位 ----
if (m_selfCheckFault && m_selfCheckIndex == 6) ok = false; // 演示:球阀D1关闭故障 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); emit selfCheckItem(m_selfCheckIndex, ok);
++m_selfCheckIndex; if (++m_selfCheckIndex < kSelfCheckItemCount)
if (m_selfCheckIndex >= 10) { return;
m_selfCheckTimer->stop();
emit selfCheckDone(!m_selfCheckFault); m_selfCheckTimer->stop();
} m_debugMode = m_debugModeBeforeCheck; // 退出调试模式,别把整车留在自检状态
emit selfCheckDone(m_selfCheckAllOk);
} }
void CarCommunication::onHeartbeatTimeout() void CarCommunication::onHeartbeatTimeout()
{ {
setCommOk(false); setCommOk(false);
// 超时后不再有收帧,cacheFaults 不会跑:这里补刷一次顶部,
// 否则顶部会一直停在“系统通信正常”上、看不出链路已经断了。
updateCommText();
} }
+62 -11
View File
@@ -14,12 +14,34 @@ class QUdpSocket;
// 摇杆量协议范围 0~4096,中位 2048(分类一各轴、Vehi_gear/Vehi_turning 通用) // 摇杆量协议范围 0~4096,中位 2048(分类一各轴、Vehi_gear/Vehi_turning 通用)
static const int kRcAxisMid = 2048; 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() 渲染的是完整一行:<组名>:[<码>-]<描述>[,<N>级],
// “底盘电机故障:18-电机过温警告,1级”;各卡片那一行太窄,只用 desc(+等级后缀)。
struct CanFault { struct CanFault {
QString system; QString system; // 故障历史分组(机器人底盘/洗消系统/气体检测仪)
QString code; QString group; // 组名(底盘电机故障/BMS系统故障/…),完整一行才用得到
QString desc; 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 串口收发) ===================== // ===================== 车端通信层(CAN 报文经 /dev/ttyS7 串口收发) =====================
@@ -30,8 +52,8 @@ struct CanFault {
// 下行 分类二/三 12 条(车端 → 遥控器):底盘 4 条、机械臂 4 条、洗消 2 条、气体检测仪 2 条, // 下行 分类二/三 12 条(车端 → 遥控器):底盘 4 条、机械臂 4 条、洗消 2 条、气体检测仪 2 条,
// 按 canId 分流转 handleFrame() 写入 Telemetry。 // 按 canId 分流转 handleFrame() 写入 Telemetry。
// 全部为 29 位扩展帧、信号一律 Intel(小端)位序。 // 全部为 29 位扩展帧、信号一律 Intel(小端)位序。
// 传输走 UDP:bind 本机 9000 端口收下行反馈,上行命令发往 0.0.0.0:9001。 // 传输走 UDP:bind 本机 9000 端口收下行反馈,上行命令发往车端 192.168.1.224:3001。
// UDP 载荷统一为 18 字节:帧头 0x5AA5FF00 + canId + DLC + 8 字节数据 + 校验和。 // UDP 载荷统一为 16 字节:帧头 0xAA + canId + DLC + 8 字节数据 + 校验和 + 帧尾 0xBB。
// 通信超时判定:连续 2s 未收到任何合法帧则判为通信异常(最慢的下行周期 1000ms)。 // 通信超时判定:连续 2s 未收到任何合法帧则判为通信异常(最慢的下行周期 1000ms)。
class CarCommunication : public QObject class CarCommunication : public QObject
{ {
@@ -46,11 +68,19 @@ public:
// 行车/机械臂模式(面板旋钮或遥控器报文)→ Arm_ControlCmd1 的 Arm_modeCmd / Arm_enable // 行车/机械臂模式(面板旋钮或遥控器报文)→ Arm_ControlCmd1 的 Arm_modeCmd / Arm_enable
void setArmMode(bool arm) { m_armMode = arm; } void setArmMode(bool arm) { m_armMode = arm; }
// 遥控器是否正在操作(由 AppContext 维护)。面板那两路入口据此让位:
// 两端都能操作,但遥控器一动手就以遥控器为准;遥控器松手后由页面重发按住的方向/轴。
void setRcActive(bool on) { m_rcActive = on; }
// ---------- 上行命令(分类一)---------- // ---------- 上行命令(分类一)----------
// 命令只更新本端控制状态,实际报文由周期定时器编码发送(CAN 控制帧本就是周期报文)。 // 命令只更新本端控制状态,实际报文由周期定时器编码发送(CAN 控制帧本就是周期报文)。
// 底盘/机械臂各有**两个入口**:面板一个、遥控器一个(名字带 Rc)。两路写的是同一份
// 轴值缓存,差别只在“遥控器在操作时面板那一路直接忽略”。
// turn 非空("left"/"right")时与 dir 同时下发,支持边前进/后退边转向 // turn 非空("left"/"right")时与 dir 同时下发,支持边前进/后退边转向
void sendChassisMove(const QString& dir, double speed, const QString& turn = QString()); 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 sendArmMove(const QString& axis, int dir); // dir: +1 / -1 / 0(停止)
void sendArmMoveRc(const QString& axis, int dir);
void sendArmReturn(); void sendArmReturn();
void sendArmPause(); void sendArmPause();
void sendGasStart(); void sendGasStart();
@@ -66,9 +96,11 @@ public:
void sendEstopReset(); void sendEstopReset();
signals: signals:
void telemetryUpdated(); // 任意遥测包到达(页面据此刷新) void telemetryUpdated(); // 一批数据报解析完(页面据此刷新,同批多帧只发一次)
void commOkChanged(bool ok); // 通信状态变化 // 顶部左区那行通信状态变化(Telemetry::commText)。周期报文内容不变时不发。
void selfCheckItem(int index, bool ok); // 自检单项结果(0~9) void commTextChanged();
void selfCheckItem(int index, bool ok); // 自检单项结果(0~kSelfCheckItemCount-1)
void selfCheckItemBusy(int index); // 自检单项开始执行(已下发指令、正等反馈)
void selfCheckDone(bool allOk); // 自检完成 void selfCheckDone(bool allOk); // 自检完成
void faultAppended(const QString& system, const QString& code, const QString& desc); void faultAppended(const QString& system, const QString& code, const QString& desc);
@@ -81,11 +113,14 @@ private slots:
void onSelfCheckTick(); void onSelfCheckTick();
private: private:
// 收包:按 canId 分流;发包:18 字节封帧后写串口 // 收包:按 canId 分流;发包:16 字节封帧后写 UDP
void handleFrame(quint32 canId, const quint8* d); void handleFrame(quint32 canId, const quint8* d);
void sendFrame(quint32 canId, const quint8* data); void sendFrame(quint32 canId, const quint8* data);
void cacheFaults(); void cacheFaults();
void updateCommText(); // 重建顶部那行通信状态(Telemetry::commText)
void updateWashSwing(); // 洗消作业期间的末端俯仰摆动(每 50ms 一拍)
void setCommOk(bool ok); void setCommOk(bool ok);
void finishSelfCheckItem(bool ok); // 记录单项结果并推进到下一项
Telemetry* m_tel = nullptr; Telemetry* m_tel = nullptr;
QUdpSocket* m_sock = nullptr; QUdpSocket* m_sock = nullptr;
@@ -96,12 +131,18 @@ private:
QTimer* m_selfCheckTimer = nullptr; QTimer* m_selfCheckTimer = nullptr;
bool m_lastCommOk = true; bool m_lastCommOk = true;
int m_commFaultCode = 0; // 最近的 All_Comm_FaultCode,0 = 通信正常
bool m_armMode = false; bool m_armMode = false;
bool m_rcActive = false; // 遥控器正在操作:面板那两路入口让位(见 setRcActive)
// 本地自检序列(CAN 矩阵无自检报文,见 .cpp TODO) // 本地自检序列(CAN 矩阵无自检报文,见 .cpp TODO)
int m_selfCheckIndex = 0; int m_selfCheckIndex = 0;
bool m_selfCheckFault = true; bool m_selfCheckFault = true;
int m_selfCheckWait = 0; // 当前项已等待的节拍数:0 = 尚未下发指令
bool m_selfCheckAllOk = true; // 已跑完各项的累计结果,决定 selfCheckDone 的入参
// 自检要临时置调试模式才能操作球阀,结束后恢复原值,免得把整车留在调试模式
bool m_debugModeBeforeCheck = false;
// ---- 分类一 上行控制状态 ---- // ---- 分类一 上行控制状态 ----
// Chassis_ControlCmd 0x18208081 // Chassis_ControlCmd 0x18208081
@@ -116,6 +157,13 @@ private:
quint16 m_roll = kRcAxisMid; quint16 m_roll = kRcAxisMid;
quint16 m_pitch = kRcAxisMid; quint16 m_pitch = kRcAxisMid;
quint16 m_yaw = 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=暂停回位 quint8 m_homingPause = 0x55; // System_homing_pause:0xAA=系统回位 0x55=暂停回位
// Decontam_ControlCmd 0x18238081 // Decontam_ControlCmd 0x18238081
@@ -143,6 +191,9 @@ private:
// 气体检测仪告警周期 1000ms、洗消故障 200ms,分开存放,否则后到的会把先到的清掉。 // 气体检测仪告警周期 1000ms、洗消故障 200ms,分开存放,否则后到的会把先到的清掉。
QVector<CanFault> m_vsf1Faults, m_vsf2Faults, m_vsf3Faults, m_vsf4Faults; QVector<CanFault> m_vsf1Faults, m_vsf2Faults, m_vsf3Faults, m_vsf4Faults;
QVector<CanFault> m_washFaults, m_pumpFaults, m_detectorFaults; QVector<CanFault> m_washFaults, m_pumpFaults, m_detectorFaults;
// 整系统通信故障码单独一份:它是全系统级的,不属于任何一张卡片,
// 只进顶部那行通信状态与故障历史,不并进 chassisFaults。
QVector<CanFault> m_commFaults;
QSet<QString> m_reportedFaults; // 已记入故障历史的“系统|码”,周期性报文只上报新增故障 QSet<QString> m_reportedFaults; // 已记入故障历史的“系统|码”,周期性报文只上报新增故障
}; };
+62 -40
View File
@@ -5,7 +5,6 @@
#include "alarmbanner.h" #include "alarmbanner.h"
#include "datacard.h" #include "datacard.h"
#include "car.h" #include "car.h"
#include "confirmdialog.h"
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
//#include <QGridLayout> // 末端按钮原为 2×3 网格,改为竖列后暂不需要 //#include <QGridLayout> // 末端按钮原为 2×3 网格,改为竖列后暂不需要
@@ -82,33 +81,32 @@ ArmPage::ArmPage(AppContext* ctx, QWidget* parent)
cardsLay->setSpacing(12); cardsLay->setSpacing(12);
m_chassisCard = new DataCard("底盘", cardsCol); m_chassisCard = new DataCard("底盘", cardsCol);
m_chassisCard->addField("底盘电量SOC", "85%"); m_chassisCard->addField("底盘电量SOC", "--");
m_chassisCard->addField("底盘电压", "360V"); m_chassisCard->addField("底盘电压", "--");
m_chassisCard->addField("底盘故障", "无");
m_washCard = new DataCard("洗消", cardsCol); m_washCard = new DataCard("洗消", cardsCol);
m_washCard->addField("A组分/清水液位", "100L"); m_washCard->addField("A组分/清水液位", "--");
m_washCard->addField("泡沫箱液位", "30L"); m_washCard->addField("泡沫箱液位", "--");
m_gasCard = new DataCard("气体检测仪", cardsCol); m_gasCard = new DataCard("气体检测仪", cardsCol);
m_gasCard->addField("主机状态", "点火状态"); m_gasCard->addField("主机状态", "--");
m_gasCard->addField("有毒物种类", "S级"); m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "二级"); m_gasCard->addField("有毒物等级", "--");
cardsLay->addWidget(m_chassisCard, 1); cardsLay->addWidget(m_chassisCard, 1);
cardsLay->addWidget(m_washCard, 1); cardsLay->addWidget(m_washCard, 1);
cardsLay->addWidget(m_gasCard, 1); cardsLay->addWidget(m_gasCard, 1);
m_armCard = new DataCard("机械臂参数", center); m_armCard = new DataCard("机械臂参数", center);
m_armCard->addField("末端位置", "XYZ"); m_armCard->addField("末端位置", "--");
m_armCard->addField("末端姿态", "RX/RY/RZ"); m_armCard->addField("末端姿态", "--");
m_armCard->addField("末端速度", "0"); m_armCard->addField("末端速度", "--");
m_armCard->addField("Joint1关节角", "30°"); m_armCard->addField("Joint1关节角", "--");
m_armCard->addField("Joint2关节角", "20°"); m_armCard->addField("Joint2关节角", "--");
m_armCard->addField("Joint3关节角", "0°"); m_armCard->addField("Joint3关节角", "--");
m_armCard->addField("Joint4关节角", "90°"); m_armCard->addField("Joint4关节角", "--");
m_armCard->addField("Joint5关节角", "45°"); m_armCard->addField("Joint5关节角", "--");
m_armCard->addField("Joint6关节角", "0°"); m_armCard->addField("Joint6关节角", "--");
centerLay->addWidget(cardsCol, 1); centerLay->addWidget(cardsCol, 1);
centerLay->addWidget(m_armCard, 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->comm(), &CarCommunication::telemetryUpdated, this, &ArmPage::refresh);
connect(ctx, &AppContext::modeChanged, this, [this](Mode) { updateModeGating(); }); connect(ctx, &AppContext::modeChanged, this, [this](Mode) { updateModeGating(); });
connect(ctx, &AppContext::estopChanged, this, [this](bool) { 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(); updateModeGating();
refresh(); 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* ArmPage::makeMoveButton(const QString& text, const QString& axis, int dir)
{ {
QPushButton* b = button(text, "", this); QPushButton* b = button(text, "", this);
b->setFixedSize(270, 76); b->setFixedSize(270, 76);
// 记下这根按钮管哪个轴/往哪边,好让"遥控器松手后重发按住的键"找得到它
b->setProperty("armAxis", axis);
b->setProperty("armDir", dir);
b->setStyleSheet(QString( b->setStyleSheet(QString(
"QPushButton{font-size:45px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;}" "QPushButton{font-size:45px;background-color:%1;color:%2;border:1px solid %3;border-radius:12px;}"
"QPushButton:hover,QPushButton:focus,QPushButton:pressed" "QPushButton:hover,QPushButton:focus,QPushButton:pressed"
"{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}") "{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}")
.arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY)); .arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY));
connect(b, &QPushButton::pressed, this, [this, axis, dir]() { 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); ctx()->comm()->sendArmMove(axis, dir);
}); });
connect(b, &QPushButton::released, this, [this, axis]() { connect(b, &QPushButton::released, this, [this, axis]() {
@@ -184,10 +196,11 @@ QPushButton* ArmPage::makeMoveButton(const QString& text, const QString& axis, i
void ArmPage::updateModeGating() void ArmPage::updateModeGating()
{ {
// 非机械臂模式、急停触发或遥控器正在操作时禁用动作按钮(遥控器优先) // 和底盘页一样:现在只有**急停**会禁用动作按钮。模式不再限制(进页面即用),
bool arm = (ctx()->mode == Mode::Arm) && !ctx()->estopTriggered && !ctx()->rcDriving(); // 遥控器也不再让位——面板和遥控器可以同时操作,两边各发各的。
for (QPushButton* b : m_moveButtons) b->setEnabled(arm); const bool ok = !ctx()->estopTriggered;
for (QPushButton* b : m_endButtons) b->setEnabled(arm); for (QPushButton* b : m_moveButtons) b->setEnabled(ok);
for (QPushButton* b : m_endButtons) b->setEnabled(ok);
} }
void ArmPage::onReturnToggled(bool on) void ArmPage::onReturnToggled(bool on)
@@ -206,20 +219,29 @@ void ArmPage::refresh()
m_banner->clear(); m_banner->clear();
} }
m_chassisCard->setField(0, QString("%1%").arg(t.soc)); // 未收到对应报文的数值统一显示 "--"(dashIf)
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0)); m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
m_chassisCard->setField(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); 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(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0)));
m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 0, 'f', 0)); m_washCard->setField(1, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0)));
m_gasCard->setField(0, t.gasHost == "detecting" ? "检测中" : (t.gasHost == "fault" ? "故障" : "待机")); m_washCard->setAlarm(0, t.levelALow);
m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); 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(0, t.pos);
m_armCard->setField(1, t.pose); 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) 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) { if (t.returnState != m_prevReturnState) {
+3
View File
@@ -19,6 +19,9 @@ class ArmPage : public BasePage
public: public:
explicit ArmPage(AppContext* ctx, QWidget* parent = nullptr); explicit ArmPage(AppContext* ctx, QWidget* parent = nullptr);
protected:
void showEvent(QShowEvent* ev) override;
private slots: private slots:
void refresh(); void refresh();
void updateModeGating(); void updateModeGating();
+92 -47
View File
@@ -12,6 +12,12 @@
#include <QTimer> #include <QTimer>
#include "global.h" #include "global.h"
namespace {
// 收起姿态取全零:任一关节角绝对值超过这个角度即认为机械臂已展开。
// 单位与 CAN 下发的关节角一致(car.cpp 直接写入 t.js,未经缩放)。
const double kArmStowedTolDeg = 5.0;
}
ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent) ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
: BasePage(ctx, parent) : BasePage(ctx, parent)
{ {
@@ -63,16 +69,17 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
centerLay->setSpacing(12); centerLay->setSpacing(12);
m_washCard = new DataCard("洗消", center); m_washCard = new DataCard("洗消", center);
m_washCard->addField("A液位", "100L"); m_washCard->addField("A组分/清水液位", "--");
m_washCard->addField("泡沫液位", "30L"); m_washCard->addField("泡沫箱液位", "--");
m_armCard = new DataCard("机械臂", center); m_armCard = new DataCard("机械臂", center);
m_armCard->addField("末端位置", "XYZ"); m_armCard->addField("末端位置", "--");
m_armCard->addField("姿态", "RX/RY/RZ"); m_armCard->addField("末端姿态", "--");
m_gasCard = new DataCard("气体检测仪", center); m_gasCard = new DataCard("气体检测仪", center);
m_gasCard->addField("主机状态", "待机"); m_gasCard->addField("主机状态", "--");
m_gasCard->addField("浓度", "0 ppb"); m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "--");
centerLay->addWidget(m_washCard, 1); centerLay->addWidget(m_washCard, 1);
centerLay->addWidget(m_armCard, 1); centerLay->addWidget(m_armCard, 1);
@@ -88,11 +95,15 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
chassisLay->setSpacing(12); chassisLay->setSpacing(12);
m_chassisCard = new DataCard("底盘状态", chassisCol); m_chassisCard = new DataCard("底盘状态", chassisCol);
m_chassisCard->addField("电量SOC", "85%"); m_chassisCard->addField("底盘电量SOC", "--");
m_chassisCard->addField("电压U", "360V"); m_chassisCard->addField("底盘电压U", "--");
m_chassisCard->addField("车速V", "3.45 km/h"); m_chassisCard->addField("底盘车速V", "--");
m_chassisCard->addField("手刹", "有效"); m_chassisCard->addField("档位状态", "--");
m_chassisFieldFault = m_chassisCard->addField("故障", "无"); m_chassisCard->addField("驻车状态", "--");
m_chassisCard->addField("四驱模式", "--");
m_chassisCard->addField("底盘Ready状态", "--");
m_chassisCard->addField("方向盘角度", "--");
m_chassisCard->addField("角度对中状态", "--");
chassisLay->addWidget(m_chassisCard, 1); chassisLay->addWidget(m_chassisCard, 1);
main->addWidget(chassisCol, 1); // 与中间卡片列平均分配中间区域 main->addWidget(chassisCol, 1); // 与中间卡片列平均分配中间区域
main->addSpacing(17); // 底盘列 ↔ 车速设置列:由 32 减小到 17(底盘列整体右移 15px) main->addSpacing(17); // 底盘列 ↔ 车速设置列:由 32 减小到 17(底盘列整体右移 15px)
@@ -119,7 +130,6 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1); root->addLayout(main, 1);
m_statusStrip = new StatusStrip(ctx, this); m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setModeVisible(false); // 本页固定行车模式,底部不显示模式徽标
connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() { emit navigate(PageJob); }); connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() { emit navigate(PageJob); });
root->addWidget(m_statusStrip); root->addWidget(m_statusStrip);
@@ -132,7 +142,14 @@ ChassisPage::ChassisPage(AppContext* ctx, QWidget* parent)
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &ChassisPage::refresh); connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &ChassisPage::refresh);
connect(ctx, &AppContext::modeChanged, this, [this](Mode) { updateModeGating(); }); connect(ctx, &AppContext::modeChanged, this, [this](Mode) { updateModeGating(); });
connect(ctx, &AppContext::estopChanged, this, [this](bool) { 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(); updateModeGating();
refresh(); refresh();
} }
@@ -155,20 +172,14 @@ QPushButton* ChassisPage::makeDirButton(const QString& text, const QString& dir)
"{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}") "{background-color:%4;color:#FFFFFF;border:2px solid #8ABFFF;}")
.arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY)); .arg(C::PANEL_HI, C::TEXT, C::BORDER, C::PRIMARY));
connect(b, &QPushButton::pressed, this, [this, dir]() { 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); m_heldDirs.insert(dir);
sendDirCommand(); sendDirCommand();
updateAlarm();
}); });
connect(b, &QPushButton::released, this, [this, dir]() { connect(b, &QPushButton::released, this, [this, dir]() {
m_heldDirs.remove(dir); m_heldDirs.remove(dir);
sendDirCommand(); sendDirCommand();
updateAlarm();
}); });
return b; return b;
} }
@@ -192,46 +203,80 @@ void ChassisPage::sendDirCommand()
void ChassisPage::updateModeGating() void ChassisPage::updateModeGating()
{ {
// 非行车模式、急停触发或遥控器正在操作时禁用方向按钮(遥控器优先) // 现在只有**急停**会禁用方向按钮:模式不再限制(进页面即用),遥控器也不再让位——
const bool drive = (ctx()->mode == Mode::Drive) && !ctx()->estopTriggered && !ctx()->rcDriving(); // 面板和遥控器可以同时操作,两边各发各的,由车端处理。
const bool ok = !ctx()->estopTriggered;
for (QPushButton* b : m_dirButtons) for (QPushButton* b : m_dirButtons)
b->setEnabled(drive); b->setEnabled(ok);
// 让位时清掉本地按住状态:按钮被禁用后不会再收到 released,否则方向会被一直挂住。 // 急停把按钮禁掉时清掉按住状态并补一条 stop。(Qt 禁用时会补发 released,
// 让位给遥控器时不补发 stop,否则会覆盖遥控器刚下发的那条指令。 // 各按钮自己那一遍已经发过了,这里只是兜底。)
if (!drive && !m_heldDirs.isEmpty()) { if (!ok && !m_heldDirs.isEmpty()) {
m_heldDirs.clear(); m_heldDirs.clear();
if (!ctx()->rcDriving()) ctx()->comm()->sendChassisMove("stop", 0);
ctx()->comm()->sendChassisMove("stop", 0);
} }
updateAlarm();
}
// 任一关节角偏离收起姿态(全零)即认为机械臂已展开
bool ChassisPage::armNotStowed() const
{
const QVector<double>& 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() void ChassisPage::refresh()
{ {
const Telemetry& t = ctx()->telemetry; const Telemetry& t = ctx()->telemetry;
// 机械臂未回位 / 通信异常告警 updateAlarm();
if (!t.commOk) {
m_banner->showAlarm("通信异常");
} else if (!t.armHomed && t.returnState != "idle") {
m_banner->showAlarm("机械臂未回位,请谨慎操作");
} else {
m_banner->clear();
}
m_chassisCard->setField(0, QString("%1%").arg(t.soc)); // 未收到对应报文的数值统一显示 "--"(dashIf)
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0)); m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
m_chassisCard->setField(2, QString("%1 km/h").arg(t.speed, 0, 'f', 2)); m_chassisCard->setField(1, dashIf(t.hasBattery, QString("%1V").arg(t.volt, 0, 'f', 0)));
m_chassisCard->setField(3, t.handbrake ? "有效" : "无效"); m_chassisCard->setField(2, dashIf(t.hasSpeed, QString("%1 km/h").arg(t.speed, 0, 'f', 2)));
m_chassisCard->setField(m_chassisFieldFault, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); m_chassisCard->setField(3, dashIf(t.hasChassis4, gearLabel(t.gearPos)));
m_chassisCard->setAlarm(m_chassisFieldFault, !t.chassisFaults.isEmpty()); 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(0, t.pos);
m_armCard->setField(1, t.pose); 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, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); 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) void ChassisPage::onSpeedChanged(double v)
+3 -1
View File
@@ -32,6 +32,9 @@ private slots:
private: private:
QPushButton* makeDirButton(const QString& text, const QString& dir); QPushButton* makeDirButton(const QString& text, const QString& dir);
void sendDirCommand(); // 按当前按住的方向组合下发(档位与转向可同时) void sendDirCommand(); // 按当前按住的方向组合下发(档位与转向可同时)
void updateAlarm(); // 横幅:通信异常 / 机械臂未回位时驱动底盘
bool armNotStowed() const; // 任一关节角偏离收起姿态(全零)
bool chassisDriving() const; // 面板方向键按住,或遥控器正在推杆
TitleBar* m_titleBar = nullptr; TitleBar* m_titleBar = nullptr;
StatusStrip* m_statusStrip = nullptr; StatusStrip* m_statusStrip = nullptr;
@@ -42,7 +45,6 @@ private:
DataCard* m_chassisCard = nullptr; DataCard* m_chassisCard = nullptr;
ParamControl* m_speed = nullptr; ParamControl* m_speed = nullptr;
QTimer* m_saveTimer = nullptr; QTimer* m_saveTimer = nullptr;
int m_chassisFieldFault = -1;
QVector<QPushButton*> m_dirButtons; QVector<QPushButton*> m_dirButtons;
QSet<QString> m_heldDirs; // 当前按住的方向,可多键同时按 QSet<QString> m_heldDirs; // 当前按住的方向,可多键同时按
}; };
+132 -55
View File
@@ -9,8 +9,13 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPushButton> #include <QPushButton>
#include <QLabel> #include <QLabel>
#include <QDateTime>
#include "global.h" #include "global.h"
// 球阀“开启/关闭故障”的判据:下发指令后这么久还等不到对应到位反馈就判故障。
// 取 8s,与④自检里球阀四项的等待时长(kValveWaitTicks 18 拍 × 450ms ≈ 8s)一致。
static const qint64 kValveFaultMs = 8000;
DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent) DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
: BasePage(ctx, parent) : BasePage(ctx, parent)
{ {
@@ -36,8 +41,42 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
QVBoxLayout* leftLay = new QVBoxLayout(left); QVBoxLayout* leftLay = new QVBoxLayout(left);
leftLay->setContentsMargins(20, 10, 10, 10); leftLay->setContentsMargins(20, 10, 10, 10);
leftLay->setSpacing(60); 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); leftLay->addStretch(1);
left->setFixedWidth(300); left->setFixedWidth(300);
main->addWidget(left); main->addWidget(left);
@@ -54,27 +93,33 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
centerLeftLay->setSpacing(10); centerLeftLay->setSpacing(10);
m_chassisCard = new DataCard("底盘", centerLeft); m_chassisCard = new DataCard("底盘", centerLeft);
m_chassisCard->addField("电量", "85%"); m_chassisCard->addField("底盘电量SOC", "--");
m_chassisCard->addField("电压", "360V"); m_chassisCard->addField("底盘电压U", "--");
m_armCard = new DataCard("机械臂", centerLeft); m_armCard = new DataCard("机械臂", centerLeft);
m_armCard->addField("末端位置", "XYZ"); // 这一列只有 474px,位置行在 42px 下约 530px,单独把数值字号收到 34(约 463px)
m_armCard->addField("姿态", "RX/RY/RZ"); m_armCard->setValueFontPx(34);
m_armCard->addField("末端位置", "--");
m_armCard->addField("末端姿态", "--");
m_gasCard = new DataCard("气体检测仪", centerLeft); m_gasCard = new DataCard("气体检测仪", centerLeft);
m_gasCard->addField("主机状态", "待机"); m_gasCard->addField("主机状态", "--");
m_gasCard->addField("浓度", "0 ppb"); m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "--");
centerLeftLay->addWidget(m_chassisCard, 1); centerLeftLay->addWidget(m_chassisCard, 1);
centerLeftLay->addWidget(m_armCard, 1); centerLeftLay->addWidget(m_armCard, 1);
centerLeftLay->addWidget(m_gasCard, 1); centerLeftLay->addWidget(m_gasCard, 1);
centerLay->addWidget(centerLeft, 1); centerLay->addWidget(centerLeft, 1);
// 与消除/消毒作业页的洗消卡同一套字段(本页只多了右侧的泵控制)
m_washCard = new DataCard("洗消参数", center); m_washCard = new DataCard("洗消参数", center);
m_washCard->addField("洗消模式", "--"); m_washCard->addField("洗消模式", "--");
m_washCard->addField("A组分液位", "100L"); m_washCard->addField("A组分液位", "--");
m_washCard->addField("A药剂流量", "0 L/min"); m_washCard->addField("B组分液位", "--");
m_washCard->addField("管路压力", "0 MPa"); m_washCard->addField("A药剂流量", "--");
m_washCard->addField("发泡倍数", "0"); m_washCard->addField("B药剂流量", "--");
m_washCard->addField("空压机故障代码", ""); m_washCard->addField("气体流量", "--");
m_washCard->addField("柱塞泵故障代码", ""); m_washCard->addField("管路压力", "--");
m_washCard->addField("空压机压力", "--");
m_washCard->addField("发泡倍数", "--");
centerLay->addWidget(m_washCard, 1); centerLay->addWidget(m_washCard, 1);
main->addWidget(center, 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;}") bottom->setStyleSheet(QString("QWidget#DebugBottom{background-color:%1;border-top:1px solid %2;}")
.arg(C::PANEL, C::BORDER)); .arg(C::PANEL, C::BORDER));
QHBoxLayout* bottomLay = new QHBoxLayout(bottom); QHBoxLayout* bottomLay = new QHBoxLayout(bottom);
// 左起 364 = 36(页面边距) + 300(球阀列) + 28(列间距),与中央“底盘”列左边缘对齐
bottomLay->setContentsMargins(364, 0, 24, 0);
bottomLay->setSpacing(48); bottomLay->setSpacing(48);
auto makeLed = [&](const QString& text) { auto makeLed = [&](const QString& text) {
LedIndicator* led = new LedIndicator(text, bottom); LedIndicator* led = new LedIndicator(text, bottom);
@@ -111,10 +154,14 @@ DebugMaintainPage::DebugMaintainPage(AppContext* ctx, QWidget* parent)
bottomLay->addWidget(led); bottomLay->addWidget(led);
return led; return led;
}; };
m_ledPlunger = makeLed("柱塞泵"); // 三个泵的指示灯已经搬进各自的泵组里(放在按钮行左侧),底栏只留两路液位低
m_ledAir = makeLed("空压机"); m_ledLevelA = makeLed("A组分/清水液位低");
m_ledLeaf = makeLed("叶片泵"); m_ledLevelB = makeLed("B组分液位低");
m_ledLevel = makeLed("液位低"); // 这两盏灯居中于中央卡片区:卡片区 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); bottomLay->addStretch(1);
QPushButton* back = button("返回", "", bottom); QPushButton* back = button("返回", "", bottom);
back->setFixedSize(160, 76); 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); QLabel* title = label(QString("D%1 球阀").arg(valve), 30, C::TEXT, true, grp);
lay->addWidget(title); lay->addWidget(title);
// 控制开/关(toggle):文字固定,底色表示状态——未选中=关闭(红)、选中=开启(绿) // 控制开/关(toggle):文字固定,底色表示状态——未选中=关闭(灰)、选中=开启(绿)
QPushButton* ctrl = button(QString("D%1 控制").arg(valve), "", grp); QPushButton* ctrl = button(QString("D%1 控制").arg(valve), "", grp);
ctrl->setCheckable(true); ctrl->setCheckable(true);
ctrl->setFixedSize(180, 64); // 窄按钮,与泵组的“上电”一致 ctrl->setFixedSize(180, 64); // 窄按钮,与泵组的“上电”一致
@@ -167,16 +214,19 @@ QWidget* DebugMaintainPage::makeValveGroup(int valve)
ctrl->setStyleSheet(QString("QPushButton{background-color:%1;color:#FFFFFF;border:none;}" ctrl->setStyleSheet(QString("QPushButton{background-color:%1;color:#FFFFFF;border:none;}"
"QPushButton:checked{background-color:%2;color:#FFFFFF;border:none;}" "QPushButton:checked{background-color:%2;color:#FFFFFF;border:none;}"
"QPushButton:focus{border:4px solid #8ABFFF;}") "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) { connect(ctrl, &QPushButton::toggled, this, [this, valve](bool on) {
ctx()->comm()->sendBallValve(valve, 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); lay->addWidget(ctrl, 0, Qt::AlignHCenter);
// 开启到位 / 关闭到位(同一行,居中)。未收到车端遥测时保持缺省显示 // 开启到位 / 关闭到位(同一行,居中)。缺省状态按“阀体默认关着”给:
// (开启绿 / 关闭红),首次收到遥测后由 refresh() 按实际到位状态刷新 // 开启到位灭(灰)、关闭到位红。首次收到遥测后由 refresh() 按实际到位状态刷新
LedIndicator* open = new LedIndicator("开启", grp); LedIndicator* open = new LedIndicator("开启", grp);
open->setState(LedIndicator::Green); open->setState(LedIndicator::Off);
LedIndicator* close = new LedIndicator("关闭", grp); LedIndicator* close = new LedIndicator("关闭", grp);
close->setState(LedIndicator::Red); close->setState(LedIndicator::Red);
QHBoxLayout* ioRow = new QHBoxLayout(); QHBoxLayout* ioRow = new QHBoxLayout();
@@ -187,23 +237,16 @@ QWidget* DebugMaintainPage::makeValveGroup(int valve)
ioRow->addStretch(1); ioRow->addStretch(1);
lay->addLayout(ioRow); lay->addLayout(ioRow);
// 故障提示 // 故障提示不在盒子里——它是盒子**上方**独立的一条,由构造函数的 addValve 建好
QLabel* fault = label("", 16, C::DANGER, true, grp);
fault->setAlignment(Qt::AlignCenter);
fault->setProperty("alert", true);
fault->hide();
lay->addWidget(fault);
if (valve == 1) { if (valve == 1) {
m_valve1Btn = ctrl; m_valve1Btn = ctrl;
m_v1Open = open; m_v1Open = open;
m_v1Close = close; m_v1Close = close;
m_valveFault1 = fault;
} else { } else {
m_valve2Btn = ctrl; m_valve2Btn = ctrl;
m_v2Open = open; m_v2Open = open;
m_v2Close = close; m_v2Close = close;
m_valveFault2 = fault;
} }
return grp; return grp;
} }
@@ -257,6 +300,7 @@ QWidget* DebugMaintainPage::makePumpGroup(const QString& pump, const QString& na
start->setChecked(false); start->setChecked(false);
ctx()->comm()->sendPumpPower(pump, false); ctx()->comm()->sendPumpPower(pump, false);
}); });
// 圆点+名称不在这里了——三个泵的上电指示灯已经搬到左列 D2 球阀下方(见构造函数)
QHBoxLayout* pwrRow = new QHBoxLayout(); QHBoxLayout* pwrRow = new QHBoxLayout();
pwrRow->setSpacing(12); pwrRow->setSpacing(12);
pwrRow->addStretch(1); pwrRow->addStretch(1);
@@ -265,9 +309,9 @@ QWidget* DebugMaintainPage::makePumpGroup(const QString& pump, const QString& na
pwrRow->addStretch(1); pwrRow->addStretch(1);
lay->addLayout(pwrRow); 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 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; return grp;
} }
@@ -275,22 +319,32 @@ void DebugMaintainPage::refresh()
{ {
const Telemetry& t = ctx()->telemetry; const Telemetry& t = ctx()->telemetry;
m_chassisCard->setField(0, QString("%1%").arg(t.soc)); // 未收到对应报文的数值统一显示 "--"(dashIf)
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0)); 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(0, t.pos);
m_armCard->setField(1, t.pose); 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, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); 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(0, t.washMode);
m_washCard->setField(1, QString("%1L").arg(t.levelA, 0, 'f', 0)); m_washCard->setField(1, dashIf(t.hasWash, 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(2, dashIf(t.hasWash, QString("%1L").arg(t.levelFoam, 0, 'f', 0)));
m_washCard->setField(3, QString("%1 MPa").arg(t.press, 0, 'f', 2)); m_washCard->setField(3, dashIf(t.hasWash, QString("%1 L/min").arg(t.flowA, 0, 'f', 1)));
m_washCard->setField(4, QString("%1").arg(t.foam, 0, 'f', 1)); m_washCard->setField(4, dashIf(t.hasWash, QString("%1 L/min").arg(t.flowB, 0, 'f', 1)));
m_washCard->setField(5, t.pumpFaults.value(0)); m_washCard->setField(5, dashIf(t.hasWash, QString("%1 L/min").arg(t.gasFlow, 0, 'f', 1)));
m_washCard->setField(6, t.pumpFaults.value(0)); 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) { if (m_telemetrySeen) {
m_v1Open->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off); m_v1Open->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off);
m_v1Close->setState(t.d1close ? LedIndicator::Red : 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); m_v2Close->setState(t.d2close ? LedIndicator::Red : LedIndicator::Off);
} }
// 球阀故障提示 // 球阀故障:协议里没有“D1开启故障/关闭故障”这种信号,只能自己推——
bool v1Fault = t.valveFault.contains("D1"); // 指令下发后 kValveFaultMs 内 MBV_Dn_Sta 还没变成想要的样子,就判该方向故障;
bool v2Fault = t.valveFault.contains("D2"); // 反馈一旦对上(到位)就解除。cmdMs 为 0 表示没有待确认的指令,不判。
if (v1Fault) { m_valveFault1->setText(t.valveFault); m_valveFault1->show(); startBlink(m_valveFault1, "blink"); } const qint64 now = QDateTime::currentMSecsSinceEpoch();
else { stopBlink(m_valveFault1, "blink"); m_valveFault1->hide(); } auto judgeValve = [&](qint64& cmdMs, int wantOpen, bool fbOpen, bool& faulted,
if (v2Fault) { m_valveFault2->setText(t.valveFault); m_valveFault2->show(); startBlink(m_valveFault2, "blink"); } QLabel* lab, int valve) {
else { stopBlink(m_valveFault2, "blink"); m_valveFault2->hide(); } 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_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_plungerStart){ m_plungerStart->setChecked(t.plunger); m_plungerStop->setChecked(!t.plunger); }
if (m_leafStart) { m_leafStart->setChecked(t.leaf); m_leafStop->setChecked(!t.leaf); } 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_ledPlunger->setState(t.plunger ? LedIndicator::Green : LedIndicator::Off);
m_ledAir->setState(t.air ? LedIndicator::Green : LedIndicator::Off); m_ledAir->setState(t.air ? LedIndicator::Green : LedIndicator::Off);
m_ledLeaf->setState(t.leaf ? 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);
} }
+15 -3
View File
@@ -58,15 +58,27 @@ private:
QPushButton* m_leafStart = nullptr; QPushButton* m_leafStart = nullptr;
QPushButton* m_leafStop = nullptr; QPushButton* m_leafStop = nullptr;
// 底部指示灯 // 泵上电指示灯(在各自泵组的按钮行左侧,由 makePumpGroup 填)+ 底部的两路液位低
LedIndicator* m_ledPlunger = nullptr; LedIndicator* m_ledPlunger = nullptr;
LedIndicator* m_ledAir = nullptr; LedIndicator* m_ledAir = nullptr;
LedIndicator* m_ledLeaf = nullptr; LedIndicator* m_ledLeaf = nullptr;
LedIndicator* m_ledLevel = nullptr; LedIndicator* m_ledLevelA = nullptr; // A组分/清水液位低
LedIndicator* m_ledLevelB = nullptr; // B组分液位低
// 是否收到过车端遥测:未收到时球阀到位灯保持“开启=绿、关闭=红”的缺省显示 // 是否收到过车端遥测:未收到时球阀到位灯保持“开启=灰、关闭=红”的缺省显示
bool m_telemetrySeen = false; 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 记录来源后原路返回 // “返回”去向:本页有两个入口(系统首页 / 作业选择),由 MainWindow 记录来源后原路返回
PageId m_returnPage = PageJob; PageId m_returnPage = PageJob;
}; };
+30 -25
View File
@@ -25,36 +25,36 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
main->setContentsMargins(36, 16, 36, 16); main->setContentsMargins(36, 16, 36, 16);
main->setSpacing(32); main->setSpacing(32);
// 中央卡片区(2×2) // 中央卡片区:左列 底盘/洗消/机械臂 纵向排列,右列由气体检测仪独占并跨满三行
QWidget* center = new QWidget(this); QWidget* center = new QWidget(this);
QGridLayout* grid = new QGridLayout(center); QGridLayout* grid = new QGridLayout(center);
grid->setSpacing(20); grid->setSpacing(20);
grid->setContentsMargins(20, 0, 20, 0); grid->setContentsMargins(20, 0, 20, 0);
m_chassisCard = new DataCard("底盘", center); m_chassisCard = new DataCard("底盘", center);
m_chassisCard->addField("电量", "85%"); m_chassisCard->addField("电量", "--");
m_chassisCard->addField("电压", "360V"); m_chassisCard->addField("电压", "--");
m_chassisCard->addField("故障", "无");
m_washCard = new DataCard("洗消", center); m_washCard = new DataCard("洗消", center);
m_washCard->addField("A液位", "100L"); m_washCard->addField("A液位", "--");
m_washCard->addField("泡沫液位", "30L"); m_washCard->addField("泡沫液位", "--");
m_armCard = new DataCard("机械臂", center); m_armCard = new DataCard("机械臂", center);
m_armCard->addField("末端位置", "XYZ"); m_armCard->addField("末端位置", "--");
m_armCard->addField("姿态", "RX/RY/RZ"); m_armCard->addField("姿态", "--");
m_gasCard = new DataCard("气体检测仪", center); m_gasCard = new DataCard("气体检测仪", center);
m_gasCard->addField("主机状态", "待机"); m_gasCard->addField("主机状态", "--");
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); // 主色描边突出 m_gasCard->setHighlight(true); // 主色描边突出
grid->addWidget(m_chassisCard, 0, 0); grid->addWidget(m_chassisCard, 0, 0);
grid->addWidget(m_washCard, 0, 1); grid->addWidget(m_washCard, 1, 0);
grid->addWidget(m_gasCard, 1, 0); grid->addWidget(m_armCard, 2, 0);
grid->addWidget(m_armCard, 1, 1); // 本页主角,独占右列:跨 3 行填满
grid->addWidget(m_gasCard, 0, 1, 3, 1);
main->addWidget(center, 1); main->addWidget(center, 1);
// 右侧按钮列(垂直居中 + 平均分布) // 右侧按钮列(垂直居中 + 平均分布)
@@ -90,7 +90,6 @@ GasDetectorPage::GasDetectorPage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1); root->addLayout(main, 1);
m_statusStrip = new StatusStrip(ctx, this); m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setModeVisible(false); // 本页不显示模式徽标
connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() { connect(m_statusStrip, &StatusStrip::backClicked, this, [this]() {
if (this->ctx()->gasRunning) { if (this->ctx()->gasRunning) {
if (ConfirmDialog::confirm(this, "提示", "检测仍在进行,是否退出?")) if (ConfirmDialog::confirm(this, "提示", "检测仍在进行,是否退出?"))
@@ -117,24 +116,30 @@ void GasDetectorPage::refresh()
{ {
const Telemetry& t = ctx()->telemetry; const Telemetry& t = ctx()->telemetry;
// 同步运行状态(以遥测为准) // 同步运行状态(以遥测为准):未收到报文(gasHost 为空)时按未运行处理
if (t.gasHost == "detecting") ctx()->gasRunning = true; ctx()->gasRunning = (t.gasHost == "detecting");
else if (t.gasHost == "standby") ctx()->gasRunning = false;
updateGating(); updateGating();
m_chassisCard->setField(0, QString("%1%").arg(t.soc)); // 未收到对应报文的数值统一显示 "--"(dashIf)
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0)); m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
m_chassisCard->setField(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); 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(0, t.pos);
m_armCard->setField(1, t.pose); 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(1, t.gasType);
m_gasCard->setField(2, t.gasLevel); 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->setAlarm(m_gasConcField, t.gasConc > 500.0);
m_gasCard->setFaults(t.detectorFaults);
} }
+33 -23
View File
@@ -60,21 +60,23 @@ HomePage::HomePage(AppContext* ctx, QWidget* parent)
grid->setContentsMargins(0, 0, 0, 0); grid->setContentsMargins(0, 0, 0, 0);
m_chassisCard = new DataCard("底盘", center); m_chassisCard = new DataCard("底盘", center);
m_chassisCard->addField("电量", "85%"); m_chassisCard->addField("底盘电量SOC", "--");
m_chassisCard->addField("电压", "360V"); m_chassisCard->addField("底盘电压U", "--");
m_chassisCard->addField("故障", "无");
m_armCard = new DataCard("机械臂", center); m_armCard = new DataCard("机械臂", center);
m_armCard->addField("末端位置", "XYZ"); // 位置/姿态两行的数值最长约 554px,超出 2×2 卡片的内容区 522px,单独把字号收小一号
m_armCard->addField("姿态", "RX/RY/RZ"); m_armCard->setValueFontPx(38);
m_armCard->addField("末端位置", "--");
m_armCard->addField("末端姿态", "--");
m_washCard = new DataCard("洗消", center); m_washCard = new DataCard("洗消", center);
m_washCard->addField("A液位", "100L"); m_washCard->addField("A组分/清水液位", "--");
m_washCard->addField("泡沫液位", "30L"); m_washCard->addField("泡沫箱液位", "--");
m_gasCard = new DataCard("气体检测仪", center); m_gasCard = new DataCard("气体检测仪", center);
m_gasCard->addField("主机状态", "待机"); m_gasCard->addField("主机状态", "--");
m_gasCard->addField("浓度", "0 ppb"); m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "--");
// 左列 底盘/机械臂,右列 洗消/气体检测仪 // 左列 底盘/机械臂,右列 洗消/气体检测仪
grid->addWidget(m_chassisCard, 0, 0); grid->addWidget(m_chassisCard, 0, 0);
@@ -113,8 +115,7 @@ HomePage::HomePage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1); root->addLayout(main, 1);
m_statusStrip = new StatusStrip(ctx, this); m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setBackVisible(false); m_statusStrip->setBackVisible(false); // 本页「返回」在右侧按钮列,底条不重复放
m_statusStrip->setModeVisible(false); // 本页不显示模式徽标(菜单已移到左右两列)
root->addWidget(m_statusStrip); root->addWidget(m_statusStrip);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &HomePage::refresh); connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &HomePage::refresh);
@@ -124,26 +125,35 @@ HomePage::HomePage(AppContext* ctx, QWidget* parent)
void HomePage::refresh() void HomePage::refresh()
{ {
const Telemetry& t = ctx()->telemetry; 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(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
m_chassisCard->setAlarm(2, !t.chassisFaults.isEmpty()); 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(0, t.pos);
m_armCard->setField(1, t.pose); m_armCard->setField(1, t.pose);
// 机械臂卡没有故障行:协议里机械臂那四帧只有位置/姿态/关节角,没有故障信号
m_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0)); m_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0)));
m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 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(0, gasHostLabel(t));
m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); m_gasCard->setField(1, t.gasType); // 有毒物种类,未收到报文时本身就是 "--"
m_gasCard->setAlarm(1, t.gasConc > 500.0); m_gasCard->setField(2, t.gasLevel); // 有毒物等级
m_gasCard->setAlarm(0, t.gasHost == "fault");
m_gasCard->setFaults(t.detectorFaults);
} }
void HomePage::onHelp() void HomePage::onHelp()
{ {
// 正文按 2+1 断句。字号 34 时最长一行 918px,正文区 944px,不会被 wordWrap 二次折行
ConfirmDialog::message(this, "操作说明", ConfirmDialog::message(this, "操作说明",
"系统自检将依次检测:通信系统、底盘通信、机械臂通信、洗消通信、" "自检过程主要检测各分系统通信、传感器信号数据是否正常、\n"
"传感器信号、清水液位及电动球阀 D1/D2 的开启与关闭。\n" "电动球阀D1~D2是否能正常开启和关闭。\n"
"全部正常后方可进入作业选择界面。"); "自检正常或人为点击“忽略故障”,则进入“系统首页面”。",
1000, 34);
} }
+28 -20
View File
@@ -61,21 +61,23 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent)
grid->setContentsMargins(0, 0, 0, 0); grid->setContentsMargins(0, 0, 0, 0);
m_chassisCard = new DataCard("底盘", center); m_chassisCard = new DataCard("底盘", center);
m_chassisCard->addField("电量", "85%"); m_chassisCard->addField("底盘电量SOC", "--");
m_chassisCard->addField("电压", "360V"); m_chassisCard->addField("底盘电压U", "--");
m_chassisCard->addField("故障", "无");
m_armCard = new DataCard("机械臂", center); m_armCard = new DataCard("机械臂", center);
m_armCard->addField("末端位置", "XYZ"); // 位置/姿态两行的数值最长约 554px,超出 2×2 卡片的内容区 522px,单独把字号收小一号
m_armCard->addField("姿态", "RX/RY/RZ"); m_armCard->setValueFontPx(38);
m_armCard->addField("末端位置", "--");
m_armCard->addField("末端姿态", "--");
m_washCard = new DataCard("洗消", center); m_washCard = new DataCard("洗消", center);
m_washCard->addField("A液位", "100L"); m_washCard->addField("A组分/清水液位", "--");
m_washCard->addField("泡沫液位", "30L"); m_washCard->addField("泡沫箱液位", "--");
m_gasCard = new DataCard("气体检测仪", center); m_gasCard = new DataCard("气体检测仪", center);
m_gasCard->addField("主机状态", "待机"); m_gasCard->addField("主机状态", "--");
m_gasCard->addField("浓度", "0 ppb"); m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "--");
// 左列 底盘/机械臂,右列 洗消/气体检测仪 // 左列 底盘/机械臂,右列 洗消/气体检测仪
grid->addWidget(m_chassisCard, 0, 0); grid->addWidget(m_chassisCard, 0, 0);
@@ -116,8 +118,7 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1); root->addLayout(main, 1);
m_statusStrip = new StatusStrip(ctx, this); m_statusStrip = new StatusStrip(ctx, this);
m_statusStrip->setBackVisible(false); m_statusStrip->setBackVisible(false); // 本页「返回」在右侧按钮列,底条不重复放
m_statusStrip->setModeVisible(false); // 本页不显示模式徽标(返回按钮已移到右侧列)
root->addWidget(m_statusStrip); root->addWidget(m_statusStrip);
connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &JobSelectPage::refresh); connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &JobSelectPage::refresh);
@@ -127,18 +128,25 @@ JobSelectPage::JobSelectPage(AppContext* ctx, QWidget* parent)
void JobSelectPage::refresh() void JobSelectPage::refresh()
{ {
const Telemetry& t = ctx()->telemetry; 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(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
m_chassisCard->setAlarm(2, !t.chassisFaults.isEmpty()); 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(0, t.pos);
m_armCard->setField(1, t.pose); m_armCard->setField(1, t.pose);
// 机械臂卡没有故障行:协议里机械臂那四帧只有位置/姿态/关节角,没有故障信号
m_washCard->setField(0, QString("%1L").arg(t.levelA, 0, 'f', 0)); m_washCard->setField(0, dashIf(t.hasWash, QString("%1L").arg(t.levelA, 0, 'f', 0)));
m_washCard->setField(1, QString("%1L").arg(t.levelFoam, 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(0, gasHostLabel(t));
m_gasCard->setField(1, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); m_gasCard->setField(1, t.gasType); // 有毒物种类,未收到报文时本身就是 "--"
m_gasCard->setAlarm(1, t.gasConc > 500.0); m_gasCard->setField(2, t.gasLevel); // 有毒物等级
m_gasCard->setAlarm(0, t.gasHost == "fault");
m_gasCard->setFaults(t.detectorFaults);
} }
+44 -27
View File
@@ -68,39 +68,45 @@ SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent)
centerLay->addWidget(m_status); centerLay->addWidget(m_status);
centerLay->addSpacing(16); centerLay->addSpacing(16);
// 10 个自检项,两列 // 14 个自检项,两列各 7 行
const int rows = kSelfCheckItemCount / 2;
QGridLayout* grid = new QGridLayout(); QGridLayout* grid = new QGridLayout();
grid->setSpacing(8); grid->setSpacing(8);
const QString names[10] = { const QString names[kSelfCheckItemCount] = {
"通信系统", "底盘通信", "机械臂通信", "洗消通信", "传感器信号", // All_Comm_FaultCode:0=通信正常,1~8=各分系统通信异常
"清水液位", "球阀D1开启", "球阀D1关闭", "球阀D2开启", "球阀D2关闭" "通信系统", "底盘电机通信", "底盘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); LedIndicator* led = new LedIndicator(names[i], center);
led->setFixedHeight(80); led->setFixedHeight(68);
led->setRadius(26); led->setRadius(22);
led->setTextPixelSize(60); led->setTextPixelSize(44); // 两列各 7 行,字号收窄以保证最长项“气体检测仪通信”不被裁切
QLabel* res = label("", 50, C::TEXT_SUB, false, center); QLabel* res = label("", 44, C::TEXT_SUB, false, center);
res->setFixedWidth(200); res->setFixedWidth(160);
QWidget* row = new QWidget(center); QWidget* row = new QWidget(center);
QHBoxLayout* rowLay = new QHBoxLayout(row); QHBoxLayout* rowLay = new QHBoxLayout(row);
rowLay->setContentsMargins(0, 0, 0, 0); rowLay->setContentsMargins(0, 0, 0, 0);
rowLay->addWidget(led, 1); rowLay->addWidget(led, 1);
rowLay->addWidget(res); rowLay->addWidget(res);
grid->addWidget(row, i % 5, i / 5); grid->addWidget(row, i % rows, i / rows);
m_leds.append(led); m_leds.append(led);
m_results.append(res); m_results.append(res);
} }
resetItems(); // 初始化 10 个自检项 resetItems(); // 初始化 14 个自检项
centerLay->addLayout(grid); centerLay->addLayout(grid);
centerLay->addSpacing(50); // 文字与进度条之间的间隙 centerLay->addSpacing(40); // 文字与进度条之间的间隙
// 进度条(位于屏幕约 3/4 高度处,文字全部在其上方) // 进度条(位于屏幕约 3/4 高度处,文字全部在其上方)
m_progress = new QProgressBar(center); m_progress = new QProgressBar(center);
m_progress->setRange(0, 100); m_progress->setRange(0, 100);
m_progress->setValue(0); m_progress->setValue(0);
m_progress->setFixedHeight(60); m_progress->setFixedHeight(56);
centerLay->addWidget(m_progress); centerLay->addWidget(m_progress);
centerLay->addStretch(3); // 下方留白(更大)→ 进度条落在约 3/4 处 centerLay->addStretch(3); // 下方留白(更大)→ 进度条落在约 3/4 处
@@ -130,6 +136,7 @@ SelfCheckPage::SelfCheckPage(AppContext* ctx, QWidget* parent)
root->addLayout(main, 1); root->addLayout(main, 1);
// 连接通信信号 // 连接通信信号
connect(ctx->comm(), &CarCommunication::selfCheckItemBusy, this, &SelfCheckPage::onSelfCheckItemBusy);
connect(ctx->comm(), &CarCommunication::selfCheckItem, this, &SelfCheckPage::onSelfCheckItem); connect(ctx->comm(), &CarCommunication::selfCheckItem, this, &SelfCheckPage::onSelfCheckItem);
connect(ctx->comm(), &CarCommunication::selfCheckDone, this, &SelfCheckPage::onSelfCheckDone); connect(ctx->comm(), &CarCommunication::selfCheckDone, this, &SelfCheckPage::onSelfCheckDone);
} }
@@ -150,13 +157,13 @@ void SelfCheckPage::showEvent(QShowEvent* ev)
}); });
} }
// 复位 10 个自检项:指示灯置灰、结果“待检测” // 复位 14 个自检项:指示灯置灰、结果“待检测”
void SelfCheckPage::resetItems() void SelfCheckPage::resetItems()
{ {
for (int i = 0; i < 10; ++i) { for (int i = 0; i < kSelfCheckItemCount; ++i) {
m_leds[i]->setState(LedIndicator::Off); m_leds[i]->setState(LedIndicator::Off);
m_results[i]->setText("待检测"); 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; 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) void SelfCheckPage::onSelfCheckItem(int index, bool ok)
{ {
if (index < 0 || index >= 10) if (index < 0 || index >= kSelfCheckItemCount)
return; return;
m_leds[index]->setState(LedIndicator::Yellow); // 检测中黄闪 → 结果
// 结果态 // 结果态
if (ok) { if (ok) {
m_leds[index]->setState(LedIndicator::Green); m_leds[index]->setState(LedIndicator::Green);
m_results[index]->setText("正常"); 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 { } else {
m_leds[index]->setState(LedIndicator::RedBlink); m_leds[index]->setState(LedIndicator::RedBlink);
m_results[index]->setText("异常"); m_results[index]->setText("故障");
m_results[index]->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::DANGER)); 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) void SelfCheckPage::onSelfCheckDone(bool allOk)
@@ -224,7 +240,6 @@ void SelfCheckPage::onSelfCheckDone(bool allOk)
void SelfCheckPage::onIgnoreFault() void SelfCheckPage::onIgnoreFault()
{ {
// emit navigate(PageJob); // 原:直接进作业选择页
emit navigate(PageHome); // 忽略故障后进入系统首页面 emit navigate(PageHome); // 忽略故障后进入系统首页面
} }
@@ -235,8 +250,10 @@ void SelfCheckPage::onJobSelect()
void SelfCheckPage::onHelp() void SelfCheckPage::onHelp()
{ {
// 与系统首页面的「操作说明」同一份文案(2+1 行)
ConfirmDialog::message(this, "操作说明", ConfirmDialog::message(this, "操作说明",
"系统自检将依次检测:通信系统、底盘通信、机械臂通信、洗消通信、" "自检过程主要检测各分系统通信、传感器信号数据是否正常、\n"
"传感器信号、清水液位及电动球阀 D1/D2 的开启与关闭。\n" "电动球阀D1~D2是否能正常开启和关闭。\n"
"全部正常后方可进入作业选择界面。"); "自检正常或人为点击“忽略故障”,则进入“系统首页面”。",
1000, 34);
} }
+2 -1
View File
@@ -24,6 +24,7 @@ protected:
void showEvent(QShowEvent* ev) override; void showEvent(QShowEvent* ev) override;
private slots: private slots:
void onSelfCheckItemBusy(int index);
void onSelfCheckItem(int index, bool ok); void onSelfCheckItem(int index, bool ok);
void onSelfCheckDone(bool allOk); void onSelfCheckDone(bool allOk);
void onIgnoreFault(); void onIgnoreFault();
@@ -31,7 +32,7 @@ private slots:
void onHelp(); void onHelp();
private: private:
void resetItems(); // 复位 10 个自检项(指示灯置灰、结果“待检测”) void resetItems(); // 复位 14 个自检项(指示灯置灰、结果“待检测”)
TitleBar* m_titleBar = nullptr; TitleBar* m_titleBar = nullptr;
QLabel* m_status = nullptr; QLabel* m_status = nullptr;
-254
View File
@@ -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 <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTimer>
#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();
}
-81
View File
@@ -1,81 +0,0 @@
#ifndef WASHBASEPAGE_H
#define WASHBASEPAGE_H
#include "base.h"
#include <QVector>
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
+86 -83
View File
@@ -10,7 +10,6 @@
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QPushButton> #include <QPushButton>
#include <QLabel> #include <QLabel>
#include <QTimer>
#include "global.h" #include "global.h"
WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent) WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
@@ -40,17 +39,17 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
leftLay->setSpacing(10); leftLay->setSpacing(10);
m_chassisCard = new DataCard("底盘", left); m_chassisCard = new DataCard("底盘", left);
m_chassisCard->addField("电量", "85%"); m_chassisCard->addField("底盘电量SOC", "--");
m_chassisCard->addField("电压", "360V"); m_chassisCard->addField("底盘电压U", "--");
m_chassisCard->addField("故障", "无");
m_armCard = new DataCard("机械臂", left); m_armCard = new DataCard("机械臂", left);
m_armCard->addField("末端位置", "XYZ"); m_armCard->addField("末端位置", "--");
m_armCard->addField("姿态", "RX/RY/RZ"); m_armCard->addField("末端姿态", "--");
m_gasCard = new DataCard("气体检测仪", left); m_gasCard = new DataCard("气体检测仪", left);
m_gasCard->addField("主机状态", "待机"); m_gasCard->addField("主机状态", "--");
m_gasCard->addField("浓度", "0 ppb"); m_gasCard->addField("有毒物种类", "--");
m_gasCard->addField("有毒物等级", "--");
leftLay->addWidget(m_chassisCard, 1); leftLay->addWidget(m_chassisCard, 1);
leftLay->addWidget(m_armCard, 1); leftLay->addWidget(m_armCard, 1);
@@ -62,16 +61,14 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
// 中央洗消卡 // 中央洗消卡
m_washCard = new DataCard("洗消", this); m_washCard = new DataCard("洗消", this);
m_fMode = m_washCard->addField("洗消模式", "--"); m_fMode = m_washCard->addField("洗消模式", "--");
m_fLevelA = m_washCard->addField("A组分液位", "100L"); m_fLevelA = m_washCard->addField("A组分液位", "--");
m_fLevelFoam = m_washCard->addField("泡沫箱液位", "30L"); m_fLevelFoam = m_washCard->addField("B组分液位", "--");
m_fFlowA = m_washCard->addField("A药剂流量", "0 L/min"); m_fFlowA = m_washCard->addField("A药剂流量", "--");
m_fFlowB = m_washCard->addField("B药剂流量", "0 L/min"); m_fFlowB = m_washCard->addField("B药剂流量", "--");
m_fGasFlow = m_washCard->addField("气体流量", "0 L/min"); m_fGasFlow = m_washCard->addField("气体流量", "--");
m_fPress = m_washCard->addField("管路压力", "0 MPa"); m_fPress = m_washCard->addField("管路压力", "--");
m_fAirPress = m_washCard->addField("空压机压力", "0 MPa"); m_fAirPress = m_washCard->addField("空压机压力", "--");
m_fFoam = m_washCard->addField("发泡倍数", "0"); m_fFoam = m_washCard->addField("发泡倍数", "--");
m_fAirFault = m_washCard->addField("空压机故障代码", "");
m_fPlungerFault = m_washCard->addField("柱塞泵故障代码", "");
main->addSpacing(60); main->addSpacing(60);
main->addWidget(m_washCard, 1); main->addWidget(m_washCard, 1);
@@ -148,10 +145,11 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
root->addLayout(main, 1); root->addLayout(main, 1);
// 底部条:左右两个按钮纵向占满两行,中间上下两排(泵数值 / 指示灯)居中 // 底部条:左右两个按钮纵向占满,中间三排(泵数值 / 指示灯 / 两路液位低)居中。
// 高度由 160 加到 188:中间那列三排约 164px 再加 8+8 的上下边距,160 装不下第三排
QWidget* bottom = new QWidget(this); QWidget* bottom = new QWidget(this);
bottom->setObjectName("WashBottom"); bottom->setObjectName("WashBottom");
bottom->setFixedHeight(160); bottom->setFixedHeight(188);
// 用 #WashBottom 限定选择器:不带选择器的样式表会连子控件一起套上边框和底色 // 用 #WashBottom 限定选择器:不带选择器的样式表会连子控件一起套上边框和底色
bottom->setStyleSheet(QString("QWidget#WashBottom{background-color:%1;border-top:1px solid %2;}") bottom->setStyleSheet(QString("QWidget#WashBottom{background-color:%1;border-top:1px solid %2;}")
.arg(C::PANEL, C::BORDER)); .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); connect(m_factory, &QPushButton::clicked, this, &WashPage::onFactory);
bottomRoot->addWidget(m_factory, 0, Qt::AlignVCenter); bottomRoot->addWidget(m_factory, 0, Qt::AlignVCenter);
// 中:上排三泵数值、下排指示灯,两排都水平居中 // 中:上排三泵数值、中排指示灯、下排两路液位低,三排都水平居中
QVBoxLayout* midLay = new QVBoxLayout(); QVBoxLayout* midLay = new QVBoxLayout();
midLay->setSpacing(6); midLay->setSpacing(6);
@@ -186,27 +184,36 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
valueLay->addStretch(1); valueLay->addStretch(1);
QHBoxLayout* ledLay = new QHBoxLayout(); QHBoxLayout* ledLay = new QHBoxLayout();
ledLay->setSpacing(10); // 8 个灯 40px 字号后总宽约 1300px ledLay->setSpacing(10);
ledLay->addStretch(1); ledLay->addStretch(1);
auto makeLed = [&](const QString& text) { // 灯要分两行放,故把目标行当参数传进来
auto makeLed = [&](QHBoxLayout* row, const QString& text) {
LedIndicator* led = new LedIndicator(text, bottom); LedIndicator* led = new LedIndicator(text, bottom);
led->setTextPixelSize(40); // 字号与卡片标题一致 led->setTextPixelSize(40); // 字号与卡片标题一致
ledLay->addWidget(led); row->addWidget(led);
return led; return led;
}; };
m_ledRun = makeLed("启停"); m_ledRun = makeLed(ledLay, "启停");
m_ledPlunger = makeLed("柱塞泵"); m_ledPlunger = makeLed(ledLay, "柱塞泵");
m_ledAir = makeLed("空压机"); m_ledAir = makeLed(ledLay, "空压机");
m_ledLeaf = makeLed("叶片泵"); m_ledLeaf = makeLed(ledLay, "叶片泵");
m_ledLevel = makeLed("液位低"); m_ledD1 = makeLed(ledLay, "D1");
m_ledD1 = makeLed("D1"); m_ledD2 = makeLed(ledLay, "D2");
m_ledD2 = makeLed("D2");
m_ledFault = makeLed("系统故障");
ledLay->addStretch(1); 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->addStretch(1);
midLay->addLayout(valueLay); midLay->addLayout(valueLay);
midLay->addLayout(ledLay); midLay->addLayout(ledLay);
midLay->addLayout(levelLay);
midLay->addStretch(1); midLay->addStretch(1);
bottomRoot->addLayout(midLay, 1); bottomRoot->addLayout(midLay, 1);
@@ -219,12 +226,6 @@ WashPage::WashPage(AppContext* ctx, Mode mode, QWidget* parent)
root->addWidget(bottom); 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); connect(ctx->comm(), &CarCommunication::telemetryUpdated, this, &WashPage::refresh);
updateGating(); updateGating();
refresh(); 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_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_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() void WashPage::onStart()
{ {
ctx()->comm()->sendWashStart(washModeTag()); ctx()->comm()->sendWashStart(washModeTag());
ctx()->washRunning = true; // 同时把机器强制到机械臂模式(见 AppContext::setWashRunning),否则 Arm_enable=0,
// 末端那条俯仰摆动指令车端不会理
updateGating(); updateGating();
// 启停灯立刻跟着变,不等下一帧遥测——否则车端不回话时按钮看着像没反应
updateLeds();
} }
void WashPage::onStop() void WashPage::onStop()
{ {
ctx()->comm()->sendWashStop(); ctx()->comm()->sendWashStop();
ctx()->washRunning = false; ctx()->setWashRunning(false);
updateGating(); updateGating();
updateLeds();
} }
void WashPage::onBack() void WashPage::onBack()
{ {
if (ctx()->washRunning) { if (ctx()->isWashRunning()) {
ConfirmDialog::toast(this, "请先停止"); ConfirmDialog::toast(this, "请先停止");
return; return;
} }
@@ -298,7 +303,7 @@ void WashPage::resetFactory()
void WashPage::updateGating() void WashPage::updateGating()
{ {
bool running = ctx()->washRunning; bool running = ctx()->isWashRunning();
m_start->setEnabled(!running); m_start->setEnabled(!running);
m_stop->setEnabled(running); m_stop->setEnabled(running);
m_back->setEnabled(!running); m_back->setEnabled(!running);
@@ -312,58 +317,56 @@ void WashPage::refresh()
else m_banner->clear(); else m_banner->clear();
// 以泵状态同步运行状态 // 以泵状态同步运行状态
if (t.plunger || t.air || t.leaf) ctx()->washRunning = true; if (t.plunger || t.air || t.leaf) ctx()->setWashRunning(true);
else if (t.washMode == "--") ctx()->washRunning = false; else if (t.washMode == "--") ctx()->setWashRunning(false);
updateGating(); updateGating();
m_chassisCard->setField(0, QString("%1%").arg(t.soc)); // 未收到对应报文的数值统一显示 "--"(dashIf)
m_chassisCard->setField(1, QString("%1V").arg(t.volt, 0, 'f', 0)); m_chassisCard->setField(0, dashIf(t.hasBattery, QString("%1%").arg(t.soc)));
m_chassisCard->setField(2, t.chassisFaults.isEmpty() ? "无" : t.chassisFaults.join(";")); 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(0, t.pos);
m_armCard->setField(1, t.pose); 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, QString("%1 ppb").arg(t.gasConc, 0, 'f', 0)); 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_fMode, t.washMode);
m_washCard->setField(m_fLevelA, QString("%1L").arg(t.levelA, 0, 'f', 0)); m_washCard->setField(m_fLevelA, dashIf(t.hasWash, 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_fLevelFoam, dashIf(t.hasWash, 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_fFlowA, dashIf(t.hasWash, 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_fFlowB, dashIf(t.hasWash, 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_fGasFlow, dashIf(t.hasWash, 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_fPress, dashIf(t.hasWashPress, 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_fAirPress, dashIf(t.hasWashPress, 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_fFoam, dashIf(t.hasWash, QString("%1").arg(t.foam, 0, 'f', 1)));
m_washCard->setAlarm(m_fLevelA, t.levelALow);
m_washCard->setAlarm(m_fLevelFoam, t.levelBLow);
m_washCard->setFaults(washCardFaults(t));
// 指示灯 updateLeds();
m_ledRun->setState(ctx()->washRunning ? LedIndicator::Green : LedIndicator::Off);
m_ledPlunger->setState(t.plunger ? LedIndicator::Green : LedIndicator::Off);
m_ledAir->setState(t.air ? LedIndicator::Green : LedIndicator::Off);
m_ledLeaf->setState(t.leaf ? LedIndicator::Green : LedIndicator::Off);
m_ledLevel->setState(t.levelA < 10 ? LedIndicator::RedBlink : LedIndicator::Off);
m_ledD1->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off);
m_ledD2->setState(t.d2open ? LedIndicator::Green : LedIndicator::Off);
m_ledFault->setState((!t.washFaults.isEmpty() || !t.pumpFaults.isEmpty())
? LedIndicator::RedBlink : LedIndicator::Off);
// 底部三个泵的数值 // 底部三个泵的数值
refreshPumpValues(); refreshPumpValues();
// 故障代码显示(静态更新,循环由 rotateFaults 驱动)
rotateFaults();
} }
void WashPage::rotateFaults() // 底栏指示灯。单独拆出来是为了让 onStart/onStop 能直接调它:
// 遥测还没回来时 refresh() 里的“以泵状态同步运行状态”会把刚置上的 washRunning 又抹掉,
// 所以启停的即时反馈不能靠整个 refresh()。
void WashPage::updateLeds()
{ {
const Telemetry& t = ctx()->telemetry; const Telemetry& t = ctx()->telemetry;
QStringList all = t.washFaults + t.pumpFaults;
if (all.isEmpty()) { m_ledRun->setState(ctx()->isWashRunning() ? LedIndicator::Green : LedIndicator::Off);
m_washCard->setField(m_fAirFault, ""); m_ledPlunger->setState(t.plunger ? LedIndicator::Green : LedIndicator::Off);
m_washCard->setField(m_fPlungerFault, ""); m_ledAir->setState(t.air ? LedIndicator::Green : LedIndicator::Off);
return; m_ledLeaf->setState(t.leaf ? LedIndicator::Green : LedIndicator::Off);
} m_ledD1->setState(t.d1open ? LedIndicator::Green : LedIndicator::Off);
m_faultIndex = m_faultIndex % all.size(); m_ledD2->setState(t.d2open ? LedIndicator::Green : LedIndicator::Off);
QString code = all[m_faultIndex]; // 液位低拆成 A / B 两路,各用各自的协议位(A_LiquidLevel_Low / B_LiquidLevel_Low);
m_washCard->setField(m_fAirFault, code); // 不再按 levelA<10 猜阈值,免得卡片液位行红闪、这颗灯却不亮
m_washCard->setField(m_fPlungerFault, code); m_ledLevelA->setState(t.levelALow ? LedIndicator::RedBlink : LedIndicator::Off);
m_faultIndex = (m_faultIndex + 1) % all.size(); m_ledLevelB->setState(t.levelBLow ? LedIndicator::RedBlink : LedIndicator::Off);
} }
+6 -9
View File
@@ -11,7 +11,6 @@ class ParamControl;
class QPushButton; class QPushButton;
class QLabel; class QLabel;
class QVBoxLayout; class QVBoxLayout;
class QTimer;
// ===================== 洗消页(⑨消除作业 / ⑩消毒作业 共用) ===================== // ===================== 洗消页(⑨消除作业 / ⑩消毒作业 共用) =====================
// 自包含页面:三列骨架、中央洗消卡、右侧启停与参数区、底部指示灯条都在本文件里。 // 自包含页面:三列骨架、中央洗消卡、右侧启停与参数区、底部指示灯条都在本文件里。
@@ -30,10 +29,10 @@ private slots:
void onBack(); void onBack();
void onFactory(); void onFactory();
void refresh(); void refresh();
void rotateFaults();
private: private:
void updateGating(); void updateGating();
void updateLeds(); // 底栏指示灯(启停 / 泵 / 液位低 / 球阀 / 系统故障)
void resetFactory(); void resetFactory();
void refreshPumpValues(); // 刷新底部三个泵的数值 void refreshPumpValues(); // 刷新底部三个泵的数值
QString washModeTag() const; // "clean" / "disinfect" QString washModeTag() const; // "clean" / "disinfect"
@@ -58,15 +57,17 @@ private:
ParamControl* m_foamPercent = nullptr; ParamControl* m_foamPercent = nullptr;
ParamControl* m_airFlow = nullptr; ParamControl* m_airFlow = nullptr;
// 底部指示灯 // 底部指示灯(第一行)
LedIndicator* m_ledRun = nullptr; LedIndicator* m_ledRun = nullptr;
LedIndicator* m_ledPlunger = nullptr; LedIndicator* m_ledPlunger = nullptr;
LedIndicator* m_ledAir = nullptr; LedIndicator* m_ledAir = nullptr;
LedIndicator* m_ledLeaf = nullptr; LedIndicator* m_ledLeaf = nullptr;
LedIndicator* m_ledLevel = nullptr;
LedIndicator* m_ledD1 = nullptr; LedIndicator* m_ledD1 = nullptr;
LedIndicator* m_ledD2 = 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; QLabel* m_valPlunger = nullptr;
@@ -77,10 +78,6 @@ private:
int m_fMode = -1, m_fLevelA = -1, m_fLevelFoam = -1; int m_fMode = -1, m_fLevelA = -1, m_fLevelFoam = -1;
int m_fFlowA = -1, m_fFlowB = -1, m_fGasFlow = -1; int m_fFlowA = -1, m_fFlowB = -1, m_fGasFlow = -1;
int m_fPress = -1, m_fAirPress = -1, m_fFoam = -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 #endif // WASHPAGE_H
View File
-1
View File
@@ -1 +0,0 @@
aa
Binary file not shown.
+59 -17
View File
@@ -2,12 +2,12 @@
"""车端 CAN 报文模拟器 / 上行抓包器(联调用,非工程代码)。 """车端 CAN 报文模拟器 / 上行抓包器(联调用,非工程代码)。
依据《洗消机器人遥控器通信协议20260810.xlsx》sheet RC(分类一/二/三,全部扩展帧、Intel 小端位序)。 依据《洗消机器人遥控器通信协议20260810.xlsx》sheet RC(分类一/二/三,全部扩展帧、Intel 小端位序)。
线格式:32 位帧头 0x5AA5FF00 + 32 位 canId + DLC(8) + 8 字节数据 + 校验和,共 18 字节, 线格式:帧头 0xAA + 32 位 canId + DLC(8) + 8 字节数据 + 校验和 + 帧尾 0xBB,共 16 字节,
多字节字段按小端排列,与 comm/udpcommunication.cpp 的 UdpCanFrame 一致。 多字节字段按小端排列,与 comm/car.cpp 的 CanFrame 一致。
用法示例: 用法示例:
# 向 UI 的 9001 端口周期发 VCU 反馈,看底盘页电量/电压/车速刷新 # 向 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) # 制造故障:MCU 码 61(电机缺相故障)+ 整系统通信异常(底盘BMS)
python can_sender.py --mcu-fault 61 --comm-fault 2 python can_sender.py --mcu-fault 61 --comm-fault 2
@@ -24,7 +24,8 @@ import socket
import struct import struct
import time import time
HDR = 0x5AA5FF00 HDR = 0xAA # 帧头
TAIL = 0xBB # 帧尾
# 分类一 遥控器控制指令(上行,UI → 车端) # 分类一 遥控器控制指令(上行,UI → 车端)
ID_CHASSIS_CMD = 0x18208081 ID_CHASSIS_CMD = 0x18208081
@@ -91,11 +92,14 @@ def get_signed(d, start, ln):
def checksum(can_id, dlc, d): 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): def pack(can_id, d):
return struct.pack("<IIB8BB", HDR, can_id, 8, *d, checksum(can_id, 8, d)) return struct.pack("<BI B 8B B B", HDR, can_id, 8, *d,
checksum(can_id, 8, d), TAIL)
def clamp(v, lo, hi): def clamp(v, lo, hi):
@@ -117,9 +121,31 @@ def fb_vehicle1(a):
def fb_vehicle2(a): def fb_vehicle2(a):
d = bytearray(8) d = bytearray(8)
set_bits(d, 22, 2, 1 if a.handbrake else 0) # Handbrake set_bits(d, 22, 2, 1 if a.handbrake else 0) # Handbrake
# Vehi_spd 16 位 0~500 × 0.01;其后字段整体后移 8 位 # Vehi_spd 协议表为 8 位 × 0.01(上限 2.55 km/h),与 comm/car.cpp 的读法一致;
set_bits(d, 24, 16, clamp(round(a.speed * 100), 0, 500)) # Vehi_spd # 早先按 16 位写、并把后续字段整体后移 8 位,会让 rdyStatus/通信故障码错位。
set_bits(d, 48, 8, a.comm_fault) # All_Comm_FaultCode set_bits(d, 24, 8, clamp(round(a.speed * 100), 0, 255)) # Vehi_spd
set_bits(d, 32, 2, a.rdy_status) # Vehi_RdyStatus
set_bits(d, 40, 8, a.comm_fault) # All_Comm_FaultCode
return d
def fb_vehicle3(a):
# 底盘状态3:46 个 EPS/制动故障位 + EPS_FaultCode,全 0 即无故障
d = bytearray(8)
set_bits(d, 48, 8, a.eps_fault) # EPS_FaultCode
return d
def fb_vehicle4(a):
d = bytearray(8)
set_bits(d, 0, 2, a.gear) # 0=N挡 1=D挡 2=R挡
set_bits(d, 2, 2, a.parking) # 0未驻车 1左电机 2右电机 3完全驻车
set_bits(d, 4, 2, a.awd) # 0二驱 1四驱 2四驱锁定
set_bits(d, 8, 16, a.steer) # Steering_Angle
# ALIGN 是状态字节:0x11=对中失败、0xEE/0x55=成功。给 0 会被判成“方向盘未对中”
set_bits(d, 24, 8, a.align) # Angle_alignment_status
set_bits(d, 32, 8, a.brake_r) # Brake_Right_Mot_fault
set_bits(d, 40, 8, a.brake_l) # Brake_Left_Mot_fault
return d return d
@@ -161,6 +187,8 @@ def fb_decontam1(a):
set_bits(d, 3, 1, 1 if a.plunger else 0) set_bits(d, 3, 1, 1 if a.plunger else 0)
set_bits(d, 4, 1, 1 if a.air else 0) set_bits(d, 4, 1, 1 if a.air else 0)
set_bits(d, 5, 1, 1 if a.leaf else 0) set_bits(d, 5, 1, 1 if a.leaf else 0)
set_bits(d, 6, 1, 1 if a.level_a_low else 0) # A_LiquidLevel_Low
set_bits(d, 7, 1, 1 if a.level_b_low else 0) # B_LiquidLevel_Low
set_bits(d, 8, 1, 1 if a.d1open else 0) # MBV_D1_Sta set_bits(d, 8, 1, 1 if a.d1open else 0) # MBV_D1_Sta
set_bits(d, 9, 1, 1 if a.d2open else 0) set_bits(d, 9, 1, 1 if a.d2open else 0)
set_bits(d, 10, 1, 1 if a.sys_fault else 0) set_bits(d, 10, 1, 1 if a.sys_fault else 0)
@@ -199,6 +227,7 @@ def fb_detector2(a):
FEEDBACK = [ FEEDBACK = [
(ID_VEHICLE_FB1, fb_vehicle1), (ID_VEHICLE_FB2, fb_vehicle2), (ID_VEHICLE_FB1, fb_vehicle1), (ID_VEHICLE_FB2, fb_vehicle2),
(ID_VEHICLE_FB3, fb_vehicle3), (ID_VEHICLE_FB4, fb_vehicle4),
(ID_ARM_FB1, fb_arm1), (ID_ARM_FB2, fb_arm2), (ID_ARM_FB1, fb_arm1), (ID_ARM_FB2, fb_arm2),
(ID_ARM_FB3, fb_arm3), (ID_ARM_FB4, fb_arm4), (ID_ARM_FB3, fb_arm3), (ID_ARM_FB4, fb_arm4),
(ID_DECONTAM_FB1, fb_decontam1), (ID_DECONTAM_FB2, fb_decontam2), (ID_DECONTAM_FB1, fb_decontam1), (ID_DECONTAM_FB2, fb_decontam2),
@@ -250,11 +279,11 @@ def listen(host, port, once):
while True: while True:
data, addr = sock.recvfrom(4096) data, addr = sock.recvfrom(4096)
off = 0 off = 0
while off + 18 <= len(data): while off + 16 <= len(data):
hdr, cid, dlc = struct.unpack_from("<IIB", data, off) hdr, cid, dlc = struct.unpack_from("<BIB", data, off)
if hdr == HDR and dlc == 8: if hdr == HDR and dlc == 8 and data[off + 15] == TAIL:
body = data[off + 9:off + 17] body = data[off + 6:off + 14]
cks = data[off + 17] cks = data[off + 14]
if cks == checksum(cid, dlc, body): if cks == checksum(cid, dlc, body):
txt = describe(cid, body) txt = describe(cid, body)
if txt: if txt:
@@ -262,7 +291,7 @@ def listen(host, port, once):
if once is None or last.get(cid) != body: if once is None or last.get(cid) != body:
last[cid] = bytes(body) last[cid] = bytes(body)
print("%-16s %s" % (NAMES.get(cid, hex(cid)), txt)) print("%-16s %s" % (NAMES.get(cid, hex(cid)), txt))
off += 18 off += 16
continue continue
off += 1 off += 1
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -272,7 +301,8 @@ def listen(host, port, once):
def main(): def main():
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
p.add_argument("--host", default="127.0.0.1") p.add_argument("--host", default="127.0.0.1")
p.add_argument("--port", type=int, default=9001, help="发送目标(UI 的 net/recv_port)") p.add_argument("--port", type=int, default=9000,
help="发送目标端口 = UI 的 kCanRxPort(默认 9000);--listen 抓上行时改 3001")
p.add_argument("--listen", action="store_true", help="改为绑端口抓 UI 上行控制帧") p.add_argument("--listen", action="store_true", help="改为绑端口抓 UI 上行控制帧")
p.add_argument("--hz", type=float, default=10.0, help="反馈帧发送频率,默认 10Hz") p.add_argument("--hz", type=float, default=10.0, help="反馈帧发送频率,默认 10Hz")
p.add_argument("--count", type=int, default=0, help="0=一直发") p.add_argument("--count", type=int, default=0, help="0=一直发")
@@ -280,13 +310,23 @@ def main():
# 电池 / 底盘 # 电池 / 底盘
p.add_argument("--soc", type=float, default=85.0, help="电量 %") p.add_argument("--soc", type=float, default=85.0, help="电量 %")
p.add_argument("--volt", type=float, default=360.0, help="总电压 V(精度 0.1)") p.add_argument("--volt", type=float, default=360.0, help="总电压 V(精度 0.1)")
p.add_argument("--speed", type=float, default=3.45, help="km/h,精度 0.01") p.add_argument("--speed", type=float, default=2.0, help="km/h,精度 0.01,协议只给 8 位(上限 2.55)")
p.add_argument("--no-handbrake", action="store_true") p.add_argument("--no-handbrake", action="store_true")
p.add_argument("--rdy-status", type=int, default=2, help="底盘 ready:0初始/1上电完成/2就绪")
p.add_argument("--mcu-fault", type=int, default=0) p.add_argument("--mcu-fault", type=int, default=0)
p.add_argument("--mcu-level", type=int, default=0) p.add_argument("--mcu-level", type=int, default=0)
p.add_argument("--celllow-level", type=int, default=0) p.add_argument("--celllow-level", type=int, default=0)
p.add_argument("--cellhigh-level", type=int, default=0) p.add_argument("--cellhigh-level", type=int, default=0)
p.add_argument("--comm-fault", type=int, default=0, help="整系统通信故障码 0~8") p.add_argument("--comm-fault", type=int, default=0, help="整系统通信故障码 0~8")
# 底盘状态3/4
p.add_argument("--eps-fault", type=int, default=0, help="EPS 故障码,0=无")
p.add_argument("--gear", type=int, default=0, help="0=N挡 1=D挡 2=R挡")
p.add_argument("--parking", type=int, default=0, help="0未驻车 1左电机 2右电机 3完全驻车")
p.add_argument("--awd", type=int, default=0, help="0二驱 1四驱 2四驱锁定")
p.add_argument("--steer", type=int, default=0, help="方向盘角度")
p.add_argument("--align", type=int, default=0x55, help="0x11对中失败 / 0xEE、0x55成功;别给 0")
p.add_argument("--brake-r", type=int, default=0, help="右制动电机故障码")
p.add_argument("--brake-l", type=int, default=0, help="左制动电机故障码")
# 机械臂 # 机械臂
p.add_argument("--posx", type=int, default=1200) p.add_argument("--posx", type=int, default=1200)
p.add_argument("--posy", type=int, default=300) p.add_argument("--posy", type=int, default=300)
@@ -306,6 +346,8 @@ def main():
p.add_argument("--d1open", action="store_true") p.add_argument("--d1open", action="store_true")
p.add_argument("--d2open", action="store_true") p.add_argument("--d2open", action="store_true")
p.add_argument("--sys-fault", action="store_true") p.add_argument("--sys-fault", action="store_true")
p.add_argument("--level-a-low", action="store_true", help="A组分/清水液位低 → 首页液位行红闪")
p.add_argument("--level-b-low", action="store_true", help="B组分/泡沫液位低 → 首页液位行红闪")
p.add_argument("--level-a", type=float, default=100) p.add_argument("--level-a", type=float, default=100)
p.add_argument("--level-foam", type=float, default=30) p.add_argument("--level-foam", type=float, default=30)
p.add_argument("--flow-a", type=float, default=20) p.add_argument("--flow-a", type=float, default=20)
+13 -3
View File
@@ -79,13 +79,15 @@ bool ConfirmDialog::confirm(QWidget* parent, const QString& title, const QString
return dlg.exec() == QDialog::Accepted; return dlg.exec() == QDialog::Accepted;
} }
void ConfirmDialog::message(QWidget* parent, const QString& title, const QString& text) void ConfirmDialog::message(QWidget* parent, const QString& title, const QString& text,
int width, int fontPx)
{ {
ConfirmDialog dlg(parent); ConfirmDialog dlg(parent);
dlg.m_panel->setFixedSize(900, 600); // 正文行数少 → 面板收窄
dlg.m_cancel->hide(); // 只保留“确认” dlg.m_cancel->hide(); // 只保留“确认”
dlg.m_title->setStyleSheet(QString("background:transparent;color:%1;font-size:50px;font-weight:700;").arg(C::TEXT)); 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->setWordWrap(true);
dlg.m_title->setContentsMargins(0, 20, 0, 0); // 标题文字距该行顶部 10px dlg.m_title->setContentsMargins(0, 20, 0, 0); // 标题文字距该行顶部 10px
dlg.m_body->setContentsMargins(0, 0, 0, 0); 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.setTitle(title);
dlg.setText(text); 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(); dlg.exec();
} }
+6 -2
View File
@@ -20,8 +20,12 @@ public:
// 模态确认,返回是否“确认” // 模态确认,返回是否“确认”
static bool confirm(QWidget* parent, const QString& title, const QString& text); static bool confirm(QWidget* parent, const QString& title, const QString& text);
// 独立信息弹窗(仅“确认”按钮,替代系统 QMessageBox;正文 50px) // 独立信息弹窗(仅“确认”按钮,替代系统 QMessageBox)。
static void message(QWidget* parent, const QString& title, const QString& text); // width = 面板宽度,fontPx = **正文字号**(标题固定 50px);正文按宽度换行、高度按内容算。
// 两个参数都是**各调用点各给各的**——以前写死共用,一处的长文案会把别处的弹窗一起撑宽,
// 想让它窄一点就只能靠截断,现在可以靠缩字号。
static void message(QWidget* parent, const QString& title, const QString& text,
int width = 1200, int fontPx = 50);
// 轻提示:显示 text,ms 毫秒后自动消失(用于“修改成功”等) // 轻提示:显示 text,ms 毫秒后自动消失(用于“修改成功”等)
static void toast(QWidget* parent, const QString& text, int ms = 2000); static void toast(QWidget* parent, const QString& text, int ms = 2000);
+194 -12
View File
@@ -2,9 +2,40 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include <QTimer>
#include <QFontMetrics>
#include <QEvent>
#include "global.h" #include "global.h"
#include <QStyle> #include <QStyle>
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) DataCard::DataCard(const QString& title, QWidget* parent)
: QWidget(parent) : QWidget(parent)
{ {
@@ -23,7 +54,11 @@ DataCard::DataCard(const QString& title, QWidget* parent)
.arg(C::TEXT, C::BORDER)); .arg(C::TEXT, C::BORDER));
lay->addWidget(m_title); lay->addWidget(m_title);
// 字段行容器 // 故障行:构造时就建好,永远占着最后一行——有无故障都不改变卡片布局。
ensureFaultRow();
// 末尾弹性:各行长到自己的上限后,剩下的高度全归它。
// 顺序因此固定为 [标题][数值行…][故障行][弹性],addField 插在倒数第二项之前。
lay->addStretch(1); lay->addStretch(1);
} }
@@ -41,18 +76,23 @@ int DataCard::addField(const QString& label, const QString& value, bool big)
QLabel* lab = new QLabel(label + ":", this); QLabel* lab = new QLabel(label + ":", this);
lab->setProperty("sub", true); 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); QLabel* val = new QLabel(value, this);
val->setStyleSheet(QString("color:%1;%2").arg(C::TEXT, val->setProperty("cardValue", true);
big ? "font-size:50px;font-weight:700;" : "font-size:42px;font-weight:700;")); val->setStyleSheet(QString("font-size:%1px;font-weight:700;").arg(big ? 50 : m_valueFontPx));
row->addWidget(lab); row->addWidget(lab);
row->addWidget(val, 1); row->addWidget(val, 1);
// 插入到标题栏之后(stretch 之前) // 插到故障行之前(末尾两项固定是 [故障行][弹性]);
// stretch = 1 让行在有富余高度时长到 kRowMaxH 为止,再高就不长了
capRow(row, kRowMaxH);
QVBoxLayout* lay = qobject_cast<QVBoxLayout*>(layout()); QVBoxLayout* lay = qobject_cast<QVBoxLayout*>(layout());
lay->insertLayout(lay->count() - 1, row); lay->insertLayout(lay->count() - 2, row, 1);
m_values.append(val); m_values.append(val);
m_labels.append(label); m_labels.append(label);
@@ -71,17 +111,159 @@ void DataCard::setAlarm(int index, bool on)
if (index < 0 || index >= m_values.size()) if (index < 0 || index >= m_values.size())
return; return;
QLabel* val = m_values[index]; QLabel* val = m_values[index];
if (on) { if (val->property("alert").toBool() == on)
val->setProperty("alert", true); return; // 每次遥测刷新都会调进来,状态没变就别重算样式
val->setProperty("alert", on);
if (on)
startBlink(val, "blink"); startBlink(val, "blink");
} else { else
stopBlink(val, "blink"); stopBlink(val, "blink");
val->setProperty("alert", false); // 置属性不会自动重算样式,不补这一次重算,红字要等下一次 blink 翻转(最多 500ms)才出现
val->style()->unpolish(val); val->style()->unpolish(val);
val->style()->polish(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<QVBoxLayout*>(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) void DataCard::setHighlight(bool on)
{ {
if (on) if (on)
+38 -2
View File
@@ -1,13 +1,21 @@
#ifndef DATACARD_H #ifndef DATACARD_H
#define DATACARD_H #define DATACARD_H
#include <QWidget> #include <QString>
#include <QStringList>
#include <QVector> #include <QVector>
#include <QWidget>
class QLabel; class QLabel;
class QHBoxLayout;
class QTimer;
class QEvent;
// ===================== 数据卡片 ===================== // ===================== 数据卡片 =====================
// 标题栏 + 若干“标签:值”行;支持指定字段红色闪烁告警 // 标题栏 + 若干“标签:值”行 + 一条常驻的“故障”行。
// 每行有最大高度、长到上限就停,多出来的高度归末尾弹性,所以有无故障都不改变布局;
// 字段行支持红色闪烁告警;故障行没有标签、字号 kFaultFontPx,有故障时红闪、
// 多条按 1s 轮流显示,文字比整行宽时自动滑动,无故障时内容为空(行仍占位)。
class DataCard : public QWidget class DataCard : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -18,13 +26,41 @@ public:
int addField(const QString& label, const QString& value = "--", bool big = false); int addField(const QString& label, const QString& value = "--", bool big = false);
void setField(int index, const QString& value); void setField(int index, const QString& value);
void setAlarm(int index, bool on); 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 setHighlight(bool on); // 主色描边突出
void indentRows(); // 字段行整体缩进到「中间偏左」(行首 : 行尾 = 2 : 3),只需调用一次 void indentRows(); // 字段行整体缩进到「中间偏左」(行首 : 行尾 = 2 : 3),只需调用一次
protected:
bool eventFilter(QObject* obj, QEvent* ev) override; // 盯故障数值标签的宽度变化
private slots:
void rotateFault();
void tickFaultScroll(); // 故障文字滑动一拍
private: private:
void ensureFaultRow();
void applyFaultText(); // 设文字、量宽度、决定要不要滑动
void updateFaultScroll(); // 放得下就停着显示,放不下才开滑动
void positionFaultLabel(); // 按当前偏移摆放故障标签
QLabel* m_title = nullptr; 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<QLabel*> m_values; QVector<QLabel*> m_values;
QVector<QString> m_labels; QVector<QString> m_labels;
int m_valueFontPx = 42; // 数值行字号;big 字段固定 50,不受它影响
}; };
#endif // DATACARD_H #endif // DATACARD_H
+16 -16
View File
@@ -26,21 +26,17 @@ LedIndicator::LedIndicator(const QString& text, QWidget* parent)
void LedIndicator::setState(State s) void LedIndicator::setState(State s)
{ {
m_state = s; m_state = s;
bool needBlink = (s == RedBlink || s == YellowBlink); const bool needBlink = (s == RedBlink || s == YellowBlink);
if (needBlink && !m_blink) { if (needBlink && !m_blinkConn) {
m_blink = new QTimer(this); // 挂全局节拍,而不是本控件自己起定时器:各自起的话相位互不相同,
m_blink->setInterval(500); // 同一屏上的几个指示灯(顶部通信状态、底栏、自检结果…)会各闪各的。
connect(m_blink, &QTimer::timeout, this, [this]() { m_blinkConn = connect(BlinkBeat::instance(), &BlinkBeat::toggled, this,
m_on = !m_on; [this](bool on) { m_on = on; update(); });
update(); m_on = BlinkBeat::instance()->on(); // 立刻对齐当前相位,别等下一拍
}); } else if (!needBlink && m_blinkConn) {
m_blink->start(); disconnect(m_blinkConn);
m_on = true; m_blinkConn = QMetaObject::Connection();
} else if (!needBlink && m_blink) { m_on = true; // 停闪=常亮
m_blink->stop();
delete m_blink;
m_blink = nullptr;
m_on = true;
} }
update(); update();
} }
@@ -103,7 +99,11 @@ void LedIndicator::paintEvent(QPaintEvent*)
p.drawEllipse(c, m_radius, m_radius); 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, const QRectF textRect(m_dotAfter ? 0 : textLeft(), 0,
m_dotAfter ? textW : width() - textLeft() - 10, height()); m_dotAfter ? textW : width() - textLeft() - 10, height());
p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, m_text); p.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, m_text);
+8 -2
View File
@@ -1,8 +1,9 @@
#ifndef LEDINDICATOR_H #ifndef LEDINDICATOR_H
#define LEDINDICATOR_H #define LEDINDICATOR_H
#include <QColor>
#include <QMetaObject>
#include <QWidget> #include <QWidget>
#include <QTimer>
// ===================== 指示灯(自绘圆形渐变 + 外发光) ===================== // ===================== 指示灯(自绘圆形渐变 + 外发光) =====================
// 状态:灰=未运行/关闭/正常,绿=运行/开启/到位,红=故障/液位低, // 状态:灰=未运行/关闭/正常,绿=运行/开启/到位,红=故障/液位低,
@@ -20,6 +21,9 @@ public:
void setTextPixelSize(int px) { m_textPx = px; updateGeometry(); update(); } // 文字字号 void setTextPixelSize(int px) { m_textPx = px; updateGeometry(); update(); } // 文字字号
void setRadius(int r) { m_radius = r; updateGeometry(); update(); } // 圆形指示点半径 void setRadius(int r) { m_radius = r; updateGeometry(); update(); } // 圆形指示点半径
void setDotAfterText(bool after) { m_dotAfter = after; 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; } State state() const { return m_state; }
QSize sizeHint() const override; // 宽度按文字实际宽度计算,避免文字被裁 QSize sizeHint() const override; // 宽度按文字实际宽度计算,避免文字被裁
@@ -32,11 +36,13 @@ private:
State m_state = Off; State m_state = Off;
QString m_text; QString m_text;
QTimer* m_blink = nullptr; QMetaObject::Connection m_blinkConn; // 订阅全局闪烁节拍(见 BlinkBeat)
bool m_on = true; bool m_on = true;
int m_radius = 16; // 圆形指示点 32×32 int m_radius = 16; // 圆形指示点 32×32
int m_textPx = 24; // 文字字号 int m_textPx = 24; // 文字字号
bool m_dotAfter = false; // false=圆点在文字前,true=圆点在文字后 bool m_dotAfter = false; // false=圆点在文字前,true=圆点在文字后
QColor m_textColor; // 无效色 = 用默认 C::TEXT
bool m_blinkText = false; // true=文字随闪烁明暗,false=文字常亮
}; };
#endif // LEDINDICATOR_H #endif // LEDINDICATOR_H
-39
View File
@@ -18,32 +18,8 @@ StatusStrip::StatusStrip(AppContext* ctx, QWidget* parent)
lay->setContentsMargins(36, 0, 24, 0); lay->setContentsMargins(36, 0, 24, 0);
lay->setSpacing(40); 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); 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 = new QPushButton("返回", this);
m_back->setFixedSize(200, 80); m_back->setFixedSize(200, 80);
@@ -52,11 +28,6 @@ StatusStrip::StatusStrip(AppContext* ctx, QWidget* parent)
lay->addWidget(m_back); lay->addWidget(m_back);
connect(m_ctx->comm(), &CarCommunication::telemetryUpdated, this, &StatusStrip::refresh); 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(); refresh();
} }
@@ -70,17 +41,7 @@ void StatusStrip::setBackEnabled(bool enabled)
m_back->setEnabled(enabled); m_back->setEnabled(enabled);
} }
void StatusStrip::setModeVisible(bool visible)
{
m_mode->setVisible(visible);
}
void StatusStrip::refresh() void StatusStrip::refresh()
{ {
// 底部已不显示遥测数据,保留空实现(仍连着 telemetryUpdated,方便以后加显示项) // 底部已不显示遥测数据,保留空实现(仍连着 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));
} }
+1 -7
View File
@@ -8,7 +8,7 @@ class QPushButton;
class AppContext; class AppContext;
// ===================== 底部状态条 ===================== // ===================== 底部状态条 =====================
// 只保留:模式徽标(仅显示)+ 返回按钮。行车/机械臂由遥控器拨钮决定,界面不参与切换。 // 只保留「返回」按钮。行车/机械臂模式徽标已撤——底部不再展示模式。
class StatusStrip : public QWidget class StatusStrip : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -17,7 +17,6 @@ public:
void setBackVisible(bool visible); void setBackVisible(bool visible);
void setBackEnabled(bool enabled); void setBackEnabled(bool enabled);
void setModeVisible(bool visible); // 固定行车模式的页面(如底盘页)可隐藏模式徽标
signals: signals:
void backClicked(); void backClicked();
@@ -27,11 +26,6 @@ private slots:
private: private:
AppContext* m_ctx; 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; QPushButton* m_back = nullptr;
}; };
+68 -26
View File
@@ -2,10 +2,22 @@
#include "ledindicator.h" #include "ledindicator.h"
#include "appcontext.h" #include "appcontext.h"
#include "car.h" #include "car.h"
#include <QGridLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLabel> #include <QLabel>
#include "global.h" #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) TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
: QWidget(parent), m_ctx(ctx) : 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;}") setStyleSheet(QString("QWidget#TitleBar{background-color:%1;border-bottom:1px solid %2;}")
.arg(C::PANEL, C::BORDER)); .arg(C::PANEL, C::BORDER));
// 左侧区(车载端通信状态) // 左侧区(车载端通信状态,固定一行,不轮播)
QWidget* leftBox = new QWidget(this); QWidget* leftBox = new QWidget(this);
QHBoxLayout* leftLay = new QHBoxLayout(leftBox); QHBoxLayout* leftLay = new QHBoxLayout(leftBox);
leftLay->setContentsMargins(260, 0, 0, 0); leftLay->setSpacing(0); // 只有指示器 + 弹簧,留间距只会白白吃掉文字宽度
leftLay->setSpacing(20); m_comm = new LedIndicator("系统通信正常", leftBox);
m_comm = new LedIndicator("通信正常", leftBox); // 高度沿用 LedIndicator 默认的 52:43px 字与 48px 直径的圆点外发光刚好容得下
m_comm->setFixedHeight(80); // 容纳放大后的文字,避免上下裁切 m_comm->setTextPixelSize(45); // 与右侧“遥控器正常”同字号
m_comm->setTextPixelSize(45); m_comm->setBlinkText(true); // 异常时文字跟着圆点一起闪
m_comm->setState(LedIndicator::Green); 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); leftLay->addStretch(1);
// 右侧区(遥控器状态:链路 + 电量) // 右侧区(遥控器状态:链路 + 电量)
@@ -37,35 +51,37 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
m_rc->setTextPixelSize(45); m_rc->setTextPixelSize(45);
m_rc->setState(LedIndicator::RedBlink); m_rc->setState(LedIndicator::RedBlink);
rightLay->addWidget(m_rc); rightLay->addWidget(m_rc);
m_rcBat = new QLabel("--%", rightBox); m_rcBat = new QLabel(rightBox);
m_rcBat->setStyleSheet(QString("color:%1;font-size:60px;").arg(C::TEXT_SUB));
m_rcBat->setAlignment(Qt::AlignCenter); m_rcBat->setAlignment(Qt::AlignCenter);
rightLay->addWidget(m_rcBat); rightLay->addWidget(m_rcBat);
setRcBattery(-1);
// 左右两侧等宽 → 中间标题恰好居中于屏幕 // 左右两侧区等宽(宽度见 kSideW:左区要放得下最长的一条异常)
// 左区需 260(左边距)+ 240(圆点区与留白 60 + 文字 4×45)= 500 leftBox->setFixedWidth(kSideW);
// 右区需 285(圆点区与留白 60 + 文字 5×45)+ 30 + 130(电量)+ 20 = 465 rightBox->setFixedWidth(kSideW);
const int sideW = 520;
leftBox->setFixedWidth(sideW);
rightBox->setFixedWidth(sideW);
// 标题(居中) // 标题(居中)
m_title = new QLabel("", this); m_title = new QLabel("", this);
m_title->setAlignment(Qt::AlignCenter); m_title->setAlignment(Qt::AlignCenter);
m_title->setStyleSheet(QString("color:%1;font-size:60px;font-weight:700;").arg(C::TEXT)); 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->setContentsMargins(24, 6, 24, 6);
lay->setSpacing(0); lay->setSpacing(0);
lay->addWidget(leftBox); lay->addWidget(leftBox, 0, 0, Qt::AlignLeft | Qt::AlignVCenter);
lay->addWidget(m_title, 1); lay->addWidget(rightBox, 0, 2, Qt::AlignRight | Qt::AlignVCenter);
lay->addWidget(rightBox); lay->addWidget(m_title, 0, 0, 1, 3, Qt::AlignCenter);
// 车载端通信状态刷新 // 车载端通信状态:只在文案变化时由通信层通知,这里不做任何轮播。
connect(m_ctx->comm(), &CarCommunication::commOkChanged, this, [this](bool ok) { connect(m_ctx->comm(), &CarCommunication::commTextChanged,
if (ok) { m_comm->setText("通信正常"); m_comm->setState(LedIndicator::Green); } this, &TitleBar::onCommTextChanged);
else { m_comm->setText("通信异常"); m_comm->setState(LedIndicator::RedBlink); }
}); // 每个页面各有自己的顶条实例,构造时通信层可能早就报过状态了;而 commTextChanged
// 只在内容变化时才发,所以这里必须先取一次当前值,否则新页面的顶条会一直停在构造时的初值。
updateComm();
// 遥控器链路与电量刷新 // 遥控器链路与电量刷新
connect(m_ctx->rc(), &RcCommunication::linkChanged, this, &TitleBar::onRcLink); connect(m_ctx->rc(), &RcCommunication::linkChanged, this, &TitleBar::onRcLink);
@@ -73,6 +89,22 @@ TitleBar::TitleBar(AppContext* ctx, QWidget* parent)
onRcLink(m_ctx->rc()->linkOk()); 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) void TitleBar::setTitle(const QString& title)
{ {
m_title->setText(title); m_title->setText(title);
@@ -86,11 +118,21 @@ void TitleBar::onRcLink(bool ok)
} else { } else {
m_rc->setText("遥控器断开"); m_rc->setText("遥控器断开");
m_rc->setState(LedIndicator::RedBlink); 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) void TitleBar::onRcFrame(const RcFrame& f)
{ {
m_rcBat->setText(QString("%1%").arg(f.battery)); setRcBattery(f.battery);
} }
+10 -1
View File
@@ -9,7 +9,9 @@ class AppContext;
class LedIndicator; class LedIndicator;
// ===================== 顶条组件(高 98px) ===================== // ===================== 顶条组件(高 98px) =====================
// 左侧“车载端通信:●正常/异常”,中央页面标题,右侧“遥控器:●正常/断开 + 电量” // 左侧“车载端通信:●系统通信正常/xx通信异常”,中央页面标题,右侧“遥控器:●正常/断开 + 电量”。
// 左区**固定**显示通信状态一行,不再轮播故障明细——各项子系统的故障明细分别进对应卡片的
// “故障”行(见 DataCard::setFaults)。文案由 CarCommunication 渲染进 Telemetry::commText。
class TitleBar : public QWidget class TitleBar : public QWidget
{ {
Q_OBJECT Q_OBJECT
@@ -21,8 +23,15 @@ public:
private slots: private slots:
void onRcLink(bool ok); void onRcLink(bool ok);
void onRcFrame(const RcFrame& f); void onRcFrame(const RcFrame& f);
void onCommTextChanged();
private: private:
// 按 Telemetry 的通信状态刷新左区指示灯
void updateComm();
// 电量文字与配色:>20% 绿色,≤20% 红色,无数据时用次文字色
void setRcBattery(int battery);
AppContext* m_ctx; AppContext* m_ctx;
LedIndicator* m_comm = nullptr; LedIndicator* m_comm = nullptr;
LedIndicator* m_rc = nullptr; LedIndicator* m_rc = nullptr;