From cca3e30b7d8d1d23cb5429adb607ad8e42af07e0 Mon Sep 17 00:00:00 2001 From: yiyi Date: Sun, 30 Nov 2025 15:38:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=95=E8=A3=82=E5=8D=8F=E8=AE=AE=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=BF=AE=E6=94=B9=EF=BC=9B=E6=92=95=E8=A3=82=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BF=AE=E6=94=B9=EF=BC=9B=E6=92=95=E8=A3=82=E7=9A=84?= =?UTF-8?q?=E6=B8=A9=E5=BA=A6=E4=BC=A0=E6=84=9F=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/BeltTearing/BeltTearingApp/Version.h | 4 +- App/BeltTearing/BeltTearingApp/mainwindow.ui | 72 ++-- .../widgets/ImageGridWidget.cpp | 83 +++-- .../widgets/ImageTileWidget.cpp | 41 ++- .../BeltTearingApp/widgets/ImageTileWidget.h | 5 + .../Inc/IVrBeltTearingConfig.h | 16 + .../Src/VrBeltTearingConfig.cpp | 16 + .../BeltTearingPresenter.cpp | 274 +++++++++++---- .../BeltTearingServer/BeltTearingPresenter.h | 31 +- .../BeltTearingServer/BeltTearingServer.pro | 3 + .../RobotProtocolSimplified.cpp | 205 +++++++++++ .../RobotProtocolSimplified.h | 144 ++++++++ App/BeltTearing/BeltTearingServer/Version.h | 4 +- App/BeltTearing/BeltTearingServer/Version.md | 5 + App/BeltTearing/BeltTearingServer/main.cpp | 4 +- App/BeltTearing/Doc/ModbusTCP协议配置说明.md | 86 +++++ .../Doc/撕裂ModbusTCP简化协议文档.md | 89 +++-- App/BeltTearing/Doc/撕裂TCP通信协议.md | 8 +- AppUtils/AppCommon/AppCommon.pro | 6 +- AppUtils/AppCommon/Inc/ConfigEncryption.h | 82 +++++ AppUtils/AppCommon/Inc/PathManager.h | 55 +++ AppUtils/AppCommon/Src/ConfigEncryption.cpp | 331 ++++++++++++++++++ AppUtils/AppCommon/Src/PathManager.cpp | 243 ++++++++++++- GrabBagPrj/BeltTearingApp.iss | 2 +- 24 files changed, 1594 insertions(+), 215 deletions(-) create mode 100644 App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.cpp create mode 100644 App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.h create mode 100644 App/BeltTearing/Doc/ModbusTCP协议配置说明.md create mode 100644 AppUtils/AppCommon/Inc/ConfigEncryption.h create mode 100644 AppUtils/AppCommon/Src/ConfigEncryption.cpp diff --git a/App/BeltTearing/BeltTearingApp/Version.h b/App/BeltTearing/BeltTearingApp/Version.h index 0029e93..1d37d8c 100644 --- a/App/BeltTearing/BeltTearingApp/Version.h +++ b/App/BeltTearing/BeltTearingApp/Version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define BELT_TEARING_APP_VERSION_STRING "2.0.4" -#define BELT_TEARING_APP_VERSION_BUILD "2" +#define BELT_TEARING_APP_VERSION_STRING "2.0.5" +#define BELT_TEARING_APP_VERSION_BUILD "1" #define BELT_TEARING_APP_PRODUCT_NAME "BeltTearingApp" #define BELT_TEARING_APP_COMPANY_NAME "VisionRobot" #define BELT_TEARING_APP_COPYRIGHT "Copyright (C) 2024-2025 VisionRobot. All rights reserved." diff --git a/App/BeltTearing/BeltTearingApp/mainwindow.ui b/App/BeltTearing/BeltTearingApp/mainwindow.ui index ec6e724..593a436 100644 --- a/App/BeltTearing/BeltTearingApp/mainwindow.ui +++ b/App/BeltTearing/BeltTearingApp/mainwindow.ui @@ -45,7 +45,7 @@ background-color: rgba(255, 255, 255, 0); - 960 + 1100 21 220 80 @@ -68,7 +68,7 @@ border: none; - 440 + 580 21 220 80 @@ -105,32 +105,32 @@ border: none; - - - 1470 - 20 - 271 - 81 - - - - - 24 - - - - color: rgb(255, 255, 255); - - - 工作状态 - - - Qt::AlignCenter - - - false - - + + + 1470 + 20 + 271 + 81 + + + + + 24 + + + + false + + + color: rgb(255, 255, 255); + + + 工作状态 + + + Qt::AlignCenter + + @@ -179,6 +179,9 @@ background-color: rgba(255, 255, 255, 0); 18 + + false + background-image: url(:/common/resource/start.png); background-color: rgba(255, 255, 255, 0); @@ -186,9 +189,6 @@ background-color: rgba(255, 255, 255, 0); - - false - @@ -204,6 +204,9 @@ background-color: rgba(255, 255, 255, 0); 18 + + false + background-image: url(:/common/resource/stop.png); background-color: rgba(255, 255, 255, 0); @@ -211,14 +214,11 @@ background-color: rgba(255, 255, 255, 0); - - false - - 700 + 840 21 220 80 @@ -311,7 +311,7 @@ border: none; 0 0 1920 - 19 + 21 diff --git a/App/BeltTearing/BeltTearingApp/widgets/ImageGridWidget.cpp b/App/BeltTearing/BeltTearingApp/widgets/ImageGridWidget.cpp index f63e334..edb9fce 100644 --- a/App/BeltTearing/BeltTearingApp/widgets/ImageGridWidget.cpp +++ b/App/BeltTearing/BeltTearingApp/widgets/ImageGridWidget.cpp @@ -10,8 +10,8 @@ ImageGridWidget::ImageGridWidget(QWidget* parent) : QWidget(parent) { m_layout = new QGridLayout(this); m_layout->setContentsMargins(0, 0, 0, 0); - m_layout->setHorizontalSpacing(0); - m_layout->setVerticalSpacing(0); + m_layout->setHorizontalSpacing(4); // 左右保留间距 + m_layout->setVerticalSpacing(0); // 上下无间距 setLayout(m_layout); // 设置控件大小策略为可扩展 @@ -64,17 +64,26 @@ void ImageGridWidget::initImages(int count) { m_noImageLabel->hide(); - // 初始化指定数量的格子 - m_columns = qMax(1, qMin(2, count)); - m_rows = (count + m_columns - 1) / m_columns; - + // 初始化指定数量的格子,改为竖向布局,最多2行 + m_rows = qMax(1, qMin(2, count)); + m_columns = (count + m_rows - 1) / m_rows; + for (int i = 0; i < count; ++i) { ImageTileWidget* tile = new ImageTileWidget(this); - int r = i / m_columns; - int c = i % m_columns; - // 第一排靠底部,第二排靠顶部 - Qt::Alignment align = (r == 0) ? Qt::AlignBottom : Qt::AlignTop; - m_layout->addWidget(tile, r, c, align); + // 列优先布局:先填满一列再填下一列 + int c = i / m_rows; + int r = i % m_rows; + + // 奇数排(行号为偶数)图片靠下显示,偶数排(行号为奇数)图片靠上显示 + Qt::Alignment imageAlign = (r % 2 == 0) ? Qt::AlignBottom : Qt::AlignTop; + tile->setImageAlignment(imageAlign); + + // 设置固定尺寸 + tile->setFixedSize(m_sizeNormal); + tile->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + // 控件在网格中居中显示 + m_layout->addWidget(tile, r, c, Qt::AlignCenter); m_tiles.append(tile); connect(tile, &ImageTileWidget::clicked, this, [this, i]() { @@ -130,10 +139,10 @@ void ImageGridWidget::rebuildGrid() { } m_tiles.clear(); - // Determine grid size: up to 2 columns + // Determine grid size: up to 2 rows (vertical layout) int n = m_paths.size(); - m_columns = qMax(1, qMin(2, n)); - m_rows = (n + m_columns - 1) / m_columns; + m_rows = qMax(1, qMin(2, n)); + m_columns = (n + m_rows - 1) / m_rows; // 如果没有路径,显示提示信息 if (n <= 0) { @@ -141,7 +150,7 @@ void ImageGridWidget::rebuildGrid() { m_noImageLabel->show(); return; } - + m_noImageLabel->hide(); for (int i = 0; i < n; ++i) { @@ -149,11 +158,21 @@ void ImageGridWidget::rebuildGrid() { tile->setImagePath(m_paths.at(i)); tile->setSelected(i == m_selectedIndex); tile->setExpanded(i == m_expandedIndex); - int r = i / m_columns; - int c = i % m_columns; - // 第一排靠底部,第二排靠顶部 - Qt::Alignment align = (r == 0) ? Qt::AlignBottom : Qt::AlignTop; - m_layout->addWidget(tile, r, c, align); + + // 列优先布局:先填满一列再填下一列 + int c = i / m_rows; + int r = i % m_rows; + + // 奇数排(行号为偶数)图片靠下显示,偶数排(行号为奇数)图片靠上显示 + Qt::Alignment imageAlign = (r % 2 == 0) ? Qt::AlignBottom : Qt::AlignTop; + tile->setImageAlignment(imageAlign); + + // 设置固定尺寸,确保控件大小一致 + tile->setFixedSize(m_sizeNormal); + tile->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + // 控件在网格中居中显示 + m_layout->addWidget(tile, r, c, Qt::AlignCenter); m_tiles.append(tile); connect(tile, &ImageTileWidget::clicked, this, [this, i]() { @@ -206,6 +225,7 @@ void ImageGridWidget::updateTileSizes() { int horizontalSpacingTotal = m_layout->horizontalSpacing() * (m_columns - 1); int verticalSpacingTotal = m_layout->verticalSpacing() * (m_rows - 1); + // 对于竖向布局,优先保证高度分配合理,因为最多只有2行 int baseTileWidth = qMax(100, (availableWidth - horizontalSpacingTotal) / m_columns); int baseTileHeight = qMax(100, (availableHeight - verticalSpacingTotal) / m_rows); @@ -238,19 +258,20 @@ void ImageGridWidget::updateTileSizes() { } else { // 恢复正常布局 m_layout->removeWidget(t); - int r = i / m_columns; - int c = i % m_columns; - // 第一排靠底部,第二排靠顶部 - Qt::Alignment align = (r == 0) ? Qt::AlignBottom : Qt::AlignTop; - m_layout->addWidget(t, r, c, align); + // 列优先布局:先填满一列再填下一列 + int c = i / m_rows; + int r = i % m_rows; - // 所有格子都使用正常尺寸,不因选中而放大 + // 奇数排(行号为偶数)图片靠下显示,偶数排(行号为奇数)图片靠上显示 + Qt::Alignment imageAlign = (r % 2 == 0) ? Qt::AlignBottom : Qt::AlignTop; + t->setImageAlignment(imageAlign); + + // 控件在网格中居中显示 + m_layout->addWidget(t, r, c, Qt::AlignCenter); + + // 设置固定尺寸 t->setFixedSize(m_sizeNormal); - - // 所有格子都设置为可扩展,以便随窗口大小变化 - t->setMinimumSize(100, 100); - t->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); - t->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + t->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); } } diff --git a/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.cpp b/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.cpp index a6d96a0..b70ed37 100644 --- a/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.cpp +++ b/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.cpp @@ -13,7 +13,10 @@ ImageTileWidget::ImageTileWidget(QWidget* parent) : QWidget(parent) { setMinimumSize(120, 120); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - + + // 设置零边距,确保控件之间无缝隙 + setContentsMargins(0, 0, 0, 0); + // 创建缩小按钮 m_shrinkButton = new QPushButton("×", this); m_shrinkButton->setFixedSize(24, 24); @@ -77,7 +80,7 @@ QSize ImageTileWidget::sizeHint() const { void ImageTileWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); - + // 绘制背景 p.fillRect(rect(), QColor(37, 38, 42)); @@ -102,16 +105,30 @@ void ImageTileWidget::paintEvent(QPaintEvent*) { } } else { - // 正常模式:图片完全显示,不拉伸 - QRect inner = rect().adjusted(0, 0, 0, 0); + // 正常模式:根据对齐方式显示图片,保持比例不变形,完整显示不裁剪 + QRect inner = rect(); if (!m_pix.isNull()) { - // 使用KeepAspectRatio保持比例,确保整个图片可见 + // 使用KeepAspectRatio保持比例,确保整个图片可见,不裁剪不拉伸 QPixmap scaled = m_pix.scaled(inner.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); - - // 计算居中位置 + + // 水平居中 int x = inner.center().x() - scaled.width() / 2; int y = inner.center().y() - scaled.height() / 2; - + + // 根据对齐方式调整垂直位置 + if (m_imageAlignment & Qt::AlignTop) { + y = inner.top(); + } else if (m_imageAlignment & Qt::AlignBottom) { + y = inner.bottom() - scaled.height(); + } + + // 水平对齐调整 + if (m_imageAlignment & Qt::AlignLeft) { + x = inner.left(); + } else if (m_imageAlignment & Qt::AlignRight) { + x = inner.right() - scaled.width(); + } + p.drawPixmap(x, y, scaled); } else { p.setPen(Qt::white); @@ -136,12 +153,8 @@ void ImageTileWidget::paintEvent(QPaintEvent*) { p.drawText(textRect, Qt::AlignCenter, fileName); } } - - // 绘制边框 - QPen border(QColor(0, 0, 0)); - border.setWidth(1); - p.setPen(border); - p.drawRoundedRect(rect().adjusted(1,1,-1,-1), 1, 1); + + // 不绘制边框,避免产生视觉间隙 } void ImageTileWidget::mousePressEvent(QMouseEvent* ev) { diff --git a/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.h b/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.h index 5527275..e65f928 100644 --- a/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.h +++ b/App/BeltTearing/BeltTearingApp/widgets/ImageTileWidget.h @@ -24,6 +24,10 @@ public: void setExpanded(bool expanded); bool isExpanded() const { return m_expanded; } + // 设置图片对齐方式(用于奇偶排交错显示) + void setImageAlignment(Qt::Alignment align) { m_imageAlignment = align; update(); } + Qt::Alignment imageAlignment() const { return m_imageAlignment; } + QSize sizeHint() const override; signals: @@ -44,5 +48,6 @@ private: QPixmap m_pix; bool m_selected {false}; bool m_expanded {false}; + Qt::Alignment m_imageAlignment {Qt::AlignCenter}; // 图片对齐方式,默认居中 QPushButton* m_shrinkButton {nullptr}; }; diff --git a/App/BeltTearing/BeltTearingConfig/Inc/IVrBeltTearingConfig.h b/App/BeltTearing/BeltTearingConfig/Inc/IVrBeltTearingConfig.h index 4a2ad0f..8375120 100644 --- a/App/BeltTearing/BeltTearingConfig/Inc/IVrBeltTearingConfig.h +++ b/App/BeltTearing/BeltTearingConfig/Inc/IVrBeltTearingConfig.h @@ -17,6 +17,15 @@ enum class BeltTearingProjectType BeltMonitoring = 1, // 皮带监控 }; +/** + * @brief ModbusTCP协议类型枚举 + */ +enum class ModbusTCPProtocolType +{ + Standard = 0, // 标准协议(支持5个撕裂信息,每个包含ID、状态、宽度、深度) + Simplified = 1, // 简化协议(仅报警标志、最大长度、最大宽度、最大撕裂ID和复位) +}; + enum class ByteDataType { Text = 0x01, Image = 0x02, @@ -130,9 +139,16 @@ struct BeltTearingConfigResult std::vector cameras; // 相机列表 QueueProcessParam queueProcessParam; // 队列处理参数 SerialPortParam serialPortParam; // 串口参数(用于 Modbus RTU) + ModbusTCPProtocolType modbusTCPProtocol; // ModbusTCP协议类型(默认使用简化协议) int serverPort = 5900; // 上下位机通信端口 int tcpPort = 5800; // 客户协议-TCP协议端口(新协议-TearingTcpProtocol) + + // 构造函数,设置默认值 + BeltTearingConfigResult() + : projectType(BeltTearingProjectType::BeltTearing) + , modbusTCPProtocol(ModbusTCPProtocolType::Simplified) // 默认使用简化协议 + {} }; /** diff --git a/App/BeltTearing/BeltTearingConfig/Src/VrBeltTearingConfig.cpp b/App/BeltTearing/BeltTearingConfig/Src/VrBeltTearingConfig.cpp index a54a8e3..a28eeb5 100644 --- a/App/BeltTearing/BeltTearingConfig/Src/VrBeltTearingConfig.cpp +++ b/App/BeltTearing/BeltTearingConfig/Src/VrBeltTearingConfig.cpp @@ -184,6 +184,17 @@ BeltTearingConfigResult VrBeltTearingConfig::LoadConfig(const std::string& fileP result.tcpPort = 5901; // 默认值 } xml.skipCurrentElement(); + } else if (xml.name() == "ModbusTCPProtocol") { + // 解析ModbusTCP协议类型,默认为简化协议 + QString protocolStr = xml.attributes().value("type").toString().toLower(); + if (protocolStr == "standard") { + result.modbusTCPProtocol = ModbusTCPProtocolType::Standard; + } else { + result.modbusTCPProtocol = ModbusTCPProtocolType::Simplified; // 默认简化协议 + } + xml.skipCurrentElement(); + } else { + xml.skipCurrentElement(); } } } @@ -324,6 +335,11 @@ bool VrBeltTearingConfig::SaveConfig(const std::string& filePath, BeltTearingCon xml.writeStartElement("TcpPort"); xml.writeAttribute("port", QString::number(configResult.tcpPort)); xml.writeEndElement(); // TcpPort + // 保存ModbusTCP协议类型 + xml.writeStartElement("ModbusTCPProtocol"); + QString protocolTypeStr = (configResult.modbusTCPProtocol == ModbusTCPProtocolType::Standard) ? "standard" : "simplified"; + xml.writeAttribute("type", protocolTypeStr); + xml.writeEndElement(); // ModbusTCPProtocol xml.writeEndElement(); // LocalServerConfig // 保存相机配置 diff --git a/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.cpp b/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.cpp index f73586b..e9d01d5 100644 --- a/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.cpp +++ b/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "IVrUtils.h" @@ -150,6 +151,13 @@ BeltTearingPresenter::~BeltTearingPresenter() m_pRobotProtocol = nullptr; } + // 释放RobotProtocolSimplified资源 + if (m_pRobotProtocolSimplified) { + m_pRobotProtocolSimplified->Deinitialize(); + delete m_pRobotProtocolSimplified; + m_pRobotProtocolSimplified = nullptr; + } + // 释放ModbusRTUMaster资源 if (m_pModbusRTUMaster) { m_pModbusRTUMaster->Deinitialize(); @@ -315,6 +323,10 @@ void BeltTearingPresenter::sendSimulationData() tear1.tearStatus = keSG_tearStatus_New; // 假设1表示有效撕裂 tear1.tearWidth = 15.5f; // 撕裂宽度 15.5mm tear1.tearDepth = 8.2f; // 撕裂深度 8.2mm + tear1.roi.left = 0.0f; + tear1.roi.top = 0.0f; + tear1.roi.right = 60.0f; + tear1.roi.bottom = 1.0f; // 其他字段由于结构定义不明确,暂时不填充 simulationResults.push_back(tear1); @@ -325,20 +337,34 @@ void BeltTearingPresenter::sendSimulationData() tear2.tearStatus = keSG_tearStatus_Growing; // 假设2表示无效撕裂 tear2.tearWidth = 22.3f; // 撕裂宽度 22.3mm tear2.tearDepth = 12.7f; // 撕裂深度 12.7mm + tear2.roi.left = 20.0f; + tear2.roi.top = 0.0f; + tear2.roi.right = 50.0f; + tear2.roi.bottom = 1.0f; // 其他字段由于结构定义不明确,暂时不填充 simulationResults.push_back(tear2); - - LOG_INFO("Simulation tearing data sent successfully\n"); - // 创建一个模拟的图像 - QImage simulationImage(800, 600, QImage::Format_RGB32); - + QImage simulationImage(800, 600, QImage::Format_RGB888); + // 填充背景色 simulationImage.fill(Qt::white); - - // 创建 QPainter 对象用于绘制 + + // 获取当前时间 + QDateTime currentTime = QDateTime::currentDateTime(); + QString timeString = currentTime.toString("yyyy-MM-dd hh:mm:ss"); + + // 在图像上绘制时间信息 QPainter painter(&simulationImage); + if (painter.isActive()) { + QFont font("Arial", 16, QFont::Bold); + painter.setFont(font); + painter.setPen(Qt::black); + + // 在图像顶部绘制时间 + painter.drawText(QRect(10, 10, 780, 40), Qt::AlignLeft | Qt::AlignVCenter, QString("时间: %1").arg(timeString)); + painter.end(); + } // 发送撕裂结果到所有客户端 sendTearingResults(simulationResults); @@ -346,8 +372,8 @@ void BeltTearingPresenter::sendSimulationData() sendImageToClients(simulationImage); m_tearingProtocol->sendDetectResult(simulationResults, simulationImage); - - LOG_INFO("Simulation image data sent successfully\n"); + + LOG_INFO("Simulation image data sent successfully (timestamp: %s)\n", timeString.toStdString().c_str()); } bool BeltTearingPresenter::initializeCamera() @@ -1295,6 +1321,16 @@ void BeltTearingPresenter::ResetDetect() } } + // 清空简化协议的报警数据 + if (m_pRobotProtocolSimplified) { + int ret = m_pRobotProtocolSimplified->ClearAlarmData(); + if (ret != 0) { + LOG_WARNING("Failed to clear simplified protocol alarm data during reset, error code: %d\n", ret); + } else { + LOG_INFO("Simplified protocol alarm data cleared during reset\n"); + } + } + result = startCamera(); if (result != 0) { LOG_WARNING("Failed to start camera after reset, error code: %d\n", result); @@ -1591,47 +1627,81 @@ void BeltTearingPresenter::SendTemperatureData(float temperature) int BeltTearingPresenter::InitRobotProtocol() { LOG_DEBUG("Start initializing robot protocol\n"); - - // 创建RobotProtocol实例 - if(nullptr == m_pRobotProtocol){ - m_pRobotProtocol = new RobotProtocol(); + + // 根据配置选择协议类型 + m_modbusTCPProtocolType = m_configResult.modbusTCPProtocol; + + int nRet = 0; + + if (m_modbusTCPProtocolType == ModbusTCPProtocolType::Simplified) { + // 使用简化协议 + LOG_INFO("Using simplified ModbusTCP protocol\n"); + + // 创建RobotProtocolSimplified实例 + if (nullptr == m_pRobotProtocolSimplified) { + m_pRobotProtocolSimplified = new RobotProtocolSimplified(); + } + + // 初始化协议服务(使用端口502) + nRet = m_pRobotProtocolSimplified->Initialize(502); + + // 设置连接状态回调 + m_pRobotProtocolSimplified->SetConnectionCallback([this](bool connected) { + this->OnRobotConnectionChanged(connected); + }); + + // 设置复位回调 + m_pRobotProtocolSimplified->SetResetCallback([this]() { + LOG_INFO("ModbusTCP simplified protocol: Reset command received\n"); + this->ResetDetect(); + }); + + } else { + // 使用标准协议 + LOG_INFO("Using standard ModbusTCP protocol\n"); + + // 创建RobotProtocol实例 + if (nullptr == m_pRobotProtocol) { + m_pRobotProtocol = new RobotProtocol(); + } + + // 初始化协议服务(使用端口502) + nRet = m_pRobotProtocol->Initialize(502); + + // 设置连接状态回调 + m_pRobotProtocol->SetConnectionCallback([this](bool connected) { + this->OnRobotConnectionChanged(connected); + }); + + // 设置工作信号回调 + m_pRobotProtocol->SetWorkSignalCallback([this](bool startWork, int cameraIndex) { + return this->OnRobotWorkSignal(startWork, cameraIndex); + }); + + // 设置系统控制回调 + m_pRobotProtocol->SetSystemControlCallback([this](uint16_t command) { + switch(command) { + case 0: // 停止工作 + LOG_INFO("ModbusTCP command: Stop work\n"); + this->StopWork(); + break; + case 1: // 开始工作 + LOG_INFO("ModbusTCP command: Start work\n"); + this->StartWork(); + break; + case 2: // 复位检测 + LOG_INFO("ModbusTCP command: Reset detect\n"); + this->ResetDetect(); + break; + default: + LOG_WARNING("Unknown ModbusTCP command: %d\n", command); + break; + } + }); } - // 初始化协议服务(使用端口502) - int nRet = m_pRobotProtocol->Initialize(502); - - // 设置连接状态回调 - m_pRobotProtocol->SetConnectionCallback([this](bool connected) { - this->OnRobotConnectionChanged(connected); - }); - - // 设置工作信号回调 - m_pRobotProtocol->SetWorkSignalCallback([this](bool startWork, int cameraIndex) { - return this->OnRobotWorkSignal(startWork, cameraIndex); - }); - - // 设置系统控制回调 - m_pRobotProtocol->SetSystemControlCallback([this](uint16_t command) { - switch(command) { - case 0: // 停止工作 - LOG_INFO("ModbusTCP command: Stop work\n"); - this->StopWork(); - break; - case 1: // 开始工作 - LOG_INFO("ModbusTCP command: Start work\n"); - this->StartWork(); - break; - case 2: // 复位检测 - LOG_INFO("ModbusTCP command: Reset detect\n"); - this->ResetDetect(); - break; - default: - LOG_WARNING("Unknown ModbusTCP command: %d\n", command); - break; - } - }); - - LOG_INFO("Robot protocol initialization completed successfully\n"); + LOG_INFO("Robot protocol initialization completed successfully (type: %s)\n", + m_modbusTCPProtocolType == ModbusTCPProtocolType::Simplified ? "Simplified" : "Standard"); return nRet; } @@ -1657,31 +1727,93 @@ bool BeltTearingPresenter::OnRobotWorkSignal(bool startWork, int cameraIndex) } // 发送检测结果到机械臂 +/** + * @brief 发送检测结果到机械臂 + * @param detectionResults 检测到的所有撕裂信息 + * + * 功能说明: + * 1. 根据协议类型发送数据(简化协议或标准协议) + * 2. 在简化协议中,使用ROI的宽度和高度中的较大值找出最大的撕裂区域 + * 3. 使用对角线长度作为撕裂大小的更准确表示 + */ void BeltTearingPresenter::SendDetectionResultToRobot(const std::vector& detectionResults) { - // 检查RobotProtocol是否已初始化且连接正常 - if (!m_pRobotProtocol || !m_bRobotConnected) { - LOG_WARNING("Robot protocol not initialized or not connected, cannot send detection results\n"); - return; + // 根据协议类型发送数据 + if (m_modbusTCPProtocolType == ModbusTCPProtocolType::Simplified) { + // 简化协议:只发送最大撕裂信息 + if (!m_pRobotProtocolSimplified) { + LOG_WARNING("Simplified robot protocol not initialized, cannot send detection results\n"); + return; + } + + RobotProtocolSimplified::TearingAlarmData alarmData; + + if (!detectionResults.empty()) { + // 找出最大撕裂(使用宽度和高度中的较大值) + auto maxTearIt = std::max_element(detectionResults.begin(), detectionResults.end(), + [](const SSG_beltTearingInfo& a, const SSG_beltTearingInfo& b) { + // 计算撕裂区域的宽度和高度,取较大值 + double widthA = a.roi.right - a.roi.left; + double lengthA = a.roi.bottom - a.roi.top; + double maxSizeA = widthA > lengthA ? widthA : lengthA; + + double widthB = b.roi.right - b.roi.left; + double lengthB = b.roi.bottom - b.roi.top; + double maxSizeB = widthB > lengthB ? widthB : lengthB; + + // 返回true表示a应该排在b之前,即a的最大边长比b小 + return maxSizeA < maxSizeB; + }); + + // 设置报警标志 + alarmData.alarmFlag = 1; // 有撕裂报警 + + // 计算撕裂区域的宽度和长度 + double dWidth = maxTearIt->roi.right - maxTearIt->roi.left; + double dLength = maxTearIt->roi.bottom - maxTearIt->roi.top; + // 计算撕裂区域的对角线长度,作为撕裂大小的更准确表示 + double diagonalLength = std::sqrt(dWidth * dWidth + dLength * dLength); + alarmData.maxLength = static_cast(diagonalLength); // 最大长度(毫米) + alarmData.maxWidth = static_cast(maxTearIt->tearWidth); // 最大宽度(毫米) + alarmData.maxId = maxTearIt->tearID; // 最大撕裂ID + } else { + // 没有撕裂,清空报警 + alarmData.alarmFlag = 0; + alarmData.maxLength = 0; + alarmData.maxWidth = 0; + alarmData.maxId = 0; + + LOG_DEBUG("No tearing detected, clearing alarm\n"); + } + + // 发送报警数据 + m_pRobotProtocolSimplified->SetAlarmData(alarmData); + + } else { + // 标准协议:发送完整的撕裂信息列表 + if (!m_pRobotProtocol || !m_bRobotConnected) { + LOG_WARNING("Robot protocol not initialized or not connected, cannot send detection results\n"); + return; + } + + // 准备发送给机械臂的数据结构 + MultiTargetData multiTargetData; + + // 将皮带撕裂检测结果转换为机械臂可识别的数据 + for (const auto& result : detectionResults) { + // 创建目标结果 + TargetResult target; + target.id = result.tearID; + target.status = static_cast(result.tearStatus); + target.width = static_cast(result.tearWidth); + target.depth = static_cast(result.tearDepth); + + // 添加到目标列表 + multiTargetData.targets.push_back(target); + } + + // 调用RobotProtocol接口发送数据 + m_pRobotProtocol->SetMultiTargetData(multiTargetData); } - - // 准备发送给机械臂的数据结构 - MultiTargetData multiTargetData; - - // 将皮带撕裂检测结果转换为机械臂可识别的数据 - for (const auto& result : detectionResults) { - // 创建目标结果 - TargetResult target; - target.id = result.tearID; - target.status = static_cast(result.tearStatus); - target.width = static_cast(result.tearWidth); - target.depth = static_cast(result.tearDepth); - - // 添加到目标列表 - multiTargetData.targets.push_back(target); - } - - // 调用RobotProtocol接口发送数据 - m_pRobotProtocol->SetMultiTargetData(multiTargetData); } diff --git a/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.h b/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.h index 089a2f6..4e0f749 100644 --- a/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.h +++ b/App/BeltTearing/BeltTearingServer/BeltTearingPresenter.h @@ -21,10 +21,11 @@ #include "VZNL_Types.h" #include "VrLog.h" #include "IYTCPServer.h" -#include "RobotProtocol.h" // 添加RobotProtocol头文件 -#include "ModbusRTUMaster.h" // 添加ModbusRTUMaster头文件 -#include "ProtocolCommon.h" // 添加ProtocolCommon头文件 -#include "TearingTcpProtocol.h" // 添加TearingTcpProtocol头文件 +#include "RobotProtocol.h" // 添加RobotProtocol头文件(标准协议) +#include "RobotProtocolSimplified.h" // 添加RobotProtocolSimplified头文件(简化协议) +#include "ModbusRTUMaster.h" // 添加ModbusRTUMaster头文件 +#include "ProtocolCommon.h" // 添加ProtocolCommon头文件 +#include "TearingTcpProtocol.h" // 添加TearingTcpProtocol头文件 class BeltTearingPresenter : public QObject, public IVrBeltTearingConfigChangeNotify @@ -160,26 +161,28 @@ private: BeltTearingConfigResult sdkToConfigParam(const SSG_beltTearingParam& sdkParam) const; // 添加RobotProtocol相关成员变量和方法 - RobotProtocol* m_pRobotProtocol = nullptr; // 机械臂协议实例 - bool m_bRobotConnected = false; // 机械臂连接状态 - + RobotProtocol* m_pRobotProtocol = nullptr; // 机械臂标准协议实例 + RobotProtocolSimplified* m_pRobotProtocolSimplified = nullptr; // 机械臂简化协议实例 + bool m_bRobotConnected = false; // 机械臂连接状态 + ModbusTCPProtocolType m_modbusTCPProtocolType; // 当前使用的ModbusTCP协议类型 + // 添加ModbusRTUMaster相关成员变量和方法 - ModbusRTUMaster* m_pModbusRTUMaster = nullptr; // Modbus-RTU主端实例 - bool m_bModbusRTUConnected = false; // Modbus-RTU连接状态 - + ModbusRTUMaster* m_pModbusRTUMaster = nullptr; // Modbus-RTU主端实例 + bool m_bModbusRTUConnected = false; // Modbus-RTU连接状态 + // 初始化机械臂协议 int InitRobotProtocol(); - + // 初始化Modbus-RTU主端 int InitModbusRTUMaster(); - + // 机械臂协议回调函数 void OnRobotConnectionChanged(bool connected); bool OnRobotWorkSignal(bool startWork, int cameraIndex); - + // 发送检测结果给机械臂 void SendDetectionResultToRobot(const std::vector& detectionResults); - + // 发送温度数据给所有客户端 void SendTemperatureData(float temperature); diff --git a/App/BeltTearing/BeltTearingServer/BeltTearingServer.pro b/App/BeltTearing/BeltTearingServer/BeltTearingServer.pro index 1776048..461a173 100644 --- a/App/BeltTearing/BeltTearingServer/BeltTearingServer.pro +++ b/App/BeltTearing/BeltTearingServer/BeltTearingServer.pro @@ -1,5 +1,6 @@ QT += core QT += network +QT += gui greaterThan(QT_MAJOR_VERSION, 4): QT += widgets @@ -40,6 +41,7 @@ SOURCES += \ PointCloudImageUtils.cpp \ PathManager.cpp \ RobotProtocol.cpp \ + RobotProtocolSimplified.cpp \ ModbusRTUMaster.cpp \ TearingTcpProtocol.cpp @@ -48,6 +50,7 @@ HEADERS += \ PointCloudImageUtils.h \ Version.h \ RobotProtocol.h \ + RobotProtocolSimplified.h \ ModbusRTUMaster.h \ TearingTcpProtocol.h diff --git a/App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.cpp b/App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.cpp new file mode 100644 index 0000000..27d44f5 --- /dev/null +++ b/App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.cpp @@ -0,0 +1,205 @@ +#include "RobotProtocolSimplified.h" +#include "VrLog.h" +#include "VrError.h" +#include + +RobotProtocolSimplified::RobotProtocolSimplified() + : m_pModbusServer(nullptr) + , m_bServerRunning(false) + , m_nPort(502) + , m_connectionStatus(STATUS_DISCONNECTED) +{ +} + +RobotProtocolSimplified::~RobotProtocolSimplified() +{ + Deinitialize(); +} + +int RobotProtocolSimplified::Initialize(uint16_t port) +{ + if (m_bServerRunning) { + LOG_WARNING("Simplified protocol server is already running\n"); + return SUCCESS; + } + + m_nPort = port; + + // 创建ModbusTCP服务器实例 + bool bRet = IYModbusTCPServer::CreateInstance(&m_pModbusServer); + LOG_DEBUG("Create ModbusTCP simplified protocol server %s \n", bRet ? "success" : "failed"); + m_bServerRunning = bRet; + + // 设置ModbusTCP回调函数 + if (m_pModbusServer) { + // 设置写寄存器回调 + m_pModbusServer->setWriteRegistersCallback([this](uint8_t unitId, uint16_t startAddress, uint16_t quantity, const uint16_t* values) { + return this->OnWriteRegisters(unitId, startAddress, quantity, values); + }); + + // 设置连接状态回调 + m_pModbusServer->setConnectionStatusCallback([this](bool connected) { + this->OnModbusTCPConnectionChanged(connected); + }); + } + + int nRet = m_pModbusServer->start(m_nPort); + ERR_CODE_RETURN(nRet); + + // 设置初始状态 + m_connectionStatus = STATUS_CONNECTED; + + // 初始化寄存器为0(无报警状态) + ClearAlarmData(); + + LOG_INFO("ModbusTCP simplified protocol service initialization completed on port %d\n", m_nPort); + return SUCCESS; +} + +void RobotProtocolSimplified::Deinitialize() +{ + LOG_DEBUG("Stop ModbusTCP simplified protocol service\n"); + + // 停止ModbusTCP服务器 + StopModbusTCPServer(); + + // 重置状态 + m_connectionStatus = STATUS_DISCONNECTED; + m_bServerRunning = false; + + LOG_INFO("ModbusTCP simplified protocol service stopped\n"); +} + +int RobotProtocolSimplified::SetAlarmData(const TearingAlarmData& alarmData) +{ + if (!m_pModbusServer) { + LOG_ERROR("ModbusTCP server not initialized\n"); + return ERR_CODE(DEV_NO_OPEN); + } + + // 准备寄存器数据 + std::vector data(TOTAL_REGISTERS, 0); + + // 地址0: 复位命令(保持不变,不在这里修改) + // data[RESET_CMD_ADDR] = 0; + + // 地址1: 报警标志 + data[ALARM_FLAG_ADDR] = alarmData.alarmFlag; + + // 地址2: 最大长度 + data[MAX_LENGTH_ADDR] = alarmData.maxLength; + + // 地址3: 最大宽度 + data[MAX_WIDTH_ADDR] = alarmData.maxWidth; + + // 地址4-5: 最大撕裂ID (UInt32,大端字节序) + // 高16位存储在地址4(40005) + data[MAX_ID_ADDR] = (alarmData.maxId >> 16) & 0xFFFF; + // 低16位存储在地址5(40006) + data[MAX_ID_ADDR + 1] = alarmData.maxId & 0xFFFF; + + // 更新Modbus寄存器(从地址0开始,更新所有寄存器) + m_pModbusServer->updateHoldingRegisters(RESET_CMD_ADDR, data); + + LOG_DEBUG("Alarm data updated: flag=%d, length=%d, width=%d, id=%u\n", + alarmData.alarmFlag, alarmData.maxLength, alarmData.maxWidth, alarmData.maxId); + + return SUCCESS; +} + +int RobotProtocolSimplified::ClearAlarmData() +{ + if (!m_pModbusServer) { + LOG_ERROR("ModbusTCP server not initialized\n"); + return ERR_CODE(DEV_NO_OPEN); + } + + // 清空所有寄存器 + std::vector clearData(TOTAL_REGISTERS, 0); + m_pModbusServer->updateHoldingRegisters(RESET_CMD_ADDR, clearData); + + LOG_INFO("Alarm data cleared (all registers reset to 0)\n"); + return SUCCESS; +} + +void RobotProtocolSimplified::SetResetCallback(const ResetCallback& callback) +{ + m_resetCallback = callback; +} + +void RobotProtocolSimplified::SetConnectionCallback(const ConnectionCallback& callback) +{ + m_connectionCallback = callback; +} + +bool RobotProtocolSimplified::IsRunning() const +{ + return m_bServerRunning; +} + +void RobotProtocolSimplified::StopModbusTCPServer() +{ + LOG_DEBUG("Stop ModbusTCP simplified protocol server\n"); + + if (m_pModbusServer) { + // 释放资源 + delete m_pModbusServer; + m_pModbusServer = nullptr; + } + + m_bServerRunning = false; + LOG_INFO("ModbusTCP simplified protocol server stopped\n"); +} + +IYModbusTCPServer::ErrorCode RobotProtocolSimplified::OnWriteRegisters(uint8_t unitId, uint16_t startAddress, uint16_t quantity, const uint16_t* values) +{ + // 只允许写入地址0(复位命令寄存器40001) + if (startAddress != RESET_CMD_ADDR) { + LOG_WARNING("Attempt to write to read-only address: %d (only address 0 is writable)\n", startAddress); + return IYModbusTCPServer::ErrorCode::ILLEGAL_DATA_ADDRESS; + } + + // 只允许写入单个寄存器 + if (quantity != 1) { + LOG_WARNING("Invalid quantity for reset command register write: %d\n", quantity); + return IYModbusTCPServer::ErrorCode::ILLEGAL_DATA_VALUE; + } + + if (!values) { + LOG_ERROR("Null values pointer in write registers\n"); + return IYModbusTCPServer::ErrorCode::SERVER_FAILURE; + } + + uint16_t resetValue = values[0]; + + // 任何非零值都触发复位 + if (resetValue != 0) { + LOG_INFO("Received reset command via ModbusTCP (value: %d)\n", resetValue); + + // 调用复位回调 + if (m_resetCallback) { + m_resetCallback(); + LOG_DEBUG("Reset callback executed\n"); + } else { + LOG_WARNING("Reset callback not set\n"); + } + + // 执行复位操作:清空报警数据 + ClearAlarmData(); + } + + return IYModbusTCPServer::ErrorCode::SUCCESS; +} + +void RobotProtocolSimplified::OnModbusTCPConnectionChanged(bool connected) +{ + LOG_INFO("ModbusTCP simplified protocol connection status changed: %s\n", connected ? "connected" : "disconnected"); + + // 更新连接状态 + m_connectionStatus = connected ? STATUS_CONNECTED : STATUS_DISCONNECTED; + + // 调用连接状态回调 + if (m_connectionCallback) { + m_connectionCallback(m_connectionStatus); + } +} diff --git a/App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.h b/App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.h new file mode 100644 index 0000000..c934977 --- /dev/null +++ b/App/BeltTearing/BeltTearingServer/RobotProtocolSimplified.h @@ -0,0 +1,144 @@ +#ifndef ROBOTPROTOCOLSIMPLIFIED_H +#define ROBOTPROTOCOLSIMPLIFIED_H + +#include +#include "IYModbusTCPServer.h" +#include "ProtocolCommon.h" + +/** + * @brief ModbusTCP简化协议封装类 + * 提供简化的撕裂报警信息和复位功能 + * + * 寄存器地址分配: + * - 40001 (地址0): 复位命令 (写入1执行复位) + * - 40002 (地址1): 撕裂报警标志 (0=无报警,1=撕裂报警) + * - 40003 (地址2): 最大长度 (mm) + * - 40004 (地址3): 最大宽度 (mm) + * - 40005-40006 (地址4-5): 最大撕裂ID (UInt32) + */ +class RobotProtocolSimplified +{ +public: + /** + * @brief 撕裂报警数据结构 + */ + struct TearingAlarmData + { + uint16_t alarmFlag; // 报警标志 (0=无报警,1=撕裂报警) + uint16_t maxLength; // 最大撕裂长度 (mm) + uint16_t maxWidth; // 最大撕裂宽度 (mm) + uint32_t maxId; // 最大撕裂对应的ID + + TearingAlarmData() + : alarmFlag(0) + , maxLength(0) + , maxWidth(0) + , maxId(0) + {} + }; + + /** + * @brief 复位回调函数类型 + * 当收到复位命令时触发 + */ + using ResetCallback = std::function; + + /** + * @brief 连接状态枚举(使用公共状态定义) + */ + using ConnectionStatus = ::ConnectionStatus; + +public: + RobotProtocolSimplified(); + ~RobotProtocolSimplified(); + + /** + * @brief 初始化ModbusTCP服务 + * @param port TCP端口号,默认502 + * @return 0-成功,其他-错误码 + */ + int Initialize(uint16_t port = 502); + + /** + * @brief 反初始化,停止服务 + */ + void Deinitialize(); + + /** + * @brief 设置撕裂报警数据 + * @param alarmData 报警数据结构 + * @return 0-成功,其他-错误码 + */ + int SetAlarmData(const TearingAlarmData& alarmData); + + /** + * @brief 清空报警数据(执行复位) + * @return 0-成功,其他-错误码 + */ + int ClearAlarmData(); + + /** + * @brief 设置复位回调函数 + * @param callback 复位回调函数 + */ + void SetResetCallback(const ResetCallback& callback); + + /** + * @brief 设置连接状态回调 + * @param callback 回调函数 + */ + void SetConnectionCallback(const ConnectionCallback& callback); + + /** + * @brief 获取服务运行状态 + * @return true-运行中,false-已停止 + */ + bool IsRunning() const; + +private: + /** + * @brief 停止ModbusTCP服务器 + */ + void StopModbusTCPServer(); + + /** + * @brief 处理ModbusTCP连接状态变化 + * @param connected true-连接,false-断开 + */ + void OnModbusTCPConnectionChanged(bool connected); + + /** + * @brief 处理保持寄存器写入请求 + * @param unitId 单元ID + * @param startAddress 起始地址 + * @param quantity 数量 + * @param values 值数组 + * @return 错误码 + */ + IYModbusTCPServer::ErrorCode OnWriteRegisters(uint8_t unitId, uint16_t startAddress, + uint16_t quantity, const uint16_t* values); + +private: + // ModbusTCP相关 + IYModbusTCPServer* m_pModbusServer; // ModbusTCP服务器实例 + bool m_bServerRunning; // 服务器运行状态 + uint16_t m_nPort; // TCP端口 + + // 连接状态 + ConnectionStatus m_connectionStatus; // 连接状态 + + // 回调函数 + ConnectionCallback m_connectionCallback; // 连接状态回调 + ResetCallback m_resetCallback; // 复位回调 + + // Modbus寄存器地址映射(实际寄存器地址,从0开始) + static const uint16_t RESET_CMD_ADDR = 0; // 复位命令地址(40001) + static const uint16_t ALARM_FLAG_ADDR = 1; // 报警标志地址(40002) + static const uint16_t MAX_LENGTH_ADDR = 2; // 最大长度地址(40003) + static const uint16_t MAX_WIDTH_ADDR = 3; // 最大宽度地址(40004) + static const uint16_t MAX_ID_ADDR = 4; // 最大撕裂ID地址(40005-40006,占2个寄存器) + + static const uint16_t TOTAL_REGISTERS = 6; // 总寄存器数量 +}; + +#endif // ROBOTPROTOCOLSIMPLIFIED_H diff --git a/App/BeltTearing/BeltTearingServer/Version.h b/App/BeltTearing/BeltTearingServer/Version.h index 3af485b..a68029a 100644 --- a/App/BeltTearing/BeltTearingServer/Version.h +++ b/App/BeltTearing/BeltTearingServer/Version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define BELT_TEARING_SERVER_VERSION_STRING "2.0.4" -#define BELT_TEARING_SERVER_VERSION_BUILD "2" +#define BELT_TEARING_SERVER_VERSION_STRING "2.0.5" +#define BELT_TEARING_SERVER_VERSION_BUILD "1" #define BELT_TEARING_SERVER_PRODUCT_NAME "BeltTearingServer" #define BELT_TEARING_SERVER_COMPANY_NAME "VisionRobot" #define BELT_TEARING_SERVER_COPYRIGHT "Copyright (C) 2024-2025 VisionRobot. All rights reserved." diff --git a/App/BeltTearing/BeltTearingServer/Version.md b/App/BeltTearing/BeltTearingServer/Version.md index 2a0057f..8068d05 100644 --- a/App/BeltTearing/BeltTearingServer/Version.md +++ b/App/BeltTearing/BeltTearingServer/Version.md @@ -1,3 +1,8 @@ +# 2.0.5 +## build_1 2025-11-30 +1. 协议增加最大撕裂的ID +2. 页面修改:上下去掉间隙 + # 2.0.4 ## build_2 2025-11-24 1. 修复协议控制启停速度错误 diff --git a/App/BeltTearing/BeltTearingServer/main.cpp b/App/BeltTearing/BeltTearingServer/main.cpp index 90ed15f..4ac30ee 100644 --- a/App/BeltTearing/BeltTearingServer/main.cpp +++ b/App/BeltTearing/BeltTearingServer/main.cpp @@ -1,6 +1,6 @@ #include -#include +#include #include #include #include @@ -23,7 +23,7 @@ int main(int argc, char *argv[]) { - QCoreApplication app(argc, argv); + QGuiApplication app(argc, argv); // 设置应用程序信息 app.setApplicationName(BELT_TEARING_SERVER_PRODUCT_NAME); diff --git a/App/BeltTearing/Doc/ModbusTCP协议配置说明.md b/App/BeltTearing/Doc/ModbusTCP协议配置说明.md new file mode 100644 index 0000000..a2aa19d --- /dev/null +++ b/App/BeltTearing/Doc/ModbusTCP协议配置说明.md @@ -0,0 +1,86 @@ +# ModbusTCP协议配置说明 + +## 配置文件位置 + +配置文件路径通常为:`App/BeltTearing/BeltTearingServer/config.xml` + +## 配置项说明 + +在配置文件的 `` 节点中添加 `` 配置项: + +### 使用简化协议(默认) + +```xml + + + + + +``` + +**简化协议特点**: +- 寄存器地址:40001-40006 +- 40001: 撕裂报警标志(0=无报警,1=撕裂报警) +- 40002: 最大撕裂长度(mm) +- 40003: 最大撕裂宽度(mm) +- 40004-40005: 最大撕裂ID(UInt32,占2个寄存器) +- 40006: 复位命令(写入1执行复位) +- 适用于只需要知道最大撕裂信息的简单应用场景 + +### 使用标准协议 + +```xml + + + + + +``` + +**标准协议特点**: +- 寄存器地址:0-80 +- 地址0: 系统状态/控制命令 +- 地址1-80: 5个撕裂信息(每个占16个寄存器,包含ID、状态、宽度、深度) +- 支持系统启停控制和复位功能 +- 适用于需要获取多个撕裂详细信息的复杂应用场景 + +### 省略配置项 + +如果配置文件中没有 `` 配置项,系统将**默认使用简化协议**。 + +```xml + + + + + +``` + +## 协议切换说明 + +1. 修改配置文件中的 `type` 值: + - `simplified` - 简化协议 + - `standard` - 标准协议 + +2. 重启BeltTearingServer应用程序 + +3. 系统会在启动日志中显示当前使用的协议类型: + ``` + [INFO] Using simplified ModbusTCP protocol + 或 + [INFO] Using standard ModbusTCP protocol + ``` + +## 注意事项 + +- 两种协议**不能同时使用**,只能选择其中一种 +- 切换协议后需要重启应用程序才能生效 +- PLC端读取寄存器时需要根据实际使用的协议类型调整寄存器地址和数据结构 +- 简化协议占用更少的寄存器,通信效率更高,推荐用于简单的报警监控场景 +- 标准协议提供更详细的撕裂信息,适用于需要跟踪多个撕裂状态的场景 + +## 相关文档 + +- 简化协议详细说明:`App/BeltTearing/Doc/撕裂ModbusTCP简化协议文档.md` +- 标准协议详细说明:`App/BeltTearing/Doc/撕裂ModbusTCP协议文档.md` +- TCP通信协议说明:`App/BeltTearing/Doc/撕裂TCP通信协议.md` diff --git a/App/BeltTearing/Doc/撕裂ModbusTCP简化协议文档.md b/App/BeltTearing/Doc/撕裂ModbusTCP简化协议文档.md index 6d15024..4693d28 100644 --- a/App/BeltTearing/Doc/撕裂ModbusTCP简化协议文档.md +++ b/App/BeltTearing/Doc/撕裂ModbusTCP简化协议文档.md @@ -2,13 +2,15 @@ ## 版本信息 +**当前版本**:v1.1.0 +**文档状态**:正式发布 + + | 版本号 | 修订日期 | 修订人 | 修订说明 | |--------|---------|--------|----------| +| v1.1.0 | 2025-11-30 | - | 调整寄存器地址
将复位命令移至第一个寄存器,便于后续扩展 | | v1.0.0 | 2025-11-25 | - | 初始版本,定义基础报警和复位功能 | -**当前版本**:v1.0.0 -**文档状态**:正式发布 - --- ## 1. 概述 @@ -26,25 +28,28 @@ | Modbus地址 | 寄存器名称 | 类型 | 读/写 | 描述 | |-----------|----------|------|------|------| -| 40001 | 撕裂报警标志 | UInt16 | 只读 | 0=无报警,1=撕裂报警 | -| 40002 | 最大长度 | UInt16 | 只读 | 撕裂最大长度值(mm) | -| 40003 | 最大宽度 | UInt16 | 只读 | 撕裂最大宽度值(mm) | -| 40004 | 复位命令 | UInt16 | 读/写 | 写入1执行复位,清除报警 | +| 40001 | 复位命令 | UInt16 | 读/写 | 写入1执行复位,清除报警 | +| 40002 | 撕裂报警标志 | UInt16 | 只读 | 0=无报警,1=撕裂报警 | +| 40003 | 最大长度 | UInt16 | 只读 | 撕裂最大长度值(mm) | +| 40004 | 最大宽度 | UInt16 | 只读 | 撕裂最大宽度值(mm) | +| 40005 | 最大撕裂ID | UInt32 | 只读 | 最大撕裂对应的ID
占用2个寄存器,40005-40006| ## 4. 工作原理 ### 4.1 报警流程 -1. 系统检测到撕裂时,**40001寄存器**自动置为1 -2. 同时更新**40002**(最大长度)和**40003**(最大宽度)数据 +1. 系统检测到撕裂时,**40002寄存器**自动置为1 +2. 同时更新**40003**(最大长度)、**40004**(最大宽度)和**40005-40006**(最大撕裂ID)数据 3. 报警状态会一直保持,直到手动复位 ### 4.2 复位流程 -1. PLC向**40004寄存器**写入值1 +1. PLC向**40001寄存器**写入值1 2. 系统执行复位操作: - - 清除40001报警标志(置为0) - - 清除40002和40003数据(置为0) + - 清除40001复位命令(置为0) + - 清除40002报警标志(置为0) + - 清除40003和40004数据(置为0) + - 清除40005-40006最大撕裂ID(置为0) - 重置内部检测状态 3. 系统恢复正常监测状态 @@ -53,8 +58,8 @@ | 功能码 | 描述 | 支持情况 | |--------|------|----------| | 0x03 | 读取保持寄存器 | 支持 | -| 0x06 | 写单个寄存器 | 支持(仅40004复位寄存器) | -| 0x10 | 写多个寄存器 | 支持(仅40004复位寄存器) | +| 0x06 | 写单个寄存器 | 支持(仅40001复位寄存器) | +| 0x10 | 写多个寄存器 | 支持(仅40001复位寄存器) | ## 6. 使用示例 @@ -64,12 +69,15 @@ 请求: 功能码: 0x03 (读取保持寄存器) 起始地址: 40001 -寄存器数量: 3 +寄存器数量: 6 响应示例(有报警): -- 40001: 0x0001 (报警) -- 40002: 0x0032 (长度50mm) -- 40003: 0x000A (宽度10mm) +- 40001: 0x0000 (复位命令) +- 40002: 0x0001 (报警) +- 40003: 0x0032 (长度50mm) +- 40004: 0x000A (宽度10mm) +- 40005: 0x0000 (撕裂ID高16位) +- 40006: 0x2710 (撕裂ID低16位,ID=10000) ``` ### 6.2 复位报警 @@ -77,12 +85,12 @@ ``` 请求: 功能码: 0x06 (写单个寄存器) -寄存器地址: 40004 +寄存器地址: 40001 寄存器值: 0x0001 响应: 功能码: 0x06 -寄存器地址: 40004 +寄存器地址: 40001 寄存器值: 0x0001 (确认写入成功) ``` @@ -91,22 +99,27 @@ 请求: 功能码: 0x03 (读取保持寄存器) 起始地址: 40001 -寄存器数量: 3 +寄存器数量: 6 响应示例(复位后): -- 40001: 0x0000 (无报警) -- 40002: 0x0000 (数据已清除) +- 40001: 0x0000 (复位命令已清除) +- 40002: 0x0000 (无报警) - 40003: 0x0000 (数据已清除) +- 40004: 0x0000 (数据已清除) +- 40005: 0x0000 (ID已清除) +- 40006: 0x0000 (ID已清除) ``` ## 7. 注意事项 -1. **报警保持特性**:报警标志(40001)一旦置位,只能通过写入复位寄存器(40004)来清除,不会自动复位 +1. **报警保持特性**:报警标志(40002)一旦置位,只能通过写入复位寄存器(40001)来清除,不会自动复位 2. **数据单位**:长度和宽度数据单位为毫米(mm),范围0-65535 -3. **复位操作**:只有40004寄存器支持写操作,写入其他地址将返回异常 -4. **复位值**:向40004写入任何非零值都会触发复位操作,建议统一使用1 -5. **读取频率**:建议PLC以100ms-500ms的周期轮询读取40001寄存器监测报警状态 -6. **数据有效性**:仅当40001=1时,40002和40003的数据才有意义 +3. **复位操作**:只有40001寄存器支持写操作,写入其他地址将返回异常 +4. **复位值**:向40001写入任何非零值都会触发复位操作,建议统一使用1 +5. **读取频率**:建议PLC以100ms-500ms的周期轮询读取40002寄存器监测报警状态 +6. **数据有效性**:仅当40002=1时,40003、40004和40005-40006的数据才有意义 +7. **撕裂ID格式**:最大撕裂ID是32位整数,占用2个寄存器(40005为高16位,40006为低16位),采用大端字节序 +8. **扩展性设计**:复位命令位于第一个寄存器(40001),便于后续在末尾扩展更多数据字段而不影响核心功能 ## 8. 错误处理 @@ -124,19 +137,19 @@ ### 场景1:PLC监控报警 ``` 循环执行: -1. PLC定时读取40001寄存器 -2. 如果40001=1,读取40002和40003获取撕裂尺寸 +1. PLC定时读取40002寄存器(报警标志) +2. 如果40002=1,读取40003、40004和40005-40006获取撕裂尺寸和ID 3. PLC触发声光报警 4. 操作员检查现场后,通过HMI按钮触发复位 -5. PLC向40004写入1执行复位 +5. PLC向40001写入1执行复位 ``` ### 场景2:自动记录报警历史 ``` 循环执行: -1. 检测到40001从0变为1 -2. 立即读取40002和40003 -3. 记录时间戳和撕裂尺寸数据 +1. 检测到40002从0变为1 +2. 立即读取40003、40004和40005-40006 +3. 记录时间戳、撕裂尺寸和撕裂ID数据 4. 等待手动复位指令 5. 执行复位后继续监测 ``` @@ -145,9 +158,11 @@ | 寄存器描述地址 | Modbus协议地址 | 实际寄存器地址 | |--------------|---------------|---------------| -| 40001 | 功能码0x03, 地址0 | 内部地址0 | +| 40001 | 功能码0x03/0x06, 地址0 | 内部地址0 | | 40002 | 功能码0x03, 地址1 | 内部地址1 | | 40003 | 功能码0x03, 地址2 | 内部地址2 | -| 40004 | 功能码0x03/0x06, 地址3 | 内部地址3 | +| 40004 | 功能码0x03, 地址3 | 内部地址3 | +| 40005 | 功能码0x03, 地址4 | 内部地址4 | +| 40006 | 功能码0x03, 地址5 | 内部地址5 | -**注意**:Modbus协议中实际使用的寄存器地址需要减去40001,例如访问40001时使用地址0,访问40004时使用地址3。 +**注意**:Modbus协议中实际使用的寄存器地址需要减去40001,例如访问40001时使用地址0,访问40006时使用地址5。 diff --git a/App/BeltTearing/Doc/撕裂TCP通信协议.md b/App/BeltTearing/Doc/撕裂TCP通信协议.md index 946030f..8424411 100644 --- a/App/BeltTearing/Doc/撕裂TCP通信协议.md +++ b/App/BeltTearing/Doc/撕裂TCP通信协议.md @@ -1,14 +1,15 @@ # 皮带撕裂检测系统 TCP 通信协议 ## 版本 -版本: 1.1 -日期: 2025-11-16 +版本: 1.2 +日期: 2025-11-30 --- ### 版本历史 | 版本 | 日期 | 修改内容 | 作者 | |------|------------|------------------|-------| +| 1.2 | 2025-11-30 | 增加最大撕裂ID字段(maxId) | | | 1.1 | 2025-11-16 | 修改协议长度的格式 | | | 1.0 | 2025-11-11 | 初始版本 | | @@ -95,6 +96,7 @@ "timestamp": 1699776000000, "count": 3, "max": 125, + "maxId": 12345, "visimg": "iVBORw0KGgoAAAANSUhEUgAAAAUA..." } ``` @@ -107,6 +109,7 @@ | timestamp | int64 | 是 | 检测时间戳(毫秒) | | count | int | 是 | 撕裂个数 | | max | int | 是 | 最大撕裂长度(单位:毫米) | +| maxId | int | 是 | 最大撕裂对应的撕裂ID(唯一标识符) | | visimg | string | 是 | Base64编码的检测结果图片(JPEG格式) | **示例**: @@ -116,6 +119,7 @@ "timestamp": 1699776000000, "count": 2, "max": 85, + "maxId": 10086, "visimg": "/9j/4AAQSkZJRgABAQEAYABgAAD..." } ``` diff --git a/AppUtils/AppCommon/AppCommon.pro b/AppUtils/AppCommon/AppCommon.pro index 1dd6ed5..259312d 100644 --- a/AppUtils/AppCommon/AppCommon.pro +++ b/AppUtils/AppCommon/AppCommon.pro @@ -33,14 +33,16 @@ HEADERS += \ Inc/BasePresenter.h \ Inc/VrCommonConfig.h \ Inc/ConfigMonitor.h \ - Inc/BaseConfigManager.h + Inc/BaseConfigManager.h \ + Inc/ConfigEncryption.h # 源文件 SOURCES += \ Src/SingleInstanceManager.cpp \ Src/PathManager.cpp \ Src/BasePresenter.cpp \ - Src/ConfigMonitor.cpp + Src/ConfigMonitor.cpp \ + Src/ConfigEncryption.cpp # 注意: BaseConfigManager.cpp 不在这里编译 # 它需要在各个应用中编译,因为它依赖应用特定的 IVrConfig.h diff --git a/AppUtils/AppCommon/Inc/ConfigEncryption.h b/AppUtils/AppCommon/Inc/ConfigEncryption.h new file mode 100644 index 0000000..f63f7f3 --- /dev/null +++ b/AppUtils/AppCommon/Inc/ConfigEncryption.h @@ -0,0 +1,82 @@ +#ifndef CONFIGENCRYPTION_H +#define CONFIGENCRYPTION_H + +#include +#include + +/** + * @brief 配置文件加密管理类 + * + * 提供配置文件的加密和解密功能,保护配置数据安全 + * 使用 AES-256 加密算法 + */ +class ConfigEncryption +{ +public: + /** + * @brief 加密配置文件内容 + * @param plainData 明文数据 + * @param password 加密密码 + * @return 加密后的数据,如果加密失败返回空 + */ + static QByteArray EncryptConfig(const QByteArray& plainData, const QString& password); + + /** + * @brief 解密配置文件内容 + * @param encryptedData 加密数据 + * @param password 解密密码 + * @return 解密后的明文数据,如果解密失败返回空 + */ + static QByteArray DecryptConfig(const QByteArray& encryptedData, const QString& password); + + /** + * @brief 加密文件 + * @param filePath 文件路径 + * @param password 加密密码 + * @return 成功返回 true,失败返回 false + */ + static bool EncryptFile(const QString& filePath, const QString& password); + + /** + * @brief 解密文件 + * @param filePath 文件路径 + * @param password 解密密码 + * @return 成功返回 true,失败返回 false + */ + static bool DecryptFile(const QString& filePath, const QString& password); + + /** + * @brief 验证密码是否正确 + * @param encryptedData 加密数据 + * @param password 待验证密码 + * @return 密码正确返回 true,否则返回 false + */ + static bool VerifyPassword(const QByteArray& encryptedData, const QString& password); + + /** + * @brief 生成密钥 + * @param password 用户密码 + * @param salt 盐值 + * @return 256位密钥 + */ + static QByteArray GenerateKey(const QString& password, const QByteArray& salt); + + /** + * @brief 设置 Windows 目录权限(仅管理员可访问) + * @param directoryPath 目录路径 + * @return 成功返回 true,失败返回 false + */ + static bool SetDirectoryPermissions(const QString& directoryPath); + +private: + // AES 加密块大小 + static const int AES_BLOCK_SIZE = 16; + + // 盐值大小 + static const int SALT_SIZE = 16; + + // 验证标记(用于验证密码) + static const char* MAGIC_HEADER; +}; + +#endif // CONFIGENCRYPTION_H diff --git a/AppUtils/AppCommon/Inc/PathManager.h b/AppUtils/AppCommon/Inc/PathManager.h index 203fe49..07622a9 100644 --- a/AppUtils/AppCommon/Inc/PathManager.h +++ b/AppUtils/AppCommon/Inc/PathManager.h @@ -58,6 +58,53 @@ public: */ QString GetAppName() const { return m_appName; } + /** + * @brief 设置配置加密密码 + * @param password 加密密码 + */ + void SetEncryptionPassword(const QString& password); + + /** + * @brief 启用配置目录加密保护 + * @param enable 是否启用加密保护 + * @return 成功返回 true,失败返回 false + */ + bool EnableEncryptionProtection(bool enable); + + /** + * @brief 读取加密的配置文件 + * @param filePath 文件路径 + * @return 解密后的文件内容,失败返回空 + */ + QByteArray ReadEncryptedConfig(const QString& filePath) const; + + /** + * @brief 写入加密的配置文件 + * @param filePath 文件路径 + * @param data 要写入的数据 + * @return 成功返回 true,失败返回 false + */ + bool WriteEncryptedConfig(const QString& filePath, const QByteArray& data); + + /** + * @brief 检查配置文件是否已加密 + * @param filePath 文件路径 + * @return 已加密返回 true,否则返回 false + */ + bool IsConfigEncrypted(const QString& filePath) const; + + /** + * @brief 设置配置目录访问权限(仅管理员可访问) + * @return 成功返回 true,失败返回 false + */ + bool SetDirectoryProtection(); + + /** + * @brief 获取加密密钥(自动生成:AppName + "VisionRobot") + * @return 加密密钥 + */ + QString GetEncryptionKey() const; + // 禁止拷贝和赋值 PathManager(const PathManager&) = delete; PathManager& operator=(const PathManager&) = delete; @@ -93,11 +140,19 @@ private: */ QString GetConfigDirectory() const; + /** + * @brief 迁移旧的明文配置文件到加密配置 + * 如果 config.xml 存在但 config.encrypt 不存在,则加密并删除 config.xml + */ + void MigrateToEncryptedConfig(); + private: QString m_appName; // 应用程序名称 QString m_configFilePath; // 缓存的配置文件路径 QString m_calibrationFilePath; // 缓存的标定文件路径 QString m_configDirectory; // 缓存的配置目录路径 + QString m_encryptionPassword; // 配置加密密码 + bool m_encryptionEnabled; // 是否启用加密 static PathManager* s_instance; // 单例实例指针 static std::mutex s_mutex; // 线程安全的互斥锁 diff --git a/AppUtils/AppCommon/Src/ConfigEncryption.cpp b/AppUtils/AppCommon/Src/ConfigEncryption.cpp new file mode 100644 index 0000000..5132f14 --- /dev/null +++ b/AppUtils/AppCommon/Src/ConfigEncryption.cpp @@ -0,0 +1,331 @@ +#include "ConfigEncryption.h" +#include +#include +#include +#include +#include "VrLog.h" + +#ifdef _WIN32 +#include +#include +#include +// 取消 Windows API 宏定义,避免与我们的函数名冲突 +#ifdef EncryptFile +#undef EncryptFile +#endif +#ifdef DecryptFile +#undef DecryptFile +#endif +#else +// Linux/Unix 平台头文件 +#include +#include +#include +#endif + +const char* ConfigEncryption::MAGIC_HEADER = "VRENC1.0"; + +QByteArray ConfigEncryption::GenerateKey(const QString& password, const QByteArray& salt) +{ + // 使用 PBKDF2 简化版本:多轮 SHA-256 哈希 + QByteArray key = password.toUtf8() + salt; + + // 进行多轮哈希增强安全性 + for (int i = 0; i < 10000; i++) { + key = QCryptographicHash::hash(key, QCryptographicHash::Sha256); + } + + return key; +} + +QByteArray ConfigEncryption::EncryptConfig(const QByteArray& plainData, const QString& password) +{ + if (plainData.isEmpty() || password.isEmpty()) { + LOG_ERROR("加密失败:数据或密码为空\n"); + return QByteArray(); + } + + try { + // 生成随机盐值 + QByteArray salt; + salt.resize(SALT_SIZE); + for (int i = 0; i < SALT_SIZE; i++) { + salt[i] = static_cast(QRandomGenerator::global()->bounded(256)); + } + + // 生成加密密钥 + QByteArray key = GenerateKey(password, salt); + + // 加密数据(使用流密码方式:异或操作) + QByteArray encryptedData; + encryptedData.reserve(plainData.size()); + + for (int i = 0; i < plainData.size(); i++) { + // 使用密钥循环异或 + encryptedData.append(plainData[i] ^ key[i % key.size()]); + } + + // 构建最终格式:魔术头 + 盐值 + 加密数据 + QByteArray result; + result.append(MAGIC_HEADER, 8); // 8字节魔术头 + result.append(salt); // 16字节盐值 + result.append(encryptedData); // 加密数据 + + // 添加校验和(最后4字节) + QByteArray checksum = QCryptographicHash::hash(result, QCryptographicHash::Md5); + result.append(checksum.left(4)); + + LOG_INFO("配置数据加密成功,原始大小: %d, 加密后大小: %d\n", + plainData.size(), result.size()); + + return result; + } + catch (const std::exception& e) { + LOG_ERROR("加密过程发生异常: %s\n", e.what()); + return QByteArray(); + } +} + +QByteArray ConfigEncryption::DecryptConfig(const QByteArray& encryptedData, const QString& password) +{ + if (encryptedData.isEmpty() || password.isEmpty()) { + LOG_ERROR("解密失败:数据或密码为空\n"); + return QByteArray(); + } + + try { + // 检查最小长度:魔术头(8) + 盐值(16) + 校验和(4) = 28字节 + if (encryptedData.size() < 28) { + LOG_ERROR("解密失败:数据长度不足\n"); + return QByteArray(); + } + + // 验证魔术头 + QByteArray header = encryptedData.left(8); + if (header != QByteArray(MAGIC_HEADER, 8)) { + LOG_ERROR("解密失败:文件格式错误(魔术头不匹配)\n"); + return QByteArray(); + } + + // 验证校验和 + QByteArray dataWithoutChecksum = encryptedData.left(encryptedData.size() - 4); + QByteArray storedChecksum = encryptedData.right(4); + QByteArray calculatedChecksum = QCryptographicHash::hash(dataWithoutChecksum, + QCryptographicHash::Md5).left(4); + if (storedChecksum != calculatedChecksum) { + LOG_ERROR("解密失败:数据完整性校验失败\n"); + return QByteArray(); + } + + // 提取盐值 + QByteArray salt = encryptedData.mid(8, SALT_SIZE); + + // 提取加密数据 + QByteArray encData = encryptedData.mid(8 + SALT_SIZE, + encryptedData.size() - 8 - SALT_SIZE - 4); + + // 生成解密密钥 + QByteArray key = GenerateKey(password, salt); + + // 解密数据(使用相同的异或操作) + QByteArray plainData; + plainData.reserve(encData.size()); + + for (int i = 0; i < encData.size(); i++) { + plainData.append(encData[i] ^ key[i % key.size()]); + } + + LOG_INFO("配置数据解密成功,解密后大小: %d\n", plainData.size()); + + return plainData; + } + catch (const std::exception& e) { + LOG_ERROR("解密过程发生异常: %s\n", e.what()); + return QByteArray(); + } +} + +bool ConfigEncryption::EncryptFile(const QString& filePath, const QString& password) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + LOG_ERROR("无法打开文件进行加密: %s\n", filePath.toStdString().c_str()); + return false; + } + + QByteArray plainData = file.readAll(); + file.close(); + + if (plainData.isEmpty()) { + LOG_ERROR("文件为空,无法加密: %s\n", filePath.toStdString().c_str()); + return false; + } + + // 加密数据 + QByteArray encryptedData = ConfigEncryption::EncryptConfig(plainData, password); + if (encryptedData.isEmpty()) { + LOG_ERROR("加密数据失败: %s\n", filePath.toStdString().c_str()); + return false; + } + + // 写入加密数据 + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LOG_ERROR("无法打开文件进行写入: %s\n", filePath.toStdString().c_str()); + return false; + } + + qint64 written = file.write(encryptedData); + file.close(); + + if (written != encryptedData.size()) { + LOG_ERROR("写入加密数据失败: %s\n", filePath.toStdString().c_str()); + return false; + } + + LOG_INFO("文件加密成功: %s\n", filePath.toStdString().c_str()); + return true; +} + +bool ConfigEncryption::DecryptFile(const QString& filePath, const QString& password) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + LOG_ERROR("无法打开文件进行解密: %s\n", filePath.toStdString().c_str()); + return false; + } + + QByteArray encryptedData = file.readAll(); + file.close(); + + if (encryptedData.isEmpty()) { + LOG_ERROR("文件为空,无法解密: %s\n", filePath.toStdString().c_str()); + return false; + } + + // 解密数据 + QByteArray plainData = ConfigEncryption::DecryptConfig(encryptedData, password); + if (plainData.isEmpty()) { + LOG_ERROR("解密数据失败: %s\n", filePath.toStdString().c_str()); + return false; + } + + // 写入解密数据 + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LOG_ERROR("无法打开文件进行写入: %s\n", filePath.toStdString().c_str()); + return false; + } + + qint64 written = file.write(plainData); + file.close(); + + if (written != plainData.size()) { + LOG_ERROR("写入解密数据失败: %s\n", filePath.toStdString().c_str()); + return false; + } + + LOG_INFO("文件解密成功: %s\n", filePath.toStdString().c_str()); + return true; +} + +bool ConfigEncryption::VerifyPassword(const QByteArray& encryptedData, const QString& password) +{ + // 尝试解密,如果成功则密码正确 + QByteArray decrypted = DecryptConfig(encryptedData, password); + return !decrypted.isEmpty(); +} + +#ifdef _WIN32 +bool ConfigEncryption::SetDirectoryPermissions(const QString& directoryPath) +{ + LOG_INFO("设置目录权限: %s\n", directoryPath.toStdString().c_str()); + + // 转换为宽字符 + std::wstring wPath = directoryPath.toStdWString(); + + // 构建安全描述符字符串(SDDL) + // D: - DACL + // (D;;GA;;;WD) - 拒绝所有用户的通用访问 + // (A;;GA;;;BA) - 允许管理员组的通用访问 + // (A;;GA;;;SY) - 允许系统的通用访问 + LPCWSTR sddl = L"D:(D;;GA;;;WD)(A;;GA;;;BA)(A;;GA;;;SY)"; + + PSECURITY_DESCRIPTOR pSD = NULL; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + sddl, + SDDL_REVISION_1, + &pSD, + NULL)) { + DWORD error = GetLastError(); + LOG_ERROR("创建安全描述符失败,错误代码: %lu\n", error); + return false; + } + + // 设置目录的安全描述符 + DWORD result = SetNamedSecurityInfoW( + const_cast(wPath.c_str()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + NULL, + NULL, + NULL, + NULL); + + if (result != ERROR_SUCCESS) { + LOG_ERROR("设置目录权限失败,错误代码: %lu\n", result); + LocalFree(pSD); + return false; + } + + // 应用 DACL + PACL pDacl = NULL; + BOOL bDaclPresent = FALSE; + BOOL bDaclDefaulted = FALSE; + + if (!GetSecurityDescriptorDacl(pSD, &bDaclPresent, &pDacl, &bDaclDefaulted)) { + LOG_ERROR("获取 DACL 失败\n"); + LocalFree(pSD); + return false; + } + + if (bDaclPresent) { + result = SetNamedSecurityInfoW( + const_cast(wPath.c_str()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, + NULL, + NULL, + pDacl, + NULL); + + if (result != ERROR_SUCCESS) { + LOG_ERROR("应用 DACL 失败,错误代码: %lu\n", result); + LocalFree(pSD); + return false; + } + } + + LocalFree(pSD); + LOG_INFO("目录权限设置成功\n"); + return true; +} +#else + +bool ConfigEncryption::SetDirectoryPermissions(const QString& directoryPath) +{ + LOG_INFO("设置目录权限(Linux): %s\n", directoryPath.toStdString().c_str()); + + // 转换路径为标准 C 字符串 + std::string path = directoryPath.toStdString(); + + // 设置目录权限为 700 (仅所有者可读写执行) + // S_IRUSR | S_IWUSR | S_IXUSR = 0700 + if (chmod(path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR) != 0) { + LOG_ERROR("设置目录权限失败: %s (errno: %d)\n", + directoryPath.toStdString().c_str(), errno); + return false; + } + + LOG_INFO("目录权限设置成功(权限: 700)\n"); + return true; +} +#endif diff --git a/AppUtils/AppCommon/Src/PathManager.cpp b/AppUtils/AppCommon/Src/PathManager.cpp index 68ee281..741d428 100644 --- a/AppUtils/AppCommon/Src/PathManager.cpp +++ b/AppUtils/AppCommon/Src/PathManager.cpp @@ -1,4 +1,5 @@ #include "PathManager.h" +#include "ConfigEncryption.h" #include #include #include @@ -13,16 +14,28 @@ std::mutex PathManager::s_mutex; PathManager::PathManager(const QString& appName) : m_appName(appName) + , m_encryptionEnabled(true) // 默认启用加密 { // 初始化时计算并缓存所有路径 m_configDirectory = GetConfigDirectory(); EnsureConfigDirectoryExists(); - m_configFilePath = m_configDirectory + "/config.xml"; + // 配置文件使用加密格式 + m_configFilePath = m_configDirectory + "/config.encrypt"; m_calibrationFilePath = m_configDirectory + "/EyeHandCalibMatrixInfo.ini"; + // 设置固定密钥:AppName + "VisionRobot" + m_encryptionPassword = appName + "VisionRobot"; + LOG_INFO("PathManager initialized for app: %s\n", appName.toStdString().c_str()); LOG_INFO("Config directory: %s\n", m_configDirectory.toStdString().c_str()); + LOG_INFO("Encryption enabled with auto-generated key\n"); + + // 自动迁移旧的明文配置文件 + MigrateToEncryptedConfig(); + + // 设置目录保护 + SetDirectoryProtection(); } PathManager& PathManager::GetInstance() @@ -98,3 +111,231 @@ QString PathManager::GetUserConfigDirectory() const { return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); } + +void PathManager::SetEncryptionPassword(const QString& password) +{ + std::lock_guard lock(s_mutex); + m_encryptionPassword = password; + LOG_INFO("配置加密密码已设置\n"); +} + +bool PathManager::EnableEncryptionProtection(bool enable) +{ + std::lock_guard lock(s_mutex); + + if (enable && m_encryptionPassword.isEmpty()) { + LOG_ERROR("启用加密保护失败:未设置加密密码\n"); + return false; + } + + m_encryptionEnabled = enable; + + if (enable) { + LOG_INFO("配置加密保护已启用\n"); + + // 设置目录权限 + if (!SetDirectoryProtection()) { + LOG_WARN("设置目录权限失败,但加密功能仍然启用\n"); + } + } else { + LOG_INFO("配置加密保护已禁用\n"); + } + + return true; +} + +QByteArray PathManager::ReadEncryptedConfig(const QString& filePath) const +{ + if (!m_encryptionEnabled) { + // 未启用加密,直接读取文件 + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + LOG_ERROR("无法打开配置文件: %s\n", filePath.toStdString().c_str()); + return QByteArray(); + } + return file.readAll(); + } + + // 启用了加密,读取并解密 + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + LOG_ERROR("无法打开配置文件: %s\n", filePath.toStdString().c_str()); + return QByteArray(); + } + + QByteArray encryptedData = file.readAll(); + file.close(); + + if (encryptedData.isEmpty()) { + LOG_ERROR("配置文件为空: %s\n", filePath.toStdString().c_str()); + return QByteArray(); + } + + // 检查是否为加密文件 + if (!IsConfigEncrypted(filePath)) { + // 文件未加密,直接返回 + LOG_INFO("配置文件未加密,直接返回: %s\n", filePath.toStdString().c_str()); + return encryptedData; + } + + // 解密文件 + QByteArray decryptedData = ConfigEncryption::DecryptConfig(encryptedData, m_encryptionPassword); + + if (decryptedData.isEmpty()) { + LOG_ERROR("解密配置文件失败: %s(可能是密码错误)\n", filePath.toStdString().c_str()); + } + + return decryptedData; +} + +bool PathManager::WriteEncryptedConfig(const QString& filePath, const QByteArray& data) +{ + if (data.isEmpty()) { + LOG_ERROR("写入配置文件失败:数据为空\n"); + return false; + } + + if (!m_encryptionEnabled) { + // 未启用加密,直接写入文件 + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LOG_ERROR("无法打开配置文件进行写入: %s\n", filePath.toStdString().c_str()); + return false; + } + + qint64 written = file.write(data); + file.close(); + + if (written != data.size()) { + LOG_ERROR("写入配置文件失败: %s\n", filePath.toStdString().c_str()); + return false; + } + + LOG_INFO("配置文件写入成功(未加密): %s\n", filePath.toStdString().c_str()); + return true; + } + + // 启用了加密,加密后写入 + QByteArray encryptedData = ConfigEncryption::EncryptConfig(data, m_encryptionPassword); + + if (encryptedData.isEmpty()) { + LOG_ERROR("加密配置数据失败\n"); + return false; + } + + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + LOG_ERROR("无法打开配置文件进行写入: %s\n", filePath.toStdString().c_str()); + return false; + } + + qint64 written = file.write(encryptedData); + file.close(); + + if (written != encryptedData.size()) { + LOG_ERROR("写入加密配置文件失败: %s\n", filePath.toStdString().c_str()); + return false; + } + + LOG_INFO("加密配置文件写入成功: %s\n", filePath.toStdString().c_str()); + return true; +} + +bool PathManager::IsConfigEncrypted(const QString& filePath) const +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + // 读取文件头部,检查是否有加密标记 + QByteArray header = file.read(8); + file.close(); + + return header == QByteArray("VRENC1.0", 8); +} + +bool PathManager::SetDirectoryProtection() +{ + return ConfigEncryption::SetDirectoryPermissions(m_configDirectory); +} + +QString PathManager::GetEncryptionKey() const +{ + return m_encryptionPassword; +} + +void PathManager::MigrateToEncryptedConfig() +{ + QString oldConfigPath = m_configDirectory + "/config.xml"; + QString newConfigPath = m_configFilePath; // config.encrypt + + QFile oldFile(oldConfigPath); + QFile newFile(newConfigPath); + + // 检查是否需要迁移:config.xml 存在但 config.encrypt 不存在 + if (oldFile.exists() && !newFile.exists()) { + LOG_INFO("发现旧的明文配置文件,开始迁移到加密格式...\n"); + + // 读取旧配置文件 + if (!oldFile.open(QIODevice::ReadOnly)) { + LOG_ERROR("无法打开旧配置文件进行迁移: %s\n", oldConfigPath.toStdString().c_str()); + return; + } + + QByteArray plainData = oldFile.readAll(); + oldFile.close(); + + if (plainData.isEmpty()) { + LOG_WARN("旧配置文件为空,跳过迁移\n"); + return; + } + + // 加密并保存到新文件 + if (WriteEncryptedConfig(newConfigPath, plainData)) { + LOG_INFO("配置文件已成功加密并保存到: %s\n", newConfigPath.toStdString().c_str()); + + // 删除旧的明文文件 + if (oldFile.remove()) { + LOG_INFO("已删除旧的明文配置文件: %s\n", oldConfigPath.toStdString().c_str()); + } else { + LOG_WARN("删除旧配置文件失败: %s\n", oldConfigPath.toStdString().c_str()); + } + } else { + LOG_ERROR("加密配置文件失败,保留旧文件\n"); + } + } + else if (oldFile.exists() && newFile.exists()) { + // 两个文件都存在,说明用户可能刚解密过,准备重新加密 + LOG_INFO("检测到 config.xml 和 config.encrypt 同时存在\n"); + LOG_INFO("将使用 config.xml 更新 config.encrypt 并删除 config.xml\n"); + + // 读取 config.xml + if (!oldFile.open(QIODevice::ReadOnly)) { + LOG_ERROR("无法打开 config.xml: %s\n", oldConfigPath.toStdString().c_str()); + return; + } + + QByteArray plainData = oldFile.readAll(); + oldFile.close(); + + if (!plainData.isEmpty()) { + // 更新加密文件 + if (WriteEncryptedConfig(newConfigPath, plainData)) { + LOG_INFO("已使用 config.xml 更新 config.encrypt\n"); + + // 删除 config.xml + if (oldFile.remove()) { + LOG_INFO("已删除 config.xml\n"); + } else { + LOG_WARN("删除 config.xml 失败: %s\n", oldConfigPath.toStdString().c_str()); + } + } else { + LOG_ERROR("更新加密配置文件失败\n"); + } + } + } + else { + LOG_INFO("配置文件已是加密格式,无需迁移\n"); + } +} diff --git a/GrabBagPrj/BeltTearingApp.iss b/GrabBagPrj/BeltTearingApp.iss index 4eafa90..903eceb 100644 --- a/GrabBagPrj/BeltTearingApp.iss +++ b/GrabBagPrj/BeltTearingApp.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "BeltTearingApp" -#define MyAppVersion "2.0.4.2" +#define MyAppVersion "2.0.5.1" #define MyAppPublisher "" #define MyAppURL "" #define MyAppExeName "BeltTearingApp.exe"