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