一.工作站上传图像 / 元数据→服务器存储→终端获取,三台电脑走 Modbus TCP/IP
终端主动拉取:服务器整理好数据后仅存储,终端按需 / 定时发起请求(如 "获取批次 XXX 的问题图像"),服务器响应并传输数据
**二.**Modbus TCP/IP 通信的关键
通信分层
图像编号、坐标、问题描述、文件路径、指令(如 "拉取批次 XXX 数据")------ModBus Tcp/Ip
原图、问题图像(20MB / 张)------SMB/NFS/FTP(工业级)
三.工作流程
工作站-服务器
- 工作站采集原图 / 问题图像→先存储到本地缓存(避免上传中断丢失);
- 工作站通过 Modbus 向服务器发送 "上传指令 + 元数据"(图像编号、坐标、问题描述、本地路径);
- 服务器通过 SMB/NFS 从工作站拉取图像文件→优先存储到 960G SSD(热数据,20MB×6000 张仅占 120GB,剩余空间缓存),历史数据归档到机械盘;
- 服务器将元数据写入数据库,关联图像文件路径,并通过 Modbus 向工作站返回 "上传成功 / 失败 + MD5 校验值"
- 工作站检测到失败后,3 次重试(间隔 10 秒),仍失败则本地告警并记录日志。
服务器-终端
- 终端通过 Modbus 向服务器发送 "拉取指令"(如 "获取批次 XXX 的问题图像列表");
- 服务器响应 Modbus 指令,返回该批次的元数据(编号、坐标、问题描述)+ 图像文件路径(如 \ 服务器 IP\industrial\batchXXX\error_1.jpg);
- 终端验证路径有效性后,通过 SMB 直接读取服务器 SSD 上的图像文件(分块读取 + MD5 校验);
- 终端读取完成后,通过 Modbus 向服务器反馈 "已成功获取批次 XXX 的图像,数量 XX",服务器记录状态(避免重复拉取);
- 兜底机制:终端读取失败时,自动切换到服务器的机械盘备份路径,同时本地告警。
正确连接
步骤 1:服务器先启动(核心前提)
服务器开机后,先启动三大核心服务并持续运行、等待连接:
- MySQL 服务(存储元数据);
- SMB/NFS 文件共享服务(存储图像文件);
- Modbus TCP 服务端程序(监听 502 端口,被动等待工作站 / 终端的连接请求)。→ 此时服务器处于 "静默等待" 状态,不主动找任何设备,仅开放端口等待连接。
步骤 2:工作站主动连接服务器(上传数据)
- 工作站开机后,先通过网线直连的静态 IP(192.168.0.2→192.168.0.1)ping 通服务器;
- 主动发起 Modbus TCP 连接(目标 IP:192.168.0.1:502),服务器接收并建立连接;
- 工作站通过 SMB 将图像文件上传到服务器的 SSD 目录;
- 工作站通过 Modbus 向服务器发送 "上传指令 + 元数据"(编号、坐标、是否问题图像);
- 服务器处理后,通过 Modbus 回复 "上传成功 / 失败",工作站收到确认后断开 Modbus 连接(或保持长连接);→ 全程是「工作站主动→服务器被动响应」,无服务器反向连接工作站。
步骤 3:终端主动连接服务器(拉取数据)
终端同样主动发起交互(核心是 "拉取",而非等服务器推送):
- 终端开机后,主动 ping 通服务器(192.168.0.3→192.168.0.1,扩展三台时的终端 IP);
- 主动发起 Modbus TCP 连接(目标 IP:192.168.0.1:502),服务器接收并建立连接;
- 终端通过 Modbus 发送 "拉取指令"(如 "获取批次 XXX 的问题图像");
- 服务器通过 Modbus 返回元数据 + 图像路径,终端收到后主动通过 SMB 读取服务器的图像文件;
- 终端读取完成后,通过 Modbus 回复 "拉取完成",断开连接(或定时重复拉取);→ 服务器从未向终端发 "连接申请",终端全程主动,这也是之前推荐 "拉取模式" 的核心 ------ 终端掌控节奏,无丢包 / 丢失风险。
#ifndef CPTEST_H
#define CPTEST_H
// ========== Qt 核心头文件 ==========
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QImage>
#include <QPixmap>
#include <QToolButton>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QListWidgetItem>
#include <QToolBar>
#include <QStackedWidget>
#include <QDateTime>
#include <QTableWidgetItem>
#include <QGroupBox>
#include <QLineEdit>
#include <QComboBox>
#include <QTextEdit>
#include <QHeaderView>
#include <QSpinBox>
#include <QTimer>
#include <QThread>
#include <QMessageBox>
#include <QListWidget>
#include <QTableWidget>
#include <QGridLayout>
#include <QFrame>
#include <QIcon>
#include <QSize>
#include <QFont>
#include <QColor>
#include <QBrush>
#include <QVector>
#include <QPair>
#include <QHostAddress>
// Modbus TCP
#include <QTcpSocket>
#include <QTcpServer>
// 第三方库
#include "libplctag.h"
// 文件/网络
#include <QFile>
#include <QFileDialog>
#include <QDataStream>
// ========== 日志级别 ==========
enum class LogLevel {
Info,
Warning,
Error,
Critical
};
//服务器多线程
// Cptest.cpp 顶部(#include 之后,Cptest 构造函数之前)
// 用于后台处理图片落盘的 worker
class ImageProcessWorker : public QObject
{
Q_OBJECT
public:
explicit ImageProcessWorker(const QString& imgDir,
const QString& metaDir,
QObject* parent = nullptr)
: QObject(parent)
, m_imageDirectory(imgDir)
, m_metadataDirectory(metaDir)
{
}
public slots:
void processMessage(const QByteArray& msg)
{
//多线程处理
const int headerSize = 16;
if (msg.size() < headerSize) {
emit logMessage("后台线程:消息长度不足,丢弃。", LogLevel::Warning);
return;
}
QDataStream in(msg);
in.setByteOrder(QDataStream::BigEndian);
qint32 cameraId;
qint32 defectCount;
quint32 origImgSize;
quint32 defectImgSize;
in >> cameraId;
in >> defectCount;
in >> origImgSize;
in >> defectImgSize;
// 原图
QByteArray origImgData;
origImgData.resize(origImgSize);
if (origImgSize > 0) {
if (in.readRawData(origImgData.data(), origImgSize) != (int)origImgSize) {
emit logMessage("后台线程:读取原图数据失败。", LogLevel::Error);
return;
}
}
// 缺陷图(预留)
QByteArray defectImgData;
if (defectImgSize > 0) {
defectImgData.resize(defectImgSize);
if (in.readRawData(defectImgData.data(), defectImgSize) != (int)defectImgSize) {
emit logMessage("后台线程:读取缺陷图数据失败。", LogLevel::Error);
return;
}
}
QVector<QPair<double, double>> coords;
coords.reserve(defectCount);
for (int i = 0; i < defectCount; ++i) {
double x, y, w, h;
quint16 defectType;
in >> x;
in >> y;
in >> w;
in >> h;
in >> defectType;
coords.push_back({ x,y });
}
// 保存图像到 E 盘(或你配置的目录)
if (origImgSize > 0) {
QString imagePath = m_imageDirectory
- QString("camera_%1_%2.png")
.arg(cameraId)
.arg(QDateTime::currentMSecsSinceEpoch());
QFile imageFile(imagePath);
if (imageFile.open(QIODevice::WriteOnly)) {
imageFile.write(origImgData);
imageFile.close();
emit logMessage(QString("后台线程:图像已保存到 %1").arg(imagePath),
LogLevel::Info);
}
else {
emit logMessage("后台线程:图像保存失败!", LogLevel::Error);
}
}
// 保存元数据到 D 盘
{
QString metadataPath = m_metadataDirectory
- QString("camera_%1_%2.txt")
.arg(cameraId)
.arg(QDateTime::currentMSecsSinceEpoch());
QFile metadataFile(metadataPath);
if (metadataFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&metadataFile);
out << "Camera ID: " << cameraId << "\n";
out << "Defect Count: " << defectCount << "\n";
out << "Defect Coordinates:\n";
for (const auto& c : coords) {
out << QString("X: %1, Y: %2\n").arg(c.first).arg(c.second);
}
metadataFile.close();
emit logMessage(QString("后台线程:元数据已保存到 %1").arg(metadataPath),
LogLevel::Info);
}
else {
emit logMessage("后台线程:元数据保存失败!", LogLevel::Error);
}
}
double firstX = 0.0;
double firstY = 0.0;
if (defectCount > 0 && !coords.isEmpty()) {
firstX = coords[0].first;
firstY = coords[0].second;
}
// 完成后可以给主线程一个"处理结束"的信号(可选)
emit messageProcessed(cameraId, defectCount, firstX, firstY);
}
signals:
void logMessage(const QString& msg, LogLevel level);
void messageProcessed(qint32 cameraId,
qint32 defectCount,
double firstX,
double firstY);//把核心信息带回主线程
private:
QString m_imageDirectory;
QString m_metadataDirectory;
};
// ========== 主界面类 ==========
class Cptest : public QWidget {
Q_OBJECT
public:
explicit Cptest(QWidget* parent = nullptr);
~Cptest() override;
// 公有控件(兼容原有代码)
QPushButton* applyConfigBtn = nullptr;
QPushButton* connectBtn = nullptr;
QPushButton* disconnectBtn = nullptr;
QPushButton* sendBtn = nullptr;
void onSendButtonClicked();
QString m_metadataDirectory = "D:/metadata/"; // D盘用于存储元数据模拟硬盘
QString m_imageDirectory = "E:/images/"; // E盘用于存储图像
signals:
void backRequestedCamera();
void imageMessageReady(const QByteArray& msg);// 再加一个信号,用来把完整消息丢给 worker
private slots:
void onBackHomeClicked();
void onReadTimerTimeout();
void onTreeItemClicked(QTreeWidgetItem* item, int column);
void onImageMessageProcessed(qint32 cameraId,
qint32 defectCount,
double firstX,
double firstY);
// Modbus
void onModbusConnectListenClicked();
void onModbusDisconnectStopClicked();
void onModbusSendClicked();
// Modbus 主站 socket 事件
void onMasterConnected();
void onMasterDisconnected();
void onMasterReadyRead();
void onMasterError(QAbstractSocket::SocketError socketError);
// Modbus 从站 socket 事件
void onSlaveNewConnection();
void onSlaveClientDisconnected();
void onSlaveClientReadyRead();
// Modbus 主站轮询定时器
void onModbusMasterPollTimeout();
// 图像传输 UI 槽函数
void onImageRoleChanged(int index);
void onImageConnectClicked();
void onImageDisconnectClicked();
void onImageSendClicked();
void onSelectImage1Clicked();
void onSelectImage2Clicked(); // [unused]
// 图像传输网络事件槽函数
void onImageNewConnection();
void onImageSocketDisconnected();
void onImageSocketReadyRead();
void onImageSocketError(QAbstractSocket::SocketError socketError);
private:
// UI
QThread* m_imageWorkerThread = nullptr;
ImageProcessWorker* m_imageWorker = nullptr;
static const int IMAGE_CHUNK_SIZE = 4 * 1024 * 1024;//分块
void setupUI();
void setTreestyle();
void setupFunctionPanels();
void setupABPLCPanel();
void setupModBusPanel();
void setupImageTransferPanel();
// 日志
void addLog(const QString& message, LogLevel level);
// EtherNet/IP
bool InitEtherNetIPConnection();
void CloseEtherNetIPConnection();
void OnBnClickedStaticCommunication();
bool ReadPLCTag(int32_t tagHandle, int& value);
bool WritePLCTag(int32_t tagHandle, int value);
// ========== UI核心控件 ==========
QToolBar* toolBar = nullptr;
QToolButton* BackHome = nullptr;
QTreeWidget* treeWidget = nullptr;
QListWidget* logList = nullptr;
QStackedWidget* functionStack = nullptr;
QTableWidget* signalTable = nullptr;
QLineEdit* inputLineEdit = nullptr;
// EtherNet/IP 相关
int32_t m_plcTagHandle = -1;
QString m_plcIpAddress;
int m_plcSlot = 0;
QTimer* readTimer = nullptr;
// EtherNet/IP 配置控件
QLineEdit* gatewayEdit = nullptr;
QLineEdit* pathEdit = nullptr;
QLineEdit* plcTypeEdit = nullptr;
QLineEdit* tagNameEdit = nullptr;
QComboBox* elemTypeCombo = nullptr;
QLineEdit* elemCountEdit = nullptr;
QLineEdit* timeoutEdit = nullptr;
// ========== Modbus TCP 配置控件 ==========
// 主站配置
QLineEdit* modbusMasterSlaveIpEdit = nullptr;
QLineEdit* modbusMasterSlavePortEdit = nullptr;
QLineEdit* modbusMasterSlaveIdEdit = nullptr;
QComboBox* modbusMasterRegTypeCombo = nullptr;
QLineEdit* modbusMasterRegStartEdit = nullptr;
QComboBox* modbusMasterDataTypeCombo = nullptr;
QLineEdit* modbusMasterRegLenEdit = nullptr;
QLineEdit* modbusMasterTimeoutEdit = nullptr;
// 从站配置
QLineEdit* modbusSlaveListenIpEdit = nullptr;
QLineEdit* modbusSlaveListenPortEdit = nullptr;
QLineEdit* modbusSlaveSelfIdEdit = nullptr;
QLineEdit* modbusSlaveRegCountEdit = nullptr;
QLineEdit* modbusSlaveTimeoutEdit = nullptr;
// Modbus通用控件
QTextEdit* modbusConfigDisplayEdit = nullptr;
QPushButton* modbusApplyConfigBtn = nullptr;
QPushButton* modbusConnectListenBtn = nullptr;
QPushButton* modbusDisconnectStopBtn = nullptr;
QLineEdit* modbusInputLineEdit = nullptr;
QPushButton* modbusSendBtn = nullptr;
// Modbus 核心变量
bool isMasterMode = true;
// Modbus 内部对象
QTcpSocket* m_modbusMasterSocket = nullptr;
QTimer* m_modbusMasterPollTimer = nullptr;
quint16 m_mbTransactionId = 1;
QTcpServer* m_modbusSlaveServer = nullptr;
QTcpSocket* m_modbusSlaveClient = nullptr;
QVector<quint16> m_slaveHoldingRegs;
// Modbus 辅助函数
void modbusMasterOpen();
void modbusMasterClose();
void modbusMasterSendReadRequest(); // 0x03 读保持寄存器
void modbusMasterSendWriteSingleRequest(quint16 v); // 0x06 写单个保持寄存器
bool modbusSlaveStart();
void modbusSlaveStop();
void modbusSlaveInitHoldingRegs(int count);
void modbusSlaveHandleRequest(const QByteArray& req, QByteArray& resp);
// 表格实时变化
quint16 m_masterBaseAddr = 0;
quint16 m_masterRegCount = 0;
// ========== 图像传输(服务器/工作站) ==========
QWidget* imageTransPanel = nullptr;
QComboBox* imgRoleCombo = nullptr;
QLabel* imgIpLabel = nullptr; // 预留
QLineEdit* imgServerIpEdit = nullptr; // 服务器绑定 IP(接收端用)
QLineEdit* imgClientIpEdit = nullptr; // 对端 IP(发送端用)
QLineEdit* imgPortEdit = nullptr;
// 协议字段
QLineEdit* imgIdEdit = nullptr;
QLineEdit* imgDefectCountEdit = nullptr;
QLineEdit* imgXEdit = nullptr;
QLineEdit* imgYEdit = nullptr;
QLineEdit* imgWEdit = nullptr;
QLineEdit* imgHEdit = nullptr;
// 不参与发送的旧控件
QLineEdit* imgZEdit = nullptr;
QTextEdit* imgTextEdit = nullptr;
QPushButton* imgSelectImg2Btn = nullptr;
QPushButton* imgSelectImg1Btn = nullptr;
QPushButton* imgConnectBtn = nullptr;
QPushButton* imgDisconnectBtn = nullptr;
QPushButton* imgSendBtn = nullptr;
QLabel* imgStatusLabel = nullptr;
QString m_imgFile1Path;
QString m_imgFile2Path;
// 网络对象
QTcpServer* m_imageServer = nullptr; // 接收端监听
QTcpSocket* m_imageServerSocket = nullptr; // 接收端连接
QTcpSocket* m_imageClientSocket = nullptr; // 发送端连接
QByteArray m_imageRecvBuffer;
quint64 m_imageRecvCount = 0;
quint64 m_imageRecvBytes = 0;
bool m_isImageServer = true;
// 图像传输辅助函数
void resetImageMonitor();
void updateImageMonitorLast(qint32 cameraId, qint32 defectCount,
const QVector<QPair<double, double>>& coords);
//void processOneImageMessage(const QByteArray& msg);
// 预留:后期将解析后的数据写入数据库
void saveImageMessageToDb(qint32 cameraId,
qint32 defectCount,
const QVector<QPair<double, double>>& coords);
};
#endif // CPTEST_H
#include "Cptest.h"
// ------------------------ 构造 / 析构 ------------------------
Cptest::Cptest(QWidget* parent)
: QWidget(parent)
{
setupUI();
// 检查目录是否存在,若不存在则创建
QDir metadataDir(m_metadataDirectory);
if (!metadataDir.exists()) {
metadataDir.mkpath(".");
}
QDir imageDir(m_imageDirectory);
if (!imageDir.exists()) {
imageDir.mkpath(".");
}
// ABPLC 定时器
readTimer = new QTimer(this);
connect(readTimer, &QTimer::timeout, this, &Cptest::onReadTimerTimeout);
// Modbus 主站轮询定时器
m_modbusMasterPollTimer = new QTimer(this);
m_modbusMasterPollTimer->setInterval(1000);
connect(m_modbusMasterPollTimer, &QTimer::timeout,
this, &Cptest::onModbusMasterPollTimeout);
// Modbus 主站 socket
m_modbusMasterSocket = new QTcpSocket(this);
connect(m_modbusMasterSocket, &QTcpSocket::connected,
this, &Cptest::onMasterConnected);
connect(m_modbusMasterSocket, &QTcpSocket::disconnected,
this, &Cptest::onMasterDisconnected);
connect(m_modbusMasterSocket, &QTcpSocket::readyRead,
this, &Cptest::onMasterReadyRead);
connect(m_modbusMasterSocket,
QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred),
this, &Cptest::onMasterError);
// Modbus 从站 server
m_modbusSlaveServer = new QTcpServer(this);
connect(m_modbusSlaveServer, &QTcpServer::newConnection,
this, &Cptest::onSlaveNewConnection);
//===============================================多线程
m_imageWorkerThread = new QThread(this);
m_imageWorker = new ImageProcessWorker(m_imageDirectory, m_metadataDirectory);
m_imageWorker->moveToThread(m_imageWorkerThread);
// 把"有完整一帧消息"的信号丢给 worker
connect(this, &Cptest::imageMessageReady,
m_imageWorker, &ImageProcessWorker::processMessage,
Qt::QueuedConnection);
//关键信息反馈给主线程
connect(m_imageWorker, &ImageProcessWorker::messageProcessed,
this, &Cptest::onImageMessageProcessed,
Qt::QueuedConnection);
// worker 把日志发回主线程,由主线程 addLog
connect(m_imageWorker, &ImageProcessWorker::logMessage,
this, &Cptest::addLog,
Qt::QueuedConnection);
// 可选:处理完成回调
connect(m_imageWorker, &ImageProcessWorker::messageProcessed,
this, [this](qint32 camId, qint32 defectCount) {
addLog(QString("后台线程处理完成:cameraId=%1, defectCount=%2")
.arg(camId).arg(defectCount),
LogLevel::Info);
},
Qt::QueuedConnection);
m_imageWorkerThread->start();
}
Cptest::~Cptest() {
if (readTimer) {
readTimer->stop();
}
//停止线程
if (m_imageWorkerThread) {
m_imageWorkerThread->quit();
m_imageWorkerThread->wait();
// m_imageWorker 由线程父子关系自动 delete,也可以手动 deleteLater
}
}
// ------------------------ UI 相关 ------------------------
void Cptest::setupUI() {
QVBoxLayout* mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// 顶部导航栏
QWidget* topBar = new QWidget(this);
QHBoxLayout* topLayout = new QHBoxLayout(topBar);
topLayout->setContentsMargins(10, 0, 10, 0);
topBar->setStyleSheet("background-color: #add8e6;");
BackHome = new QToolButton(topBar);
BackHome->setIcon(QIcon("../image/home.png"));
BackHome->setIconSize(QSize(40, 40));
BackHome->setToolTip("返回主界面");
BackHome->setStyleSheet("QToolButton { background-color: transparent; border: none; }");
QLabel* titleLabel = new QLabel("通信测试模块", topBar);
titleLabel->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
topLayout->addWidget(BackHome);
topLayout->addWidget(titleLabel);
topLayout->addStretch();
mainLayout->addWidget(topBar);
// 主内容区域
QWidget* contentWidget = new QWidget(this);
QHBoxLayout* contentLayout = new QHBoxLayout(contentWidget);
contentLayout->setContentsMargins(0, 0, 0, 0);
contentLayout->setSpacing(0);
// 左侧树形导航
treeWidget = new QTreeWidget(this);
treeWidget->setHeaderHidden(true);
treeWidget->setFixedWidth(200);
setTreestyle();
QTreeWidgetItem* processingRoot =
new QTreeWidgetItem(treeWidget, QStringList() << "ABPLC通信协议TCP/IP");
QTreeWidgetItem* projectCreateItem =
new QTreeWidgetItem(processingRoot, QStringList() << "ABPLC通信测试");
QTreeWidgetItem* processingRoot1 =
new QTreeWidgetItem(treeWidget, QStringList() << "ModbusTcp通信协议TCP/IP");
QTreeWidgetItem* projectCreateItem1 =
new QTreeWidgetItem(processingRoot1, QStringList() << "ModbusTcp通信测试");
QTreeWidgetItem* projectCreateItem2 =
new QTreeWidgetItem(processingRoot1, QStringList() << "服务器");
treeWidget->expandAll();
contentLayout->addWidget(treeWidget);
// 右侧区域
QWidget* rightWidget = new QWidget(contentWidget);
QVBoxLayout* rightLayout = new QVBoxLayout(rightWidget);
rightLayout->setContentsMargins(10, 10, 10, 10);
rightLayout->setSpacing(10);
// 功能区 + 显示区
QWidget* contentContainer = new QWidget(rightWidget);
QHBoxLayout* containerLayout = new QHBoxLayout(contentContainer);
containerLayout->setContentsMargins(0, 0, 0, 0);
containerLayout->setSpacing(15);
// 左侧功能区
QWidget* functionPanel = new QWidget(contentContainer);
functionPanel->setMinimumWidth(300);
QVBoxLayout* functionPanelLayout = new QVBoxLayout(functionPanel);
functionPanelLayout->setContentsMargins(0, 0, 0, 0);
functionStack = new QStackedWidget(functionPanel);
functionPanelLayout->addWidget(functionStack);
setupFunctionPanels();
// 右侧表格区
QWidget* ValuePanel = new QWidget(contentContainer);
QVBoxLayout* ValueLayout = new QVBoxLayout(ValuePanel);
ValueLayout->setContentsMargins(0, 0, 0, 0);
signalTable = new QTableWidget(10, 2, ValuePanel);
signalTable->setStyleSheet("QTableWidget {"
" border: 1px solid #cccccc;"
" gridline-color: #cccccc;"
" background-color: #f5f5f5;"
"}"
"QHeaderView::section {"
" background-color: #e0e0e0;"
" font-weight: bold;"
" border: 1px solid #cccccc;"
" padding: 4px;"
"}"
"QTableWidget::item {"
" background-color: #f5f5f5;"
" color: #333333;"
"}"
"QTableWidget::item:selected {"
" background-color: #d0d0d0;"
" color: #000000;"
"}"
"QTableWidget::item:alternate {"
" background-color: #eeeeee;"
"}");
signalTable->setHorizontalHeaderLabels({ "接收", "发送" });
signalTable->setVerticalHeaderLabels({ "0","1","2","3","4","5","6","7","8","9" });
signalTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
signalTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
signalTable->verticalHeader()->setDefaultSectionSize(30);
for (int row = 0; row < 10; ++row) {
for (int col = 0; col < 2; ++col) {
QTableWidgetItem* item = new QTableWidgetItem("");
item->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(row, col, item);
}
}
signalTable->setFont(QFont("Arial", 10));
signalTable->setAlternatingRowColors(true);
ValueLayout->addWidget(signalTable);
containerLayout->addWidget(functionPanel, 3);
containerLayout->addWidget(ValuePanel, 7);
// 底部日志区域
QWidget* logWidget = new QWidget(rightWidget);
logWidget->setFixedHeight(150);
logWidget->setStyleSheet("background-color: #2c3e50;");
QVBoxLayout* logLayout = new QVBoxLayout(logWidget);
logLayout->setContentsMargins(10, 5, 10, 5);
QLabel* logTitle = new QLabel("操作日志", logWidget);
logTitle->setStyleSheet("color: white; font-weight: bold;");
logList = new QListWidget(logWidget);
logList->setStyleSheet("background-color: #34495e; color: #ecf0f1; border: none;");
logLayout->addWidget(logTitle);
logLayout->addWidget(logList);
rightLayout->addWidget(contentContainer, 1);
rightLayout->addWidget(logWidget);
contentLayout->addWidget(rightWidget, 1);
mainLayout->addWidget(contentWidget, 1);
connect(BackHome, &QToolButton::clicked, this, &Cptest::onBackHomeClicked);
connect(treeWidget, &QTreeWidget::itemClicked, this, &Cptest::onTreeItemClicked);
processingRoot->setExpanded(true);
treeWidget->setCurrentItem(projectCreateItem);
functionStack->setCurrentIndex(0);
addLog("欢迎使用通信测试模块", LogLevel::Info);
addLog("已进入ABPLC通信协议测试功能", LogLevel::Info);
}
void Cptest::setupFunctionPanels() {
setupABPLCPanel();
setupModBusPanel();
setupImageTransferPanel();
}
void Cptest::onBackHomeClicked() {
emit backRequestedCamera();
}
void Cptest::onTreeItemClicked(QTreeWidgetItem* item, int column) {
Q_UNUSED(column);
QString itemText = item->text(0);
if (itemText == "ABPLC通信测试") {
functionStack->setCurrentIndex(0);
addLog("切换到通信测试", LogLevel::Info);
}
else if (itemText == "ModbusTcp通信测试") {
functionStack->setCurrentIndex(1);
addLog("切换到通信测试", LogLevel::Info);
}
else if (itemText == "服务器") {
functionStack->setCurrentIndex(2);
addLog("切换到 图像传输测试(工作站/服务器)页面", LogLevel::Info);
}
}
// ------------------------ ABPLC UI & 通信 ------------------------
void Cptest::setupABPLCPanel() {
QWidget* mainPanel = new QWidget();
QVBoxLayout* mainLayout = new QVBoxLayout(mainPanel);
mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(15);
// 通信配置
QGroupBox* configGroup = new QGroupBox("通信配置", mainPanel);
QGridLayout* configLayout = new QGridLayout(configGroup);
configLayout->setSpacing(10);
configLayout->setColumnStretch(1, 1);
QLabel* plcIpLabel = new QLabel("PLC地址:", configGroup);
gatewayEdit = new QLineEdit("172.16.2.51", configGroup);
configLayout->addWidget(plcIpLabel, 0, 0);
configLayout->addWidget(gatewayEdit, 0, 1);
QLabel* pathLabel = new QLabel("路径:", configGroup);
pathEdit = new QLineEdit("1,0", configGroup);
configLayout->addWidget(pathLabel, 1, 0);
configLayout->addWidget(pathEdit, 1, 1);
QLabel* plcTypeLabel = new QLabel("PLC类型:", configGroup);
plcTypeEdit = new QLineEdit("ControlLogix", configGroup);
configLayout->addWidget(plcTypeLabel, 2, 0);
configLayout->addWidget(plcTypeEdit, 2, 1);
QLabel* tagNameLabel = new QLabel("标签名:", configGroup);
tagNameEdit = new QLineEdit("LMY20250617", configGroup);
configLayout->addWidget(tagNameLabel, 3, 0);
configLayout->addWidget(tagNameEdit, 3, 1);
QLabel* elemTypeLabel = new QLabel("元素类型:", configGroup);
elemTypeCombo = new QComboBox(configGroup);
elemTypeCombo->addItems({ "SINT", "INT", "DINT", "REAL", "BOOL", "BYTE", "WORD", "DWORD" });
elemTypeCombo->setCurrentText("SINT");
configLayout->addWidget(elemTypeLabel, 4, 0);
configLayout->addWidget(elemTypeCombo, 4, 1);
QLabel* elemCountLabel = new QLabel("元素数量:", configGroup);
elemCountEdit = new QLineEdit("1", configGroup);
configLayout->addWidget(elemCountLabel, 5, 0);
configLayout->addWidget(elemCountEdit, 5, 1);
QLabel* timeoutLabel = new QLabel("超时时间(毫秒):", configGroup);
timeoutEdit = new QLineEdit("5000", configGroup);
configLayout->addWidget(timeoutLabel, 6, 0);
configLayout->addWidget(timeoutEdit, 6, 1);
QFrame* line = new QFrame(configGroup);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
configLayout->addWidget(line, 7, 0, 1, 2);
QLabel* configDisplayLabel = new QLabel("当前配置:", configGroup);
QTextEdit* configDisplayEdit = new QTextEdit(configGroup);
configDisplayEdit->setReadOnly(true);
configDisplayEdit->setMinimumHeight(80);
configLayout->addWidget(configDisplayLabel, 8, 0);
configLayout->addWidget(configDisplayEdit, 9, 0, 1, 2);
QHBoxLayout* btnLayout = new QHBoxLayout();
applyConfigBtn = new QPushButton("应用配置", configGroup);
connectBtn = new QPushButton("连接", configGroup);
disconnectBtn = new QPushButton("断开", configGroup);
connectBtn->setEnabled(false);
disconnectBtn->setEnabled(false);
btnLayout->addWidget(applyConfigBtn);
btnLayout->addWidget(connectBtn);
btnLayout->addWidget(disconnectBtn);
configLayout->addLayout(btnLayout, 10, 0, 1, 2);
connect(applyConfigBtn, &QPushButton::clicked, [=]() {
QString config = QString("gateway=%1&path=%2&plc=%3&name=%4&elem_type=%5&elem_count=%6&timeout=%7")
.arg(gatewayEdit->text())
.arg(pathEdit->text())
.arg(plcTypeEdit->text())
.arg(tagNameEdit->text())
.arg(elemTypeCombo->currentText())
.arg(elemCountEdit->text())
.arg(timeoutEdit->text());
configDisplayEdit->setPlainText(config);
connectBtn->setEnabled(true);
disconnectBtn->setEnabled(false);
addLog("已应用ABPLC通信配置", LogLevel::Info);
});
connect(connectBtn, &QPushButton::clicked, this, &Cptest::OnBnClickedStaticCommunication);
connect(disconnectBtn, &QPushButton::clicked, [=]() {
CloseEtherNetIPConnection();
connectBtn->setEnabled(true);
disconnectBtn->setEnabled(false);
addLog("已断开与ABPLC的连接", LogLevel::Info);
});
mainLayout->addWidget(configGroup);
// 通信测试
QGroupBox* testGroup = new QGroupBox("通信测试", mainPanel);
QHBoxLayout* testLayout = new QHBoxLayout(testGroup);
testLayout->setSpacing(10);
QLabel* inputLabel = new QLabel("输入测试:", testGroup);
inputLineEdit = new QLineEdit(testGroup);
inputLineEdit->setPlaceholderText("输入要发送的数据...");
sendBtn = new QPushButton("发送", testGroup);
sendBtn->setFixedWidth(100);
connect(sendBtn, &QPushButton::clicked, this, &Cptest::onSendButtonClicked);
sendBtn->setEnabled(false);
testLayout->addWidget(inputLabel);
testLayout->addWidget(inputLineEdit, 1);
testLayout->addWidget(sendBtn);
mainLayout->addWidget(testGroup);
mainLayout->addStretch();
functionStack->addWidget(mainPanel);
}
// ABPLC 连接
bool Cptest::InitEtherNetIPConnection()
{
plc_tag_set_debug_level(PLCTAG_DEBUG_WARN);
QString protocol = "ab_eip";
QString gateway = gatewayEdit->text();
QString path = pathEdit->text();
QString plcType = plcTypeEdit->text();
QString tagName = tagNameEdit->text();
QString elemType = elemTypeCombo->currentText();
QString elemCountStr = elemCountEdit->text();
QString timeoutStr = timeoutEdit->text();
bool elemCountOk;
int elemCount = elemCountStr.toInt(&elemCountOk);
if (!elemCountOk || elemCount < 1) {
addLog("错误:元素数量必须是正整数", LogLevel::Error);
return false;
}
bool timeoutOk;
int timeout = timeoutStr.toInt(&timeoutOk);
if (!timeoutOk || timeout < 0) {
addLog("错误:超时时间必须是非负整数", LogLevel::Error);
return false;
}
QString attrib_str = QString(
"protocol=%1&"
"gateway=%2&"
"path=%3&"
"plc=%4&"
"name=%5&"
"elem_type=%6&"
"elem_count=%7&"
"timeout=%8"
).arg(protocol, gateway, path, plcType, tagName, elemType,
QString::number(elemCount), QString::number(timeout));
QByteArray attrib_ba = attrib_str.toUtf8();
const char* attrib_cstr = attrib_ba.constData();
m_plcTagHandle = plc_tag_create(attrib_cstr, 5000);
if (m_plcTagHandle < 0) {
addLog(QString("EtherNet/IP连接失败: %1").arg(plc_tag_decode_error(m_plcTagHandle)),
LogLevel::Warning);
return false;
}
int status = plc_tag_status(m_plcTagHandle);
if (status != PLCTAG_STATUS_OK) {
addLog(QString("EtherNet/IP连接失败: %1").arg(plc_tag_decode_error(status)),
LogLevel::Warning);
return false;
}
readTimer->start(1000);
return true;
}
void Cptest::CloseEtherNetIPConnection()
{
if (m_plcTagHandle >= 0) {
plc_tag_destroy(m_plcTagHandle);
m_plcTagHandle = -1;
readTimer->stop();
addLog("已断开PLC连接并停止数据读取", LogLevel::Info);
if (connectBtn) connectBtn->setEnabled(true);
if (disconnectBtn) disconnectBtn->setEnabled(false);
if (sendBtn) sendBtn->setEnabled(false);
}
}
void Cptest::OnBnClickedStaticCommunication() {
addLog("请求连接ABPLC", LogLevel::Info);
if (InitEtherNetIPConnection()) {
connectBtn->setEnabled(false);
disconnectBtn->setEnabled(true);
sendBtn->setEnabled(true);
addLog("PLC连接成功!", LogLevel::Info);
}
else {
addLog("PLC连接失败!", LogLevel::Info);
}
}
bool Cptest::ReadPLCTag(int32_t tagHandle, int& value)
{
if (tagHandle < 0) {
addLog("无效的PLC标签句柄!", LogLevel::Warning);
return false;
}
int ret = plc_tag_read(tagHandle, 5000);
if (ret != PLCTAG_STATUS_OK) {
addLog(QString("读取失败: %1").arg(plc_tag_decode_error(ret)), LogLevel::Warning);
return false;
}
int8_t readValue = plc_tag_get_int8(tagHandle, 0);
value = static_cast<int>(readValue);
return true;
}
bool Cptest::WritePLCTag(int32_t tagHandle, int value)
{
if (tagHandle < 0) {
addLog("无效的PLC标签句柄!", LogLevel::Warning);
return false;
}
if (value < -128 || value > 127) {
addLog("错误:写入值超出SINT范围 (-128 到 127)", LogLevel::Warning);
return false;
}
plc_tag_set_int8(tagHandle, 0, static_cast<int8_t>(value));
int ret = plc_tag_write(tagHandle, 8000);
if (ret != PLCTAG_STATUS_OK) {
addLog(QString("写入失败: %1").arg(plc_tag_decode_error(ret)), LogLevel::Warning);
return false;
}
return true;
}
void Cptest::onReadTimerTimeout() {
int value;
if (ReadPLCTag(m_plcTagHandle, value)) {
QTableWidgetItem* item = signalTable->item(0, 0);
if (!item) {
item = new QTableWidgetItem();
signalTable->setItem(0, 0, item);
}
item->setText(QString::number(value));
addLog(QString("读取PLC数据成功,值:%1").arg(value), LogLevel::Info);
}
else {
addLog("读取PLC数据失败", LogLevel::Warning);
}
}
void Cptest::onSendButtonClicked() {
QString strValue = inputLineEdit->text();
if (strValue.isEmpty()) {
addLog("错误:请输入要写入的值", LogLevel::Warning);
return;
}
bool ok;
int writeValue = strValue.toInt(&ok);
if (!ok || writeValue < -128 || writeValue > 127) {
addLog("错误:输入值必须是-128到127之间的整数", LogLevel::Warning);
return;
}
if (WritePLCTag(m_plcTagHandle, writeValue)) {
addLog("写入PLC成功", LogLevel::Info);
QTableWidgetItem* item = signalTable->item(0, 1);
if (!item) {
item = new QTableWidgetItem();
signalTable->setItem(0, 1, item);
}
item->setText(QString::number(writeValue));
}
else {
addLog("写入PLC失败", LogLevel::Warning);
}
}
// ------------------------ Modbus UI & 逻辑 ------------------------
void Cptest::setupModBusPanel() {
isMasterMode = true;
QWidget* ModBusmainPanel = new QWidget();
QVBoxLayout* ModBusmainLayout = new QVBoxLayout(ModBusmainPanel);
ModBusmainLayout->setContentsMargins(10, 10, 10, 10);
ModBusmainLayout->setSpacing(15);
QGroupBox* configGroup = new QGroupBox("Modbus TCP 通信配置", ModBusmainPanel);
QVBoxLayout* configRootLayout = new QVBoxLayout(configGroup);
configRootLayout->setSpacing(20);
QWidget* switchWidget = new QWidget(configGroup);
QHBoxLayout* switchLayout = new QHBoxLayout(switchWidget);
switchLayout->setContentsMargins(0, 0, 0, 0);
switchLayout->setSpacing(10);
QLabel* modeLabel = new QLabel("当前模式:主站(主动请求)", switchWidget);
QPushButton* switchModeBtn = new QPushButton("切换到从站模式", switchWidget);
switchModeBtn->setFixedWidth(120);
switchLayout->addWidget(modeLabel);
switchLayout->addStretch();
switchLayout->addWidget(switchModeBtn);
configRootLayout->addWidget(switchWidget);
// 主站配置
QGroupBox* masterGroup = new QGroupBox("主站(主动请求)配置", configGroup);
QGridLayout* masterLayout = new QGridLayout(masterGroup);
masterLayout->setSpacing(10);
masterLayout->setColumnStretch(1, 1);
QLabel* masterSlaveIpLabel = new QLabel("从站IP地址:", masterGroup);
modbusMasterSlaveIpEdit = new QLineEdit("127.0.0.1", masterGroup);
masterLayout->addWidget(masterSlaveIpLabel, 0, 0);
masterLayout->addWidget(modbusMasterSlaveIpEdit, 0, 1);
QLabel* masterSlavePortLabel = new QLabel("从站端口:", masterGroup);
modbusMasterSlavePortEdit = new QLineEdit("502", masterGroup);
masterLayout->addWidget(masterSlavePortLabel, 1, 0);
masterLayout->addWidget(modbusMasterSlavePortEdit, 1, 1);
QLabel* masterSlaveIdLabel = new QLabel("从站ID:", masterGroup);
modbusMasterSlaveIdEdit = new QLineEdit("1", masterGroup);
masterLayout->addWidget(masterSlaveIdLabel, 2, 0);
masterLayout->addWidget(modbusMasterSlaveIdEdit, 2, 1);
QLabel* masterRegTypeLabel = new QLabel("寄存器类型:", masterGroup);
modbusMasterRegTypeCombo = new QComboBox(masterGroup);
modbusMasterRegTypeCombo->addItems({
"保持寄存器(HR)",
"输入寄存器(IR)",
"线圈(Coil)",
"离散输入(DI)"
});
modbusMasterRegTypeCombo->setCurrentText("保持寄存器(HR)");
masterLayout->addWidget(masterRegTypeLabel, 3, 0);
masterLayout->addWidget(modbusMasterRegTypeCombo, 3, 1);
QLabel* masterRegStartLabel = new QLabel("起始地址:", masterGroup);
modbusMasterRegStartEdit = new QLineEdit("0", masterGroup);
masterLayout->addWidget(masterRegStartLabel, 4, 0);
masterLayout->addWidget(modbusMasterRegStartEdit, 4, 1);
QLabel* masterDataTypeLabel = new QLabel("数据类型:", masterGroup);
modbusMasterDataTypeCombo = new QComboBox(masterGroup);
modbusMasterDataTypeCombo->addItems({
"uint16(16位无符号整型)",
"int16(16位有符号整型)",
"uint32(32位无符号整型)",
"int32(32位有符号整型)",
"float(32位浮点型)"
});
modbusMasterDataTypeCombo->setCurrentText("uint16(16位无符号整型)");
masterLayout->addWidget(masterDataTypeLabel, 5, 0);
masterLayout->addWidget(modbusMasterDataTypeCombo, 5, 1);
QLabel* masterRegLenLabel = new QLabel("读写长度:", masterGroup);
modbusMasterRegLenEdit = new QLineEdit("1", masterGroup);
masterLayout->addWidget(masterRegLenLabel, 6, 0);
masterLayout->addWidget(modbusMasterRegLenEdit, 6, 1);
QLabel* masterTimeoutLabel = new QLabel("超时时间(毫秒):", masterGroup);
modbusMasterTimeoutEdit = new QLineEdit("5000", masterGroup);
masterLayout->addWidget(masterTimeoutLabel, 7, 0);
masterLayout->addWidget(modbusMasterTimeoutEdit, 7, 1);
// 从站配置
QGroupBox* slaveGroup = new QGroupBox("从站(被动监听)配置", configGroup);
QGridLayout* slaveLayout = new QGridLayout(slaveGroup);
slaveLayout->setSpacing(10);
slaveLayout->setColumnStretch(1, 1);
QLabel* slaveListenIpLabel = new QLabel("监听IP:", slaveGroup);
modbusSlaveListenIpEdit = new QLineEdit("0.0.0.0", slaveGroup);
slaveLayout->addWidget(slaveListenIpLabel, 0, 0);
slaveLayout->addWidget(modbusSlaveListenIpEdit, 0, 1);
QLabel* slaveListenPortLabel = new QLabel("监听端口:", slaveGroup);
modbusSlaveListenPortEdit = new QLineEdit("502", slaveGroup);
slaveLayout->addWidget(slaveListenPortLabel, 1, 0);
slaveLayout->addWidget(modbusSlaveListenPortEdit, 1, 1);
QLabel* slaveSelfIdLabel = new QLabel("自身从站ID:", slaveGroup);
modbusSlaveSelfIdEdit = new QLineEdit("1", slaveGroup);
slaveLayout->addWidget(slaveSelfIdLabel, 2, 0);
slaveLayout->addWidget(modbusSlaveSelfIdEdit, 2, 1);
QLabel* slaveRegCountLabel = new QLabel("保持寄存器数量:", slaveGroup);
modbusSlaveRegCountEdit = new QLineEdit("10", slaveGroup);
slaveLayout->addWidget(slaveRegCountLabel, 3, 0);
slaveLayout->addWidget(modbusSlaveRegCountEdit, 3, 1);
QLabel* slaveTimeoutLabel = new QLabel("监听超时(毫秒):", slaveGroup);
modbusSlaveTimeoutEdit = new QLineEdit("5000", slaveGroup);
slaveLayout->addWidget(slaveTimeoutLabel, 4, 0);
slaveLayout->addWidget(modbusSlaveTimeoutEdit, 4, 1);
slaveGroup->setVisible(false);
QFrame* line = new QFrame(configGroup);
line->setFrameShape(QFrame::HLine);
line->setFrameShadow(QFrame::Sunken);
QLabel* configDisplayLabel = new QLabel("当前配置:", configGroup);
modbusConfigDisplayEdit = new QTextEdit(configGroup);
modbusConfigDisplayEdit->setReadOnly(true);
modbusConfigDisplayEdit->setMinimumHeight(80);
QWidget* btnWidget = new QWidget(configGroup);
QHBoxLayout* btnLayout = new QHBoxLayout(btnWidget);
btnLayout->setContentsMargins(0, 0, 0, 0);
btnLayout->setSpacing(10);
modbusApplyConfigBtn = new QPushButton("应用配置", btnWidget);
modbusConnectListenBtn = new QPushButton("主站连接", btnWidget);
modbusDisconnectStopBtn = new QPushButton("断开连接", btnWidget);
modbusConnectListenBtn->setEnabled(false);
modbusDisconnectStopBtn->setEnabled(false);
btnLayout->addWidget(modbusApplyConfigBtn);
btnLayout->addWidget(modbusConnectListenBtn);
btnLayout->addWidget(modbusDisconnectStopBtn);
btnLayout->addStretch();
configRootLayout->addWidget(masterGroup);
configRootLayout->addWidget(slaveGroup);
configRootLayout->addWidget(line);
configRootLayout->addWidget(configDisplayLabel);
configRootLayout->addWidget(modbusConfigDisplayEdit);
configRootLayout->addWidget(btnWidget);
// 通信测试
QGroupBox* testGroup = new QGroupBox("通信测试", ModBusmainPanel);
QHBoxLayout* testLayout = new QHBoxLayout(testGroup);
testLayout->setSpacing(10);
QLabel* inputLabel = new QLabel("输入测试值:", testGroup);
modbusInputLineEdit = new QLineEdit(testGroup);
modbusInputLineEdit->setPlaceholderText("输入要发送/写入的数据...");
modbusSendBtn = new QPushButton("发送/写入", testGroup);
modbusSendBtn->setFixedWidth(100);
modbusSendBtn->setEnabled(false);
testLayout->addWidget(inputLabel);
testLayout->addWidget(modbusInputLineEdit, 1);
testLayout->addWidget(modbusSendBtn);
ModBusmainLayout->addWidget(configGroup);
ModBusmainLayout->addWidget(testGroup);
ModBusmainLayout->addStretch();
// 切换主/从
connect(switchModeBtn, &QPushButton::clicked, [=]() {
isMasterMode = !isMasterMode;
if (isMasterMode) {
masterGroup->setVisible(true);
slaveGroup->setVisible(false);
modeLabel->setText("当前模式:主站(主动请求)");
switchModeBtn->setText("切换到从站模式");
modbusConnectListenBtn->setText("主站连接");
modbusDisconnectStopBtn->setText("断开连接");
}
else {
masterGroup->setVisible(false);
slaveGroup->setVisible(true);
modeLabel->setText("当前模式:从站(被动监听)");
switchModeBtn->setText("切换到主站模式");
modbusConnectListenBtn->setText("从站启动监听");
modbusDisconnectStopBtn->setText("停止监听");
}
modbusConnectListenBtn->setEnabled(false);
modbusDisconnectStopBtn->setEnabled(false);
modbusSendBtn->setEnabled(false);
modbusConfigDisplayEdit->clear();
});
connect(modbusApplyConfigBtn, &QPushButton::clicked, [=]() {
QString currentConfig;
if (isMasterMode) {
currentConfig = QString(
"【当前模式:主站(主动请求)】\n"
"从站IP:%1 | 从站端口:%2 | 从站ID:%3\n"
"寄存器类型:%4 | 起始地址:%5 | 数据类型:%6 | 读写长度:%7 | 超时:%8ms"
).arg(
modbusMasterSlaveIpEdit->text(),
modbusMasterSlavePortEdit->text(),
modbusMasterSlaveIdEdit->text(),
modbusMasterRegTypeCombo->currentText(),
modbusMasterRegStartEdit->text(),
modbusMasterDataTypeCombo->currentText(),
modbusMasterRegLenEdit->text(),
modbusMasterTimeoutEdit->text()
);
m_masterBaseAddr = modbusMasterRegStartEdit->text().toUShort();
m_masterRegCount = modbusMasterRegLenEdit->text().toUShort();
if (m_masterRegCount == 0) m_masterRegCount = 1;
signalTable->setRowCount(m_masterRegCount);
QStringList vHeaders;
for (int i = 0; i < m_masterRegCount; ++i) {
vHeaders << QString::number(m_masterBaseAddr + i);
}
signalTable->setVerticalHeaderLabels(vHeaders);
signalTable->clearContents();
}
else {
currentConfig = QString(
"【当前模式:从站(被动监听)】\n"
"监听IP:%1 | 监听端口:%2 | 自身从站ID:%3\n"
"保持寄存器数量:%4 | 监听超时:%5ms"
).arg(
modbusSlaveListenIpEdit->text(),
modbusSlaveListenPortEdit->text(),
modbusSlaveSelfIdEdit->text(),
modbusSlaveRegCountEdit->text(),
modbusSlaveTimeoutEdit->text()
);
}
modbusConfigDisplayEdit->setPlainText(currentConfig);
modbusConnectListenBtn->setEnabled(true);
modbusDisconnectStopBtn->setEnabled(false);
modbusSendBtn->setEnabled(true);
addLog(QString("已应用%1通信配置").arg(isMasterMode ? "主站" : "从站"),
LogLevel::Info);
});
connect(modbusConnectListenBtn, &QPushButton::clicked,
this, &Cptest::onModbusConnectListenClicked);
connect(modbusDisconnectStopBtn, &QPushButton::clicked,
this, &Cptest::onModbusDisconnectStopClicked);
connect(modbusSendBtn, &QPushButton::clicked,
this, &Cptest::onModbusSendClicked);
functionStack->addWidget(ModBusmainPanel);
}
// Modbus 主/从操作
void Cptest::onModbusConnectListenClicked()
{
if (isMasterMode) {
modbusMasterOpen();
}
else {
if (modbusSlaveStart()) {
modbusConnectListenBtn->setEnabled(false);
modbusDisconnectStopBtn->setEnabled(true);
modbusSendBtn->setEnabled(false);
}
}
}
void Cptest::onModbusDisconnectStopClicked()
{
if (isMasterMode) {
modbusMasterClose();
}
else {
modbusSlaveStop();
}
}
void Cptest::onModbusSendClicked()
{
if (!isMasterMode) {
addLog("当前为从站模式,发送/写入按钮无效", LogLevel::Warning);
return;
}
if (m_modbusMasterSocket->state() != QAbstractSocket::ConnectedState) {
addLog("主站未连接,无法写入从站", LogLevel::Warning);
return;
}
bool ok = false;
quint16 v = modbusInputLineEdit->text().toUShort(&ok);
if (!ok) {
addLog("请输入正确的无符号整数(0~65535)作为写入值", LogLevel::Warning);
return;
}
bool okAddr = false;
quint16 startAddr = modbusMasterRegStartEdit->text().toUShort(&okAddr);
if (!okAddr) startAddr = 0;
int row = 0;
if (row >= 0 && row < signalTable->rowCount()) {
QTableWidgetItem* item = signalTable->item(row, 1);
if (!item) {
item = new QTableWidgetItem();
item->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(row, 1, item);
}
item->setText(QString::number(v));
}
modbusMasterSendWriteSingleRequest(v);
addLog(QString("主站写单寄存器:地址=%1, 值=%2")
.arg(startAddr).arg(v),
LogLevel::Info);
modbusMasterSendReadRequest();
}
// 主站 socket 连接
void Cptest::modbusMasterOpen()
{
if (m_modbusMasterSocket->state() != QAbstractSocket::UnconnectedState) {
m_modbusMasterSocket->abort();
}
QString ip = modbusMasterSlaveIpEdit->text();
quint16 port = modbusMasterSlavePortEdit->text().toUShort();
addLog(QString("主站尝试连接 %1:%2").arg(ip).arg(port), LogLevel::Info);
m_modbusMasterSocket->connectToHost(ip, port);
}
void Cptest::modbusMasterClose()
{
m_modbusMasterPollTimer->stop();
if (m_modbusMasterSocket->state() != QAbstractSocket::UnconnectedState) {
m_modbusMasterSocket->disconnectFromHost();
}
}
void Cptest::onMasterConnected()
{
addLog("Modbus 主站连接成功", LogLevel::Info);
modbusConnectListenBtn->setEnabled(false);
modbusDisconnectStopBtn->setEnabled(true);
modbusSendBtn->setEnabled(true);
if (!m_modbusMasterPollTimer->isActive()) {
m_modbusMasterPollTimer->start();
}
}
void Cptest::onMasterDisconnected()
{
addLog("Modbus 主站连接断开", LogLevel::Warning);
if (m_modbusMasterPollTimer->isActive()) {
m_modbusMasterPollTimer->stop();
}
modbusConnectListenBtn->setEnabled(true);
modbusDisconnectStopBtn->setEnabled(false);
modbusSendBtn->setEnabled(false);
}
void Cptest::onMasterError(QAbstractSocket::SocketError socketError)
{
Q_UNUSED(socketError);
addLog(QString("Modbus 主站 socket 错误:%1")
.arg(m_modbusMasterSocket->errorString()),
LogLevel::Error);
}
void Cptest::onModbusMasterPollTimeout()
{
if (m_modbusMasterSocket->state() == QAbstractSocket::ConnectedState) {
modbusMasterSendReadRequest();
}
}
// 主站 0x03 读保持寄存器
void Cptest::modbusMasterSendReadRequest()
{
quint8 unitId = modbusMasterSlaveIdEdit->text().toUShort();
quint16 startReg = modbusMasterRegStartEdit->text().toUShort();
quint16 regCount = modbusMasterRegLenEdit->text().toUShort();
if (regCount < 1) regCount = 1;
QByteArray buf;
buf.resize(12);
quint16 tid = m_mbTransactionId++;
buf[0] = tid >> 8;
buf[1] = tid & 0xFF;
buf[2] = 0x00;
buf[3] = 0x00;
quint16 len = 6;
buf[4] = len >> 8;
buf[5] = len & 0xFF;
buf[6] = unitId;
buf[7] = 0x03;
buf[8] = startReg >> 8;
buf[9] = startReg & 0xFF;
buf[10] = regCount >> 8;
buf[11] = regCount & 0xFF;
m_modbusMasterSocket->write(buf);
}
// 主站 0x06 写单寄存器
void Cptest::modbusMasterSendWriteSingleRequest(quint16 value)
{
quint8 unitId = modbusMasterSlaveIdEdit->text().toUShort();
quint16 startReg = modbusMasterRegStartEdit->text().toUShort();
QByteArray buf;
buf.resize(12);
quint16 tid = m_mbTransactionId++;
buf[0] = tid >> 8;
buf[1] = tid & 0xFF;
buf[2] = 0x00;
buf[3] = 0x00;
quint16 len = 6;
buf[4] = len >> 8;
buf[5] = len & 0xFF;
buf[6] = unitId;
buf[7] = 0x06;
buf[8] = startReg >> 8;
buf[9] = startReg & 0xFF;
buf[10] = value >> 8;
buf[11] = value & 0xFF;
m_modbusMasterSocket->write(buf);
if (signalTable) {
QTableWidgetItem* item = signalTable->item(0, 1);
if (!item) {
item = new QTableWidgetItem();
signalTable->setItem(0, 1, item);
}
item->setText(QString::number(value));
}
addLog(QString("主站写单寄存器:地址=%1, 值=%2").arg(startReg).arg(value),
LogLevel::Info);
}
void Cptest::onMasterReadyRead()
{
QByteArray data = m_modbusMasterSocket->readAll();
if (data.size() < 9) {
addLog("Modbus 主站收到的数据太短", LogLevel::Warning);
return;
}
quint8 unitId = (quint8)data[6];
Q_UNUSED(unitId);
quint8 func = (quint8)data[7];
if (func == 0x03) {
quint8 byteCount = (quint8)data[8];
if (data.size() < 9 + byteCount) {
addLog("Modbus 主站 0x03 响应长度错误", LogLevel::Warning);
return;
}
int regCount = byteCount / 2;
bool okStart = false, okLen = false;
quint16 startAddr = modbusMasterRegStartEdit->text().toUShort(&okStart);
quint16 cfgLen = modbusMasterRegLenEdit->text().toUShort(&okLen);
if (!okStart) startAddr = 0;
if (!okLen || cfgLen == 0) cfgLen = regCount;
int rows = qMax<int>(cfgLen, regCount);
if (signalTable->rowCount() != rows) {
signalTable->setRowCount(rows);
}
QStringList vHeaders;
for (int i = 0; i < rows; ++i) {
vHeaders << QString::number(startAddr + i);
}
signalTable->setVerticalHeaderLabels(vHeaders);
for (int i = 0; i < regCount; ++i) {
int idx = 9 + i * 2;
quint16 v = ((quint8)data[idx] << 8) | (quint8)data[idx + 1];
quint16 addr = startAddr + i;
int row = addr - startAddr;
if (row >= 0 && row < signalTable->rowCount()) {
QTableWidgetItem* item = signalTable->item(row, 0);
if (!item) {
item = new QTableWidgetItem();
item->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(row, 0, item);
}
item->setText(QString::number(v));
}
}
addLog(QString("主站收到保持寄存器数据,起始地址=%1,寄存器数=%2")
.arg(startAddr).arg(regCount),
LogLevel::Info);
}
else if (func == 0x06) {
addLog("主站收到写单寄存器响应", LogLevel::Info);
}
else if (func & 0x80) {
quint8 ex = (quint8)data[8];
addLog(QString("主站收到 Modbus 异常响应,功能码=0x%1, 异常码=%2")
.arg(func, 2, 16, QLatin1Char('0'))
.arg(ex),
LogLevel::Warning);
}
else {
addLog(QString("主站收到未处理功能码:0x%1")
.arg(func, 2, 16, QLatin1Char('0')),
LogLevel::Warning);
}
}
// 从站寄存器初始化/启动/停止/解析
void Cptest::modbusSlaveInitHoldingRegs(int count)
{
if (count < 1) count = 1;
m_slaveHoldingRegs.resize(count);
for (int i = 0; i < count; ++i) {
m_slaveHoldingRegs[i] = (quint16)i;
}
}
bool Cptest::modbusSlaveStart()
{
QString listenIp = modbusSlaveListenIpEdit->text();
quint16 port = modbusSlaveListenPortEdit->text().toUShort();
int regCount = modbusSlaveRegCountEdit->text().toInt();
modbusSlaveInitHoldingRegs(regCount);
QHostAddress addr;
if (listenIp == "0.0.0.0") {
addr = QHostAddress::Any;
}
else {
addr = QHostAddress(listenIp);
}
if (!m_modbusSlaveServer->listen(addr, port)) {
addLog(QString("从站监听失败:%1")
.arg(m_modbusSlaveServer->errorString()),
LogLevel::Error);
return false;
}
addLog(QString("从站开始监听 %1:%2").arg(listenIp).arg(port), LogLevel::Info);
return true;
}
void Cptest::modbusSlaveStop()
{
if (m_modbusSlaveClient) {
m_modbusSlaveClient->disconnectFromHost();
m_modbusSlaveClient = nullptr;
}
if (m_modbusSlaveServer->isListening()) {
m_modbusSlaveServer->close();
}
addLog("从站停止监听", LogLevel::Info);
modbusConnectListenBtn->setEnabled(true);
modbusDisconnectStopBtn->setEnabled(false);
}
void Cptest::onSlaveNewConnection()
{
if (m_modbusSlaveClient) {
m_modbusSlaveClient->disconnectFromHost();
}
m_modbusSlaveClient = m_modbusSlaveServer->nextPendingConnection();
addLog("从站收到新的主站连接", LogLevel::Info);
connect(m_modbusSlaveClient, &QTcpSocket::disconnected,
this, &Cptest::onSlaveClientDisconnected);
connect(m_modbusSlaveClient, &QTcpSocket::readyRead,
this, &Cptest::onSlaveClientReadyRead);
}
void Cptest::onSlaveClientDisconnected()
{
addLog("主站与从站连接断开", LogLevel::Warning);
if (m_modbusSlaveClient) {
m_modbusSlaveClient->deleteLater();
m_modbusSlaveClient = nullptr;
}
}
void Cptest::onSlaveClientReadyRead()
{
if (!m_modbusSlaveClient) return;
QByteArray req = m_modbusSlaveClient->readAll();
if (req.size() < 8) {
addLog("从站收到的数据太短", LogLevel::Warning);
return;
}
QByteArray resp;
modbusSlaveHandleRequest(req, resp);
if (!resp.isEmpty()) {
m_modbusSlaveClient->write(resp);
}
}
void Cptest::modbusSlaveHandleRequest(const QByteArray& req, QByteArray& resp)
{
resp.clear();
if (req.size() < 12) {
addLog("从站收到异常 Modbus 请求(长度不足)", LogLevel::Warning);
return;
}
quint16 tid = ((quint8)req[0] << 8) | (quint8)req[1];
quint8 unitId = (quint8)req[6];
quint8 func = (quint8)req[7];
quint16 addr = ((quint8)req[8] << 8) | (quint8)req[9];
auto makeException = [&](quint8 code) {
resp.resize(9);
resp[0] = tid >> 8;
resp[1] = tid & 0xFF;
resp[2] = 0x00;
resp[3] = 0x00;
resp[4] = 0x00;
resp[5] = 0x03;
resp[6] = unitId;
resp[7] = func | 0x80;
resp[8] = code;
};
if (func == 0x03) {
quint16 count = ((quint8)req[10] << 8) | (quint8)req[11];
if (count < 1) count = 1;
if (addr + count > (quint16)m_slaveHoldingRegs.size()) {
addLog("从站收到 0x03 请求,超出寄存器范围", LogLevel::Warning);
makeException(0x02);
return;
}
quint8 byteCount = (quint8)(count * 2);
resp.resize(9 + byteCount);
resp[0] = tid >> 8;
resp[1] = tid & 0xFF;
resp[2] = 0x00;
resp[3] = 0x00;
quint16 len = 3 + byteCount;
resp[4] = len >> 8;
resp[5] = len & 0xFF;
resp[6] = unitId;
resp[7] = func;
resp[8] = byteCount;
for (int i = 0; i < count; ++i) {
quint16 v = m_slaveHoldingRegs[addr + i];
int idx = 9 + i * 2;
resp[idx] = v >> 8;
resp[idx + 1] = v & 0xFF;
}
addLog(QString("从站处理 0x03 请求:addr=%1, count=%2")
.arg(addr).arg(count),
LogLevel::Info);
}
else if (func == 0x06) {
quint16 value = ((quint8)req[10] << 8) | (quint8)req[11];
if (addr >= (quint16)m_slaveHoldingRegs.size()) {
addLog("从站收到 0x06 请求,超出寄存器范围", LogLevel::Warning);
makeException(0x02);
return;
}
m_slaveHoldingRegs[addr] = value;
resp = req.left(12);
addLog(QString("从站写单寄存器:addr=%1, value=%2")
.arg(addr).arg(value),
LogLevel::Info);
}
else {
addLog(QString("从站收到未支持功能码:0x%1")
.arg(func, 2, 16, QLatin1Char('0')),
LogLevel::Warning);
makeException(0x01);
}
}
// ------------------------ 图像传输(服务器 / 工作站) ------------------------
void Cptest::setupImageTransferPanel()
{
imageTransPanel = new QWidget();
QVBoxLayout* mainLayout = new QVBoxLayout(imageTransPanel);
mainLayout->setContentsMargins(10, 10, 10, 10);
mainLayout->setSpacing(10);
// 角色与连接配置
QGroupBox* roleGroup = new QGroupBox("角色与连接配置", imageTransPanel);
QGridLayout* roleLayout = new QGridLayout(roleGroup);
roleLayout->setSpacing(8);
QLabel* roleLabel = new QLabel("当前角色:", roleGroup);
imgRoleCombo = new QComboBox(roleGroup);
imgRoleCombo->addItems(QStringList() << "发送端(工作站模拟)" << "接收端(服务器模拟)");
imgRoleCombo->setCurrentIndex(1);
m_isImageServer = true;
QLabel* serverIpLabel = new QLabel("服务器绑定 IP:", roleGroup);
imgServerIpEdit = new QLineEdit("0.0.0.0", roleGroup);
QLabel* clientIpLabel = new QLabel("工作站(对端) IP:", roleGroup);
imgClientIpEdit = new QLineEdit("127.0.0.1", roleGroup);
QLabel* portLabel = new QLabel("通信端口:", roleGroup);
imgPortEdit = new QLineEdit("9000", roleGroup);
imgConnectBtn = new QPushButton("连接 / 启动监听", roleGroup);
imgDisconnectBtn = new QPushButton("断开 / 停止", roleGroup);
imgDisconnectBtn->setEnabled(false);
imgStatusLabel = new QLabel("当前:接收端(服务器)未监听", roleGroup);
roleLayout->addWidget(roleLabel, 0, 0);
roleLayout->addWidget(imgRoleCombo, 0, 1, 1, 2);
roleLayout->addWidget(serverIpLabel, 1, 0);
roleLayout->addWidget(imgServerIpEdit, 1, 1, 1, 2);
roleLayout->addWidget(clientIpLabel, 2, 0);
roleLayout->addWidget(imgClientIpEdit, 2, 1, 1, 2);
roleLayout->addWidget(portLabel, 3, 0);
roleLayout->addWidget(imgPortEdit, 3, 1, 1, 2);
roleLayout->addWidget(imgConnectBtn, 4, 0);
roleLayout->addWidget(imgDisconnectBtn, 4, 1);
roleLayout->addWidget(imgStatusLabel, 5, 0, 1, 3);
mainLayout->addWidget(roleGroup);
// 数据内容配置
QGroupBox* dataGroup = new QGroupBox("数据内容(相机编号 / 缺陷个数 / 图片 / 坐标)", imageTransPanel);
QGridLayout* dataLayout = new QGridLayout(dataGroup);
dataLayout->setSpacing(8);
QLabel* idLabel = new QLabel("相机编号 cameraId:", dataGroup);
imgIdEdit = new QLineEdit("1", dataGroup);
QLabel* defectCountLabel = new QLabel("缺陷个数 defectCount:", dataGroup);
imgDefectCountEdit = new QLineEdit("0", dataGroup);
QLabel* xLabel = new QLabel("缺陷坐标 X:", dataGroup);
imgXEdit = new QLineEdit("0.0", dataGroup);
QLabel* yLabel = new QLabel("缺陷坐标 Y:", dataGroup);
imgYEdit = new QLineEdit("0.0", dataGroup);
// ★★★ 修改开始:新增 W/H ★★★
QLabel* wLabel = new QLabel("缺陷宽 W:", dataGroup);
imgWEdit = new QLineEdit("0.0", dataGroup);
QLabel* hLabel = new QLabel("缺陷高 H:", dataGroup);
imgHEdit = new QLineEdit("0.0", dataGroup);
// ★★★ 修改结束:新增 W/H ★★★
QLabel* zLabel = new QLabel("坐标 Z(不发送):", dataGroup);
imgZEdit = new QLineEdit("0.0", dataGroup);
QLabel* textLabel = new QLabel("文本(不发送):", dataGroup);
imgTextEdit = new QTextEdit(dataGroup);
imgTextEdit->setPlaceholderText("不参与发送");
imgSelectImg1Btn = new QPushButton("选择图片", dataGroup);
imgSelectImg2Btn = new QPushButton("选择图片2(不发送)", dataGroup);
imgSendBtn = new QPushButton("发送一条测试消息", dataGroup);
imgSendBtn->setEnabled(false);
dataLayout->addWidget(idLabel, 0, 0);
dataLayout->addWidget(imgIdEdit, 0, 1);
dataLayout->addWidget(defectCountLabel, 0, 2);
dataLayout->addWidget(imgDefectCountEdit, 0, 3);
//X,Y,Z坐标
dataLayout->addWidget(xLabel, 1, 0);
dataLayout->addWidget(imgXEdit, 1, 1);
dataLayout->addWidget(yLabel, 1, 2);
dataLayout->addWidget(imgYEdit, 1, 3);
dataLayout->addWidget(zLabel, 1, 4);
dataLayout->addWidget(imgZEdit, 1, 5);
//矩形框
dataLayout->addWidget(wLabel, 2, 0);
dataLayout->addWidget(imgWEdit, 2, 1);
dataLayout->addWidget(hLabel, 2, 2);
dataLayout->addWidget(imgHEdit, 2, 3);
dataLayout->addWidget(textLabel, 3, 0);
dataLayout->addWidget(imgTextEdit, 3, 1, 1, 5);
dataLayout->addWidget(imgSelectImg1Btn, 4, 0, 1, 2);
dataLayout->addWidget(imgSelectImg2Btn, 4, 2, 1, 2);
dataLayout->addWidget(imgSendBtn, 4, 4, 1, 2);
mainLayout->addWidget(dataGroup);
mainLayout->addStretch();
connect(imgRoleCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &Cptest::onImageRoleChanged);//切换角色
connect(imgConnectBtn, &QPushButton::clicked,
this, &Cptest::onImageConnectClicked);//接收/发送切换接受逻辑,多线程监听
connect(imgDisconnectBtn, &QPushButton::clicked,
this, &Cptest::onImageDisconnectClicked);//接收端断开
connect(imgSendBtn, &QPushButton::clicked,
this, &Cptest::onImageSendClicked);//发送(分块)
connect(imgSelectImg1Btn, &QPushButton::clicked,
this, &Cptest::onSelectImage1Clicked);
connect(imgSelectImg2Btn, &QPushButton::clicked,
this, &Cptest::onSelectImage2Clicked);
m_imageServer = new QTcpServer(this);
connect(m_imageServer, &QTcpServer::newConnection,
this, &Cptest::onImageNewConnection);
functionStack->addWidget(imageTransPanel);
}
void Cptest::onImageRoleChanged(int index)
{
if (index == 0) {
m_isImageServer = false;
imgServerIpEdit->setEnabled(false);
imgClientIpEdit->setEnabled(true);
imgStatusLabel->setText("当前:发送端(工作站)未连接");
addLog("图像传输模式切换为:发送端(工作站模拟)", LogLevel::Info);
}
else {
m_isImageServer = true;
imgServerIpEdit->setEnabled(true);
imgClientIpEdit->setEnabled(false);
imgStatusLabel->setText("当前:接收端(服务器)未监听");
addLog("图像传输模式切换为:接收端(服务器模拟)", LogLevel::Info);
}
imgConnectBtn->setEnabled(true);
imgDisconnectBtn->setEnabled(false);
imgSendBtn->setEnabled(!m_isImageServer);
}
void Cptest::onImageConnectClicked()
{
quint16 port = imgPortEdit->text().toUShort();
if (m_isImageServer) {
if (m_imageServer->isListening()) {
addLog("图像接收端已经在监听中。", LogLevel::Warning);
return;
}
QString bindIp = imgServerIpEdit->text().trimmed();
if (bindIp.isEmpty()) bindIp = "0.0.0.0";
//监听,没有填写IP则默认为0.0.0.0
//监听所有网卡
QHostAddress addr;
if (bindIp == "0.0.0.0") {
addr = QHostAddress::Any;//监听所有接口
}
else {
addr = QHostAddress(bindIp);
if (addr.isNull()) {// IP 格式无效
addLog(QString("无效的服务器绑定 IP:%1").arg(bindIp), LogLevel::Warning);
imgStatusLabel->setText("接收端监听失败:IP 无效");
return;
}
}
if (!m_imageServer->listen(addr, port)) {//listen(addr, port)启动监听,成功返回 true;失败返回 false
addLog(QString("图像接收端监听失败:%1").arg(m_imageServer->errorString()),
LogLevel::Error);
imgStatusLabel->setText("接收端监听失败");
return;
}
imgConnectBtn->setEnabled(false);// 禁用连接按钮(避免重复操作)
imgDisconnectBtn->setEnabled(true);
imgSendBtn->setEnabled(false);
imgStatusLabel->setText(QString("接收端正在监听 %1:%2").arg(bindIp).arg(port));
addLog(QString("图像接收端开始监听 %1:%2").arg(bindIp).arg(port), LogLevel::Info);
}
else {//发送端
QString ip = imgClientIpEdit->text().trimmed();
if (ip.isEmpty()) {
addLog("发送端:请填写对端 IP 地址。", LogLevel::Warning);
return;
}
if (!m_imageClientSocket) {
m_imageClientSocket = new QTcpSocket(this);// 父对象为当前类,自动内存管理
// 绑定 Socket 关键信号
connect(m_imageClientSocket, &QTcpSocket::readyRead,
this, &Cptest::onImageSocketReadyRead);//有数据可读
connect(m_imageClientSocket, &QTcpSocket::disconnected,
this, &Cptest::onImageSocketDisconnected);//断开
connect(m_imageClientSocket,
QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred),
this, &Cptest::onImageSocketError);//报错
}
addLog(QString("发送端尝试连接 %1:%2").arg(ip).arg(port), LogLevel::Info);
m_imageClientSocket->connectToHost(ip, port);
if (!m_imageClientSocket->waitForConnected(3000)) {
addLog(QString("发送端连接失败:%1").arg(m_imageClientSocket->errorString()),
LogLevel::Error);
imgStatusLabel->setText("发送端连接失败");
return;
}
imgConnectBtn->setEnabled(false);
imgDisconnectBtn->setEnabled(true);
imgSendBtn->setEnabled(true);
imgStatusLabel->setText(QString("发送端已连接到 %1:%2").arg(ip).arg(port));
addLog("发送端连接成功,可以发送图像测试数据。", LogLevel::Info);
}
}
void Cptest::onImageDisconnectClicked()
{
if (m_isImageServer) {
if (m_imageServerSocket) {
m_imageServerSocket->disconnectFromHost();
m_imageServerSocket->deleteLater();
m_imageServerSocket = nullptr;
}
if (m_imageServer->isListening()) {
m_imageServer->close();
}
imgStatusLabel->setText("接收端已停止监听");
addLog("图像接收端停止监听。", LogLevel::Info);
}
else {
if (m_imageClientSocket) {
m_imageClientSocket->disconnectFromHost();
}
imgStatusLabel->setText("发送端已断开连接");
addLog("发送端断开连接。", LogLevel::Info);
}
imgConnectBtn->setEnabled(true);
imgDisconnectBtn->setEnabled(false);
imgSendBtn->setEnabled(false);
}
void Cptest::onSelectImage1Clicked()
{
QString file = QFileDialog::getOpenFileName(this, "选择图片1", QString(),
"Images (*.png *.jpg *.jpeg *.bmp)");
if (!file.isEmpty()) {
m_imgFile1Path = file;
addLog(QString("已选择图片1:%1").arg(file), LogLevel::Info);
}
}
void Cptest::onSelectImage2Clicked()
{
QString file = QFileDialog::getOpenFileName(this, "选择图片2", QString(),
"Images (*.png *.jpg *.jpeg *.bmp)");
if (!file.isEmpty()) {
m_imgFile2Path = file;
addLog(QString("已选择图片2:%1").arg(file), LogLevel::Info);
}
}
void Cptest::onImageSendClicked()
{
if (m_isImageServer) {
addLog("当前为接收端模式,不能发送。", LogLevel::Warning);
return;
}
if (!m_imageClientSocket ||
m_imageClientSocket->state() != QAbstractSocket::ConnectedState) {
addLog("发送端未连接,无法发送。", LogLevel::Warning);
return;
}
// 1) 基本参数:cameraId / defectCount
bool okCam = false, okCnt = false;
qint32 cameraId = imgIdEdit->text().toInt(&okCam);//cameraId 必须是有效整数
qint32 defectCount = imgDefectCountEdit->text().toInt(&okCnt);//defectCount 必须是有效整数且 ≥0(缺陷数不能为负)
if (!okCam) {
addLog("相机编号 cameraId 不是有效整数。", LogLevel::Warning);
return;
}
if (!okCnt || defectCount < 0) {
addLog("缺陷个数 defectCount 必须是 >=0 的整数。", LogLevel::Warning);
return;
}
// 2) 图片数据(原图)
if (m_imgFile1Path.isEmpty()) {
addLog("请先选择图片再发送。", LogLevel::Warning);
return;
}
QFile f1(m_imgFile1Path);
if (!f1.open(QIODevice::ReadOnly)) {
addLog(QString("无法打开图片:%1").arg(m_imgFile1Path), LogLevel::Error);
return;
}
// 这里不再一次性 readAll,而是只拿文件大小
quint32 origImgSize = static_cast<quint32>(f1.size());
// 目前缺陷图先不发送,长度为 0
quint32 defectImgSize = 0;
// 3) 缺陷位置信息:X / Y / W / H
bool okX = false, okY = false, okW = false, okH = false;
double x0 = imgXEdit->text().toDouble(&okX);
double y0 = imgYEdit->text().toDouble(&okY);
double w0 = imgWEdit->text().toDouble(&okW);
double h0 = imgHEdit->text().toDouble(&okH);
if (!okX || !okY) {
addLog("坐标 X/Y 存在非法数值。", LogLevel::Warning);
return;
}
if (!okW || !okH) {
addLog("宽度 W / 高度 H 存在非法数值。", LogLevel::Warning);
return;
}
// 3.1 先单独把"缺陷列表"序列化到一个小 buffer
QByteArray defectBuf;//Qt 字节数组,用于存储序列化后的字节流
{
QDataStream defectOut(&defectBuf, QIODevice::WriteOnly);//Qt 序列化工具类,支持将基本类型(int/double/quint16)直接写入字节数组
defectOut.setByteOrder(QDataStream::BigEndian);
//defectCount 重复写入缺陷信息
for (int i = 0; i < defectCount; ++i) {
double x = x0;
double y = y0;
double w = w0;
double h = h0;
quint16 defectType = 0; // 预留缺陷类型
defectOut << x;
defectOut << y;
defectOut << w;
defectOut << h;
defectOut << defectType;
}
}
// 先发送 16 字节头部
QByteArray headerBuf;
{
QDataStream headerOut(&headerBuf, QIODevice::WriteOnly);
headerOut.setByteOrder(QDataStream::BigEndian);
headerOut << cameraId; // qint32
headerOut << defectCount; // qint32
headerOut << origImgSize; // quint32 原图大小
headerOut << defectImgSize; // quint32 缺陷图大小(当前为 0)
}
qint64 totalBytesToWrite = headerBuf.size()
-
static_cast<qint64>(origImgSize)
-
static_cast<qint64>(defectImgSize) // 现为0
-
defectBuf.size();
qint64 written = 0;
// 4.1 发送头部
{
qint64 w = m_imageClientSocket->write(headerBuf);
if (w != headerBuf.size()) {
addLog("发送头部数据失败(write 未写完)。", LogLevel::Error);
return;
}
written += w;
}
// 5) 分块发送原图数据
QByteArray chunk;
chunk.resize(IMAGE_CHUNK_SIZE);
quint64 remain = origImgSize;
while (remain > 0) {
// 从文件读取一块
qint64 toRead = qMin<quint64>(remain, IMAGE_CHUNK_SIZE);// IMAGE_CHUNK_SIZE 是宏定义(如4096/8192字节
qint64 r = f1.read(chunk.data(), toRead);
if (r <= 0) {
addLog("读取图片文件时出错或提前结束。", LogLevel::Error);
return;
}
qint64 w = m_imageClientSocket->write(chunk.constData(), r);
if (w != r) {
addLog("发送图片块时 write 未写完。", LogLevel::Error);
return;
}
written += w;
remain -= r;
// 可选:打印每块发送进度
addLog(QString("发送图片数据块,本次=%1, 剩余=%2").arg(w).arg(remain), LogLevel::Info);
}
f1.close();
// ★★★ 6) 发送缺陷图数据(当前为空) ★★★
// 如果未来有缺陷图,同样按块读文件 + write
// ★★★ 7) 最后发送缺陷列表(通常很小,一次 write 就够) ★★★
if (!defectBuf.isEmpty()) {
qint64 w = m_imageClientSocket->write(defectBuf);
if (w != defectBuf.size()) {
addLog("发送缺陷列表时 write 未写完。", LogLevel::Error);
return;
}
written += w;
}
addLog(QString("发送图像消息:cameraId=%1, defectCount=%2, origImgBytes=%3, totalBytes=%4, 实际写入=%5")
.arg(cameraId)
.arg(defectCount)
.arg(origImgSize)
.arg(totalBytesToWrite)
.arg(written),
LogLevel::Info);
// 右侧表格发送计数 +1(第0行,第1列)
QTableWidgetItem* sendCountItem = signalTable->item(0, 1);
if (!sendCountItem) {
sendCountItem = new QTableWidgetItem();
sendCountItem->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(0, 1, sendCountItem);
}
int sendCount = sendCountItem->text().toInt();
sendCountItem->setText(QString::number(sendCount + 1));
}
void Cptest::onImageNewConnection()
{
if (!m_imageServer) return;
if (m_imageServerSocket) {
m_imageServerSocket->disconnectFromHost();
m_imageServerSocket->deleteLater();
m_imageServerSocket = nullptr;
}
m_imageServerSocket = m_imageServer->nextPendingConnection();
m_imageRecvBuffer.clear();
m_imageRecvCount = 0;
m_imageRecvBytes = 0;
resetImageMonitor();
connect(m_imageServerSocket, &QTcpSocket::readyRead,
this, &Cptest::onImageSocketReadyRead);
connect(m_imageServerSocket, &QTcpSocket::disconnected,
this, &Cptest::onImageSocketDisconnected);
connect(m_imageServerSocket,
QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::errorOccurred),
this, &Cptest::onImageSocketError);
addLog("图像接收端:新连接已建立。", LogLevel::Info);
imgStatusLabel->setText("接收端:已与对端建立连接");
}
void Cptest::onImageSocketDisconnected()
{
QTcpSocket* sock = qobject_cast<QTcpSocket*>(sender());
if (sock == m_imageServerSocket) {
addLog("图像接收端:对端断开连接。", LogLevel::Warning);
m_imageServerSocket->deleteLater();
m_imageServerSocket = nullptr;
imgStatusLabel->setText("接收端:连接已断开");
}
else if (sock == m_imageClientSocket) {
addLog("发送端:对端断开连接。", LogLevel::Warning);
imgStatusLabel->setText("发送端:连接已断开");
}
imgConnectBtn->setEnabled(true);
imgDisconnectBtn->setEnabled(false);
imgSendBtn->setEnabled(false);
}
void Cptest::onImageSocketError(QAbstractSocket::SocketError socketError)
{
Q_UNUSED(socketError);
QTcpSocket* sock = qobject_cast<QTcpSocket*>(sender());
if (!sock) return;
addLog(QString("图像传输 socket 错误:%1").arg(sock->errorString()),
LogLevel::Error);
}
void Cptest::onImageSocketReadyRead()
{
// 这里只做接收端解析
if (!m_isImageServer || !m_imageServerSocket) {
return;
}
QByteArray data = m_imageServerSocket->readAll();
if (data.isEmpty()) return;
m_imageRecvBuffer.append(data);
const int headerSize = 16; // cameraId(4) + defectCount(4) + origImgSize(4)+defectImgSize(4)
while (true) {
if (m_imageRecvBuffer.size() < headerSize) {
break;
}
qint32 cameraId;//相机序号
qint32 defectCount;//缺陷数量
quint32 origImgSize;//原图
quint32 defectImgSize;//缺陷图
{
QByteArray headerBytes = m_imageRecvBuffer.left(headerSize);// 截取头部16字节
QDataStream in(&headerBytes, QIODevice::ReadOnly);// 数据流读取二进制数据
in.setByteOrder(QDataStream::BigEndian); //关键:网络传输用大端序,必须统一
in >> cameraId;
in >> defectCount;
in >> origImgSize;
in >> defectImgSize;
}
// 步骤3:头部数据合法性校验(容错处理)
if (defectCount < 0) {
addLog("接收端解析到非法 defectCount < 0,丢弃缓存。", LogLevel::Warning);
m_imageRecvBuffer.clear();
return;
}
// 合理的上限:
const quint32 MAX_IMG_SIZE = 50 * 1024 * 1024; // 单张图最大 50MB
const qint32 MAX_DEFECT_COUNT = 20; // 最大缺陷数
const qint64 MAX_MESSAGE_BYTES = 100 * 1024 * 1024; // 整个消息最大 100MB
// 读完 header 后加判断:
if (origImgSize == 0 || origImgSize > MAX_IMG_SIZE ||
defectImgSize > MAX_IMG_SIZE || // 缺陷图可以为 0,但限制上限
defectCount < 0 || defectCount > MAX_DEFECT_COUNT) {
addLog("接收端解析到异常的长度/缺陷数量,丢弃连接。", LogLevel::Warning);
m_imageRecvBuffer.clear();
m_imageServerSocket->disconnectFromHost(); // 直接断开
return;
}
// 步骤4:计算当前消息的总长度(判断缓冲区是否有完整消息)
qint64 totalSize = headerSize
-
static_cast<qint64>(origImgSize)
-
static_cast<qint64>(defectImgSize)
-
static_cast<qint64>(defectCount) * 34; // 每个缺陷坐标 (x,y) 16 字节+(quint16)defectType =2
if (totalSize <= 0 || totalSize > MAX_MESSAGE_BYTES) {
addLog("接收端计算到异常的 totalSize,丢弃连接。", LogLevel::Warning);
m_imageRecvBuffer.clear();
m_imageServerSocket->disconnectFromHost();
return;
}
// 缓冲区数据不足一个完整消息,退出循环(等后续数据)
if (m_imageRecvBuffer.size() < totalSize) {
break;
}
// 步骤5:提取完整消息 + 清理缓冲区
QByteArray oneMsg = m_imageRecvBuffer.left(totalSize);// 截取一个完整消息
m_imageRecvBuffer.remove(0, totalSize);// 从缓冲区中移除已处理的消息
// 把完整消息丢到后台线程去解析 + 落盘
emit imageMessageReady(oneMsg);
m_imageRecvCount++;
m_imageRecvBytes += oneMsg.size();
//processOneImageMessage(oneMsg);
}
}
//多线程屏蔽掉了
//void Cptest::processOneImageMessage(const QByteArray& msg)
//{
// const int headerSize = 16;
// if (msg.size() < headerSize) return;
//
// QDataStream in(msg);
// in.setByteOrder(QDataStream::BigEndian);
//
// // 解析消息头
// qint32 cameraId;
// qint32 defectCount;
// quint32 origImgSize;
// quint32 defectImgSize;
//
// in >> cameraId;
// in >> defectCount;
// in >> origImgSize;
// in >> defectImgSize;
//
// // 原图
// QByteArray origImgData;
// origImgData.resize(origImgSize);
// if (origImgSize > 0) {
// in.readRawData(origImgData.data(), origImgSize);
// }
//
// // 缺陷图(当前发送端 defectImgSize=0,这里做兼容,以后加上也不用改)
// QByteArray defectImgData;
// if (defectImgSize > 0) {
// defectImgData.resize(defectImgSize);
// in.readRawData(defectImgData.data(), defectImgSize);
// }
//
// // 每个缺陷:x,y,w,h,type
// QVector<QPair<double, double>> coords; // 先维持原来接口,只存 X/Y
// coords.reserve(defectCount);
//
// for (int i = 0; i < defectCount; ++i) {
// double x, y, w, h;
// quint16 defectType;
// in >> x;
// in >> y;
// in >> w;
// in >> h;
// in >> defectType;
//
// coords.push_back({ x, y });
// }
//
// // 处理图像文件存储到E盘
// if (origImgSize > 0) {
// QString imagePath = m_imageDirectory + QString("camera_%1_image.png").arg(cameraId);
// QFile imageFile(imagePath);
// if (imageFile.open(QIODevice::WriteOnly)) {
// imageFile.write(origImgData);
// imageFile.close();
// addLog(QString("图像已保存到:%1").arg(imagePath), LogLevel::Info);
// }
// else {
// addLog("图像保存失败!", LogLevel::Error);
// }
// }
//
// // 处理元数据存储到D盘
// if (defectCount > 0) {
// QString metadataPath = m_metadataDirectory + QString("camera_%1_metadata.txt").arg(cameraId);
// QFile metadataFile(metadataPath);
// if (metadataFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
// QTextStream out(&metadataFile);
// out << "Camera ID: " << cameraId << "\n";
// out << "Defect Count: " << defectCount << "\n";
// out << "Defect Coordinates: \n";
// for (const auto& coord : coords) {
// out << QString("X: %1, Y: %2\n").arg(coord.first).arg(coord.second);
// }
// metadataFile.close();
// addLog(QString("元数据已保存到:%1").arg(metadataPath), LogLevel::Info);
// }
// else {
// addLog("元数据保存失败!", LogLevel::Error);
// }
// }
//}
void Cptest::resetImageMonitor()
{
if (!signalTable) return;
signalTable->setRowCount(6);
QStringList vHeaders;
vHeaders << "条数(接收/发送)"
<< "总字节"
<< "最后相机ID"
<< "最后缺陷个数"
<< "最后缺陷X"
<< "最后缺陷Y";
signalTable->setVerticalHeaderLabels(vHeaders);
for (int r = 0; r < signalTable->rowCount(); ++r) {
for (int c = 0; c < signalTable->columnCount(); ++c) {
QTableWidgetItem* item = signalTable->item(r, c);
if (!item) {
item = new QTableWidgetItem();
item->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(r, c, item);
}
item->setText("");
}
}
}
void Cptest::updateImageMonitorLast(qint32 cameraId, qint32 defectCount,
const QVector<QPair<double, double>>& coords)
{
auto setCell = [&](int row, int col, const QString& val) {
QTableWidgetItem* it = signalTable->item(row, col);
if (!it) {
it = new QTableWidgetItem();
it->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(row, col, it);
}
it->setText(val);
};
setCell(0, 0, QString::number(m_imageRecvCount));
setCell(1, 0, QString::number(m_imageRecvBytes));
setCell(2, 0, QString::number(cameraId));
setCell(3, 0, QString::number(defectCount));
if (defectCount > 0 && !coords.isEmpty()) {
setCell(4, 0, QString::number(coords[0].first, 'f', 3));
setCell(5, 0, QString::number(coords[0].second, 'f', 3));
}
else {
setCell(4, 0, "-");
setCell(5, 0, "-");
}
}
// 预留:后期数据库写入
void Cptest::saveImageMessageToDb(qint32 cameraId,
qint32 defectCount,
const QVector<QPair<double, double>>& coords)
{
Q_UNUSED(coords);
// TODO: 在这里把 cameraId / defectCount / coords 写入数据库
addLog(QString("【预留】写入数据库:cameraId=%1, defectCount=%2, coords个数=%3")
.arg(cameraId)
.arg(defectCount)
.arg(coords.size()),
LogLevel::Info);
}
//------------------------------反馈给主线程
void Cptest::onImageMessageProcessed(qint32 cameraId,
qint32 defectCount,
double firstX,
double firstY)
{
// 这里是在主线程执行,可以安心改 UI
auto setCell = [&](int row, int col, const QString& val) {
QTableWidgetItem* it = signalTable->item(row, col);
if (!it) {
it = new QTableWidgetItem();
it->setTextAlignment(Qt::AlignCenter);
signalTable->setItem(row, col, it);
}
it->setText(val);
};
// 假设 resetImageMonitor 把 0~5 行的含义设成:
// 0: 条数(接收/发送) 1: 总字节 2: 最后相机ID
// 3: 最后缺陷个数 4: 最后缺陷X 5: 最后缺陷Y
// 0 行 0 列:已接收条数(这里简单 +1,也可以在 readyRead 里累加)
int cnt = signalTable->item(0, 0) ? signalTable->item(0, 0)->text().toInt() : 0;
setCell(0, 0, QString::number(cnt + 1));
// 2 行 0 列:cameraId
setCell(2, 0, QString::number(cameraId));
// 3 行 0 列:defectCount
setCell(3, 0, QString::number(defectCount));
// 4/5 行 0 列:第一个缺陷的 X/Y(如果没有缺陷就写 "-")
if (defectCount > 0) {
setCell(4, 0, QString::number(firstX, 'f', 3));
setCell(5, 0, QString::number(firstY, 'f', 3));
}
else {
setCell(4, 0, "-");
setCell(5, 0, "-");
}
// 顺便打一条日志
addLog(QString("主线程:一条图像消息处理完成 cameraId=%1, defectCount=%2")
.arg(cameraId).arg(defectCount),
LogLevel::Info);
}
// ------------------------ 日志 & 树样式 ------------------------
void Cptest::addLog(const QString& message, LogLevel level) {
QListWidgetItem* item = new QListWidgetItem(logList);
QString levelText;
QColor textColor;
switch (level) {
case LogLevel::Info:
textColor = QColor(52, 152, 219);
levelText = "INFO";
break;
case LogLevel::Warning:
textColor = QColor(241, 196, 15);
levelText = "WARN";
break;
case LogLevel::Error:
textColor = QColor(231, 76, 60);
levelText = "ERROR";
break;
case LogLevel::Critical:
textColor = QColor(155, 89, 182);
levelText = "CRITICAL";
break;
}
item->setForeground(QBrush(textColor));
QString timeStr = QDateTime::currentDateTime().toString("HH:mm:ss");
item->setText(QString("[%1] [%2] %3").arg(timeStr, levelText, message));
logList->addItem(item);
logList->scrollToBottom();
}
void Cptest::setTreestyle() {
treeWidget->setStyleSheet(R"(
QTreeView::branch {
border-image: none;
}
QTreeView::branch:has-children:!has-siblings,
QTreeView::branch:has-children:has-siblings {
border-left: 1px dashed #7a7a7a;
}
QTreeView::branch:closed:has-children {
image: url(../image/open.png);
border-left: 1px solid #7a7a7a;
}
QTreeView::branch:open:has-children {
image: url(../image/close.png);
border-left: 1px solid #7a7a7a;
}
QTreeWidget {
background-color: #34495e;
color: #ecf0f1;
font-size: 11pt;
}
QTreeWidget::item {
height: 20px;
padding: 5px;
}
QTreeWidget::item:selected {
background-color: #3498db;
color: white;
}
QTreeWidget::item:!has-children {
font-size: 6pt;
font-weight: normal;
}
QTreeWidget::item:has-children {
font-size: 8pt;
font-weight: bold;
}
)");
}