diff --git a/App/BagThreadPosition/BagThreadPositionApp/mainwindow.cpp b/App/BagThreadPosition/BagThreadPositionApp/mainwindow.cpp index 7ace51c..03f8b28 100644 --- a/App/BagThreadPosition/BagThreadPositionApp/mainwindow.cpp +++ b/App/BagThreadPosition/BagThreadPositionApp/mainwindow.cpp @@ -83,9 +83,8 @@ MainWindow::MainWindow(QWidget *parent) QGraphicsScene* scene = new QGraphicsScene(this); ui->detect_image->setScene(scene); - // 初始化日志模型 - m_logModel = new QStringListModel(this); - ui->detect_log->setModel(m_logModel); + // 初始化日志辅助类 + m_logHelper = new DetectLogHelper(ui->detect_log, this); // 注册自定义类型,使其能够在信号槽中跨线程传递 qRegisterMetaType("DetectionResult"); @@ -98,12 +97,6 @@ MainWindow::MainWindow(QWidget *parent) // 连接检测结果更新信号槽 connect(this, &MainWindow::detectionResultUpdateRequested, this, &MainWindow::updateDetectionResultDisplay); - // 连接日志更新信号槽 - connect(this, &MainWindow::logUpdateRequested, this, &MainWindow::updateDetectionLog); - - // 连接清空日志信号槽 - connect(this, &MainWindow::logClearRequested, this, &MainWindow::clearDetectionLogUI); - // 初始化右键菜单 setupContextMenu(); @@ -129,14 +122,18 @@ MainWindow::~MainWindow() void MainWindow::updateStatusLog(const QString& message) { - // 通过信号槽机制更新detect_log控件 - emit logUpdateRequested(message); + // 使用日志辅助类更新日志 + if (m_logHelper) { + m_logHelper->appendLog(message); + } } void MainWindow::clearDetectionLog() { - // 通过信号槽机制清空日志 - emit logClearRequested(); + // 使用日志辅助类清空日志 + if (m_logHelper) { + m_logHelper->clearLog(); + } } void MainWindow::Init() @@ -369,65 +366,13 @@ void MainWindow::updateWorkStatusLabel(WorkStatus status) void MainWindow::updateDetectionResultDisplay(const DetectionResult& result) { // 显示检测结果图像 - updateDetectionLog(tr("检测结果更新")); + updateStatusLog(tr("检测结果更新")); displayImage(result.image); // 更新检测结果到列表 addDetectionResult(result); } -void MainWindow::updateDetectionLog(const QString& message) -{ - // 在UI线程中更新detect_log控件(QListView) - if (!m_logModel) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 检查是否与最后一条消息相同 - if (message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - - // 替换最后一条 - logList[logList.size() - 1] = logEntry; - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2").arg(timestamp).arg(message); - - // 添加新的日志条目 - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - ui->detect_log->scrollTo(lastIndex); -} - -void MainWindow::clearDetectionLogUI() -{ - // 在UI线程中清空检测日志 - if (m_logModel) { - m_logModel->setStringList(QStringList()); - } - - // 重置日志计数器 - m_lastLogMessage.clear(); - m_lastLogCount = 0; -} - void MainWindow::on_btn_start_clicked() { // 检查Presenter是否已初始化 diff --git a/App/BagThreadPosition/BagThreadPositionApp/mainwindow.h b/App/BagThreadPosition/BagThreadPositionApp/mainwindow.h index 15169ed..eafcf88 100644 --- a/App/BagThreadPosition/BagThreadPositionApp/mainwindow.h +++ b/App/BagThreadPosition/BagThreadPositionApp/mainwindow.h @@ -16,6 +16,7 @@ #include "dialogalgoarg.h" #include "CommonDialogCameraLevel.h" #include "DeviceStatusWidget.h" +#include "DetectLogHelper.h" #include "BagThreadPositionPresenter.h" QT_BEGIN_NAMESPACE @@ -51,12 +52,6 @@ signals: // 检测结果更新信号 void detectionResultUpdateRequested(const DetectionResult& result); - // 日志更新信号 - void logUpdateRequested(const QString& message); - - // 清空日志信号 - void logClearRequested(); - private slots: // 工作状态更新槽函数 @@ -65,12 +60,6 @@ private slots: // 检测结果更新槽函数 void updateDetectionResultDisplay(const DetectionResult& result); - // 日志更新槽函数 - void updateDetectionLog(const QString& message); - - // 清空日志槽函数 - void clearDetectionLogUI(); - // 处理相机点击事件 void onCameraClicked(int cameraIndex); @@ -103,8 +92,8 @@ private: // 业务逻辑处理类 BagThreadPositionPresenter* m_presenter = nullptr; - // 日志模型 - QStringListModel* m_logModel = nullptr; + // 日志辅助类 + DetectLogHelper* m_logHelper = nullptr; // 设备状态显示widget的引用 DeviceStatusWidget* m_deviceStatusWidget = nullptr; @@ -134,10 +123,5 @@ private: void setupContextMenu(); void showContextMenu(const QPoint& pos, QGraphicsView* view); bool saveDetectionDataToFile(const QString& filePath, int cameraIndex); - -private: - // 日志去重相关 - QString m_lastLogMessage; // 最后一条日志消息(不含时间戳) - int m_lastLogCount = 0; // 最后一条日志的重复次数 }; #endif // MAINWINDOW_H diff --git a/App/GrabBag/GrabBagApp/Presenter/Inc/BolunteProtocol.h b/App/GrabBag/GrabBagApp/Presenter/Inc/BolunteProtocol.h index c1464d8..28f2f06 100644 --- a/App/GrabBag/GrabBagApp/Presenter/Inc/BolunteProtocol.h +++ b/App/GrabBag/GrabBagApp/Presenter/Inc/BolunteProtocol.h @@ -40,9 +40,10 @@ public: /** * @brief 发送坐标数据 * @param multiTargetData 多目标数据 + * @param errorCode 检测错误码,0表示成功,-2201表示空垛,其他表示错误 * @return 0-成功,其他-错误码 */ - int SendCoordinateData(const MultiTargetData& multiTargetData); + int SendCoordinateData(const MultiTargetData& multiTargetData, int errorCode = 0); /** * @brief 获取连接状态 @@ -73,9 +74,16 @@ public: * @param dsID 附加命令的dsID * @param address 附加命令的地址 * @param successValue 检测成功时的值 - * @param failValue 检测失败时的值 + * @param emptyValue 检测结果空时的值 + * @param startValue 开启检测/检测失败时的值 */ - void SetPostCoordinateParams(const std::string& dsID, int address, int successValue, int failValue); + void SetPostCoordinateParams(const std::string& dsID, int address, int successValue, int emptyValue, int startValue); + + /** + * @brief 发送开启检测/检测失败附加指令 + * @return 0-成功,其他-错误码 + */ + int SendStartDetectCommand(); /** * @brief 设置第二个坐标的Z轴偏移量 @@ -138,7 +146,8 @@ private: std::string m_strPostCoordDsID; // 坐标发送后附加命令的dsID int m_nPostCoordAddress; // 坐标发送后附加命令的地址 int m_nPostCoordSuccessValue; // 检测成功时的值 - int m_nPostCoordFailValue; // 检测失败时的值 + int m_nPostCoordEmptyValue; // 检测结果空时的值 + int m_nPostCoordStartValue; // 开启检测/检测失败时的值 double m_dZOffset; // 第二个坐标Z轴偏移量 double m_dZMin; // Z轴最小值 double m_dZMax; // Z轴最大值 diff --git a/App/GrabBag/GrabBagApp/Presenter/Inc/DetectPresenter.h b/App/GrabBag/GrabBagApp/Presenter/Inc/DetectPresenter.h index a94a417..ce2d8bb 100644 --- a/App/GrabBag/GrabBagApp/Presenter/Inc/DetectPresenter.h +++ b/App/GrabBag/GrabBagApp/Presenter/Inc/DetectPresenter.h @@ -25,16 +25,17 @@ public: int DetectBag( std::vector& detectionDataCache, - const SG_bagPositionParam& algoParam, - const SSG_planeCalibPara& cameraCalibParam, + const SG_bagPositionParam& algoParam, + const SSG_planeCalibPara& cameraCalibParam, const VrDebugParam& debugParam, LaserDataLoader& dataLoader, const double clibMatrix[16], - DetectionResult& detectionResult); - + DetectionResult& detectionResult, + double planeHeightOffset = -10.0); + int DetectBag( std::vector& detectionDataCache, - const SG_bagPositionParam& algoParam, - const SSG_planeCalibPara& cameraCalibParam, + const SG_bagPositionParam& algoParam, + const SSG_planeCalibPara& cameraCalibParam, const VrDebugParam& debugParam, LaserDataLoader& dataLoader, const double clibMatrix[16], @@ -42,15 +43,16 @@ public: const RGB& rgbColorPattern, const double frontColorTemplate[RGN_HIST_SIZE], const double backColorTemplate[RGN_HIST_SIZE], - DetectionResult& detectionResult); + DetectionResult& detectionResult, + double planeHeightOffset = -10.0); + - // 新增:处理统一数据格式和项目类型的DetectBag函数 int DetectBag( int cameraIndex, std::vector>& laserLines, ProjectType projectType, - const SG_bagPositionParam& algoParam, - const SSG_planeCalibPara& cameraCalibParam, + const SG_bagPositionParam& algoParam, + const SSG_planeCalibPara& cameraCalibParam, const VrDebugParam& debugParam, LaserDataLoader& dataLoader, const double clibMatrix[16], @@ -58,7 +60,8 @@ public: const RGB& rgbColorPattern, const double frontColorTemplate[RGN_HIST_SIZE], const double backColorTemplate[RGN_HIST_SIZE], - DetectionResult& detectionResult); + DetectionResult& detectionResult, + double planeHeightOffset = -10.0); }; diff --git a/App/GrabBag/GrabBagApp/Presenter/Inc/GrabBagPresenter.h b/App/GrabBag/GrabBagApp/Presenter/Inc/GrabBagPresenter.h index 2d03709..8a03c4f 100644 --- a/App/GrabBag/GrabBagApp/Presenter/Inc/GrabBagPresenter.h +++ b/App/GrabBag/GrabBagApp/Presenter/Inc/GrabBagPresenter.h @@ -36,6 +36,7 @@ struct CurrentExecutionParams int cameraIndex; // 相机序号 VrCameraParams cameraParam; // 相机参数 SSG_planeCalibPara planeCalibParam; // 调平参数 + double planeHeightOffset = -10.0; // 平面高度偏移量 CalibMatrix handEyeCalibMatrix; // 手眼标定参数 VrBagParam bagParam; // 包裹参数(尺寸) const TCPClient* pClient = nullptr; // 请求的客户端指针 diff --git a/App/GrabBag/GrabBagApp/Presenter/Src/BolunteProtocol.cpp b/App/GrabBag/GrabBagApp/Presenter/Src/BolunteProtocol.cpp index 0ee5abe..a77bc89 100644 --- a/App/GrabBag/GrabBagApp/Presenter/Src/BolunteProtocol.cpp +++ b/App/GrabBag/GrabBagApp/Presenter/Src/BolunteProtocol.cpp @@ -2,6 +2,7 @@ #include "json/json.h" #include "VrLog.h" #include "VrError.h" +#include "SG_errCode.h" #include #include #include @@ -15,7 +16,8 @@ BolunteProtocol::BolunteProtocol() , m_strPostCoordDsID("www.hc-system.com.RemoteMonitor") , m_nPostCoordAddress(800) , m_nPostCoordSuccessValue(10) - , m_nPostCoordFailValue(12) + , m_nPostCoordEmptyValue(12) + , m_nPostCoordStartValue(13) , m_dZOffset(300.0) , m_dZMin(-1500.0) , m_dZMax(2500.0) @@ -120,7 +122,7 @@ int BolunteProtocol::SendPhotoResult(const std::string& packID, int camID, bool } } -int BolunteProtocol::SendCoordinateData(const MultiTargetData& multiTargetData) +int BolunteProtocol::SendCoordinateData(const MultiTargetData& multiTargetData, int errorCode) { if (!m_bConnected) { @@ -128,46 +130,102 @@ int BolunteProtocol::SendCoordinateData(const MultiTargetData& multiTargetData) return ERR_CODE(NET_ERR_NOTINIT); } - std::string jsonStr = GenerateCoordinateJson(multiTargetData); - bool ret = m_pTcpServer->SendAllData(jsonStr.c_str(), jsonStr.length()); + bool hasTargets = !multiTargetData.targets.empty(); + + // 根据检测结果和错误码确定返回值 + // 1. 正常(有结果):发送坐标 + 附加指令 10 + // 2. 空垛(errorCode == -2201):只发送附加指令 12 + // 3. 错误(相机开启失败、没有结果且不是空垛):只发送附加指令 13 + int resultValue; + if (hasTargets) { + resultValue = m_nPostCoordSuccessValue; // 10 + } else if (SX_BAG_TRAY_EMPTY == errorCode) { + resultValue = m_nPostCoordEmptyValue; // 12 + } else { + resultValue = m_nPostCoordStartValue; // 13 + } + + // 正常情况:发送坐标数据 + if (hasTargets) { + std::string jsonStr = GenerateCoordinateJson(multiTargetData); + bool ret = m_pTcpServer->SendAllData(jsonStr.c_str(), jsonStr.length()); + if (!ret) { + LOG_ERROR("Failed to send coordinate data\n"); + return ERR_CODE(NET_ERR_SEND_DATA); + } + LOG_INFO("Sent coordinate data: count=%d\n", multiTargetData.count); + } + + // 发送附加命令 + if (!m_strPostCoordDsID.empty()) + { + if (hasTargets) { + // 正常情况:延迟100ms后发送附加指令 + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // 生成附加命令JSON + VA::Json::Value postCmd; + postCmd["dsID"] = m_strPostCoordDsID; + postCmd["reqType"] = "command"; + + VA::Json::Value cmdData(VA::Json::arrayValue); + cmdData.append("rewriteData"); + cmdData.append(std::to_string(m_nPostCoordAddress)); + cmdData.append(std::to_string(resultValue)); + cmdData.append("0"); + postCmd["cmdData"] = cmdData; + + VA::Json::FastWriter writer; + std::string postCmdStr = writer.write(postCmd); + + m_pTcpServer->SendAllData(postCmdStr.c_str(), postCmdStr.length()); + LOG_INFO("Sent post coordinate command: hasTargets=%d, errorCode=%d, resultValue=%d\n", hasTargets, errorCode, resultValue); + } + + return SUCCESS; +} + +int BolunteProtocol::SendStartDetectCommand() +{ + if (!m_bConnected) + { + LOG_ERROR("TCP server has no connected client\n"); + return ERR_CODE(NET_ERR_NOTINIT); + } + + // 如果附加指令dsID为空则不发送 + if (m_strPostCoordDsID.empty()) + { + LOG_INFO("PostCoordinateDsID is empty, skip sending start detect command\n"); + return SUCCESS; + } + + // 生成开启检测附加命令JSON + VA::Json::Value postCmd; + postCmd["dsID"] = m_strPostCoordDsID; + postCmd["reqType"] = "command"; + + VA::Json::Value cmdData(VA::Json::arrayValue); + cmdData.append("rewriteData"); + cmdData.append(std::to_string(m_nPostCoordAddress)); + cmdData.append(std::to_string(m_nPostCoordStartValue)); + cmdData.append("0"); + postCmd["cmdData"] = cmdData; + + VA::Json::FastWriter writer; + std::string postCmdStr = writer.write(postCmd); + + bool ret = m_pTcpServer->SendAllData(postCmdStr.c_str(), postCmdStr.length()); if (ret) { - LOG_INFO("Sent coordinate data: count=%d\n", multiTargetData.count); - - // 100ms后发送附加命令,根据检测结果拼接成功/失败值 - if (!m_strPostCoordDsID.empty()) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // 根据检测结果选择成功/失败值 - bool detectionSuccess = !multiTargetData.targets.empty(); - int resultValue = detectionSuccess ? m_nPostCoordSuccessValue : m_nPostCoordFailValue; - - // 动态生成附加命令JSON - VA::Json::Value postCmd; - postCmd["dsID"] = m_strPostCoordDsID; - postCmd["reqType"] = "command"; - - VA::Json::Value cmdData(VA::Json::arrayValue); - cmdData.append("rewriteData"); - cmdData.append(std::to_string(m_nPostCoordAddress)); - cmdData.append(std::to_string(resultValue)); - cmdData.append("0"); - postCmd["cmdData"] = cmdData; - - VA::Json::FastWriter writer; - std::string postCmdStr = writer.write(postCmd); - - m_pTcpServer->SendAllData(postCmdStr.c_str(), postCmdStr.length()); - LOG_INFO("Sent post coordinate command: success=%d, value=%d\n", detectionSuccess, resultValue); - } - + LOG_INFO("Sent start detect command: value=%d\n", m_nPostCoordStartValue); return SUCCESS; } else { - LOG_ERROR("Failed to send coordinate data\n"); + LOG_ERROR("Failed to send start detect command\n"); return ERR_CODE(NET_ERR_SEND_DATA); } } @@ -192,12 +250,13 @@ void BolunteProtocol::SetDsID(const std::string& dsID) m_strDsID = dsID; } -void BolunteProtocol::SetPostCoordinateParams(const std::string& dsID, int address, int successValue, int failValue) +void BolunteProtocol::SetPostCoordinateParams(const std::string& dsID, int address, int successValue, int emptyValue, int startValue) { m_strPostCoordDsID = dsID; m_nPostCoordAddress = address; m_nPostCoordSuccessValue = successValue; - m_nPostCoordFailValue = failValue; + m_nPostCoordEmptyValue = emptyValue; + m_nPostCoordStartValue = startValue; } void BolunteProtocol::SetZOffset(double zOffset) diff --git a/App/GrabBag/GrabBagApp/Presenter/Src/DetectPresenter.cpp b/App/GrabBag/GrabBagApp/Presenter/Src/DetectPresenter.cpp index c4d093d..f790340 100644 --- a/App/GrabBag/GrabBagApp/Presenter/Src/DetectPresenter.cpp +++ b/App/GrabBag/GrabBagApp/Presenter/Src/DetectPresenter.cpp @@ -11,12 +11,13 @@ DetectPresenter::~DetectPresenter() } int DetectPresenter::DetectBag(std::vector& detectionDataCache, - const SG_bagPositionParam& algoParam, - const SSG_planeCalibPara& cameraCalibParam, + const SG_bagPositionParam& algoParam, + const SSG_planeCalibPara& cameraCalibParam, const VrDebugParam& debugParam, LaserDataLoader& dataLoader, const double clibMatrix[16], - DetectionResult& detectionResult) + DetectionResult& detectionResult, + double planeHeightOffset) { if (detectionDataCache.empty()) { LOG_WARNING("No cached detection data available\n"); @@ -25,7 +26,7 @@ int DetectPresenter::DetectBag(std::vector& detectionDataCache // 2. 数据预处理:调平和去除地面(使用当前相机的调平参数) for (size_t i = 0; i < detectionDataCache.size(); i++) { - sg_lineDataR(&detectionDataCache[i], cameraCalibParam.planeCalib, cameraCalibParam.planeHeight); + sg_lineDataR(&detectionDataCache[i], cameraCalibParam.planeCalib, cameraCalibParam.planeHeight + planeHeightOffset); } // 3. 调用算法检测接口(使用当前相机的调平参数) @@ -77,10 +78,8 @@ int DetectPresenter::DetectBag(std::vector& detectionDataCache detectionResult.positions.push_back(pos); if(debugParam.enableDebug && debugParam.printDetailLog){ - LOG_INFO("[Algo Thread] Object %zu Eye Coords: X=%.2f, Y=%.2f, Z=%.2f\n", - i, obj.centerPos.x, obj.centerPos.y, obj.centerPos.z); - LOG_INFO("[Algo Thread] Object %zu Robot Coords: X=%.2f, Y=%.2f, Z=%.2f, Roll=%.2f, Pitch=%.2f, Yaw=%.2f\n", - i, pos.x, pos.y, pos.z, pos.roll, pos.pitch, pos.yaw); + LOG_INFO("[Algo Thread] Object %zu Eye Coords: X=%.2f, Y=%.2f, Z=%.2f\n", i, obj.centerPos.x, obj.centerPos.y, obj.centerPos.z); + LOG_INFO("[Algo Thread] Object %zu Robot Coords: X=%.2f, Y=%.2f, Z=%.2f, Roll=%.2f, Pitch=%.2f, Yaw=%.2f\n", i, pos.x, pos.y, pos.z, pos.roll, pos.pitch, pos.yaw); } } @@ -90,8 +89,8 @@ int DetectPresenter::DetectBag(std::vector& detectionDataCache int DetectPresenter::DetectBag(std::vector& detectionDataCache, - const SG_bagPositionParam& algoParam, - const SSG_planeCalibPara& cameraCalibParam, + const SG_bagPositionParam& algoParam, + const SSG_planeCalibPara& cameraCalibParam, const VrDebugParam& debugParam, LaserDataLoader& dataLoader, const double clibMatrix[16], @@ -99,7 +98,8 @@ int DetectPresenter::DetectBag(std::vector& detectionData const RGB& rgbColorPattern, const double frontColorTemplate[RGN_HIST_SIZE], const double backColorTemplate[RGN_HIST_SIZE], - DetectionResult& detectionResult) + DetectionResult& detectionResult, + double planeHeightOffset) { if (detectionDataCache.empty()) { @@ -109,7 +109,7 @@ int DetectPresenter::DetectBag(std::vector& detectionData // 2. 数据预处理:调平和去除地面(使用当前相机的调平参数) for (size_t i = 0; i < detectionDataCache.size(); i++) { - sg_lineDataR_RGBD(&detectionDataCache[i], cameraCalibParam.planeCalib, cameraCalibParam.planeHeight); + sg_lineDataR_RGBD(&detectionDataCache[i], cameraCalibParam.planeCalib, cameraCalibParam.planeHeight + planeHeightOffset); } @@ -183,8 +183,8 @@ int DetectPresenter::DetectBag( int cameraIndex, std::vector>& laserLines, ProjectType projectType, - const SG_bagPositionParam& algoParam, - const SSG_planeCalibPara& cameraCalibParam, + const SG_bagPositionParam& algoParam, + const SSG_planeCalibPara& cameraCalibParam, const VrDebugParam& debugParam, LaserDataLoader& dataLoader, const double clibMatrix[16], @@ -192,7 +192,8 @@ int DetectPresenter::DetectBag( const RGB& rgbColorPattern, const double frontColorTemplate[RGN_HIST_SIZE], const double backColorTemplate[RGN_HIST_SIZE], - DetectionResult& detectionResult) + DetectionResult& detectionResult, + double planeHeightOffset) { if (laserLines.empty()) { LOG_WARNING("No laser lines data available\n"); @@ -234,7 +235,7 @@ int DetectPresenter::DetectBag( algoParam.growParam.yDeviation_max, algoParam.growParam.zDeviation_max, algoParam.growParam.maxLineSkipNum, algoParam.growParam.maxSkipDistance); - LOG_INFO(" Plane height: %.3f\n", cameraCalibParam.planeHeight); + LOG_INFO(" Plane height: %.3f offset: %.3f\n", cameraCalibParam.planeHeight, planeHeightOffset); LOG_INFO(" Plane calibration matrix: [%.3f, %.3f, %.3f; %.3f, %.3f, %.3f; %.3f, %.3f, %.3f]\n", cameraCalibParam.planeCalib[0], cameraCalibParam.planeCalib[1], cameraCalibParam.planeCalib[2], cameraCalibParam.planeCalib[3], cameraCalibParam.planeCalib[4], cameraCalibParam.planeCalib[5], @@ -248,7 +249,8 @@ int DetectPresenter::DetectBag( std::vector rgbdData; int convertResult = dataLoader.ConvertToSVzNLXYZRGBDLaserLine(laserLines, rgbdData); if (convertResult == SUCCESS && !rgbdData.empty()) { - nRet = DetectBag(rgbdData, algoParam, cameraCalibParam, debugParam, dataLoader, clibMatrix, hsvCmpParam, rgbColorPattern, frontColorTemplate, backColorTemplate, detectionResult); + nRet = DetectBag(rgbdData, algoParam, cameraCalibParam, debugParam, dataLoader, clibMatrix, hsvCmpParam, rgbColorPattern, frontColorTemplate, + backColorTemplate, detectionResult, planeHeightOffset); // 清理RGBD内存 dataLoader.FreeConvertedData(rgbdData); @@ -263,7 +265,7 @@ int DetectPresenter::DetectBag( std::vector xyzData; int convertResult = dataLoader.ConvertToSVzNL3DLaserLine(laserLines, xyzData); if (convertResult == SUCCESS && !xyzData.empty()) { - nRet = DetectBag(xyzData, algoParam, cameraCalibParam, debugParam, dataLoader, clibMatrix, detectionResult); + nRet = DetectBag(xyzData, algoParam, cameraCalibParam, debugParam, dataLoader, clibMatrix, detectionResult, planeHeightOffset); // 清理XYZ内存 dataLoader.FreeConvertedData(xyzData); diff --git a/App/GrabBag/GrabBagApp/Presenter/Src/GrabBagPresenter.cpp b/App/GrabBag/GrabBagApp/Presenter/Src/GrabBagPresenter.cpp index 34d9d08..97d7f41 100644 --- a/App/GrabBag/GrabBagApp/Presenter/Src/GrabBagPresenter.cpp +++ b/App/GrabBag/GrabBagApp/Presenter/Src/GrabBagPresenter.cpp @@ -452,7 +452,8 @@ int GrabBagPresenter::InitBolunteProtocol() configResult.bolunteConfig.postCoordinateDsID, configResult.bolunteConfig.postCoordinateAddress, configResult.bolunteConfig.postCoordinateSuccessValue, - configResult.bolunteConfig.postCoordinateFailValue); + configResult.bolunteConfig.postCoordinateEmptyValue, + configResult.bolunteConfig.postCoordinateStartValue); // 设置连接状态回调 m_pBolunteProtocol->SetConnectionCallback([this](bool connected) { @@ -1546,7 +1547,8 @@ int GrabBagPresenter::_DetectTask() m_pParameterManager->GetHsvCmpParam(), m_pParameterManager->GetRgbColorPattern(), m_pParameterManager->GetFrontColorTemplate(), - m_pParameterManager->GetBackColorTemplate(), detectionResult); + m_pParameterManager->GetBackColorTemplate(), detectionResult, + m_currentExecutionParams.planeHeightOffset); LOG_INFO("[Algo Thread] DetectBag return: %d\n", nRet); } catch (const std::exception& e) { LOG_ERROR("[Algo Thread] Exception in DetectBag: %s\n", e.what()); @@ -1567,10 +1569,6 @@ int GrabBagPresenter::_DetectTask() m_pStatus->OnStatusUpdate(QString("检测%1").arg(SUCCESS == nRet ? "成功": err).toStdString()); } - if(SUCCESS != nRet){ - ERR_CODE_RETURN(nRet); - } - LOG_INFO("[Algo Thread] algo detected %zu objects time : %.2f ms\n", detectionResult.positions.size(), oTimeUtils.GetElapsedTimeInMilliSec()); // 8. 返回检测结果 @@ -1588,7 +1586,6 @@ int GrabBagPresenter::_DetectTask() // 更新机械臂协议状态(发送转换后的目标位置数据) _SendDetectionResultToRobot(detectionResult, m_currentExecutionParams.cameraIndex); - // 设置机械臂工作状态为相应相机工作完成(相机ID从1开始) if (m_pRobotProtocol) { uint16_t workStatus = (m_currentExecutionParams.cameraIndex == 1) ? RobotProtocol::WORK_STATUS_CAMERA1_DONE : RobotProtocol::WORK_STATUS_CAMERA2_DONE; @@ -1734,12 +1731,11 @@ void GrabBagPresenter::_SendDetectionResultToRobot(const DetectionResult& detect // 设置相机ID multiTargetData.cameraId = cameraIndex; - // 发送坐标数据 - int result = m_pBolunteProtocol->SendCoordinateData(multiTargetData); + // 发送坐标数据,传递错误码以区分正常/空垛/错误 + int result = m_pBolunteProtocol->SendCoordinateData(multiTargetData, detectionResult.errorCode); if (result == SUCCESS) { - LOG_INFO("[Bolunte] SendCoordinateData successful for camera: %d, targets: %d\n", - cameraIndex, multiTargetData.count); + LOG_INFO("[Bolunte] SendCoordinateData successful for camera: %d, targets: %d\n", cameraIndex, multiTargetData.count); if (m_pStatus) { m_pStatus->OnStatusUpdate(QString("机械臂坐标发送成功").toStdString()); } @@ -1982,6 +1978,10 @@ bool GrabBagPresenter::OnBoluntePhotoCommand(int camID) if (result != SUCCESS) { LOG_ERROR("Detection failed for camera %d, error: %d\n", cameraIndex, result); SetWorkStatus(WorkStatus::Error); + // 开启检测失败,发送开启检测失败附加指令 + if (m_pBolunteProtocol && m_bBolunteConnected) { + m_pBolunteProtocol->SendStartDetectCommand(); + } return false; } @@ -2282,6 +2282,7 @@ void GrabBagPresenter::_UpdateCurrentExecutionParams() m_currentExecutionParams.planeCalibParam.invRMatrix[i] = identityMatrix9[i]; } m_currentExecutionParams.planeCalibParam.planeHeight = -1.0; + m_currentExecutionParams.planeHeightOffset = -10.0; // 默认高度偏移 // 3. 初始化手眼标定矩阵为单位矩阵(默认值) double identityMatrix16[16] = { @@ -2357,6 +2358,7 @@ void GrabBagPresenter::_UpdateCurrentExecutionParams() m_currentExecutionParams.planeCalibParam.invRMatrix[i] = currentCamera->planeCalibParam.invRMatrix[i]; } m_currentExecutionParams.planeCalibParam.planeHeight = currentCamera->planeCalibParam.planeHeight; + m_currentExecutionParams.planeHeightOffset = currentCamera->planeCalibParam.planeHeightOffset; LOG_INFO("Using calibrated plane parameters from camera %s (height=%.3f)\n", currentCamera->cameraName.c_str(), currentCamera->planeCalibParam.planeHeight); } else { LOG_INFO("Camera %s is not calibrated, using identity matrix for plane calibration\n", currentCamera->cameraName.c_str()); @@ -2403,6 +2405,7 @@ void GrabBagPresenter::_UpdateCurrentExecutionParams(const WorkPositionConfig* w m_currentExecutionParams.planeCalibParam.invRMatrix[i] = identityMatrix9[i]; } m_currentExecutionParams.planeCalibParam.planeHeight = -1.0; + m_currentExecutionParams.planeHeightOffset = -10.0; // 默认高度偏移 // 3. 初始化手眼标定矩阵为单位矩阵(默认值) double identityMatrix16[16] = { @@ -2450,11 +2453,11 @@ void GrabBagPresenter::_UpdateCurrentExecutionParams(const WorkPositionConfig* w m_currentExecutionParams.planeCalibParam.invRMatrix[i] = camera->planeCalibParam.invRMatrix[i]; } m_currentExecutionParams.planeCalibParam.planeHeight = camera->planeCalibParam.planeHeight; + m_currentExecutionParams.planeHeightOffset = camera->planeCalibParam.planeHeightOffset; LOG_INFO("Using calibrated plane parameters from camera %s (height=%.3f)\n", camera->cameraName.c_str(), camera->planeCalibParam.planeHeight); } else { - LOG_INFO("Camera %s is not calibrated, using identity matrix for plane calibration\n", - camera->cameraName.c_str()); + LOG_INFO("Camera %s is not calibrated, using identity matrix for plane calibration\n", camera->cameraName.c_str()); } // 6. 设置手眼标定参数(从相机配置中获取) @@ -2466,8 +2469,7 @@ void GrabBagPresenter::_UpdateCurrentExecutionParams(const WorkPositionConfig* w } LOG_INFO("Using calibrated hand-eye matrix from camera %s\n", camera->cameraName.c_str()); } else { - LOG_INFO("Camera %s hand-eye is not calibrated, using identity matrix\n", - camera->cameraName.c_str()); + LOG_INFO("Camera %s hand-eye is not calibrated, using identity matrix\n", camera->cameraName.c_str()); } LOG_INFO("Current execution parameters updated successfully\n"); diff --git a/App/GrabBag/GrabBagApp/Version.h b/App/GrabBag/GrabBagApp/Version.h index da416bf..767e7cb 100644 --- a/App/GrabBag/GrabBagApp/Version.h +++ b/App/GrabBag/GrabBagApp/Version.h @@ -2,7 +2,7 @@ #define VERSION_H #define GRABBAG_VERSION_STRING "1.3.5" -#define GRABBAG_BUILD_STRING "5" +#define GRABBAG_BUILD_STRING "7" #define GRABBAG_FULL_VERSION_STRING "V" GRABBAG_VERSION_STRING "_" GRABBAG_BUILD_STRING // 获取版本信息的便捷函数 diff --git a/App/GrabBag/GrabBagApp/Version.md b/App/GrabBag/GrabBagApp/Version.md index b4a5714..352d999 100644 --- a/App/GrabBag/GrabBagApp/Version.md +++ b/App/GrabBag/GrabBagApp/Version.md @@ -1,8 +1,10 @@ # 1.3.5 +## build_6 & 7 20260128 +1. 修改协议附属命令结果增加失败回复13 + ## build_5 20260126 1. 修改协议附属命令结果 - ## build_4 20260124 1. 修复算法检测线程在析构时未退出的问题 diff --git a/App/GrabBag/GrabBagApp/dialogcamera.cpp b/App/GrabBag/GrabBagApp/dialogcamera.cpp index 0a9d8b0..9cabf57 100644 --- a/App/GrabBag/GrabBagApp/dialogcamera.cpp +++ b/App/GrabBag/GrabBagApp/dialogcamera.cpp @@ -392,7 +392,8 @@ void DialogCamera::LoadBolunteConfig() ui->lineEdit_bolunte_postcmd->setText(QString::fromStdString(configResult->bolunteConfig.postCoordinateDsID)); ui->spinBox_bolunte_postaddr->setValue(configResult->bolunteConfig.postCoordinateAddress); ui->spinBox_bolunte_postsuccess->setValue(configResult->bolunteConfig.postCoordinateSuccessValue); - ui->spinBox_bolunte_postfail->setValue(configResult->bolunteConfig.postCoordinateFailValue); + ui->spinBox_bolunte_postempty->setValue(configResult->bolunteConfig.postCoordinateEmptyValue); + ui->spinBox_bolunte_poststart->setValue(configResult->bolunteConfig.postCoordinateStartValue); LOG_INFO("Loaded Bolunte config: port=%d, zOffset=%.1f\n", configResult->bolunteConfig.port, @@ -424,7 +425,8 @@ bool DialogCamera::SaveBolunteConfig() configResult->bolunteConfig.postCoordinateDsID = ui->lineEdit_bolunte_postcmd->text().toStdString(); configResult->bolunteConfig.postCoordinateAddress = ui->spinBox_bolunte_postaddr->value(); configResult->bolunteConfig.postCoordinateSuccessValue = ui->spinBox_bolunte_postsuccess->value(); - configResult->bolunteConfig.postCoordinateFailValue = ui->spinBox_bolunte_postfail->value(); + configResult->bolunteConfig.postCoordinateEmptyValue = ui->spinBox_bolunte_postempty->value(); + configResult->bolunteConfig.postCoordinateStartValue = ui->spinBox_bolunte_poststart->value(); LOG_INFO("Saving Bolunte config: port=%d, zOffset=%.1f\n", configResult->bolunteConfig.port, diff --git a/App/GrabBag/GrabBagApp/dialogcamera.ui b/App/GrabBag/GrabBagApp/dialogcamera.ui index 278f2ca..4675d3b 100644 --- a/App/GrabBag/GrabBagApp/dialogcamera.ui +++ b/App/GrabBag/GrabBagApp/dialogcamera.ui @@ -7,7 +7,7 @@ 0 0 660 - 650 + 690 @@ -226,7 +226,7 @@ QPushButton:disabled { 50 330 560 - 261 + 301 @@ -669,7 +669,7 @@ background-color: rgb(47, 48, 52); 10 - + 360 @@ -687,10 +687,10 @@ background-color: rgb(47, 48, 52); color: rgb(221, 225, 233); - 失败值: + 空值: - + 420 @@ -718,12 +718,61 @@ background-color: rgb(47, 48, 52); 12 + + + + 20 + 260 + 80 + 30 + + + + + 14 + + + + color: rgb(221, 225, 233); + + + 开启值: + + + + + + 110 + 260 + 80 + 30 + + + + + 14 + + + + color: rgb(221, 225, 233); +background-color: rgb(47, 48, 52); + + + 0 + + + 65535 + + + 13 + + 190 - 600 + 640 111 38 @@ -744,7 +793,7 @@ background-color: rgb(47, 48, 52); 340 - 600 + 640 111 38 diff --git a/App/GrabBag/GrabBagApp/dialogcameralevel.cpp b/App/GrabBag/GrabBagApp/dialogcameralevel.cpp index 77f2c45..a098451 100644 --- a/App/GrabBag/GrabBagApp/dialogcameralevel.cpp +++ b/App/GrabBag/GrabBagApp/dialogcameralevel.cpp @@ -987,6 +987,9 @@ void DialogCameraLevel::checkAndDisplayCalibrationStatus(int workPosIndex, int c .arg(cameraName)); ui->label_level_result->setAlignment(Qt::AlignCenter); } + + // 加载高度偏移值 + loadHeightOffset(workPosIndex, cameraIndex); } // 设置调平时的状态回调 @@ -1015,15 +1018,125 @@ void DialogCameraLevel::restorePresenterStatusCallback() LOG_DEBUG("Presenter status callback already restored, skipping\n"); return; } - + if (!m_presenter) { LOG_ERROR("Presenter is null, cannot restore status callback\n"); return; } - + // 恢复Presenter的状态回调 - 使用GrabBagPresenter的静态回调 m_presenter->SetCameraStatusCallback(&GrabBagPresenter::_StaticCameraNotify, m_presenter); - + LOG_INFO("Presenter status callback restored for all cameras\n"); } +// 加载高度偏移值 +void DialogCameraLevel::loadHeightOffset(int workPosIndex, int cameraIndex) +{ + if (!m_pConfigResult || !m_presenter) { + return; + } + + if (workPosIndex < 0 || workPosIndex >= static_cast(m_pConfigResult->workPositions.size())) { + return; + } + + if (cameraIndex < 0 || cameraIndex >= static_cast(m_cameraList.size())) { + return; + } + + const auto& workPos = m_pConfigResult->workPositions[workPosIndex]; + int configCameraIndex = cameraIndex + 1; // 转换为1-based索引 + + // 查找对应的相机配置 + for (const auto& cam : workPos.cameras) { + if (cam.cameraIndex == configCameraIndex) { + ui->doubleSpinBox_heightoffset->setValue(cam.planeCalibParam.planeHeightOffset); + LOG_INFO("Loaded height offset: %.2f for camera %d at work position %s\n", + cam.planeCalibParam.planeHeightOffset, configCameraIndex, + workPos.name.c_str()); + return; + } + } + + // 如果没找到,使用默认值 + ui->doubleSpinBox_heightoffset->setValue(-10.0); +} + +// 保存高度偏移值 +bool DialogCameraLevel::saveHeightOffset(int workPosIndex, int cameraIndex, double heightOffset) +{ + if (!m_pConfigResult || !m_presenter) { + LOG_ERROR("ConfigResult or Presenter is null\n"); + return false; + } + + if (workPosIndex < 0 || workPosIndex >= static_cast(m_pConfigResult->workPositions.size())) { + LOG_ERROR("Invalid work position index: %d\n", workPosIndex); + return false; + } + + if (cameraIndex < 0 || cameraIndex >= static_cast(m_cameraList.size())) { + LOG_ERROR("Invalid camera index: %d\n", cameraIndex); + return false; + } + + try { + IVrConfig* config = m_presenter->GetConfig(); + if (!config) { + LOG_ERROR("Config is null\n"); + return false; + } + + QString configPath = PathManager::GetInstance().GetConfigFilePath(); + ConfigResult configResult = config->LoadConfig(configPath.toStdString()); + + const auto& workPos = m_pConfigResult->workPositions[workPosIndex]; + int configCameraIndex = cameraIndex + 1; + + // 查找并更新对应的相机配置 + for (auto& wp : configResult.workPositions) { + if (wp.id == workPos.id) { + for (auto& cam : wp.cameras) { + if (cam.cameraIndex == configCameraIndex) { + cam.planeCalibParam.planeHeightOffset = heightOffset; + + // 保存配置 + bool saveResult = config->SaveConfig(configPath.toStdString(), configResult); + if (saveResult) { + LOG_INFO("Height offset saved: %.2f for camera %d at work position %s\n", + heightOffset, configCameraIndex, workPos.name.c_str()); + } + return saveResult; + } + } + break; + } + } + + LOG_ERROR("Camera config not found for camera index %d\n", configCameraIndex); + return false; + + } catch (const std::exception& e) { + LOG_ERROR("Exception in saveHeightOffset: %s\n", e.what()); + return false; + } +} + +// 保存高度偏移按钮点击 +void DialogCameraLevel::on_btn_save_heightoffset_clicked() +{ + if (m_currentWorkPosIndex < 0 || m_currentCameraIndex < 0) { + StyledMessageBox::warning(this, "错误", "请先选择工位和相机!"); + return; + } + + double heightOffset = ui->doubleSpinBox_heightoffset->value(); + + if (saveHeightOffset(m_currentWorkPosIndex, m_currentCameraIndex, heightOffset)) { + StyledMessageBox::information(this, "成功", "高度偏移已保存!"); + } else { + StyledMessageBox::warning(this, "失败", "保存高度偏移失败!"); + } +} + diff --git a/App/GrabBag/GrabBagApp/dialogcameralevel.h b/App/GrabBag/GrabBagApp/dialogcameralevel.h index 915b2d9..2988733 100644 --- a/App/GrabBag/GrabBagApp/dialogcameralevel.h +++ b/App/GrabBag/GrabBagApp/dialogcameralevel.h @@ -39,6 +39,7 @@ private slots: void on_btn_cancel_clicked(); void on_combo_camera_currentIndexChanged(int index); void on_combo_workposition_currentIndexChanged(int index); + void on_btn_save_heightoffset_clicked(); private: Ui::DialogCameraLevel *ui; @@ -115,6 +116,10 @@ private: // 检查并显示相机标定状态 void checkAndDisplayCalibrationStatus(int workPosIndex, int cameraIndex); + + // 加载和保存高度偏移 + void loadHeightOffset(int workPosIndex, int cameraIndex); + bool saveHeightOffset(int workPosIndex, int cameraIndex, double heightOffset); }; #endif // DialogCameraLevel_H diff --git a/App/GrabBag/GrabBagApp/dialogcameralevel.ui b/App/GrabBag/GrabBagApp/dialogcameralevel.ui index 5fbef5a..52505de 100644 --- a/App/GrabBag/GrabBagApp/dialogcameralevel.ui +++ b/App/GrabBag/GrabBagApp/dialogcameralevel.ui @@ -20,7 +20,7 @@ 20 - 150 + 190 111 31 @@ -37,6 +37,93 @@ 调平结果: + + + + 20 + 150 + 111 + 31 + + + + + 16 + + + + color: rgb(221, 225, 233); + + + 高度偏移: + + + + + + 140 + 150 + 120 + 31 + + + + + 14 + + + + color: rgb(221, 225, 233); +background-color: rgb(47, 48, 52); + + + -1000.000000000000000 + + + 1000.000000000000000 + + + -10.000000000000000 + + + mm + + + + + + 280 + 150 + 80 + 31 + + + + + 14 + + + + +QPushButton { + color: rgb(221, 225, 233); + background-color: rgb(0, 120, 212); + border: none; + border-radius: 3px; + padding: 5px 10px; +} +QPushButton:hover { + background-color: rgb(0, 140, 232); +} +QPushButton:pressed { + background-color: rgb(0, 100, 192); +} + + + + 保存 + + @@ -107,9 +194,9 @@ 140 - 150 + 190 501 - 241 + 201 diff --git a/App/GrabBag/GrabBagConfig/Inc/IVrConfig.h b/App/GrabBag/GrabBagConfig/Inc/IVrConfig.h index 63fd460..e66be3d 100644 --- a/App/GrabBag/GrabBagConfig/Inc/IVrConfig.h +++ b/App/GrabBag/GrabBagConfig/Inc/IVrConfig.h @@ -70,7 +70,8 @@ struct BolunteConfig std::string postCoordinateDsID = "www.hc-system.com.RemoteMonitor"; // 坐标发送后附加命令的dsID int postCoordinateAddress = 800; // 坐标发送后附加命令的地址 int postCoordinateSuccessValue = 10; // 检测成功时的值 - int postCoordinateFailValue = 12; // 检测失败时的值 + int postCoordinateEmptyValue = 12; // 检测结果空时的值 + int postCoordinateStartValue = 13; // 开启检测/检测失败时的值 }; /** diff --git a/App/GrabBag/GrabBagConfig/Src/VrConfig.cpp b/App/GrabBag/GrabBagConfig/Src/VrConfig.cpp index 216e28c..0e89d09 100644 --- a/App/GrabBag/GrabBagConfig/Src/VrConfig.cpp +++ b/App/GrabBag/GrabBagConfig/Src/VrConfig.cpp @@ -269,7 +269,9 @@ ConfigResult CVrConfig::LoadConfig(const std::string& filePath) cameraParam.isCalibrated = cameraCalibParamElement->BoolAttribute("isCalibrated"); if (cameraCalibParamElement->Attribute("planeHeight")) cameraParam.planeHeight = cameraCalibParamElement->DoubleAttribute("planeHeight"); - + if (cameraCalibParamElement->Attribute("planeHeightOffset")) + cameraParam.planeHeightOffset = cameraCalibParamElement->DoubleAttribute("planeHeightOffset"); + // 读取旋转矩阵planeCalib[9] (3x3矩阵) if (cameraCalibParamElement->Attribute("planeCalib_00")) cameraParam.planeCalib[0] = cameraCalibParamElement->DoubleAttribute("planeCalib_00"); @@ -494,10 +496,25 @@ ConfigResult CVrConfig::LoadConfig(const std::string& filePath) result.bolunteConfig.postCoordinateSuccessValue = postSuccessElement->IntText(); } - XMLElement* postFailElement = bolunteConfigElement->FirstChildElement("PostCoordinateFailValue"); - if (postFailElement) + XMLElement* postEmptyElement = bolunteConfigElement->FirstChildElement("PostCoordinateEmptyValue"); + if (postEmptyElement) { - result.bolunteConfig.postCoordinateFailValue = postFailElement->IntText(); + result.bolunteConfig.postCoordinateEmptyValue = postEmptyElement->IntText(); + } + else + { + // 兼容旧配置:如果没有 EmptyValue,尝试读取旧的 FailValue + XMLElement* postFailElement = bolunteConfigElement->FirstChildElement("PostCoordinateFailValue"); + if (postFailElement) + { + result.bolunteConfig.postCoordinateEmptyValue = postFailElement->IntText(); + } + } + + XMLElement* postStartElement = bolunteConfigElement->FirstChildElement("PostCoordinateStartValue"); + if (postStartElement) + { + result.bolunteConfig.postCoordinateStartValue = postStartElement->IntText(); } } @@ -649,7 +666,8 @@ bool CVrConfig::SaveConfig(const std::string& filePath, ConfigResult& configResu cameraCalibParamElement->SetAttribute("cameraName", cameraParam.cameraName.c_str()); cameraCalibParamElement->SetAttribute("isCalibrated", cameraParam.isCalibrated); cameraCalibParamElement->SetAttribute("planeHeight", cameraParam.planeHeight); - + cameraCalibParamElement->SetAttribute("planeHeightOffset", cameraParam.planeHeightOffset); + // 保存旋转矩阵planeCalib[9] (3x3矩阵) cameraCalibParamElement->SetAttribute("planeCalib_00", cameraParam.planeCalib[0]); cameraCalibParamElement->SetAttribute("planeCalib_01", cameraParam.planeCalib[1]); @@ -788,9 +806,13 @@ bool CVrConfig::SaveConfig(const std::string& filePath, ConfigResult& configResu boluntePostSuccessElement->SetText(configResult.bolunteConfig.postCoordinateSuccessValue); bolunteConfigElement->InsertEndChild(boluntePostSuccessElement); - XMLElement* boluntePostFailElement = doc.NewElement("PostCoordinateFailValue"); - boluntePostFailElement->SetText(configResult.bolunteConfig.postCoordinateFailValue); - bolunteConfigElement->InsertEndChild(boluntePostFailElement); + XMLElement* boluntePostEmptyElement = doc.NewElement("PostCoordinateEmptyValue"); + boluntePostEmptyElement->SetText(configResult.bolunteConfig.postCoordinateEmptyValue); + bolunteConfigElement->InsertEndChild(boluntePostEmptyElement); + + XMLElement* boluntePostStartElement = doc.NewElement("PostCoordinateStartValue"); + boluntePostStartElement->SetText(configResult.bolunteConfig.postCoordinateStartValue); + bolunteConfigElement->InsertEndChild(boluntePostStartElement); // 保存工作点配置(新增) SaveWorkPositions(doc, root, configResult.workPositions); diff --git a/App/GrabBag/GrabBagConfig/Src/VrConfigExt.cpp b/App/GrabBag/GrabBagConfig/Src/VrConfigExt.cpp index e1c570d..43453f4 100644 --- a/App/GrabBag/GrabBagConfig/Src/VrConfigExt.cpp +++ b/App/GrabBag/GrabBagConfig/Src/VrConfigExt.cpp @@ -135,6 +135,8 @@ void CVrConfig::ParseWorkPositions(XMLElement* workPositionsElement, ConfigResul cam.planeCalibParam.isCalibrated = planeCalibElement->BoolAttribute("isCalibrated"); if (planeCalibElement->Attribute("planeHeight")) cam.planeCalibParam.planeHeight = planeCalibElement->DoubleAttribute("planeHeight"); + if (planeCalibElement->Attribute("planeHeightOffset")) + cam.planeCalibParam.planeHeightOffset = planeCalibElement->DoubleAttribute("planeHeightOffset"); ParseMatrix3x3(planeCalibElement, "planeCalib", cam.planeCalibParam.planeCalib); ParseMatrix3x3(planeCalibElement, "invRMatrix", cam.planeCalibParam.invRMatrix); @@ -258,17 +260,20 @@ void CVrConfig::SaveWorkPositions(XMLDocument& doc, XMLElement* root, const std: if (!cam.defaultPackageId.empty()) cameraElement->SetAttribute("defaultPackageId", cam.defaultPackageId.c_str()); - // 保存平面校准参数 - if (cam.planeCalibParam.isCalibrated) + // 保存平面校准参数(即使未校准也保存planeHeightOffset) { XMLElement* planeCalibElement = doc.NewElement("PlaneCalib"); planeCalibElement->SetAttribute("cameraIndex", cam.planeCalibParam.cameraIndex); planeCalibElement->SetAttribute("cameraName", cam.planeCalibParam.cameraName.c_str()); planeCalibElement->SetAttribute("isCalibrated", cam.planeCalibParam.isCalibrated); planeCalibElement->SetAttribute("planeHeight", cam.planeCalibParam.planeHeight); + planeCalibElement->SetAttribute("planeHeightOffset", cam.planeCalibParam.planeHeightOffset); - SaveMatrix3x3(planeCalibElement, "planeCalib", cam.planeCalibParam.planeCalib); - SaveMatrix3x3(planeCalibElement, "invRMatrix", cam.planeCalibParam.invRMatrix); + if (cam.planeCalibParam.isCalibrated) + { + SaveMatrix3x3(planeCalibElement, "planeCalib", cam.planeCalibParam.planeCalib); + SaveMatrix3x3(planeCalibElement, "invRMatrix", cam.planeCalibParam.invRMatrix); + } cameraElement->InsertEndChild(planeCalibElement); } diff --git a/App/GrabBag/GrabBagConfig/config/config.xml b/App/GrabBag/GrabBagConfig/config/config.xml index e96fee6..988758b 100644 --- a/App/GrabBag/GrabBagConfig/config/config.xml +++ b/App/GrabBag/GrabBagConfig/config/config.xml @@ -279,6 +279,7 @@ www.hc-system.com.RemoteMonitor 800 10 - 12 + 12 + 13 \ No newline at end of file diff --git a/App/ParticleSize/ParticleSizeApp/mainwindow.cpp b/App/ParticleSize/ParticleSizeApp/mainwindow.cpp index 1f35a8e..c9a1c4a 100644 --- a/App/ParticleSize/ParticleSizeApp/mainwindow.cpp +++ b/App/ParticleSize/ParticleSizeApp/mainwindow.cpp @@ -96,10 +96,9 @@ MainWindow::MainWindow(QWidget *parent) // 为第二个图像视图初始化GraphicsScene QGraphicsScene* scene2 = new QGraphicsScene(this); ui->detect_image_2->setScene(scene2); - - // 初始化日志模型 - m_logModel = new QStringListModel(this); - ui->detect_log->setModel(m_logModel); + + // 初始化日志辅助类 + m_logHelper = new DetectLogHelper(ui->detect_log, this); // 注册自定义类型,使其能够在信号槽中跨线程传递 qRegisterMetaType("DetectionResult"); @@ -108,16 +107,10 @@ MainWindow::MainWindow(QWidget *parent) // 连接工作状态更新信号槽 connect(this, &MainWindow::workStatusUpdateRequested, this, &MainWindow::updateWorkStatusLabel); - + // 连接检测结果更新信号槽 connect(this, &MainWindow::detectionResultUpdateRequested, this, &MainWindow::updateDetectionResultDisplay); - - // 连接日志更新信号槽 - connect(this, &MainWindow::logUpdateRequested, this, &MainWindow::updateDetectionLog); - - // 连接清空日志信号槽 - connect(this, &MainWindow::logClearRequested, this, &MainWindow::clearDetectionLogUI); - + // 初始化右键菜单 setupContextMenu(); @@ -143,17 +136,18 @@ MainWindow::~MainWindow() void MainWindow::updateStatusLog(const QString& message) { - // 更新状态栏 - // statusBar()->showMessage(message); - - // 通过信号槽机制更新detect_log控件 - emit logUpdateRequested(message); + // 使用日志辅助类更新日志 + if (m_logHelper) { + m_logHelper->appendLog(message); + } } void MainWindow::clearDetectionLog() { - // 通过信号槽机制清空日志 - emit logClearRequested(); + // 使用日志辅助类清空日志 + if (m_logHelper) { + m_logHelper->clearLog(); + } } void MainWindow::Init() @@ -447,70 +441,18 @@ void MainWindow::updateDetectionResultDisplay(const DetectionResult& result) // 根据相机索引决定显示在哪个图像视图上 if (result.cameraIndex == 2) { // 第二个相机的结果显示在detect_image_2上 - updateDetectionLog(tr("相机2检测结果更新")); + updateStatusLog(tr("相机2检测结果更新")); displayImageInSecondView(result.image); } else { // 第一个相机或默认显示在detect_image上 - updateDetectionLog(tr("相机1检测结果更新")); + updateStatusLog(tr("相机1检测结果更新")); displayImage(result.image); } - + // 更新检测结果到列表 addDetectionResult(result); } -void MainWindow::updateDetectionLog(const QString& message) -{ - // 在UI线程中更新detect_log控件(QListView) - if (!m_logModel) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 检查是否与最后一条消息相同 - if (message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - - // 替换最后一条 - logList[logList.size() - 1] = logEntry; - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2").arg(timestamp).arg(message); - - // 添加新的日志条目 - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - ui->detect_log->scrollTo(lastIndex); -} - -void MainWindow::clearDetectionLogUI() -{ - // 在UI线程中清空检测日志 - if (m_logModel) { - m_logModel->setStringList(QStringList()); - } - - // 重置日志计数器 - m_lastLogMessage.clear(); - m_lastLogCount = 0; -} - void MainWindow::on_btn_start_clicked() { // 检查Presenter是否已初始化 diff --git a/App/ParticleSize/ParticleSizeApp/mainwindow.h b/App/ParticleSize/ParticleSizeApp/mainwindow.h index 87ac2e9..81e1b53 100644 --- a/App/ParticleSize/ParticleSizeApp/mainwindow.h +++ b/App/ParticleSize/ParticleSizeApp/mainwindow.h @@ -16,6 +16,7 @@ #include "dialogalgoarg.h" #include "CommonDialogCameraLevel.h" #include "DeviceStatusWidget.h" +#include "DetectLogHelper.h" #include "ParticleSizePresenter.h" QT_BEGIN_NAMESPACE @@ -47,30 +48,18 @@ public: signals: // 工作状态更新信号 void workStatusUpdateRequested(WorkStatus status); - + // 检测结果更新信号 void detectionResultUpdateRequested(const DetectionResult& result); - - // 日志更新信号 - void logUpdateRequested(const QString& message); - - // 清空日志信号 - void logClearRequested(); private slots: // 工作状态更新槽函数 void updateWorkStatusLabel(WorkStatus status); - + // 检测结果更新槽函数 void updateDetectionResultDisplay(const DetectionResult& result); - - // 日志更新槽函数 - void updateDetectionLog(const QString& message); - - // 清空日志槽函数 - void clearDetectionLogUI(); - + // 处理相机点击事件 void onCameraClicked(int cameraIndex); @@ -102,10 +91,10 @@ private: // 业务逻辑处理类 ParticleSizePresenter* m_presenter = nullptr; - - // 日志模型 - QStringListModel* m_logModel = nullptr; - + + // 日志辅助类 + DetectLogHelper* m_logHelper = nullptr; + // 设备状态显示widget的引用 DeviceStatusWidget* m_deviceStatusWidget = nullptr; @@ -148,9 +137,5 @@ private: private: QLabel* m_cpuSerialLabel = nullptr; // CPU序列号标签 - - // 日志去重相关 - QString m_lastLogMessage; // 最后一条日志消息(不含时间戳) - int m_lastLogCount = 0; // 最后一条日志的重复次数 }; #endif // MAINWINDOW_H diff --git a/App/ScrewPosition/ScrewPositionApp/mainwindow.cpp b/App/ScrewPosition/ScrewPositionApp/mainwindow.cpp index a925e6a..75eb536 100644 --- a/App/ScrewPosition/ScrewPositionApp/mainwindow.cpp +++ b/App/ScrewPosition/ScrewPositionApp/mainwindow.cpp @@ -83,9 +83,8 @@ MainWindow::MainWindow(QWidget *parent) QGraphicsScene* scene = new QGraphicsScene(this); ui->detect_image->setScene(scene); - // 初始化日志模型 - m_logModel = new QStringListModel(this); - ui->detect_log->setModel(m_logModel); + // 初始化日志辅助类 + m_logHelper = new DetectLogHelper(ui->detect_log, this); // 注册自定义类型,使其能够在信号槽中跨线程传递 qRegisterMetaType("DetectionResult"); @@ -98,12 +97,6 @@ MainWindow::MainWindow(QWidget *parent) // 连接检测结果更新信号槽 connect(this, &MainWindow::detectionResultUpdateRequested, this, &MainWindow::updateDetectionResultDisplay); - // 连接日志更新信号槽 - connect(this, &MainWindow::logUpdateRequested, this, &MainWindow::updateDetectionLog); - - // 连接清空日志信号槽 - connect(this, &MainWindow::logClearRequested, this, &MainWindow::clearDetectionLogUI); - // 初始化右键菜单 setupContextMenu(); @@ -129,14 +122,18 @@ MainWindow::~MainWindow() void MainWindow::updateStatusLog(const QString& message) { - // 通过信号槽机制更新detect_log控件 - emit logUpdateRequested(message); + // 使用日志辅助类更新日志 + if (m_logHelper) { + m_logHelper->appendLog(message); + } } void MainWindow::clearDetectionLog() { - // 通过信号槽机制清空日志 - emit logClearRequested(); + // 使用日志辅助类清空日志 + if (m_logHelper) { + m_logHelper->clearLog(); + } } void MainWindow::Init() @@ -374,65 +371,13 @@ void MainWindow::updateWorkStatusLabel(WorkStatus status) void MainWindow::updateDetectionResultDisplay(const DetectionResult& result) { // 显示检测结果图像 - updateDetectionLog(tr("检测结果更新")); + updateStatusLog(tr("检测结果更新")); displayImage(result.image); // 更新检测结果到列表 addDetectionResult(result); } -void MainWindow::updateDetectionLog(const QString& message) -{ - // 在UI线程中更新detect_log控件(QListView) - if (!m_logModel) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 检查是否与最后一条消息相同 - if (message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - - // 替换最后一条 - logList[logList.size() - 1] = logEntry; - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2").arg(timestamp).arg(message); - - // 添加新的日志条目 - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - ui->detect_log->scrollTo(lastIndex); -} - -void MainWindow::clearDetectionLogUI() -{ - // 在UI线程中清空检测日志 - if (m_logModel) { - m_logModel->setStringList(QStringList()); - } - - // 重置日志计数器 - m_lastLogMessage.clear(); - m_lastLogCount = 0; -} - void MainWindow::on_btn_start_clicked() { // 检查Presenter是否已初始化 diff --git a/App/ScrewPosition/ScrewPositionApp/mainwindow.h b/App/ScrewPosition/ScrewPositionApp/mainwindow.h index b1fc0ba..262a64b 100644 --- a/App/ScrewPosition/ScrewPositionApp/mainwindow.h +++ b/App/ScrewPosition/ScrewPositionApp/mainwindow.h @@ -16,6 +16,7 @@ #include "dialogalgoarg.h" #include "CommonDialogCameraLevel.h" #include "DeviceStatusWidget.h" +#include "DetectLogHelper.h" #include "ScrewPositionPresenter.h" QT_BEGIN_NAMESPACE @@ -51,12 +52,6 @@ signals: // 检测结果更新信号 void detectionResultUpdateRequested(const DetectionResult& result); - // 日志更新信号 - void logUpdateRequested(const QString& message); - - // 清空日志信号 - void logClearRequested(); - private slots: // 工作状态更新槽函数 @@ -65,12 +60,6 @@ private slots: // 检测结果更新槽函数 void updateDetectionResultDisplay(const DetectionResult& result); - // 日志更新槽函数 - void updateDetectionLog(const QString& message); - - // 清空日志槽函数 - void clearDetectionLogUI(); - // 处理相机点击事件 void onCameraClicked(int cameraIndex); @@ -103,8 +92,8 @@ private: // 业务逻辑处理类 ScrewPositionPresenter* m_presenter = nullptr; - // 日志模型 - QStringListModel* m_logModel = nullptr; + // 日志辅助类 + DetectLogHelper* m_logHelper = nullptr; // 设备状态显示widget的引用 DeviceStatusWidget* m_deviceStatusWidget = nullptr; @@ -134,10 +123,5 @@ private: void setupContextMenu(); void showContextMenu(const QPoint& pos, QGraphicsView* view); bool saveDetectionDataToFile(const QString& filePath, int cameraIndex); - -private: - // 日志去重相关 - QString m_lastLogMessage; // 最后一条日志消息(不含时间戳) - int m_lastLogCount = 0; // 最后一条日志的重复次数 }; #endif // MAINWINDOW_H diff --git a/App/WheelMeasure/WheelMeasureApp/mainwindow.cpp b/App/WheelMeasure/WheelMeasureApp/mainwindow.cpp index e336137..a102dc0 100644 --- a/App/WheelMeasure/WheelMeasureApp/mainwindow.cpp +++ b/App/WheelMeasure/WheelMeasureApp/mainwindow.cpp @@ -86,12 +86,8 @@ MainWindow::MainWindow(QWidget *parent) // 设置版本信息显示 setupVersionDisplay(); - // 初始化日志模型 - m_logModel = new QStringListModel(this); - ui->detect_log->setModel(m_logModel); - - // 连接日志更新信号 - connect(this, &MainWindow::logUpdateRequested, this, &MainWindow::updateDetectionLog); + // 初始化日志辅助类 + m_logHelper = new DetectLogHelper(ui->detect_log, this); m_presenter->Init(); } @@ -197,18 +193,18 @@ void MainWindow::on_btn_test_clicked() m_measureResultWidget->clearAllResults(); } - // emit logUpdateRequested(QString("正在加载调试数据: %1").arg(fileName)); + // if (m_logHelper) m_logHelper->appendLog(QString("正在加载调试数据: %1").arg(fileName)); // 在后台线程中执行加载和检测 std::thread t([this, fileName]() { int result = m_presenter->LoadDebugDataAndDetect(fileName.toStdString()); if (result == 0) { QMetaObject::invokeMethod(this, [this]() { - emit logUpdateRequested("调试数据加载和检测成功"); + if (m_logHelper) m_logHelper->appendLog("调试数据加载和检测成功"); }, Qt::QueuedConnection); } else { QMetaObject::invokeMethod(this, [this, result]() { - emit logUpdateRequested(QString("调试数据复检失败: %1").arg(result)); + if (m_logHelper) m_logHelper->appendLog(QString("调试数据复检失败: %1").arg(result)); }, Qt::QueuedConnection); } }); @@ -224,7 +220,7 @@ void MainWindow::on_btn_start_clicked() // 启动所有相机的检测 m_presenter->StartAllDetection(); - emit logUpdateRequested("已启动所有相机检测"); + if (m_logHelper) m_logHelper->appendLog("已启动所有相机检测"); } void MainWindow::on_btn_stop_clicked() @@ -235,7 +231,7 @@ void MainWindow::on_btn_stop_clicked() // 停止所有相机的检测 m_presenter->StopAllDetection(); - emit logUpdateRequested("已停止所有相机检测"); + if (m_logHelper) m_logHelper->appendLog("已停止所有相机检测"); } void MainWindow::on_btn_camera_config_clicked() @@ -341,7 +337,7 @@ void MainWindow::onDeviceClicked(const QString& deviceName) } if (foundIndex < 0) { - emit logUpdateRequested(QString("未找到相机: %1").arg(deviceName)); + if (m_logHelper) m_logHelper->appendLog(QString("未找到相机: %1").arg(deviceName)); return; } @@ -352,7 +348,7 @@ void MainWindow::onDeviceClicked(const QString& deviceName) // 直接开始检测 m_presenter->ResetDetect(foundIndex); - emit logUpdateRequested(QString("已启动相机 \"%1\" 的检测").arg(deviceName)); + if (m_logHelper) m_logHelper->appendLog(QString("已启动相机 \"%1\" 的检测").arg(deviceName)); } void MainWindow::onConfigSaved() @@ -374,7 +370,7 @@ void MainWindow::onTileRightClicked(int index, const QString& alias) // 检查点击的是否是当前检测的相机 if (index != currentDetectIndex) { - emit logUpdateRequested(QString("设备 \"%1\" 无缓存数据").arg(alias)); + if (m_logHelper) m_logHelper->appendLog(QString("设备 \"%1\" 无缓存数据").arg(alias)); StyledMessageBox::information(this, "提示", QString("设备 \"%1\" 无缓存数据\n\n当前缓存的是设备 %2 的数据") .arg(alias) @@ -385,7 +381,7 @@ void MainWindow::onTileRightClicked(int index, const QString& alias) // 检查是否有缓存数据 int cacheSize = m_presenter->GetDetectionDataCacheSize(); if (cacheSize <= 0) { - emit logUpdateRequested(QString("设备 \"%1\" 无缓存数据").arg(alias)); + if (m_logHelper) m_logHelper->appendLog(QString("设备 \"%1\" 无缓存数据").arg(alias)); StyledMessageBox::information(this, "提示", QString("设备 \"%1\" 无缓存数据").arg(alias)); return; } @@ -409,10 +405,10 @@ void MainWindow::onTileRightClicked(int index, const QString& alias) // 调用Presenter保存数据 int result = m_presenter->SaveDetectionDataToFile(filePath.toStdString()); if (result == 0) { - emit logUpdateRequested(QString("检测数据已保存: %1").arg(filePath)); + if (m_logHelper) m_logHelper->appendLog(QString("检测数据已保存: %1").arg(filePath)); StyledMessageBox::information(this, "成功", QString("检测数据已保存到:\n%1").arg(filePath)); } else { - emit logUpdateRequested(QString("保存检测数据失败: %1").arg(result)); + if (m_logHelper) m_logHelper->appendLog(QString("保存检测数据失败: %1").arg(result)); StyledMessageBox::warning(this, "失败", QString("保存检测数据失败,错误码: %1").arg(result)); } } @@ -420,7 +416,7 @@ void MainWindow::onTileRightClicked(int index, const QString& alias) // IWheelMeasureStatus接口方法实现 void MainWindow::OnStatusUpdate(const QString &statusMessage) { - emit logUpdateRequested(statusMessage); + if (m_logHelper) m_logHelper->appendLog(statusMessage); LOG_DEBUG("Status update: %s \n", statusMessage.toStdString().c_str()); } @@ -477,14 +473,14 @@ void MainWindow::OnMeasureResult(const WheelMeasureResult &result) void MainWindow::OnCameraConnected(const QString &cameraName) { QString message = QString("相机已连接: %1").arg(cameraName); - emit logUpdateRequested(message); + if (m_logHelper) m_logHelper->appendLog(message); LOG_DEBUG("%s \n", message.toStdString().c_str()); } void MainWindow::OnCameraDisconnected(const QString &cameraName) { QString message = QString("相机断开连接: %1").arg(cameraName); - emit logUpdateRequested(message); + if (m_logHelper) m_logHelper->appendLog(message); LOG_DEBUG("%s \n", message.toStdString().c_str()); } @@ -516,13 +512,13 @@ void MainWindow::OnWorkStatusChangedImpl(WorkStatus status) } QString message = QString("工作状态: %1").arg(statusStr); - emit logUpdateRequested(message); + if (m_logHelper) m_logHelper->appendLog(message); LOG_DEBUG("%s \n", message.toStdString().c_str()); } void MainWindow::OnErrorOccurred(const QString &errorMessage) { - emit logUpdateRequested("错误: " + errorMessage); + if (m_logHelper) m_logHelper->appendLog("错误: " + errorMessage); LOG_ERROR("Error occurred: %s \n", errorMessage.toStdString().c_str()); } @@ -563,43 +559,3 @@ void MainWindow::setupVersionDisplay() LOG_INFO("Version display initialized: %s\n", versionText.toStdString().c_str()); } - -void MainWindow::updateDetectionLog(const QString& message) -{ - // 在UI线程中更新detect_log控件(QListView) - if (!m_logModel) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 获取当前时间 - QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss"); - - // 检查是否与最后一条消息相同 - if (message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - QString lastEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - logList.replace(logList.size() - 1, lastEntry); - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - // 限制日志条数,最多保留200条 - if (logList.size() >= 200) { - logList.removeFirst(); - } - - // 添加新的日志条目 - QString logEntry = QString("[%1] %2").arg(timestamp).arg(message); - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - ui->detect_log->scrollTo(lastIndex); -} diff --git a/App/WheelMeasure/WheelMeasureApp/mainwindow.h b/App/WheelMeasure/WheelMeasureApp/mainwindow.h index cbe9cc5..5304893 100644 --- a/App/WheelMeasure/WheelMeasureApp/mainwindow.h +++ b/App/WheelMeasure/WheelMeasureApp/mainwindow.h @@ -10,6 +10,7 @@ #include #include "Presenter/Inc/WheelMeasurePresenter.h" #include "IWheelMeasureStatus.h" +#include "DetectLogHelper.h" // 前向声明widgets class ImageGridWidget; @@ -30,9 +31,6 @@ class MainWindow : public QMainWindow, public IWheelMeasureStatus { Q_OBJECT -signals: - void logUpdateRequested(const QString& message); - public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); @@ -64,7 +62,6 @@ private slots: void onConfigSaved(); void onDeviceClicked(const QString& deviceName); void onTileRightClicked(int index, const QString& alias); - void updateDetectionLog(const QString& message); private: void setupVersionDisplay(); @@ -80,10 +77,8 @@ private: WheelMeasurePresenter* m_presenter = nullptr; QLabel* m_versionLabel = nullptr; - // 日志相关 - QStringListModel* m_logModel = nullptr; - QString m_lastLogMessage; - int m_lastLogCount = 0; + // 日志辅助类 + DetectLogHelper* m_logHelper = nullptr; }; #endif // MAINWINDOW_H diff --git a/App/Workpiece/WorkpieceApp/mainwindow.cpp b/App/Workpiece/WorkpieceApp/mainwindow.cpp index fc73013..56f680e 100644 --- a/App/Workpiece/WorkpieceApp/mainwindow.cpp +++ b/App/Workpiece/WorkpieceApp/mainwindow.cpp @@ -95,10 +95,9 @@ MainWindow::MainWindow(QWidget *parent) // 为第二个图像视图初始化GraphicsScene QGraphicsScene* scene2 = new QGraphicsScene(this); ui->detect_image_2->setScene(scene2); - - // 初始化日志模型 - m_logModel = new QStringListModel(this); - ui->detect_log->setModel(m_logModel); + + // 初始化日志辅助类 + m_logHelper = new DetectLogHelper(ui->detect_log, this); // 注册自定义类型,使其能够在信号槽中跨线程传递 qRegisterMetaType("DetectionResult"); @@ -107,16 +106,10 @@ MainWindow::MainWindow(QWidget *parent) // 连接工作状态更新信号槽 connect(this, &MainWindow::workStatusUpdateRequested, this, &MainWindow::updateWorkStatusLabel); - + // 连接检测结果更新信号槽 connect(this, &MainWindow::detectionResultUpdateRequested, this, &MainWindow::updateDetectionResultDisplay); - - // 连接日志更新信号槽 - connect(this, &MainWindow::logUpdateRequested, this, &MainWindow::updateDetectionLog); - - // 连接清空日志信号槽 - connect(this, &MainWindow::logClearRequested, this, &MainWindow::clearDetectionLogUI); - + // 初始化右键菜单 setupContextMenu(); @@ -139,17 +132,18 @@ MainWindow::~MainWindow() void MainWindow::updateStatusLog(const QString& message) { - // 更新状态栏 - // statusBar()->showMessage(message); - - // 通过信号槽机制更新detect_log控件 - emit logUpdateRequested(message); + // 使用日志辅助类更新日志 + if (m_logHelper) { + m_logHelper->appendLog(message); + } } void MainWindow::clearDetectionLog() { - // 通过信号槽机制清空日志 - emit logClearRequested(); + // 使用日志辅助类清空日志 + if (m_logHelper) { + m_logHelper->clearLog(); + } } void MainWindow::Init() @@ -497,70 +491,18 @@ void MainWindow::updateDetectionResultDisplay(const DetectionResult& result) // 根据相机索引决定显示在哪个图像视图上 if (result.cameraIndex == 2) { // 第二个相机的结果显示在detect_image_2上 - updateDetectionLog(tr("相机2检测结果更新")); + updateStatusLog(tr("相机2检测结果更新")); displayImageInSecondView(result.image); } else { // 第一个相机或默认显示在detect_image上 - updateDetectionLog(tr("相机1检测结果更新")); + updateStatusLog(tr("相机1检测结果更新")); displayImage(result.image); } - + // 更新检测结果到列表 addDetectionResult(result); } -void MainWindow::updateDetectionLog(const QString& message) -{ - // 在UI线程中更新detect_log控件(QListView) - if (!m_logModel) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 检查是否与最后一条消息相同 - if (message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - - // 替换最后一条 - logList[logList.size() - 1] = logEntry; - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2").arg(timestamp).arg(message); - - // 添加新的日志条目 - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - ui->detect_log->scrollTo(lastIndex); -} - -void MainWindow::clearDetectionLogUI() -{ - // 在UI线程中清空检测日志 - if (m_logModel) { - m_logModel->setStringList(QStringList()); - } - - // 重置日志计数器 - m_lastLogMessage.clear(); - m_lastLogCount = 0; -} - void MainWindow::on_btn_start_clicked() { // 检查Presenter是否已初始化 diff --git a/App/Workpiece/WorkpieceApp/mainwindow.h b/App/Workpiece/WorkpieceApp/mainwindow.h index 85ed5d5..9f899a5 100644 --- a/App/Workpiece/WorkpieceApp/mainwindow.h +++ b/App/Workpiece/WorkpieceApp/mainwindow.h @@ -16,6 +16,7 @@ #include "dialogalgoarg.h" #include "CommonDialogCameraLevel.h" #include "DeviceStatusWidget.h" +#include "DetectLogHelper.h" #include "WorkpiecePresenter.h" QT_BEGIN_NAMESPACE @@ -47,30 +48,18 @@ public: signals: // 工作状态更新信号 void workStatusUpdateRequested(WorkStatus status); - + // 检测结果更新信号 void detectionResultUpdateRequested(const DetectionResult& result); - - // 日志更新信号 - void logUpdateRequested(const QString& message); - - // 清空日志信号 - void logClearRequested(); private slots: // 工作状态更新槽函数 void updateWorkStatusLabel(WorkStatus status); - + // 检测结果更新槽函数 void updateDetectionResultDisplay(const DetectionResult& result); - - // 日志更新槽函数 - void updateDetectionLog(const QString& message); - - // 清空日志槽函数 - void clearDetectionLogUI(); - + // 处理相机点击事件 void onCameraClicked(int cameraIndex); @@ -102,10 +91,10 @@ private: // 业务逻辑处理类 WorkpiecePresenter* m_presenter = nullptr; - - // 日志模型 - QStringListModel* m_logModel = nullptr; - + + // 日志辅助类 + DetectLogHelper* m_logHelper = nullptr; + // 设备状态显示widget的引用 DeviceStatusWidget* m_deviceStatusWidget = nullptr; @@ -148,9 +137,5 @@ private: private: QLabel* m_cpuSerialLabel = nullptr; // CPU序列号标签 - - // 日志去重相关 - QString m_lastLogMessage; // 最后一条日志消息(不含时间戳) - int m_lastLogCount = 0; // 最后一条日志的重复次数 }; #endif // MAINWINDOW_H diff --git a/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.cpp b/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.cpp index e44ccb0..c528eb1 100644 --- a/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.cpp +++ b/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.cpp @@ -97,9 +97,8 @@ MainWindow::MainWindow(QWidget *parent) QGraphicsScene* scene2 = new QGraphicsScene(this); ui->detect_image_2->setScene(scene2); - // 初始化日志模型 - m_logModel = new QStringListModel(this); - ui->detect_log->setModel(m_logModel); + // 初始化日志辅助类 + m_logHelper = new DetectLogHelper(ui->detect_log, this); // 注册自定义类型,使其能够在信号槽中跨线程传递 qRegisterMetaType("WorkpieceHoleDetectionResult"); @@ -112,12 +111,6 @@ MainWindow::MainWindow(QWidget *parent) // 连接检测结果更新信号槽 connect(this, &MainWindow::detectionResultUpdateRequested, this, &MainWindow::updateDetectionResultDisplay); - // 连接日志更新信号槽 - connect(this, &MainWindow::logUpdateRequested, this, &MainWindow::updateDetectionLog); - - // 连接清空日志信号槽 - connect(this, &MainWindow::logClearRequested, this, &MainWindow::clearDetectionLogUI); - // 初始化右键菜单 setupContextMenu(); @@ -140,17 +133,18 @@ MainWindow::~MainWindow() void MainWindow::updateStatusLog(const QString& message) { - // 更新状态栏 - // statusBar()->showMessage(message); - - // 通过信号槽机制更新detect_log控件 - emit logUpdateRequested(message); + // 使用日志辅助类更新日志 + if (m_logHelper) { + m_logHelper->appendLog(message); + } } void MainWindow::clearDetectionLog() { - // 通过信号槽机制清空日志 - emit logClearRequested(); + // 使用日志辅助类清空日志 + if (m_logHelper) { + m_logHelper->clearLog(); + } } void MainWindow::Init() @@ -403,11 +397,11 @@ void MainWindow::updateDetectionResultDisplay(const WorkpieceHoleDetectionResult // 根据相机索引决定显示在哪个图像视图上 if (result.cameraIndex == 2) { // 第二个相机的结果显示在detect_image_2上 - updateDetectionLog(tr("相机2检测结果更新")); + updateStatusLog(tr("相机2检测结果更新")); displayImageInSecondView(result.image); } else { // 第一个相机或默认显示在detect_image上 - updateDetectionLog(tr("相机1检测结果更新")); + updateStatusLog(tr("相机1检测结果更新")); displayImage(result.image); } @@ -415,58 +409,6 @@ void MainWindow::updateDetectionResultDisplay(const WorkpieceHoleDetectionResult addDetectionResult(result); } -void MainWindow::updateDetectionLog(const QString& message) -{ - // 在UI线程中更新detect_log控件(QListView) - if (!m_logModel) return; - - // 获取当前数据 - QStringList logList = m_logModel->stringList(); - - // 检查是否与最后一条消息相同 - if (message == m_lastLogMessage && !logList.isEmpty()) { - // 相同消息,增加计数并替换最后一条 - m_lastLogCount++; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); - - // 替换最后一条 - logList[logList.size() - 1] = logEntry; - } else { - // 新消息,重置计数 - m_lastLogMessage = message; - m_lastLogCount = 1; - - // 添加时间戳 - QString timestamp = QDateTime::currentDateTime().toString("hh:mm:ss"); - QString logEntry = QString("[%1] %2").arg(timestamp).arg(message); - - // 添加新的日志条目 - logList.append(logEntry); - } - - // 更新模型 - m_logModel->setStringList(logList); - - // 自动滚动到最底部 - QModelIndex lastIndex = m_logModel->index(logList.size() - 1); - ui->detect_log->scrollTo(lastIndex); -} - -void MainWindow::clearDetectionLogUI() -{ - // 在UI线程中清空检测日志 - if (m_logModel) { - m_logModel->setStringList(QStringList()); - } - - // 重置日志计数器 - m_lastLogMessage.clear(); - m_lastLogCount = 0; -} - void MainWindow::on_btn_start_clicked() { // 检查Presenter是否已初始化 diff --git a/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.h b/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.h index 0e0aafe..92bf377 100644 --- a/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.h +++ b/App/WorkpieceHole/WorkpieceHoleApp/mainwindow.h @@ -16,6 +16,7 @@ #include "dialogalgoarg.h" #include "CommonDialogCameraLevel.h" #include "DeviceStatusWidget.h" +#include "DetectLogHelper.h" #include "WorkpieceHolePresenter.h" QT_BEGIN_NAMESPACE @@ -51,12 +52,6 @@ signals: // 检测结果更新信号 void detectionResultUpdateRequested(const WorkpieceHoleDetectionResult& result); - // 日志更新信号 - void logUpdateRequested(const QString& message); - - // 清空日志信号 - void logClearRequested(); - private slots: // 工作状态更新槽函数 @@ -65,12 +60,6 @@ private slots: // 检测结果更新槽函数 void updateDetectionResultDisplay(const WorkpieceHoleDetectionResult& result); - // 日志更新槽函数 - void updateDetectionLog(const QString& message); - - // 清空日志槽函数 - void clearDetectionLogUI(); - // 处理相机点击事件 void onCameraClicked(int cameraIndex); @@ -103,8 +92,8 @@ private: // 业务逻辑处理类 WorkpieceHolePresenter* m_presenter = nullptr; - // 日志模型 - QStringListModel* m_logModel = nullptr; + // 日志辅助类 + DetectLogHelper* m_logHelper = nullptr; // 设备状态显示widget的引用 DeviceStatusWidget* m_deviceStatusWidget = nullptr; @@ -148,9 +137,5 @@ private: private: QLabel* m_cpuSerialLabel = nullptr; // CPU序列号标签 - - // 日志去重相关 - QString m_lastLogMessage; // 最后一条日志消息(不含时间戳) - int m_lastLogCount = 0; // 最后一条日志的重复次数 }; #endif // MAINWINDOW_H diff --git a/AppUtils/AppCommon/AppCommon.pro b/AppUtils/AppCommon/AppCommon.pro index 64bc6a3..e0b49b9 100644 --- a/AppUtils/AppCommon/AppCommon.pro +++ b/AppUtils/AppCommon/AppCommon.pro @@ -25,6 +25,7 @@ INCLUDEPATH += $$PWD/../../SDK/Device/VzNLSDK/Inc INCLUDEPATH += $$PWD/../../Module/ShareMem/Inc # ConfigMonitor INCLUDEPATH += $$PWD/../../Module/ModbusTCPServer/Inc # ModbusTCP服务器接口 INCLUDEPATH += $$PWD/../../Module/AuthModule/Inc # 授权模块 +INCLUDEPATH += $$PWD/../../VrNets/TCPServer/Inc # TCP服务器接口 # 头文件 HEADERS += \ @@ -35,7 +36,9 @@ HEADERS += \ Inc/VrCommonConfig.h \ Inc/ConfigMonitor.h \ Inc/BaseConfigManager.h \ - Inc/ConfigEncryption.h + Inc/ConfigEncryption.h \ + Inc/ProtocolCommon.h \ + Inc/TCPServerProtocol.h # 源文件 SOURCES += \ @@ -43,7 +46,8 @@ SOURCES += \ Src/PathManager.cpp \ Src/BasePresenter.cpp \ Src/ConfigMonitor.cpp \ - Src/ConfigEncryption.cpp + Src/ConfigEncryption.cpp \ + Src/TCPServerProtocol.cpp # 注意: BaseConfigManager.cpp 不在这里编译 # 它需要在各个应用中编译,因为它依赖应用特定的 IVrConfig.h diff --git a/AppUtils/AppCommon/Inc/ProtocolCommon.h b/AppUtils/AppCommon/Inc/ProtocolCommon.h new file mode 100644 index 0000000..5a226e1 --- /dev/null +++ b/AppUtils/AppCommon/Inc/ProtocolCommon.h @@ -0,0 +1,72 @@ +#ifndef PROTOCOLCOMMON_H +#define PROTOCOLCOMMON_H + +#include +#include +#include + +/** + * @brief 机械臂坐标结构体 + */ +struct RobotCoordinate { + float x; // X坐标 + float y; // Y坐标 + float z; // Z坐标 + float rx; // X轴旋转 + float ry; // Y轴旋转 + float rz; // Z轴旋转 +}; + +/** + * @brief 单个目标位置数据(简化版,只包含必要的x,y,z,rz) + */ +struct TargetPosition { + float x; // X坐标 + float y; // Y坐标 + float z; // Z坐标 + float rz; // Z轴旋转(yaw角) + uint16_t cameraId; // 相机ID(从1开始编号) +}; + +/** + * @brief 多目标检测结果数据结构 + */ +struct MultiTargetData { + uint16_t count; // 目标数量 + uint16_t cameraId; // 当前检测的相机ID(从1开始编号) + std::vector targets; // 目标位置列表 + + MultiTargetData() : count(0), cameraId(1) {} // 默认相机ID为1 +}; + +/** + * @brief 通用连接状态枚举 + */ +enum ConnectionStatus { + STATUS_DISCONNECTED = 0, // 断开连接 + STATUS_CONNECTED = 1, // 已连接 + STATUS_IDLE = 2, // 空闲状态 + STATUS_WORKING = 3, // 工作中 + STATUS_ERROR = 4 // 错误状态 +}; + +/** + * @brief 连接状态回调函数类型 + * @param connected true-连接,false-断开 + */ +using ConnectionCallback = std::function; + +/** + * @brief 开始工作信号回调函数类型 + * @param startWork true-开始工作,false-停止工作 + * @param cameraId 相机ID,从1开始编号(1,2,...) + */ +using WorkSignalCallback = std::function; + +// 工作状态定义(公共常量) +static const uint16_t WORK_STATUS_IDLE = 0; // 空闲 +static const uint16_t WORK_STATUS_CAMERA1_DONE = 1; // 相机1工作完成 +static const uint16_t WORK_STATUS_CAMERA2_DONE = 2; // 相机2工作完成 +static const uint16_t WORK_STATUS_BUSY = 3; // 忙碌 + +#endif // PROTOCOLCOMMON_H diff --git a/AppUtils/AppCommon/Inc/TCPServerProtocol.h b/AppUtils/AppCommon/Inc/TCPServerProtocol.h new file mode 100644 index 0000000..db72f07 --- /dev/null +++ b/AppUtils/AppCommon/Inc/TCPServerProtocol.h @@ -0,0 +1,151 @@ +#ifndef TCPSERVERPROTOCOL_H +#define TCPSERVERPROTOCOL_H + +#include +#include +#include +#include +#include +#include +#include +#include "IYTCPServer.h" +#include "ProtocolCommon.h" + +/** + * @brief TCP服务器协议封装类 + * + * 通用的TCP/IP通信协议基类,支持: + * - 服务端模式,等待客户端连接 + * - JSON格式数据交换 + * - 支持start_detection/ScanRequest指令 + * - 返回检测结果 + */ +class TCPServerProtocol +{ +public: + /** + * @brief 检测结果结构体 + */ + struct DetectionResultData { + int code = 0; // 响应码:0-成功,其他-错误 + bool success = true; // 是否成功 + QString message = "检测成功"; // 响应消息 + qint64 timestamp = 0; // 时间戳(毫秒) + std::vector> result; // 检测结果:3D坐标数组 + }; + + /** + * @brief 连接状态枚举(使用公共状态定义) + */ + using TCPStatus = ConnectionStatus; + + /** + * @brief 检测结果回调函数类型 + * @param success 是否成功触发检测 + * @param cameraIndex 相机索引(从开始检测指令中解析,-1表示所有相机) + * @param timestamp 请求时间戳 + */ + using DetectionTriggerCallback = std::function; + +public: + TCPServerProtocol(); + virtual ~TCPServerProtocol(); + + /** + * @brief 初始化TCP服务器 + * @param port TCP端口号,默认5020 + * @return 0-成功,其他-错误码 + */ + int Initialize(uint16_t port = 5020); + + /** + * @brief 反初始化,停止服务 + */ + void Deinitialize(); + + /** + * @brief 发送检测结果给客户端 + * @param result 检测结果数据 + * @param pClient 目标客户端,如果为nullptr则发送给所有客户端 + * @return 0-成功,其他-错误码 + */ + int SendDetectionResult(const QJsonObject& result, const TCPClient* pClient = nullptr); + + /** + * @brief 获取当前连接状态 + * @return 当前状态 + */ + TCPStatus GetConnectionStatus() const; + + /** + * @brief 设置连接状态回调 + * @param callback 回调函数 + */ + void SetConnectionCallback(const ConnectionCallback& callback); + + /** + * @brief 设置检测触发回调 + * @param callback 回调函数 + */ + void SetDetectionTriggerCallback(const DetectionTriggerCallback& callback); + + /** + * @brief 获取服务运行状态 + * @return true-运行中,false-已停止 + */ + bool IsRunning() const; + +protected: + /** + * @brief TCP客户端连接事件处理 + * @param pClient 客户端对象 + * @param eventType 事件类型 + */ + virtual void OnTCPEvent(const TCPClient* pClient, TCPServerEventType eventType); + + /** + * @brief TCP数据接收处理 + * @param pClient 客户端对象 + * @param pData 数据指针 + * @param nLen 数据长度 + */ + virtual void OnTCPDataReceived(const TCPClient* pClient, const char* pData, unsigned int nLen); + + /** + * @brief 解析JSON命令(子类可重写以支持自定义命令) + * @param pClient 客户端对象 + * @param jsonData JSON数据 + */ + virtual void ParseJSONCommand(const TCPClient* pClient, const QJsonObject& jsonData); + + /** + * @brief 处理开始检测命令 + * @param pClient 客户端对象 + * @param timestamp 时间戳 + */ + virtual void HandleStartDetectionCommand(const TCPClient* pClient, qint64 timestamp); + + /** + * @brief 发送错误响应 + * @param pClient 客户端对象 + * @param code 错误码 + * @param message 错误消息 + * @param timestamp 时间戳 + */ + void SendErrorResponse(const TCPClient* pClient, int code, const QString& message, qint64 timestamp); + +protected: + // TCP服务器相关 + IYTCPServer* m_pTCPServer; // TCP服务器实例 + bool m_bServerRunning; // 服务器运行状态 + uint16_t m_nPort; // TCP端口 + + // 连接状态 + TCPStatus m_connectionStatus; // TCP连接状态 + + // 回调函数 + ConnectionCallback m_connectionCallback; // 连接状态回调 + DetectionTriggerCallback m_detectionTriggerCallback; // 检测触发回调 +}; + +#endif // TCPSERVERPROTOCOL_H diff --git a/AppUtils/AppCommon/Inc/VrCommonConfig.h b/AppUtils/AppCommon/Inc/VrCommonConfig.h index b9fd2bd..1f8b563 100644 --- a/AppUtils/AppCommon/Inc/VrCommonConfig.h +++ b/AppUtils/AppCommon/Inc/VrCommonConfig.h @@ -104,6 +104,7 @@ struct VrCameraPlaneCalibParam std::string cameraName = ""; // 相机名称 double planeCalib[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; // 旋转矩阵(默认单位矩阵) double planeHeight = -1.0; // 参考平面的高度,用于去除地面数据 + double planeHeightOffset = -10.0; // 平面高度偏移量,用于数据预处理时去除地面的余量 double invRMatrix[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; // 逆旋转矩阵(默认单位矩阵) bool isCalibrated = false; // 是否已经校准 @@ -114,6 +115,7 @@ struct VrCameraPlaneCalibParam cameraName = other.cameraName; memcpy(planeCalib, other.planeCalib, sizeof(planeCalib)); planeHeight = other.planeHeight; + planeHeightOffset = other.planeHeightOffset; memcpy(invRMatrix, other.invRMatrix, sizeof(invRMatrix)); isCalibrated = other.isCalibrated; } @@ -125,6 +127,7 @@ struct VrCameraPlaneCalibParam : cameraIndex(other.cameraIndex) , cameraName(other.cameraName) , planeHeight(other.planeHeight) + , planeHeightOffset(other.planeHeightOffset) , isCalibrated(other.isCalibrated) { memcpy(planeCalib, other.planeCalib, sizeof(planeCalib)); memcpy(invRMatrix, other.invRMatrix, sizeof(invRMatrix)); diff --git a/AppUtils/AppCommon/Src/TCPServerProtocol.cpp b/AppUtils/AppCommon/Src/TCPServerProtocol.cpp new file mode 100644 index 0000000..99752dc --- /dev/null +++ b/AppUtils/AppCommon/Src/TCPServerProtocol.cpp @@ -0,0 +1,252 @@ +#include "TCPServerProtocol.h" +#include "VrLog.h" +#include +#include +#include +#include + +TCPServerProtocol::TCPServerProtocol() + : m_pTCPServer(nullptr) + , m_bServerRunning(false) + , m_nPort(5020) + , m_connectionStatus(STATUS_DISCONNECTED) +{ +} + +TCPServerProtocol::~TCPServerProtocol() +{ + Deinitialize(); +} + +int TCPServerProtocol::Initialize(uint16_t port) +{ + LOG_DEBUG("Initializing TCP server protocol on port %d\n", port); + + m_nPort = port; + + // 创建TCP服务器实例 + if (!VrCreatYTCPServer(&m_pTCPServer)) { + LOG_ERROR("Failed to create TCP server instance\n"); + return -1; + } + + // 初始化TCP服务器 + if (!m_pTCPServer->Init(port)) { + LOG_ERROR("Failed to initialize TCP server on port %d\n", port); + delete m_pTCPServer; + m_pTCPServer = nullptr; + return -2; + } + + // 设置事件回调 + m_pTCPServer->SetEventCallback([this](const TCPClient* pClient, TCPServerEventType eventType) { + this->OnTCPEvent(pClient, eventType); + }); + + // 启动TCP服务器 + if (!m_pTCPServer->Start([this](const TCPClient* pClient, const char* pData, const unsigned int nLen) { + this->OnTCPDataReceived(pClient, pData, nLen); + })) { + LOG_ERROR("Failed to start TCP server\n"); + m_pTCPServer->Close(); + delete m_pTCPServer; + m_pTCPServer = nullptr; + return -3; + } + + m_bServerRunning = true; + m_connectionStatus = STATUS_CONNECTED; + + LOG_DEBUG("TCP server protocol initialized successfully on port %d\n", port); + return 0; +} + +void TCPServerProtocol::Deinitialize() +{ + if (m_pTCPServer) { + LOG_DEBUG("Stopping TCP server protocol\n"); + + m_bServerRunning = false; + m_connectionStatus = ConnectionStatus::STATUS_DISCONNECTED; + + m_pTCPServer->Stop(); + m_pTCPServer->Close(); + delete m_pTCPServer; + m_pTCPServer = nullptr; + + LOG_DEBUG("TCP server protocol stopped\n"); + } +} + +int TCPServerProtocol::SendDetectionResult(const QJsonObject& result, const TCPClient* pClient) +{ + if (!m_pTCPServer || !m_bServerRunning) { + LOG_ERROR("TCP server is not running\n"); + return -1; + } + + // 转换为JSON字符串 + QJsonDocument doc(result); + QByteArray jsonData = doc.toJson(QJsonDocument::Compact); + + // 发送数据 + bool success = false; + if (pClient) { + // 发送给指定客户端 + success = m_pTCPServer->SendData(pClient, jsonData.constData(), jsonData.size()); + LOG_DEBUG("Sent detection result to specific client, size: %d bytes\n", jsonData.size()); + } else { + // 发送给所有客户端 + success = m_pTCPServer->SendAllData(jsonData.constData(), jsonData.size()); + LOG_DEBUG("Sent detection result to all clients, size: %d bytes\n", jsonData.size()); + } + + if (!success) { + LOG_ERROR("Failed to send detection result\n"); + return -2; + } + + return 0; +} + +TCPServerProtocol::TCPStatus TCPServerProtocol::GetConnectionStatus() const +{ + return m_connectionStatus; +} + +void TCPServerProtocol::SetConnectionCallback(const ConnectionCallback& callback) +{ + m_connectionCallback = callback; +} + +void TCPServerProtocol::SetDetectionTriggerCallback(const DetectionTriggerCallback& callback) +{ + m_detectionTriggerCallback = callback; +} + +bool TCPServerProtocol::IsRunning() const +{ + return m_bServerRunning; +} + +void TCPServerProtocol::OnTCPEvent(const TCPClient* pClient, TCPServerEventType eventType) +{ + switch (eventType) { + case TCP_EVENT_CLIENT_CONNECTED: + LOG_DEBUG("TCP client connected: %p\n", pClient); + m_connectionStatus = STATUS_CONNECTED; + if (m_connectionCallback) { + m_connectionCallback(true); + } + break; + + case TCP_EVENT_CLIENT_DISCONNECTED: + LOG_DEBUG("TCP client disconnected: %p\n", pClient); + // 注意:这里不立即设置为断开状态,因为可能还有其他客户端连接 + if (m_connectionCallback) { + m_connectionCallback(false); + } + break; + + case TCP_EVENT_CLIENT_EXCEPTION: + LOG_WARNING("TCP client exception: %p\n", pClient); + if (m_connectionCallback) { + m_connectionCallback(false); + } + break; + } +} + +void TCPServerProtocol::OnTCPDataReceived(const TCPClient* pClient, const char* pData, unsigned int nLen) +{ + if (!pData || nLen == 0) { + LOG_WARNING("Received empty data from client %p\n", pClient); + return; + } + + LOG_DEBUG("Received TCP data from client %p, size: %u bytes\n", pClient, nLen); + + // 解析JSON数据 + QByteArray data(pData, nLen); + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + LOG_ERROR("JSON parse error: %s\n", error.errorString().toStdString().c_str()); + SendErrorResponse(pClient, -1, "JSON格式错误", 0); + return; + } + + if (!doc.isObject()) { + LOG_ERROR("Received JSON is not an object\n"); + SendErrorResponse(pClient, -2, "JSON数据必须是对象格式", 0); + return; + } + + ParseJSONCommand(pClient, doc.object()); +} + +void TCPServerProtocol::ParseJSONCommand(const TCPClient* pClient, const QJsonObject& jsonData) +{ + // 获取消息类型和时间戳 + QString messageType = jsonData["MessageType"].toString(); + QString timestamp = jsonData["Timestamp"].toString(); + + LOG_DEBUG("Received MessageType: %s, Timestamp: %s\n", messageType.toStdString().c_str(), timestamp.toStdString().c_str()); + + if (messageType == "ScanRequest") { + HandleStartDetectionCommand(pClient, QDateTime::currentMSecsSinceEpoch()); + } else { + LOG_WARNING("Unknown MessageType: %s\n", messageType.toStdString().c_str()); + SendErrorResponse(pClient, -3, QString("未知消息类型: %1").arg(messageType), QDateTime::currentMSecsSinceEpoch()); + } +} + +void TCPServerProtocol::HandleStartDetectionCommand(const TCPClient* pClient, qint64 timestamp) +{ + LOG_DEBUG("Handling start_detection command, timestamp: %lld\n", timestamp); + + // 先发送ScanResponse确认收到请求 + QJsonObject response; + response["MessageType"] = "ScanResponse"; + response["Timestamp"] = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + QJsonObject data; + data["Status"] = "Accepted"; + response["Data"] = data; + SendDetectionResult(response, pClient); + + bool success = false; + int cameraIndex = -1; // -1表示所有相机 + + // 触发检测回调 + if (m_detectionTriggerCallback) { + success = m_detectionTriggerCallback(true, cameraIndex, timestamp); + } else { + LOG_WARNING("Detection trigger callback not set\n"); + SendErrorResponse(pClient, -4, "检测服务未准备就绪", timestamp); + return; + } + + if (!success) { + LOG_ERROR("Failed to trigger detection\n"); + SendErrorResponse(pClient, -5, "检测启动失败", timestamp); + return; + } + + // 注意:这里不立即发送结果,实际的检测结果将通过 SendDetectionResult 方法异步发送 + LOG_DEBUG("Detection triggered successfully, waiting for results...\n"); +} + +void TCPServerProtocol::SendErrorResponse(const TCPClient* pClient, int code, const QString& message, qint64 timestamp) +{ + QJsonObject response; + response["MessageType"] = "Error"; + response["Timestamp"] = QDateTime::currentDateTime().toString("yyyy-MM-dd HH:mm:ss"); + + QJsonObject data; + data["ErrorCode"] = code; + data["ErrorMessage"] = message; + response["Data"] = data; + + SendDetectionResult(response, pClient); +} diff --git a/AppUtils/UICommon/Inc/DetectImageWidget.h b/AppUtils/UICommon/Inc/DetectImageWidget.h new file mode 100644 index 0000000..09be743 --- /dev/null +++ b/AppUtils/UICommon/Inc/DetectImageWidget.h @@ -0,0 +1,198 @@ +#ifndef DETECTIMAGEWIDGET_H +#define DETECTIMAGEWIDGET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @brief 放大查看控件 + * + * 支持滚轮缩放、拖拽平移、双击还原 + */ +class ImageZoomView : public QWidget +{ + Q_OBJECT + +public: + explicit ImageZoomView(QWidget* parent = nullptr); + + /** + * @brief 设置显示的图像 + * @param image 图像 + * @param title 标题(可选) + */ + void setImage(const QImage& image, const QString& title = ""); + + /** + * @brief 重置缩放到适合窗口 + */ + void resetZoom(); + +signals: + /** + * @brief 请求退出放大视图 + */ + void exitRequested(); + +protected: + void wheelEvent(QWheelEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + void mouseReleaseEvent(QMouseEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void keyPressEvent(QKeyEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void showEvent(QShowEvent* event) override; + +private: + void updateImageDisplay(); + + QScrollArea* m_scrollArea = nullptr; + QLabel* m_imageLabel = nullptr; + QLabel* m_titleLabel = nullptr; + QLabel* m_infoLabel = nullptr; + QImage m_originalImage; + double m_scaleFactor = 1.0; + bool m_dragging = false; + bool m_needResetZoom = false; + QPoint m_lastPos; + + static constexpr double MIN_SCALE = 0.1; + static constexpr double MAX_SCALE = 10.0; + static constexpr double SCALE_STEP = 0.1; +}; + +/** + * @brief 检测结果图像显示控件 + * + * 统一的图像显示组件,支持: + * - 图像显示与自适应缩放 + * - 点击放大查看(滚轮缩放、拖拽平移) + * - 右键菜单保存数据 + */ +class DetectImageWidget : public QWidget +{ + Q_OBJECT + +public: + /** + * @brief 保存数据回调函数类型 + * @param filePath 保存路径 + * @return true-保存成功,false-保存失败 + */ + using SaveDataCallback = std::function; + + explicit DetectImageWidget(QWidget* parent = nullptr); + ~DetectImageWidget(); + + /** + * @brief 设置显示的图像 + * @param image 图像 + */ + void setImage(const QImage& image); + + /** + * @brief 获取当前图像 + * @return 当前显示的图像 + */ + QImage getImage() const; + + /** + * @brief 清空图像 + */ + void clearImage(); + + /** + * @brief 设置标题(用于放大视图显示) + * @param title 标题 + */ + void setTitle(const QString& title); + + /** + * @brief 获取标题 + */ + QString title() const { return m_title; } + + /** + * @brief 启用/禁用保存数据菜单项 + * @param enabled 是否启用 + */ + void setSaveDataEnabled(bool enabled); + + /** + * @brief 设置保存数据回调函数 + * @param callback 回调函数 + */ + void setSaveDataCallback(const SaveDataCallback& callback); + + /** + * @brief 设置保存数据菜单项文本 + * @param text 菜单项文本 + */ + void setSaveDataMenuText(const QString& text); + + /** + * @brief 显示放大视图 + */ + void showZoomView(); + + /** + * @brief 显示普通视图 + */ + void showNormalView(); + +signals: + /** + * @brief 图像被点击 + */ + void clicked(); + + /** + * @brief 图像被右键点击 + */ + void rightClicked(); + + /** + * @brief 保存数据请求信号 + * @param widget 触发保存的控件 + */ + void saveDataRequested(DetectImageWidget* widget); + +protected: + void resizeEvent(QResizeEvent* event) override; + bool eventFilter(QObject* obj, QEvent* event) override; + +private slots: + void onSaveDataTriggered(); + void showContextMenu(const QPoint& pos); + +private: + void setupUI(); + void updateImageDisplay(); + + // 界面元素 + QStackedWidget* m_stackedWidget = nullptr; + QGraphicsView* m_graphicsView = nullptr; + QGraphicsScene* m_scene = nullptr; + ImageZoomView* m_zoomView = nullptr; + + // 右键菜单 + QMenu* m_contextMenu = nullptr; + QAction* m_saveDataAction = nullptr; + + // 数据 + QImage m_image; + QString m_title; + bool m_saveDataEnabled = false; + SaveDataCallback m_saveDataCallback; +}; + +#endif // DETECTIMAGEWIDGET_H diff --git a/AppUtils/UICommon/Inc/DetectLogHelper.h b/AppUtils/UICommon/Inc/DetectLogHelper.h new file mode 100644 index 0000000..069acdd --- /dev/null +++ b/AppUtils/UICommon/Inc/DetectLogHelper.h @@ -0,0 +1,111 @@ +#ifndef DETECTLOGHELPER_H +#define DETECTLOGHELPER_H + +#include +#include +#include +#include + +/** + * @brief 检测日志辅助类 + * + * 用于管理现有 QListView 的日志显示逻辑,支持: + * - 日志去重(相同消息显示计数,如 "x2", "x3") + * - 自动添加时间戳 + * - 自动滚动到底部 + * - 跨线程安全更新 + * + * 使用方式: + * 1. 在 mainwindow.h 中声明: DetectLogHelper* m_logHelper; + * 2. 在构造函数中初始化: m_logHelper = new DetectLogHelper(ui->detect_log, this); + * 3. 添加日志: m_logHelper->appendLog("消息"); + * 4. 清空日志: m_logHelper->clearLog(); + */ +class DetectLogHelper : public QObject +{ + Q_OBJECT + +public: + /** + * @brief 构造函数 + * @param listView 要管理的 QListView 指针(通常是 ui->detect_log) + * @param parent 父对象 + */ + explicit DetectLogHelper(QListView* listView, QObject *parent = nullptr); + ~DetectLogHelper(); + + /** + * @brief 添加日志消息(线程安全,可从任意线程调用) + * @param message 日志消息内容 + */ + void appendLog(const QString& message); + + /** + * @brief 清空所有日志(线程安全,可从任意线程调用) + */ + void clearLog(); + + /** + * @brief 设置日期格式 + * @param format 日期格式字符串,默认 "hh:mm:ss" + */ + void setTimestampFormat(const QString& format); + + /** + * @brief 设置是否显示时间戳 + * @param show 是否显示,默认 true + */ + void setShowTimestamp(bool show); + + /** + * @brief 设置是否启用日志去重 + * @param enable 是否启用,默认 true + */ + void setDeduplicationEnabled(bool enable); + + /** + * @brief 获取内部 QStringListModel 指针 + * @return QStringListModel 指针 + */ + QStringListModel* model() const; + +signals: + /** + * @brief 日志更新请求信号(内部使用,用于跨线程更新) + */ + void logUpdateRequested(const QString& message); + + /** + * @brief 日志清空请求信号(内部使用,用于跨线程更新) + */ + void logClearRequested(); + +private slots: + /** + * @brief 在UI线程中更新日志 + */ + void updateLogInUI(const QString& message); + + /** + * @brief 在UI线程中清空日志 + */ + void clearLogInUI(); + +private: + QListView* m_listView; + QStringListModel* m_logModel; + + // 日志去重相关 + QString m_lastLogMessage; // 最后一条日志消息(不含时间戳) + int m_lastLogCount; // 最后一条日志的重复次数 + + // 配置项 + QString m_timestampFormat; // 时间戳格式 + bool m_showTimestamp; // 是否显示时间戳 + bool m_deduplicationEnabled; // 是否启用去重 + + // 初始化信号槽连接 + void initConnections(); +}; + +#endif // DETECTLOGHELPER_H diff --git a/AppUtils/UICommon/Src/DetectImageWidget.cpp b/AppUtils/UICommon/Src/DetectImageWidget.cpp new file mode 100644 index 0000000..10426f4 --- /dev/null +++ b/AppUtils/UICommon/Src/DetectImageWidget.cpp @@ -0,0 +1,447 @@ +#include "DetectImageWidget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ============================================================================ +// ImageZoomView 实现 +// ============================================================================ +ImageZoomView::ImageZoomView(QWidget* parent) + : QWidget(parent) +{ + setStyleSheet("background-color: rgb(25, 26, 28);"); + + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(10, 10, 10, 10); + mainLayout->setSpacing(10); + + // 顶部标题栏 + QHBoxLayout* topLayout = new QHBoxLayout(); + + m_titleLabel = new QLabel(this); + m_titleLabel->setStyleSheet("QLabel { color: rgb(221, 225, 233); font-size: 16px; font-weight: bold; background: transparent; }"); + topLayout->addWidget(m_titleLabel); + + topLayout->addStretch(); + + // 返回按钮 + QPushButton* btnBack = new QPushButton(QString::fromUtf8("返回"), this); + btnBack->setFixedSize(80, 30); + btnBack->setStyleSheet( + "QPushButton { background-color: rgb(60, 62, 68); color: rgb(221, 225, 233); " + "border: 1px solid rgb(80, 82, 88); border-radius: 5px; font-size: 14px; }" + "QPushButton:hover { background-color: rgb(80, 82, 88); }" + "QPushButton:pressed { background-color: rgb(50, 52, 58); }" + ); + connect(btnBack, &QPushButton::clicked, this, &ImageZoomView::exitRequested); + topLayout->addWidget(btnBack); + + mainLayout->addLayout(topLayout); + + // 滚动区域 + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(false); + m_scrollArea->setAlignment(Qt::AlignCenter); + m_scrollArea->setStyleSheet( + "QScrollArea { background-color: rgb(38, 40, 47); border: none; }" + "QScrollBar:vertical { background-color: rgb(38, 40, 47); width: 12px; }" + "QScrollBar::handle:vertical { background-color: rgb(80, 82, 88); border-radius: 6px; min-height: 20px; }" + "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; }" + "QScrollBar:horizontal { background-color: rgb(38, 40, 47); height: 12px; }" + "QScrollBar::handle:horizontal { background-color: rgb(80, 82, 88); border-radius: 6px; min-width: 20px; }" + "QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0px; }" + ); + + m_imageLabel = new QLabel(m_scrollArea); + m_imageLabel->setAlignment(Qt::AlignCenter); + m_imageLabel->setStyleSheet("background-color: rgb(38, 40, 47);"); + m_imageLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + m_imageLabel->setScaledContents(false); + m_scrollArea->setWidget(m_imageLabel); + + mainLayout->addWidget(m_scrollArea, 1); + + // 底部信息栏 + m_infoLabel = new QLabel(this); + m_infoLabel->setStyleSheet("QLabel { color: rgb(180, 180, 180); font-size: 12px; background: transparent; }"); + m_infoLabel->setText(QString::fromUtf8("滚轮缩放 | 拖拽平移 | 双击还原 | 点击返回按钮退出")); + mainLayout->addWidget(m_infoLabel); + + setFocusPolicy(Qt::StrongFocus); +} + +void ImageZoomView::setImage(const QImage& image, const QString& title) +{ + m_originalImage = image; + m_titleLabel->setText(title); + m_needResetZoom = true; + + updateImageDisplay(); +} + +void ImageZoomView::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + if (m_needResetZoom && !m_originalImage.isNull()) { + m_needResetZoom = false; + QMetaObject::invokeMethod(this, &ImageZoomView::resetZoom, Qt::QueuedConnection); + } +} + +void ImageZoomView::updateImageDisplay() +{ + if (m_originalImage.isNull()) { + m_imageLabel->setText(QString::fromUtf8("无图像")); + return; + } + + int newWidth = static_cast(m_originalImage.width() * m_scaleFactor); + int newHeight = static_cast(m_originalImage.height() * m_scaleFactor); + + QImage scaledImage = m_originalImage.scaled(newWidth, newHeight, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + m_imageLabel->setPixmap(QPixmap::fromImage(scaledImage)); + m_imageLabel->resize(scaledImage.size()); + + QString info = QString::fromUtf8("缩放: %1% | 尺寸: %2x%3 | 滚轮缩放 | 拖拽平移 | 双击还原") + .arg(static_cast(m_scaleFactor * 100)) + .arg(m_originalImage.width()) + .arg(m_originalImage.height()); + m_infoLabel->setText(info); +} + +void ImageZoomView::resetZoom() +{ + if (m_originalImage.isNull()) return; + + QSize viewSize = m_scrollArea->viewport()->size(); + double scaleX = static_cast(viewSize.width() - 20) / m_originalImage.width(); + double scaleY = static_cast(viewSize.height() - 20) / m_originalImage.height(); + m_scaleFactor = qMin(scaleX, scaleY); + m_scaleFactor = qMax(m_scaleFactor, MIN_SCALE); + + updateImageDisplay(); + + m_scrollArea->horizontalScrollBar()->setValue( + (m_imageLabel->width() - m_scrollArea->viewport()->width()) / 2); + m_scrollArea->verticalScrollBar()->setValue( + (m_imageLabel->height() - m_scrollArea->viewport()->height()) / 2); +} + +void ImageZoomView::wheelEvent(QWheelEvent* event) +{ + if (m_originalImage.isNull()) return; + + double delta = event->angleDelta().y() > 0 ? SCALE_STEP : -SCALE_STEP; + + double newScale = m_scaleFactor + delta; + newScale = qMax(newScale, MIN_SCALE); + newScale = qMin(newScale, MAX_SCALE); + + if (qAbs(newScale - m_scaleFactor) > 0.001) { + QScrollBar* hBar = m_scrollArea->horizontalScrollBar(); + QScrollBar* vBar = m_scrollArea->verticalScrollBar(); + + double hRatio = hBar->maximum() > 0 ? static_cast(hBar->value()) / hBar->maximum() : 0.5; + double vRatio = vBar->maximum() > 0 ? static_cast(vBar->value()) / vBar->maximum() : 0.5; + + m_scaleFactor = newScale; + updateImageDisplay(); + + hBar->setValue(static_cast(hRatio * hBar->maximum())); + vBar->setValue(static_cast(vRatio * vBar->maximum())); + } + + event->accept(); +} + +void ImageZoomView::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + m_dragging = true; + m_lastPos = event->pos(); + setCursor(Qt::ClosedHandCursor); + } + QWidget::mousePressEvent(event); +} + +void ImageZoomView::mouseMoveEvent(QMouseEvent* event) +{ + if (m_dragging) { + QPoint delta = event->pos() - m_lastPos; + m_lastPos = event->pos(); + + QScrollBar* hBar = m_scrollArea->horizontalScrollBar(); + QScrollBar* vBar = m_scrollArea->verticalScrollBar(); + + hBar->setValue(hBar->value() - delta.x()); + vBar->setValue(vBar->value() - delta.y()); + } + QWidget::mouseMoveEvent(event); +} + +void ImageZoomView::mouseReleaseEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + m_dragging = false; + setCursor(Qt::ArrowCursor); + } + QWidget::mouseReleaseEvent(event); +} + +void ImageZoomView::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) { + resetZoom(); + } + QWidget::mouseDoubleClickEvent(event); +} + +void ImageZoomView::keyPressEvent(QKeyEvent* event) +{ + switch (event->key()) { + case Qt::Key_Escape: + emit exitRequested(); + break; + case Qt::Key_Plus: + case Qt::Key_Equal: + m_scaleFactor = qMin(m_scaleFactor + SCALE_STEP, MAX_SCALE); + updateImageDisplay(); + break; + case Qt::Key_Minus: + m_scaleFactor = qMax(m_scaleFactor - SCALE_STEP, MIN_SCALE); + updateImageDisplay(); + break; + case Qt::Key_0: + m_scaleFactor = 1.0; + updateImageDisplay(); + break; + case Qt::Key_F: + resetZoom(); + break; + default: + QWidget::keyPressEvent(event); + } +} + +void ImageZoomView::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + if (!m_originalImage.isNull()) { + resetZoom(); + } +} + +// ============================================================================ +// DetectImageWidget 实现 +// ============================================================================ +DetectImageWidget::DetectImageWidget(QWidget* parent) + : QWidget(parent) +{ + setupUI(); +} + +DetectImageWidget::~DetectImageWidget() +{ +} + +void DetectImageWidget::setupUI() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // 使用 QStackedWidget 切换普通视图和放大视图 + m_stackedWidget = new QStackedWidget(this); + + // 普通视图 - QGraphicsView + m_graphicsView = new QGraphicsView(this); + m_scene = new QGraphicsScene(this); + m_graphicsView->setScene(m_scene); + m_graphicsView->setRenderHint(QPainter::Antialiasing); + m_graphicsView->setRenderHint(QPainter::SmoothPixmapTransform); + m_graphicsView->setDragMode(QGraphicsView::NoDrag); + m_graphicsView->setStyleSheet("QGraphicsView { background-color: rgb(47, 48, 52); border: none; }"); + m_graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_graphicsView->setCursor(Qt::PointingHandCursor); + + // 设置点击事件过滤 + m_graphicsView->viewport()->installEventFilter(this); + + m_stackedWidget->addWidget(m_graphicsView); + + // 放大视图 + m_zoomView = new ImageZoomView(this); + connect(m_zoomView, &ImageZoomView::exitRequested, this, &DetectImageWidget::showNormalView); + m_stackedWidget->addWidget(m_zoomView); + + mainLayout->addWidget(m_stackedWidget); + + // 默认显示普通视图 + m_stackedWidget->setCurrentIndex(0); + + // 设置右键菜单 + m_graphicsView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_graphicsView, &QGraphicsView::customContextMenuRequested, + this, &DetectImageWidget::showContextMenu); + + // 创建右键菜单 + m_contextMenu = new QMenu(this); + m_saveDataAction = new QAction(QString::fromUtf8("保存检测数据"), this); + m_saveDataAction->setEnabled(false); + + // 设置菜单样式 + m_contextMenu->setStyleSheet( + "QMenu { background-color: rgb(60, 62, 68); color: rgb(221, 225, 233); border: 1px solid rgb(80, 82, 88); }" + "QMenu::item { padding: 8px 25px; }" + "QMenu::item:selected { background-color: rgb(80, 82, 88); }" + "QMenu::item:disabled { color: rgb(120, 120, 120); }" + ); + + m_contextMenu->addAction(m_saveDataAction); + connect(m_saveDataAction, &QAction::triggered, this, &DetectImageWidget::onSaveDataTriggered); +} + +bool DetectImageWidget::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == m_graphicsView->viewport()) { + if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent* mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + // 点击放大 + if (!m_image.isNull()) { + showZoomView(); + emit clicked(); + } + return true; + } + } + } + return QWidget::eventFilter(obj, event); +} + +void DetectImageWidget::setImage(const QImage& image) +{ + m_image = image; + updateImageDisplay(); +} + +QImage DetectImageWidget::getImage() const +{ + return m_image; +} + +void DetectImageWidget::clearImage() +{ + m_image = QImage(); + m_scene->clear(); +} + +void DetectImageWidget::setTitle(const QString& title) +{ + m_title = title; +} + +void DetectImageWidget::setSaveDataEnabled(bool enabled) +{ + m_saveDataEnabled = enabled; + m_saveDataAction->setEnabled(enabled); +} + +void DetectImageWidget::setSaveDataCallback(const SaveDataCallback& callback) +{ + m_saveDataCallback = callback; +} + +void DetectImageWidget::setSaveDataMenuText(const QString& text) +{ + m_saveDataAction->setText(text); +} + +void DetectImageWidget::showZoomView() +{ + if (m_image.isNull()) { + return; + } + + m_zoomView->setImage(m_image, m_title); + m_stackedWidget->setCurrentIndex(1); + m_zoomView->setFocus(); +} + +void DetectImageWidget::showNormalView() +{ + m_stackedWidget->setCurrentIndex(0); +} + +void DetectImageWidget::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + updateImageDisplay(); +} + +void DetectImageWidget::updateImageDisplay() +{ + if (m_image.isNull()) { + return; + } + + m_scene->clear(); + + QPixmap pixmap = QPixmap::fromImage(m_image); + m_scene->addPixmap(pixmap); + m_scene->setSceneRect(pixmap.rect()); + + // 自适应显示 + m_graphicsView->fitInView(m_scene->sceneRect(), Qt::KeepAspectRatio); +} + +void DetectImageWidget::showContextMenu(const QPoint& pos) +{ + // 检查是否有数据可保存 + if (!m_saveDataEnabled) { + return; + } + + m_contextMenu->exec(m_graphicsView->mapToGlobal(pos)); +} + +void DetectImageWidget::onSaveDataTriggered() +{ + // 发射信号,让外部处理保存逻辑 + emit saveDataRequested(this); + + // 如果设置了回调,也调用回调 + if (m_saveDataCallback) { + // 获取默认保存路径 + QString defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss"); + QString defaultFileName = QString("%1/detect_data_%2.pcd").arg(defaultPath).arg(timestamp); + + QString filePath = QFileDialog::getSaveFileName( + this, + QString::fromUtf8("保存检测数据"), + defaultFileName, + QString::fromUtf8("点云文件 (*.pcd);;所有文件 (*.*)") + ); + + if (!filePath.isEmpty()) { + m_saveDataCallback(filePath); + } + } +} diff --git a/AppUtils/UICommon/Src/DetectLogHelper.cpp b/AppUtils/UICommon/Src/DetectLogHelper.cpp new file mode 100644 index 0000000..323f37e --- /dev/null +++ b/AppUtils/UICommon/Src/DetectLogHelper.cpp @@ -0,0 +1,132 @@ +#include "DetectLogHelper.h" + +DetectLogHelper::DetectLogHelper(QListView* listView, QObject *parent) + : QObject(parent) + , m_listView(listView) + , m_logModel(nullptr) + , m_lastLogCount(0) + , m_timestampFormat("hh:mm:ss") + , m_showTimestamp(true) + , m_deduplicationEnabled(true) +{ + if (m_listView) { + // 创建并设置 QStringListModel + m_logModel = new QStringListModel(this); + m_listView->setModel(m_logModel); + + // 设置 QListView 属性 + m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_listView->setSelectionMode(QAbstractItemView::NoSelection); + m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + } + + initConnections(); +} + +DetectLogHelper::~DetectLogHelper() +{ +} + +void DetectLogHelper::initConnections() +{ + // 连接信号槽,支持跨线程更新 + connect(this, &DetectLogHelper::logUpdateRequested, + this, &DetectLogHelper::updateLogInUI, + Qt::QueuedConnection); + + connect(this, &DetectLogHelper::logClearRequested, + this, &DetectLogHelper::clearLogInUI, + Qt::QueuedConnection); +} + +void DetectLogHelper::appendLog(const QString& message) +{ + // 通过信号槽机制,确保在UI线程中更新 + emit logUpdateRequested(message); +} + +void DetectLogHelper::clearLog() +{ + // 通过信号槽机制,确保在UI线程中清空 + emit logClearRequested(); +} + +void DetectLogHelper::setTimestampFormat(const QString& format) +{ + m_timestampFormat = format; +} + +void DetectLogHelper::setShowTimestamp(bool show) +{ + m_showTimestamp = show; +} + +void DetectLogHelper::setDeduplicationEnabled(bool enable) +{ + m_deduplicationEnabled = enable; +} + +QStringListModel* DetectLogHelper::model() const +{ + return m_logModel; +} + +void DetectLogHelper::updateLogInUI(const QString& message) +{ + if (!m_logModel || !m_listView) return; + + // 获取当前数据 + QStringList logList = m_logModel->stringList(); + + // 构建日志条目 + QString logEntry; + + if (m_deduplicationEnabled && message == m_lastLogMessage && !logList.isEmpty()) { + // 相同消息,增加计数并替换最后一条 + m_lastLogCount++; + + if (m_showTimestamp) { + QString timestamp = QDateTime::currentDateTime().toString(m_timestampFormat); + logEntry = QString("[%1] %2 (x%3)").arg(timestamp).arg(message).arg(m_lastLogCount); + } else { + logEntry = QString("%1 (x%2)").arg(message).arg(m_lastLogCount); + } + + // 替换最后一条 + logList[logList.size() - 1] = logEntry; + } else { + // 新消息,重置计数 + m_lastLogMessage = message; + m_lastLogCount = 1; + + if (m_showTimestamp) { + QString timestamp = QDateTime::currentDateTime().toString(m_timestampFormat); + logEntry = QString("[%1] %2").arg(timestamp).arg(message); + } else { + logEntry = message; + } + + // 添加新的日志条目 + logList.append(logEntry); + } + + // 更新模型 + m_logModel->setStringList(logList); + + // 自动滚动到最底部 + if (!logList.isEmpty()) { + QModelIndex lastIndex = m_logModel->index(logList.size() - 1); + m_listView->scrollTo(lastIndex); + } +} + +void DetectLogHelper::clearLogInUI() +{ + if (m_logModel) { + m_logModel->setStringList(QStringList()); + } + + // 重置日志计数器 + m_lastLogMessage.clear(); + m_lastLogCount = 0; +} diff --git a/AppUtils/UICommon/UICommon.pro b/AppUtils/UICommon/UICommon.pro index 3bb9a38..01da5b6 100644 --- a/AppUtils/UICommon/UICommon.pro +++ b/AppUtils/UICommon/UICommon.pro @@ -29,7 +29,9 @@ HEADERS += \ Inc/CommonDialogCamera.h \ Inc/CommonDialogCameraLevel.h \ Inc/DialogImageViewer.h \ - Inc/AuthView.h + Inc/AuthView.h \ + Inc/DetectLogHelper.h \ + Inc/DetectImageWidget.h SOURCES += \ Src/StyledMessageBox.cpp \ @@ -38,7 +40,9 @@ SOURCES += \ Src/CommonDialogCamera.cpp \ Src/CommonDialogCameraLevel.cpp \ Src/DialogImageViewer.cpp \ - Src/AuthView.cpp + Src/AuthView.cpp \ + Src/DetectLogHelper.cpp \ + Src/DetectImageWidget.cpp FORMS += \ DeviceStatusWidget.ui \