撕裂协议进行修改;撕裂页面修改;撕裂的温度传感器

This commit is contained in:
yiyi 2025-11-30 15:38:27 +08:00
parent 0905b6f6e8
commit cca3e30b7d
24 changed files with 1594 additions and 215 deletions

View File

@ -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."

View File

@ -45,7 +45,7 @@ background-color: rgba(255, 255, 255, 0);</string>
<widget class="QPushButton" name="btn_test">
<property name="geometry">
<rect>
<x>960</x>
<x>1100</x>
<y>21</y>
<width>220</width>
<height>80</height>
@ -68,7 +68,7 @@ border: none;</string>
<widget class="QPushButton" name="btn_camera">
<property name="geometry">
<rect>
<x>440</x>
<x>580</x>
<y>21</y>
<width>220</width>
<height>80</height>
@ -105,32 +105,32 @@ border: none;</string>
<string/>
</property>
<widget class="QLabel" name="label_work">
<property name="geometry">
<rect>
<x>1470</x>
<y>20</y>
<width>271</width>
<height>81</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);</string>
</property>
<property name="text">
<string>工作状态</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
<property name="geometry">
<rect>
<x>1470</x>
<y>20</y>
<width>271</width>
<height>81</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">color: rgb(255, 255, 255);</string>
</property>
<property name="text">
<string>工作状态</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
<widget class="QPushButton" name="btn_close">
<property name="geometry">
<rect>
@ -179,6 +179,9 @@ background-color: rgba(255, 255, 255, 0);</string>
<pointsize>18</pointsize>
</font>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background-image: url(:/common/resource/start.png);
background-color: rgba(255, 255, 255, 0);</string>
@ -186,9 +189,6 @@ background-color: rgba(255, 255, 255, 0);</string>
<property name="text">
<string/>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
<widget class="QPushButton" name="btn_stop">
<property name="geometry">
@ -204,6 +204,9 @@ background-color: rgba(255, 255, 255, 0);</string>
<pointsize>18</pointsize>
</font>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="styleSheet">
<string notr="true">background-image: url(:/common/resource/stop.png);
background-color: rgba(255, 255, 255, 0);</string>
@ -211,14 +214,11 @@ background-color: rgba(255, 255, 255, 0);</string>
<property name="text">
<string/>
</property>
<property name="visible">
<bool>false</bool>
</property>
</widget>
<widget class="QPushButton" name="btn_algo_config">
<property name="geometry">
<rect>
<x>700</x>
<x>840</x>
<y>21</y>
<width>220</width>
<height>80</height>
@ -311,7 +311,7 @@ border: none;</string>
<x>0</x>
<y>0</y>
<width>1920</width>
<height>19</height>
<height>21</height>
</rect>
</property>
<property name="styleSheet">

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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};
};

View File

@ -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<CameraParam> 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) // 默认使用简化协议
{}
};
/**

View File

@ -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
// 保存相机配置

View File

@ -19,6 +19,7 @@
#include <future>
#include <QPainter>
#include <QPainterPath>
#include <QFont>
#include <cmath>
#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<SSG_beltTearingInfo>& 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<uint16_t>(diagonalLength); // 最大长度(毫米)
alarmData.maxWidth = static_cast<uint16_t>(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<uint16_t>(result.tearStatus);
target.width = static_cast<float>(result.tearWidth);
target.depth = static_cast<float>(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<uint16_t>(result.tearStatus);
target.width = static_cast<float>(result.tearWidth);
target.depth = static_cast<float>(result.tearDepth);
// 添加到目标列表
multiTargetData.targets.push_back(target);
}
// 调用RobotProtocol接口发送数据
m_pRobotProtocol->SetMultiTargetData(multiTargetData);
}

View File

@ -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<SSG_beltTearingInfo>& detectionResults);
// 发送温度数据给所有客户端
void SendTemperatureData(float temperature);

View File

@ -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

View File

@ -0,0 +1,205 @@
#include "RobotProtocolSimplified.h"
#include "VrLog.h"
#include "VrError.h"
#include <cstring>
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<uint16_t> 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位存储在地址440005
data[MAX_ID_ADDR] = (alarmData.maxId >> 16) & 0xFFFF;
// 低16位存储在地址540006
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<uint16_t> 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);
}
}

View File

@ -0,0 +1,144 @@
#ifndef ROBOTPROTOCOLSIMPLIFIED_H
#define ROBOTPROTOCOLSIMPLIFIED_H
#include <functional>
#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<void()>;
/**
* @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

View File

@ -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."

View File

@ -1,3 +1,8 @@
# 2.0.5
## build_1 2025-11-30
1. 协议增加最大撕裂的ID
2. 页面修改:上下去掉间隙
# 2.0.4
## build_2 2025-11-24
1. 修复协议控制启停速度错误

View File

@ -1,6 +1,6 @@
#include <iostream>
#include <QCoreApplication>
#include <QGuiApplication>
#include <QDir>
#include <QFile>
#include <QTimer>
@ -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);

View File

@ -0,0 +1,86 @@
# ModbusTCP协议配置说明
## 配置文件位置
配置文件路径通常为:`App/BeltTearing/BeltTearingServer/config.xml`
## 配置项说明
在配置文件的 `<LocalServerConfig>` 节点中添加 `<ModbusTCPProtocol>` 配置项:
### 使用简化协议(默认)
```xml
<LocalServerConfig>
<ServerPort port="5900"/>
<TcpPort port="5800"/>
<ModbusTCPProtocol type="simplified"/>
</LocalServerConfig>
```
**简化协议特点**
- 寄存器地址40001-40006
- 40001: 撕裂报警标志0=无报警1=撕裂报警)
- 40002: 最大撕裂长度mm
- 40003: 最大撕裂宽度mm
- 40004-40005: 最大撕裂IDUInt32占2个寄存器
- 40006: 复位命令写入1执行复位
- 适用于只需要知道最大撕裂信息的简单应用场景
### 使用标准协议
```xml
<LocalServerConfig>
<ServerPort port="5900"/>
<TcpPort port="5800"/>
<ModbusTCPProtocol type="standard"/>
</LocalServerConfig>
```
**标准协议特点**
- 寄存器地址0-80
- 地址0: 系统状态/控制命令
- 地址1-80: 5个撕裂信息每个占16个寄存器包含ID、状态、宽度、深度
- 支持系统启停控制和复位功能
- 适用于需要获取多个撕裂详细信息的复杂应用场景
### 省略配置项
如果配置文件中没有 `<ModbusTCPProtocol>` 配置项,系统将**默认使用简化协议**。
```xml
<LocalServerConfig>
<ServerPort port="5900"/>
<TcpPort port="5800"/>
<!-- 未配置ModbusTCPProtocol将使用默认的简化协议 -->
</LocalServerConfig>
```
## 协议切换说明
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`

View File

@ -2,13 +2,15 @@
## 版本信息
**当前版本**v1.1.0
**文档状态**:正式发布
| 版本号 | 修订日期 | 修订人 | 修订说明 |
|--------|---------|--------|----------|
| v1.1.0 | 2025-11-30 | - | 调整寄存器地址<br>将复位命令移至第一个寄存器,便于后续扩展 |
| 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<br>占用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 @@
### 场景1PLC监控报警
```
循环执行:
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

View File

@ -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..."
}
```

View File

@ -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

View File

@ -0,0 +1,82 @@
#ifndef CONFIGENCRYPTION_H
#define CONFIGENCRYPTION_H
#include <QString>
#include <QByteArray>
/**
* @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

View File

@ -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; // 线程安全的互斥锁

View File

@ -0,0 +1,331 @@
#include "ConfigEncryption.h"
#include <QCryptographicHash>
#include <QFile>
#include <QRandomGenerator>
#include <QDateTime>
#include "VrLog.h"
#ifdef _WIN32
#include <windows.h>
#include <aclapi.h>
#include <sddl.h>
// 取消 Windows API 宏定义,避免与我们的函数名冲突
#ifdef EncryptFile
#undef EncryptFile
#endif
#ifdef DecryptFile
#undef DecryptFile
#endif
#else
// Linux/Unix 平台头文件
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#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<char>(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<LPWSTR>(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<LPWSTR>(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

View File

@ -1,4 +1,5 @@
#include "PathManager.h"
#include "ConfigEncryption.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QFileInfo>
#include <QtCore/QDir>
@ -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<std::mutex> lock(s_mutex);
m_encryptionPassword = password;
LOG_INFO("配置加密密码已设置\n");
}
bool PathManager::EnableEncryptionProtection(bool enable)
{
std::lock_guard<std::mutex> 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");
}
}

View File

@ -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"