服务器传输推送

一.工作站上传图像 / 元数据→服务器存储→终端获取,三台电脑走 Modbus TCP/IP

终端主动拉取:服务器整理好数据后仅存储,终端按需 / 定时发起请求(如 "获取批次 XXX 的问题图像"),服务器响应并传输数据

**二.**Modbus TCP/IP 通信的关键

通信分层

图像编号、坐标、问题描述、文件路径、指令(如 "拉取批次 XXX 数据")------ModBus Tcp/Ip

原图、问题图像(20MB / 张)------SMB/NFS/FTP(工业级)

三.工作流程

工作站-服务器

  1. 工作站采集原图 / 问题图像→先存储到本地缓存(避免上传中断丢失);
  2. 工作站通过 Modbus 向服务器发送 "上传指令 + 元数据"(图像编号、坐标、问题描述、本地路径);
  3. 服务器通过 SMB/NFS 从工作站拉取图像文件→优先存储到 960G SSD(热数据,20MB×6000 张仅占 120GB,剩余空间缓存),历史数据归档到机械盘;
  4. 服务器将元数据写入数据库,关联图像文件路径,并通过 Modbus 向工作站返回 "上传成功 / 失败 + MD5 校验值"
  5. 工作站检测到失败后,3 次重试(间隔 10 秒),仍失败则本地告警并记录日志。

服务器-终端

  1. 终端通过 Modbus 向服务器发送 "拉取指令"(如 "获取批次 XXX 的问题图像列表");
  2. 服务器响应 Modbus 指令,返回该批次的元数据(编号、坐标、问题描述)+ 图像文件路径(如 \ 服务器 IP\industrial\batchXXX\error_1.jpg);
  3. 终端验证路径有效性后,通过 SMB 直接读取服务器 SSD 上的图像文件(分块读取 + MD5 校验);
  4. 终端读取完成后,通过 Modbus 向服务器反馈 "已成功获取批次 XXX 的图像,数量 XX",服务器记录状态(避免重复拉取);
  5. 兜底机制:终端读取失败时,自动切换到服务器的机械盘备份路径,同时本地告警。

正确连接

步骤 1:服务器先启动(核心前提)

服务器开机后,先启动三大核心服务并持续运行、等待连接

  1. MySQL 服务(存储元数据);
  2. SMB/NFS 文件共享服务(存储图像文件);
  3. Modbus TCP 服务端程序(监听 502 端口,被动等待工作站 / 终端的连接请求)。→ 此时服务器处于 "静默等待" 状态,不主动找任何设备,仅开放端口等待连接。

步骤 2:工作站主动连接服务器(上传数据)

  1. 工作站开机后,先通过网线直连的静态 IP(192.168.0.2→192.168.0.1)ping 通服务器;
  2. 主动发起 Modbus TCP 连接(目标 IP:192.168.0.1:502),服务器接收并建立连接;
  3. 工作站通过 SMB 将图像文件上传到服务器的 SSD 目录;
  4. 工作站通过 Modbus 向服务器发送 "上传指令 + 元数据"(编号、坐标、是否问题图像);
  5. 服务器处理后,通过 Modbus 回复 "上传成功 / 失败",工作站收到确认后断开 Modbus 连接(或保持长连接);→ 全程是「工作站主动→服务器被动响应」,无服务器反向连接工作站。

步骤 3:终端主动连接服务器(拉取数据)

终端同样主动发起交互(核心是 "拉取",而非等服务器推送):

  1. 终端开机后,主动 ping 通服务器(192.168.0.3→192.168.0.1,扩展三台时的终端 IP);
  2. 主动发起 Modbus TCP 连接(目标 IP:192.168.0.1:502),服务器接收并建立连接;
  3. 终端通过 Modbus 发送 "拉取指令"(如 "获取批次 XXX 的问题图像");
  4. 服务器通过 Modbus 返回元数据 + 图像路径,终端收到后主动通过 SMB 读取服务器的图像文件;
  5. 终端读取完成后,通过 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;

}

)");

}

相关推荐
知识分享小能手19 小时前
CentOS Stream 9入门学习教程,从入门到精通,CentOS Stream 9 配置网络功能 —语法详解与实战案例(10)
网络·学习·centos
Joren的学习记录19 小时前
【Linux运维进阶知识】Nginx负载均衡
linux·运维·nginx
专业开发者19 小时前
Wi-Fi®:可持续的优选连接方案
网络·物联网
GIS数据转换器19 小时前
综合安防数智管理平台
大数据·网络·人工智能·安全·无人机
Jtti20 小时前
服务器防御SYN Flood攻击的方法
运维·服务器
2501_9419820520 小时前
RPA 的跨平台部署与统一自动化策略
运维·自动化·rpa
b***251120 小时前
电池自动分选机:精密分选保障新能源产业质量核心
运维·自动化·制造
数数科技的数据干货20 小时前
游戏流失分析:一套经实战检验的「流程化操作指南」
大数据·运维·人工智能·游戏
蒟蒻要翻身21 小时前
在同一局域网内共享打印机设置指南
运维
一点晖光21 小时前
搭建内网穿透的ngrok服务器
服务器·内网穿透·ngrok