1127 lines
48 KiB
C++
1127 lines
48 KiB
C++
#include "mainwindow.h"
|
||
|
||
#include <QCheckBox>
|
||
#include <QCloseEvent>
|
||
#include <QAbstractItemView>
|
||
#include <QDateTime>
|
||
#include <QDoubleSpinBox>
|
||
#include <QDir>
|
||
#include <QFile>
|
||
#include <QFileDialog>
|
||
#include <QFontDatabase>
|
||
#include <QGridLayout>
|
||
#include <QGroupBox>
|
||
#include <QHeaderView>
|
||
#include <QHBoxLayout>
|
||
#include <QJsonDocument>
|
||
#include <QJsonArray>
|
||
#include <QJsonObject>
|
||
#include <QJsonValue>
|
||
#include <QLabel>
|
||
#include <QLineEdit>
|
||
#include <QPlainTextEdit>
|
||
#include <QPixmap>
|
||
#include <QPushButton>
|
||
#include <QResizeEvent>
|
||
#include <QSpinBox>
|
||
#include <QSplitter>
|
||
#include <QTableWidget>
|
||
#include <QTableWidgetItem>
|
||
#include <QTextBlock>
|
||
#include <QTextCursor>
|
||
#include <QTextDocument>
|
||
#include <QTextStream>
|
||
#include <QTabWidget>
|
||
#include <QTcpSocket>
|
||
#include <QTimer>
|
||
#include <QVBoxLayout>
|
||
#include <QDataStream>
|
||
#include <utility>
|
||
#include "PointCloudGLWidget.h"
|
||
#include "LaserDataLoader.h"
|
||
#include "VZNL_Types.h"
|
||
|
||
namespace
|
||
{
|
||
constexpr quint8 kTextType = 0x01;
|
||
constexpr quint8 kImageType = 0x02;
|
||
constexpr quint8 kReadConfigType = 0x03;
|
||
constexpr quint8 kWriteConfigType = 0x04;
|
||
constexpr quint8 kPointCloudType = 0x05;
|
||
constexpr int kFrameHeaderSize = 5;
|
||
constexpr int kFrameTailSize = 11;
|
||
constexpr quint64 kMaxPayloadSize = 256ULL * 1024ULL * 1024ULL;
|
||
const QByteArray kFrameTail("___END___\r\n");
|
||
const QByteArray kJsonFrameHeader("##START#");
|
||
const QByteArray kJsonFrameTail("#END");
|
||
constexpr int kJsonFrameHeaderSize = 8;
|
||
constexpr int kJsonFrameLengthSize = 8;
|
||
constexpr int kJsonFrameTailSize = 4;
|
||
constexpr quint32 kMaxJsonPayloadSize = 10U * 1024U * 1024U;
|
||
|
||
quint32 readUInt32BE(const QByteArray& data, int offset)
|
||
{
|
||
return (static_cast<quint32>(static_cast<quint8>(data.at(offset))) << 24)
|
||
| (static_cast<quint32>(static_cast<quint8>(data.at(offset + 1))) << 16)
|
||
| (static_cast<quint32>(static_cast<quint8>(data.at(offset + 2))) << 8)
|
||
| static_cast<quint32>(static_cast<quint8>(data.at(offset + 3)));
|
||
}
|
||
|
||
QString formatRange(double minValue, double maxValue)
|
||
{
|
||
return QStringLiteral("%1 .. %2").arg(minValue, 0, 'f', 3).arg(maxValue, 0, 'f', 3);
|
||
}
|
||
}
|
||
|
||
MainWindow::MainWindow(QWidget* parent)
|
||
: QMainWindow(parent),
|
||
m_socket(new QTcpSocket(this))
|
||
{
|
||
setupUi();
|
||
connect(m_socket, &QTcpSocket::connected, this, &MainWindow::onConnected);
|
||
connect(m_socket, &QTcpSocket::disconnected, this, &MainWindow::onDisconnected);
|
||
connect(m_socket, &QTcpSocket::readyRead, this, &MainWindow::onReadyRead);
|
||
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
||
connect(m_socket, &QAbstractSocket::errorOccurred,
|
||
this, &MainWindow::onSocketError);
|
||
#else
|
||
connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)),
|
||
this, SLOT(onSocketError(QAbstractSocket::SocketError)));
|
||
#endif
|
||
updateConnectionControls(false);
|
||
}
|
||
|
||
MainWindow::~MainWindow()
|
||
{
|
||
disconnectFromServer();
|
||
}
|
||
|
||
void MainWindow::setupUi()
|
||
{
|
||
setWindowTitle(QStringLiteral("BeltTearing TCP 协议测试"));
|
||
resize(1080, 760);
|
||
setMinimumSize(900, 620);
|
||
|
||
auto* central = new QWidget(this);
|
||
auto* mainLayout = new QVBoxLayout(central);
|
||
mainLayout->setContentsMargins(6, 6, 6, 6);
|
||
mainLayout->setSpacing(4);
|
||
|
||
auto* connectionGroup = new QGroupBox(QStringLiteral("连接"), central);
|
||
auto* connectionLayout = new QGridLayout(connectionGroup);
|
||
connectionLayout->setContentsMargins(8, 6, 8, 6);
|
||
connectionLayout->setHorizontalSpacing(6);
|
||
connectionLayout->setVerticalSpacing(4);
|
||
m_hostEdit = new QLineEdit(QStringLiteral("127.0.0.1"), connectionGroup);
|
||
m_portSpin = new QSpinBox(connectionGroup);
|
||
m_portSpin->setRange(1, 65535);
|
||
m_portSpin->setValue(5800);
|
||
m_connectButton = new QPushButton(QStringLiteral("连接"), connectionGroup);
|
||
m_disconnectButton = new QPushButton(QStringLiteral("断开"), connectionGroup);
|
||
m_connectionLabel = new QLabel(QStringLiteral("未连接"), connectionGroup);
|
||
connectionLayout->addWidget(new QLabel(QStringLiteral("服务器地址"), connectionGroup), 0, 0);
|
||
connectionLayout->addWidget(m_hostEdit, 0, 1);
|
||
connectionLayout->addWidget(new QLabel(QStringLiteral("BeltTearing TCP 端口"), connectionGroup), 0, 2);
|
||
connectionLayout->addWidget(m_portSpin, 0, 3);
|
||
connectionLayout->addWidget(m_connectButton, 0, 4);
|
||
connectionLayout->addWidget(m_disconnectButton, 0, 5);
|
||
connectionLayout->addWidget(m_connectionLabel, 0, 6);
|
||
connectionLayout->setColumnStretch(1, 1);
|
||
connectionLayout->setColumnStretch(6, 1);
|
||
mainLayout->addWidget(connectionGroup);
|
||
|
||
auto* realtimeGroup = new QGroupBox(QStringLiteral("控制测试"), central);
|
||
auto* realtimeLayout = new QHBoxLayout(realtimeGroup);
|
||
realtimeLayout->setContentsMargins(8, 4, 8, 4);
|
||
realtimeLayout->setSpacing(6);
|
||
m_realtimeCheck = new QCheckBox(QStringLiteral("启用实时上报"), realtimeGroup);
|
||
m_resetAlarmButton = new QPushButton(QStringLiteral("RESET_ALARM"), realtimeGroup);
|
||
m_restDetectButton = new QPushButton(QStringLiteral("REST_DETECT"), realtimeGroup);
|
||
realtimeLayout->addWidget(m_realtimeCheck);
|
||
realtimeLayout->addWidget(m_resetAlarmButton);
|
||
realtimeLayout->addWidget(m_restDetectButton);
|
||
realtimeLayout->addStretch();
|
||
mainLayout->addWidget(realtimeGroup);
|
||
|
||
auto* configGroup = new QGroupBox(QStringLiteral("参数配置测试(GET_CONFIG / SET_CONFIG)"), central);
|
||
auto* configLayout = new QGridLayout(configGroup);
|
||
configLayout->setContentsMargins(8, 6, 8, 6);
|
||
configLayout->setHorizontalSpacing(10);
|
||
configLayout->setVerticalSpacing(4);
|
||
auto makeDoubleSpinBox = [configGroup](double value, double minimum = 0.0) {
|
||
auto* spinBox = new QDoubleSpinBox(configGroup);
|
||
spinBox->setRange(minimum, 1000000000.0);
|
||
spinBox->setDecimals(4);
|
||
spinBox->setValue(value);
|
||
return spinBox;
|
||
};
|
||
auto makeIntSpinBox = [configGroup](int value, int minimum = 1) {
|
||
auto* spinBox = new QSpinBox(configGroup);
|
||
spinBox->setRange(minimum, 1000000000);
|
||
spinBox->setValue(value);
|
||
return spinBox;
|
||
};
|
||
auto addField = [configLayout, configGroup](int row, int column, const QString& title, QWidget* field) {
|
||
configLayout->addWidget(new QLabel(title, configGroup), row, column);
|
||
configLayout->addWidget(field, row, column + 1);
|
||
};
|
||
|
||
m_scanXScaleSpin = makeDoubleSpinBox(1.0, 0.0001);
|
||
m_scanYScaleSpin = makeDoubleSpinBox(1.0, 0.0001);
|
||
m_differenceBinThresholdSpin = makeDoubleSpinBox(1.0);
|
||
m_tearingMinLengthSpin = makeDoubleSpinBox(5.0);
|
||
m_tearingMinWidthSpin = makeDoubleSpinBox(0.0);
|
||
m_tearingMinDepthSpin = makeDoubleSpinBox(0.0);
|
||
m_tearingMinGapSpin = makeDoubleSpinBox(2.0);
|
||
m_sameGapThresholdSpin = makeDoubleSpinBox(2.0);
|
||
m_gapCheckWindowSpin = makeIntSpinBox(5);
|
||
addField(0, 0, QStringLiteral("X 缩放"), m_scanXScaleSpin);
|
||
addField(0, 2, QStringLiteral("Y 缩放"), m_scanYScaleSpin);
|
||
addField(0, 4, QStringLiteral("差分阈值"), m_differenceBinThresholdSpin);
|
||
addField(1, 0, QStringLiteral("最小撕裂长度"), m_tearingMinLengthSpin);
|
||
addField(1, 2, QStringLiteral("最小撕裂宽度"), m_tearingMinWidthSpin);
|
||
addField(1, 4, QStringLiteral("最小撕裂深度"), m_tearingMinDepthSpin);
|
||
addField(2, 0, QStringLiteral("最小撕裂间隙"), m_tearingMinGapSpin);
|
||
addField(2, 2, QStringLiteral("同裂间隙阈值"), m_sameGapThresholdSpin);
|
||
addField(2, 4, QStringLiteral("间隙检查窗口"), m_gapCheckWindowSpin);
|
||
|
||
m_cameraIpEdit = new QLineEdit(QStringLiteral("192.168.1.100"), configGroup);
|
||
m_maxQueueSizeSpin = makeIntSpinBox(300);
|
||
m_generationIntervalSpin = makeIntSpinBox(100);
|
||
m_imageLineCountSpin = makeIntSpinBox(300);
|
||
addField(3, 0, QStringLiteral("相机 IP"), m_cameraIpEdit);
|
||
addField(3, 2, QStringLiteral("最大队列数"), m_maxQueueSizeSpin);
|
||
addField(3, 4, QStringLiteral("生成间隔"), m_generationIntervalSpin);
|
||
addField(4, 0, QStringLiteral("图像线数"), m_imageLineCountSpin);
|
||
|
||
m_enabledCheck = new QCheckBox(QStringLiteral("启用点云发送"), configGroup);
|
||
m_enabledCheck->setChecked(true);
|
||
m_lineStepSpin = makeIntSpinBox(1);
|
||
m_pointStepSpin = makeIntSpinBox(1);
|
||
m_readConfigButton = new QPushButton(QStringLiteral("读取参数配置"), configGroup);
|
||
m_setConfigButton = new QPushButton(QStringLiteral("写入参数配置"), configGroup);
|
||
configLayout->addWidget(m_enabledCheck, 4, 2, 1, 2);
|
||
addField(4, 4, QStringLiteral("抽线步长"), m_lineStepSpin);
|
||
addField(5, 0, QStringLiteral("抽点步长"), m_pointStepSpin);
|
||
configLayout->addWidget(m_readConfigButton, 5, 2, 1, 2);
|
||
configLayout->addWidget(m_setConfigButton, 5, 4, 1, 2);
|
||
for (int column = 1; column < 6; column += 2)
|
||
configLayout->setColumnStretch(column, 1);
|
||
configLayout->setRowStretch(6, 1);
|
||
mainLayout->addWidget(configGroup);
|
||
|
||
auto* pointCloudGroup = new QGroupBox(QStringLiteral("点云测试"), central);
|
||
auto* pointCloudLayout = new QVBoxLayout(pointCloudGroup);
|
||
pointCloudLayout->setContentsMargins(6, 6, 6, 6);
|
||
pointCloudLayout->setSpacing(4);
|
||
auto* pointCloudToolbar = new QHBoxLayout;
|
||
pointCloudToolbar->setContentsMargins(0, 0, 0, 0);
|
||
pointCloudToolbar->setSpacing(6);
|
||
m_pointCloudClearButton = new QPushButton(QStringLiteral("清空显示"), pointCloudGroup);
|
||
m_displayLineCountSpin = new QSpinBox(pointCloudGroup);
|
||
m_displayLineCountSpin->setRange(1, 10000);
|
||
m_displayLineCountSpin->setValue(200);
|
||
m_saveCloudUtilsCheck = new QCheckBox(QStringLiteral("使用 CloudUtils 保存"), pointCloudGroup);
|
||
pointCloudToolbar->addWidget(m_pointCloudClearButton);
|
||
pointCloudToolbar->addWidget(new QLabel(QStringLiteral("显示线数"), pointCloudGroup));
|
||
pointCloudToolbar->addWidget(m_displayLineCountSpin);
|
||
pointCloudToolbar->addWidget(m_saveCloudUtilsCheck);
|
||
pointCloudToolbar->addStretch();
|
||
pointCloudLayout->addLayout(pointCloudToolbar);
|
||
m_cloudView = new PointCloudGLWidget(pointCloudGroup);
|
||
m_cloudView->setMinimumHeight(260);
|
||
pointCloudLayout->addWidget(m_cloudView, 1);
|
||
|
||
auto* storageGroup = new QGroupBox(QStringLiteral("保存选项"), central);
|
||
auto* storageLayout = new QGridLayout(storageGroup);
|
||
storageLayout->setContentsMargins(8, 6, 8, 6);
|
||
storageLayout->setHorizontalSpacing(8);
|
||
storageLayout->setVerticalSpacing(4);
|
||
m_savePointsCheck = new QCheckBox(QStringLiteral("追加点云到 CSV"), storageGroup);
|
||
m_saveImagesCheck = new QCheckBox(QStringLiteral("保存 JPEG 图像"), storageGroup);
|
||
m_outputDirEdit = new QLineEdit(QDir::current().filePath(QStringLiteral("tcp_test_output")), storageGroup);
|
||
auto* browseButton = new QPushButton(QStringLiteral("选择目录"), storageGroup);
|
||
storageLayout->addWidget(m_savePointsCheck, 0, 0);
|
||
storageLayout->addWidget(m_saveImagesCheck, 0, 1);
|
||
storageLayout->addWidget(new QLabel(QStringLiteral("输出目录"), storageGroup), 1, 0);
|
||
storageLayout->addWidget(m_outputDirEdit, 1, 1);
|
||
storageLayout->addWidget(browseButton, 1, 2);
|
||
storageLayout->setColumnStretch(1, 1);
|
||
storageLayout->setRowStretch(2, 1);
|
||
|
||
auto* statsGroup = new QGroupBox(QStringLiteral("接收统计"), central);
|
||
auto* statsLayout = new QGridLayout(statsGroup);
|
||
statsLayout->setContentsMargins(6, 6, 6, 6);
|
||
statsLayout->setHorizontalSpacing(6);
|
||
statsLayout->setVerticalSpacing(4);
|
||
auto addStat = [statsLayout, statsGroup](int row, int column, const QString& title, QLabel*& value) {
|
||
auto* cell = new QWidget(statsGroup);
|
||
auto* cellLayout = new QVBoxLayout(cell);
|
||
cellLayout->setContentsMargins(4, 0, 4, 0);
|
||
cellLayout->setSpacing(1);
|
||
cellLayout->addWidget(new QLabel(title, cell));
|
||
value = new QLabel(QStringLiteral("-"), cell);
|
||
value->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
cellLayout->addWidget(value);
|
||
statsLayout->addWidget(cell, row, column);
|
||
};
|
||
addStat(0, 0, QStringLiteral("接收点云行数"), m_linesValue);
|
||
addStat(0, 1, QStringLiteral("接收字节数"), m_bytesValue);
|
||
addStat(0, 2, QStringLiteral("协议错误数"), m_errorsValue);
|
||
addStat(1, 0, QStringLiteral("最近帧"), m_frameValue);
|
||
addStat(1, 1, QStringLiteral("点数 原始/抽样"), m_countValue);
|
||
addStat(1, 2, QStringLiteral("步长 抽线/抽点"), m_stepsValue);
|
||
addStat(2, 0, QStringLiteral("X 范围"), m_xRangeValue);
|
||
addStat(2, 1, QStringLiteral("Y 范围"), m_yRangeValue);
|
||
addStat(2, 2, QStringLiteral("Z 范围"), m_zRangeValue);
|
||
for (int column = 0; column < 3; ++column)
|
||
statsLayout->setColumnStretch(column, 1);
|
||
|
||
auto* realtimeSplitter = new QSplitter(Qt::Horizontal, statsGroup);
|
||
auto* realtimeDataGroup = new QGroupBox(QStringLiteral("实时撕裂结果"), realtimeSplitter);
|
||
auto* realtimeDataLayout = new QVBoxLayout(realtimeDataGroup);
|
||
realtimeDataLayout->setContentsMargins(4, 4, 4, 4);
|
||
realtimeDataLayout->setSpacing(3);
|
||
m_realtimeSummaryLabel = new QLabel(QStringLiteral("时间:- 数量:0"), realtimeDataGroup);
|
||
m_realtimeTearTable = new QTableWidget(realtimeDataGroup);
|
||
m_realtimeTearTable->setColumnCount(5);
|
||
m_realtimeTearTable->setHorizontalHeaderLabels({
|
||
QStringLiteral("ID"), QStringLiteral("状态"), QStringLiteral("长度(mm)"),
|
||
QStringLiteral("宽度(mm)"), QStringLiteral("深度(mm)")
|
||
});
|
||
m_realtimeTearTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||
m_realtimeTearTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||
m_realtimeTearTable->verticalHeader()->setVisible(false);
|
||
m_realtimeTearTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||
realtimeDataLayout->addWidget(m_realtimeSummaryLabel);
|
||
realtimeDataLayout->addWidget(m_realtimeTearTable, 1);
|
||
|
||
auto* realtimeImageGroup = new QGroupBox(QStringLiteral("结果图片"), realtimeSplitter);
|
||
auto* realtimeImageLayout = new QVBoxLayout(realtimeImageGroup);
|
||
realtimeImageLayout->setContentsMargins(4, 4, 4, 4);
|
||
m_realtimeImageLabel = new QLabel(QStringLiteral("暂无图片"), realtimeImageGroup);
|
||
m_realtimeImageLabel->setAlignment(Qt::AlignCenter);
|
||
m_realtimeImageLabel->setMinimumSize(200, 140);
|
||
m_realtimeImageLabel->setStyleSheet(QStringLiteral(
|
||
"QLabel { background-color: rgb(37, 38, 42); color: rgb(180, 180, 180); }"));
|
||
realtimeImageLayout->addWidget(m_realtimeImageLabel, 1);
|
||
realtimeSplitter->addWidget(realtimeDataGroup);
|
||
realtimeSplitter->addWidget(realtimeImageGroup);
|
||
realtimeSplitter->setStretchFactor(0, 3);
|
||
realtimeSplitter->setStretchFactor(1, 5);
|
||
statsLayout->addWidget(realtimeSplitter, 3, 0, 1, 3);
|
||
statsLayout->setRowStretch(3, 1);
|
||
auto* tabs = new QTabWidget(central);
|
||
tabs->addTab(statsGroup, QStringLiteral("接收统计"));
|
||
tabs->addTab(configGroup, QStringLiteral("参数配置测试"));
|
||
tabs->addTab(pointCloudGroup, QStringLiteral("点云测试"));
|
||
tabs->addTab(storageGroup, QStringLiteral("保存选项"));
|
||
mainLayout->addWidget(tabs, 2);
|
||
|
||
auto* logGroup = new QGroupBox(QStringLiteral("协议日志"), central);
|
||
auto* logLayout = new QVBoxLayout(logGroup);
|
||
logLayout->setContentsMargins(4, 4, 4, 4);
|
||
logLayout->setSpacing(2);
|
||
m_logEdit = new QPlainTextEdit(logGroup);
|
||
m_logEdit->setReadOnly(true);
|
||
m_logEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
|
||
m_logEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||
logLayout->addWidget(m_logEdit);
|
||
mainLayout->addWidget(logGroup, 1);
|
||
setCentralWidget(central);
|
||
|
||
connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::connectToServer);
|
||
connect(m_disconnectButton, &QPushButton::clicked, this, &MainWindow::disconnectFromServer);
|
||
connect(m_readConfigButton, &QPushButton::clicked, this, &MainWindow::readConfig);
|
||
connect(m_setConfigButton, &QPushButton::clicked, this, &MainWindow::setConfig);
|
||
connect(m_realtimeCheck, &QCheckBox::toggled, this, [this](bool) {
|
||
setRealtime();
|
||
});
|
||
connect(m_resetAlarmButton, &QPushButton::clicked, this, &MainWindow::resetAlarm);
|
||
connect(m_restDetectButton, &QPushButton::clicked, this, &MainWindow::restDetect);
|
||
connect(m_displayLineCountSpin, qOverload<int>(&QSpinBox::valueChanged),
|
||
this, [this](int) {
|
||
trimDisplayedPointClouds();
|
||
});
|
||
connect(realtimeSplitter, &QSplitter::splitterMoved, this, [this](int, int) {
|
||
updateRealtimeImageDisplay();
|
||
});
|
||
connect(m_pointCloudClearButton, &QPushButton::clicked,
|
||
this, [this]() {
|
||
m_cloudView->clearPointClouds();
|
||
logMessage(QStringLiteral("已清空点云显示"));
|
||
});
|
||
connect(browseButton, &QPushButton::clicked, this, &MainWindow::chooseOutputDir);
|
||
}
|
||
|
||
void MainWindow::connectToServer()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::UnconnectedState)
|
||
m_socket->abort();
|
||
|
||
m_receiveBuffer.clear();
|
||
m_connectionLabel->setText(QStringLiteral("连接中..."));
|
||
logMessage(QStringLiteral("连接 %1:%2").arg(m_hostEdit->text().trimmed()).arg(m_portSpin->value()));
|
||
m_socket->connectToHost(m_hostEdit->text().trimmed(), static_cast<quint16>(m_portSpin->value()));
|
||
}
|
||
|
||
void MainWindow::disconnectFromServer()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::UnconnectedState)
|
||
m_socket->abort();
|
||
updateConnectionControls(false);
|
||
}
|
||
|
||
void MainWindow::readConfig()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::ConnectedState) {
|
||
recordError(QStringLiteral("读取配置失败:尚未连接"));
|
||
return;
|
||
}
|
||
QJsonObject request;
|
||
request[QStringLiteral("msgType")] = QStringLiteral("GET_CONFIG");
|
||
request[QStringLiteral("protocolVersion")] = QStringLiteral("1.6");
|
||
request[QStringLiteral("requestId")] = QStringLiteral("cfg-%1")
|
||
.arg(QDateTime::currentMSecsSinceEpoch());
|
||
sendJsonMessage(request);
|
||
logMessage(QStringLiteral("已发送 GET_CONFIG 请求"));
|
||
}
|
||
|
||
void MainWindow::setConfig()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::ConnectedState) {
|
||
recordError(QStringLiteral("写入参数配置失败:尚未连接"));
|
||
return;
|
||
}
|
||
|
||
QJsonObject request;
|
||
QJsonObject algorithm;
|
||
algorithm[QStringLiteral("scanXScale")] = m_scanXScaleSpin->value();
|
||
algorithm[QStringLiteral("scanYScale")] = m_scanYScaleSpin->value();
|
||
algorithm[QStringLiteral("differnceBinTh")] = m_differenceBinThresholdSpin->value();
|
||
algorithm[QStringLiteral("tearingMinLen")] = m_tearingMinLengthSpin->value();
|
||
algorithm[QStringLiteral("tearingMinWidthY")] = m_tearingMinWidthSpin->value();
|
||
algorithm[QStringLiteral("tearingMinDepthZ")] = m_tearingMinDepthSpin->value();
|
||
algorithm[QStringLiteral("tearingMinGap")] = m_tearingMinGapSpin->value();
|
||
algorithm[QStringLiteral("sameGapTh")] = m_sameGapThresholdSpin->value();
|
||
algorithm[QStringLiteral("gapChkWin")] = m_gapCheckWindowSpin->value();
|
||
|
||
QJsonObject camera;
|
||
camera[QStringLiteral("cameraIP")] = m_cameraIpEdit->text().trimmed();
|
||
|
||
QJsonObject queue;
|
||
queue[QStringLiteral("maxQueueSize")] = m_maxQueueSizeSpin->value();
|
||
queue[QStringLiteral("generationInterval")] = m_generationIntervalSpin->value();
|
||
queue[QStringLiteral("imageLineCount")] = m_imageLineCountSpin->value();
|
||
|
||
QJsonObject pointCloud;
|
||
pointCloud[QStringLiteral("enabled")] = m_enabledCheck->isChecked();
|
||
pointCloud[QStringLiteral("lineStep")] = m_lineStepSpin->value();
|
||
pointCloud[QStringLiteral("pointStep")] = m_pointStepSpin->value();
|
||
QJsonObject config;
|
||
config[QStringLiteral("algorithmParams")] = algorithm;
|
||
config[QStringLiteral("cameraConfig")] = camera;
|
||
config[QStringLiteral("queueProcessParam")] = queue;
|
||
config[QStringLiteral("pointCloudSendParam")] = pointCloud;
|
||
request[QStringLiteral("msgType")] = QStringLiteral("SET_CONFIG");
|
||
request[QStringLiteral("protocolVersion")] = QStringLiteral("1.6");
|
||
request[QStringLiteral("requestId")] = QStringLiteral("cfg-%1")
|
||
.arg(QDateTime::currentMSecsSinceEpoch());
|
||
request[QStringLiteral("config")] = config;
|
||
sendJsonMessage(request);
|
||
logMessage(QStringLiteral("已发送 SET_CONFIG:算法、相机、队列和点云参数"));
|
||
}
|
||
|
||
void MainWindow::setRealtime()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::ConnectedState) {
|
||
recordError(QStringLiteral("设置实时上报失败:尚未连接"));
|
||
return;
|
||
}
|
||
|
||
QJsonObject request;
|
||
request[QStringLiteral("msgType")] = QStringLiteral("SET_REALTIME");
|
||
request[QStringLiteral("protocolVersion")] = QStringLiteral("1.6");
|
||
request[QStringLiteral("timestamp")] = QDateTime::currentMSecsSinceEpoch();
|
||
request[QStringLiteral("enable")] = m_realtimeCheck->isChecked();
|
||
sendJsonMessage(request);
|
||
logMessage(QStringLiteral("已发送 SET_REALTIME:enable=%1")
|
||
.arg(m_realtimeCheck->isChecked() ? QStringLiteral("true") : QStringLiteral("false")));
|
||
}
|
||
|
||
void MainWindow::resetAlarm()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::ConnectedState) {
|
||
recordError(QStringLiteral("RESET_ALARM 失败:尚未连接"));
|
||
return;
|
||
}
|
||
|
||
QJsonObject request;
|
||
request[QStringLiteral("msgType")] = QStringLiteral("RESET_ALARM");
|
||
request[QStringLiteral("protocolVersion")] = QStringLiteral("1.6");
|
||
request[QStringLiteral("timestamp")] = QDateTime::currentMSecsSinceEpoch();
|
||
sendJsonMessage(request);
|
||
logMessage(QStringLiteral("已发送 RESET_ALARM"));
|
||
}
|
||
|
||
void MainWindow::restDetect()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::ConnectedState) {
|
||
recordError(QStringLiteral("REST_DETECT 失败:尚未连接"));
|
||
return;
|
||
}
|
||
|
||
QJsonObject request;
|
||
request[QStringLiteral("msgType")] = QStringLiteral("REST_DETECT");
|
||
request[QStringLiteral("protocolVersion")] = QStringLiteral("1.6");
|
||
request[QStringLiteral("timestamp")] = QDateTime::currentMSecsSinceEpoch();
|
||
sendJsonMessage(request);
|
||
logMessage(QStringLiteral("已发送 REST_DETECT"));
|
||
}
|
||
|
||
void MainWindow::trimDisplayedPointClouds()
|
||
{
|
||
if (!m_cloudView || !m_displayLineCountSpin) {
|
||
return;
|
||
}
|
||
|
||
const size_t maximumLines = static_cast<size_t>(m_displayLineCountSpin->value());
|
||
while (m_cloudView->getCloudCount() > maximumLines) {
|
||
if (!m_cloudView->removeOldestPointCloud()) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void MainWindow::chooseOutputDir()
|
||
{
|
||
const QString selected = QFileDialog::getExistingDirectory(this, QStringLiteral("选择输出目录"),
|
||
m_outputDirEdit->text());
|
||
if (!selected.isEmpty())
|
||
m_outputDirEdit->setText(selected);
|
||
}
|
||
|
||
void MainWindow::onConnected()
|
||
{
|
||
m_resultLogKeys.clear();
|
||
m_resultLogMessages.clear();
|
||
m_resultLogTimes.clear();
|
||
m_resultLogBlocks.clear();
|
||
m_resultLogRepeatCounts.clear();
|
||
updateConnectionControls(true);
|
||
logMessage(QStringLiteral("已连接到 %1:%2").arg(m_hostEdit->text().trimmed()).arg(m_portSpin->value()));
|
||
QTimer::singleShot(0, this, &MainWindow::requestProtocolInfo);
|
||
}
|
||
|
||
void MainWindow::onDisconnected()
|
||
{
|
||
updateConnectionControls(false);
|
||
logMessage(QStringLiteral("连接已断开"));
|
||
}
|
||
|
||
void MainWindow::onReadyRead()
|
||
{
|
||
m_receiveBuffer.append(m_socket->readAll());
|
||
processFrames();
|
||
}
|
||
|
||
void MainWindow::onSocketError(QAbstractSocket::SocketError error)
|
||
{
|
||
Q_UNUSED(error);
|
||
recordError(QStringLiteral("Socket 错误:%1").arg(m_socket->errorString()));
|
||
}
|
||
|
||
void MainWindow::closeEvent(QCloseEvent* event)
|
||
{
|
||
disconnectFromServer();
|
||
event->accept();
|
||
}
|
||
|
||
void MainWindow::resizeEvent(QResizeEvent* event)
|
||
{
|
||
QMainWindow::resizeEvent(event);
|
||
updateRealtimeImageDisplay();
|
||
}
|
||
|
||
void MainWindow::updateConnectionControls(bool connected)
|
||
{
|
||
m_connectButton->setEnabled(!connected);
|
||
m_disconnectButton->setEnabled(connected);
|
||
m_readConfigButton->setEnabled(connected);
|
||
m_setConfigButton->setEnabled(connected);
|
||
m_realtimeCheck->setEnabled(connected);
|
||
m_resetAlarmButton->setEnabled(connected);
|
||
m_restDetectButton->setEnabled(connected);
|
||
m_hostEdit->setEnabled(!connected);
|
||
m_portSpin->setEnabled(!connected);
|
||
if (!connected && m_socket->state() == QAbstractSocket::UnconnectedState)
|
||
m_connectionLabel->setText(QStringLiteral("未连接"));
|
||
else if (connected)
|
||
m_connectionLabel->setText(QStringLiteral("已连接"));
|
||
}
|
||
|
||
void MainWindow::requestProtocolInfo()
|
||
{
|
||
if (m_socket->state() != QAbstractSocket::ConnectedState)
|
||
return;
|
||
|
||
QJsonObject request;
|
||
request[QStringLiteral("msgType")] = QStringLiteral("GET_PROTOCOL_INFO");
|
||
request[QStringLiteral("protocolVersion")] = QStringLiteral("1.6");
|
||
sendJsonMessage(request);
|
||
logMessage(QStringLiteral("已发送新协议 GET_PROTOCOL_INFO 请求"));
|
||
}
|
||
|
||
void MainWindow::sendFrame(quint8 dataType, const QByteArray& payload)
|
||
{
|
||
QByteArray frame;
|
||
QDataStream stream(&frame, QIODevice::WriteOnly);
|
||
stream.setByteOrder(QDataStream::BigEndian);
|
||
stream << dataType << static_cast<quint32>(payload.size());
|
||
frame.append(payload);
|
||
frame.append(kFrameTail);
|
||
m_socket->write(frame);
|
||
m_socket->flush();
|
||
}
|
||
|
||
void MainWindow::sendJsonMessage(const QJsonObject& message)
|
||
{
|
||
const QByteArray payload = QJsonDocument(message).toJson(QJsonDocument::Compact);
|
||
QByteArray frame;
|
||
frame.append(kJsonFrameHeader);
|
||
frame.append(QByteArray::number(payload.size()).rightJustified(kJsonFrameLengthSize, '0'));
|
||
frame.append(payload);
|
||
frame.append(kJsonFrameTail);
|
||
m_socket->write(frame);
|
||
m_socket->flush();
|
||
}
|
||
|
||
void MainWindow::processFrames()
|
||
{
|
||
while (!m_receiveBuffer.isEmpty()) {
|
||
const int sizeBefore = m_receiveBuffer.size();
|
||
if (m_receiveBuffer.startsWith(kJsonFrameHeader))
|
||
processJsonFrames();
|
||
else
|
||
processPointCloudFrames();
|
||
if (m_receiveBuffer.size() == sizeBefore)
|
||
break;
|
||
}
|
||
}
|
||
|
||
void MainWindow::processPointCloudFrames()
|
||
{
|
||
const int minimumFrameSize = kFrameHeaderSize + kFrameTailSize;
|
||
while (m_receiveBuffer.size() >= minimumFrameSize) {
|
||
if (m_receiveBuffer.startsWith(kJsonFrameHeader))
|
||
return;
|
||
|
||
const quint8 dataType = static_cast<quint8>(m_receiveBuffer.at(0));
|
||
const quint32 payloadSize = readUInt32BE(m_receiveBuffer, 1);
|
||
if (payloadSize > kMaxPayloadSize) {
|
||
recordError(QStringLiteral("点云帧负载长度非法:%1").arg(payloadSize));
|
||
m_receiveBuffer.remove(0, 1);
|
||
continue;
|
||
}
|
||
|
||
const quint64 frameSize = static_cast<quint64>(kFrameHeaderSize)
|
||
+ payloadSize + kFrameTailSize;
|
||
if (static_cast<quint64>(m_receiveBuffer.size()) < frameSize)
|
||
return;
|
||
|
||
const int tailOffset = kFrameHeaderSize + static_cast<int>(payloadSize);
|
||
if (m_receiveBuffer.mid(tailOffset, kFrameTailSize) != kFrameTail) {
|
||
recordError(QStringLiteral("点云帧尾非法"));
|
||
m_receiveBuffer.remove(0, 1);
|
||
continue;
|
||
}
|
||
|
||
const QByteArray payload = m_receiveBuffer.mid(
|
||
kFrameHeaderSize, static_cast<int>(payloadSize));
|
||
m_receiveBuffer.remove(0, static_cast<int>(frameSize));
|
||
handleFrame(dataType, payload, static_cast<int>(frameSize));
|
||
}
|
||
}
|
||
|
||
void MainWindow::processJsonFrames()
|
||
{
|
||
const int minimumFrameSize = kJsonFrameHeaderSize + kJsonFrameLengthSize + kJsonFrameTailSize;
|
||
while (m_receiveBuffer.size() >= minimumFrameSize) {
|
||
if (!m_receiveBuffer.startsWith(kJsonFrameHeader))
|
||
return;
|
||
const int headerPos = m_receiveBuffer.indexOf(kJsonFrameHeader);
|
||
if (headerPos < 0)
|
||
return;
|
||
if (headerPos > 0)
|
||
m_receiveBuffer.remove(0, headerPos);
|
||
if (m_receiveBuffer.size() < kJsonFrameHeaderSize + kJsonFrameLengthSize)
|
||
return;
|
||
|
||
bool lengthOk = false;
|
||
const quint32 payloadSize = m_receiveBuffer
|
||
.mid(kJsonFrameHeaderSize, kJsonFrameLengthSize).toUInt(&lengthOk);
|
||
if (!lengthOk || payloadSize > kMaxJsonPayloadSize) {
|
||
recordError(QStringLiteral("新协议负载长度非法"));
|
||
m_receiveBuffer.remove(0, kJsonFrameHeaderSize);
|
||
continue;
|
||
}
|
||
|
||
const quint64 frameSize = static_cast<quint64>(kJsonFrameHeaderSize)
|
||
+ kJsonFrameLengthSize + payloadSize + kJsonFrameTailSize;
|
||
if (static_cast<quint64>(m_receiveBuffer.size()) < frameSize)
|
||
return;
|
||
|
||
const int tailOffset = kJsonFrameHeaderSize + kJsonFrameLengthSize + static_cast<int>(payloadSize);
|
||
if (m_receiveBuffer.mid(tailOffset, kJsonFrameTailSize) != kJsonFrameTail) {
|
||
recordError(QStringLiteral("新协议帧尾非法"));
|
||
m_receiveBuffer.remove(0, kJsonFrameHeaderSize);
|
||
continue;
|
||
}
|
||
|
||
const QByteArray payload = m_receiveBuffer.mid(
|
||
kJsonFrameHeaderSize + kJsonFrameLengthSize, static_cast<int>(payloadSize));
|
||
m_receiveBuffer.remove(0, static_cast<int>(frameSize));
|
||
m_receivedBytes += static_cast<qint64>(frameSize);
|
||
m_bytesValue->setText(QString::number(m_receivedBytes));
|
||
handleNewJsonPayload(payload);
|
||
}
|
||
}
|
||
|
||
void MainWindow::handleFrame(quint8 dataType, const QByteArray& payload, int frameSize)
|
||
{
|
||
m_receivedBytes += frameSize;
|
||
m_bytesValue->setText(QString::number(m_receivedBytes));
|
||
|
||
if (dataType == kPointCloudType) {
|
||
PointCloudSummary summary;
|
||
QVector<PointCloudPoint> points;
|
||
QString error;
|
||
if (!parsePointCloud(payload, summary, points, error)) {
|
||
recordError(QStringLiteral("点云帧解析失败:%1").arg(error));
|
||
return;
|
||
}
|
||
++m_pointLineCount;
|
||
++m_pointCloudFrameCount;
|
||
m_linesValue->setText(QString::number(m_pointLineCount));
|
||
updatePointCloudStats(summary);
|
||
PointCloudXYZRGB cloud;
|
||
cloud.reserve(static_cast<size_t>(points.size()));
|
||
for (const PointCloudPoint& point : points) {
|
||
cloud.push_back(Point3DRGB(static_cast<float>(point.x),
|
||
static_cast<float>(point.y),
|
||
static_cast<float>(point.z),
|
||
170, 170, 170), 0);
|
||
}
|
||
m_cloudView->addPointCloud(cloud,
|
||
QStringLiteral("frame-%1").arg(summary.frameIndex),
|
||
false);
|
||
trimDisplayedPointClouds();
|
||
if (m_savePointsCheck->isChecked())
|
||
appendPoints(summary, points);
|
||
if (m_saveCloudUtilsCheck->isChecked())
|
||
saveCloudUtilsFrame(summary, points);
|
||
return;
|
||
}
|
||
|
||
if (dataType == kImageType) {
|
||
++m_imageCount;
|
||
if (m_saveImagesCheck->isChecked())
|
||
saveImage(payload);
|
||
if (m_imageCount <= 3 || m_imageCount % 20 == 0)
|
||
logMessage(QStringLiteral("图像 #%1:%2 字节").arg(m_imageCount).arg(payload.size()));
|
||
return;
|
||
}
|
||
|
||
if (dataType == kTextType || dataType == kReadConfigType || dataType == kWriteConfigType) {
|
||
handleJsonPayload(dataType, payload);
|
||
return;
|
||
}
|
||
|
||
recordError(QStringLiteral("未知数据类型:0x%1").arg(dataType, 2, 16, QLatin1Char('0')));
|
||
}
|
||
|
||
void MainWindow::handleJsonPayload(quint8 dataType, const QByteArray& payload)
|
||
{
|
||
QJsonParseError parseError;
|
||
const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
|
||
if (document.isNull()) {
|
||
recordError(QStringLiteral("JSON 解析失败:%1").arg(parseError.errorString()));
|
||
return;
|
||
}
|
||
|
||
const QJsonObject object = document.object();
|
||
const QJsonObject config = object.value(QStringLiteral("config")).toObject();
|
||
const QJsonObject pointCloud = config.value(QStringLiteral("pointCloudSendParam")).toObject();
|
||
if (!pointCloud.isEmpty()) {
|
||
m_enabledCheck->setChecked(pointCloud.value(QStringLiteral("enabled")).toBool(true));
|
||
m_lineStepSpin->setValue(qMax(1, pointCloud.value(QStringLiteral("lineStep")).toInt(1)));
|
||
m_pointStepSpin->setValue(qMax(1, pointCloud.value(QStringLiteral("pointStep")).toInt(1)));
|
||
}
|
||
|
||
const QString typeName = dataType == kTextType ? QStringLiteral("Text")
|
||
: dataType == kReadConfigType ? QStringLiteral("ReadConfig")
|
||
: QStringLiteral("WriteConfig");
|
||
logMessage(QStringLiteral("%1: %2").arg(typeName,
|
||
QString::fromUtf8(QJsonDocument(object).toJson(QJsonDocument::Compact))));
|
||
}
|
||
|
||
void MainWindow::handleNewJsonPayload(const QByteArray& payload)
|
||
{
|
||
QJsonParseError parseError;
|
||
const QJsonDocument document = QJsonDocument::fromJson(payload, &parseError);
|
||
if (document.isNull() || !document.isObject()) {
|
||
recordError(QStringLiteral("新协议 JSON 解析失败:%1").arg(parseError.errorString()));
|
||
return;
|
||
}
|
||
|
||
const QJsonObject object = document.object();
|
||
const QJsonObject config = object.value(QStringLiteral("config")).toObject();
|
||
if (!config.isEmpty())
|
||
updateConfigControls(config);
|
||
|
||
const QString messageType = object.value(QStringLiteral("msgType")).toString();
|
||
if (messageType == QStringLiteral("REALTIME_RESULT") ||
|
||
messageType == QStringLiteral("DETECT_RESULT")) {
|
||
const QJsonArray tears = object.value(QStringLiteral("tears")).toArray();
|
||
const int count = object.value(QStringLiteral("count")).toInt(tears.size());
|
||
const bool hasImage = !object.value(QStringLiteral("visimg")).toString().isEmpty();
|
||
updateRealtimeResult(object);
|
||
m_frameValue->setText(QStringLiteral("实时 ts=%1")
|
||
.arg(object.value(QStringLiteral("timestamp")).toVariant().toLongLong()));
|
||
m_countValue->setText(QString::number(count));
|
||
QJsonObject repeatObject = object;
|
||
repeatObject.remove(QStringLiteral("timestamp"));
|
||
repeatObject.remove(QStringLiteral("visimg"));
|
||
const QString repeatKey = messageType + QLatin1Char(':')
|
||
+ QString::fromUtf8(QJsonDocument(repeatObject).toJson(QJsonDocument::Compact));
|
||
logResultMessage(messageType,
|
||
QStringLiteral("%1:count=%2 tears=%3 visimg=%4")
|
||
.arg(messageType)
|
||
.arg(count)
|
||
.arg(tears.size())
|
||
.arg(hasImage ? QStringLiteral("有") : QStringLiteral("无")),
|
||
repeatKey);
|
||
return;
|
||
}
|
||
logMessage(QStringLiteral("新协议 %1: %2").arg(
|
||
messageType.isEmpty() ? QStringLiteral("消息") : messageType,
|
||
QString::fromUtf8(QJsonDocument(object).toJson(QJsonDocument::Compact))));
|
||
}
|
||
|
||
void MainWindow::updateRealtimeResult(const QJsonObject& object)
|
||
{
|
||
const QJsonArray tears = object.value(QStringLiteral("tears")).toArray();
|
||
const int count = object.value(QStringLiteral("count")).toInt(tears.size());
|
||
const qint64 timestamp = object.value(QStringLiteral("timestamp")).toVariant().toLongLong();
|
||
const QString timeText = timestamp > 0
|
||
? QDateTime::fromMSecsSinceEpoch(timestamp).toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz"))
|
||
: QStringLiteral("-");
|
||
m_realtimeSummaryLabel->setText(
|
||
QStringLiteral("时间:%1 数量:%2").arg(timeText).arg(count));
|
||
|
||
m_realtimeTearTable->setRowCount(tears.size());
|
||
auto statusText = [](int status) {
|
||
switch (status) {
|
||
case 1: return QStringLiteral("新发现");
|
||
case 2: return QStringLiteral("增长中");
|
||
case 3: return QStringLiteral("已结束");
|
||
case 4: return QStringLiteral("无效");
|
||
default: return QStringLiteral("未知");
|
||
}
|
||
};
|
||
for (int row = 0; row < tears.size(); ++row) {
|
||
const QJsonObject tear = tears.at(row).toObject();
|
||
const QStringList values = {
|
||
QString::number(tear.value(QStringLiteral("id")).toInt()),
|
||
statusText(tear.value(QStringLiteral("status")).toInt()),
|
||
QString::number(tear.value(QStringLiteral("length")).toDouble()),
|
||
QString::number(tear.value(QStringLiteral("width")).toDouble()),
|
||
QString::number(tear.value(QStringLiteral("depth")).toDouble())
|
||
};
|
||
for (int column = 0; column < values.size(); ++column) {
|
||
auto* item = new QTableWidgetItem(values.at(column));
|
||
item->setTextAlignment(Qt::AlignCenter);
|
||
m_realtimeTearTable->setItem(row, column, item);
|
||
}
|
||
}
|
||
|
||
QByteArray encodedImage = object.value(QStringLiteral("visimg")).toString().toLatin1();
|
||
const int dataSeparator = encodedImage.indexOf(',');
|
||
if (encodedImage.startsWith("data:") && dataSeparator >= 0) {
|
||
encodedImage.remove(0, dataSeparator + 1);
|
||
}
|
||
const QByteArray imageData = QByteArray::fromBase64(encodedImage);
|
||
QImage image;
|
||
if (!imageData.isEmpty() && image.loadFromData(imageData)) {
|
||
m_realtimeImage = image;
|
||
updateRealtimeImageDisplay();
|
||
} else if (encodedImage.isEmpty()) {
|
||
m_realtimeImage = QImage();
|
||
m_realtimeImageLabel->setPixmap(QPixmap());
|
||
m_realtimeImageLabel->setText(QStringLiteral("暂无图片"));
|
||
} else {
|
||
recordError(QStringLiteral("实时结果图片解码失败"));
|
||
}
|
||
}
|
||
|
||
void MainWindow::updateRealtimeImageDisplay()
|
||
{
|
||
if (!m_realtimeImageLabel || m_realtimeImage.isNull()) {
|
||
return;
|
||
}
|
||
const QSize targetSize = m_realtimeImageLabel->contentsRect().size();
|
||
if (targetSize.isEmpty()) {
|
||
return;
|
||
}
|
||
m_realtimeImageLabel->setText(QString());
|
||
m_realtimeImageLabel->setPixmap(QPixmap::fromImage(m_realtimeImage).scaled(
|
||
targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
}
|
||
|
||
void MainWindow::updateConfigControls(const QJsonObject& config)
|
||
{
|
||
const QJsonObject algorithm = config.value(QStringLiteral("algorithmParams")).toObject();
|
||
auto updateDouble = [&algorithm](const QString& key, QDoubleSpinBox* spinBox) {
|
||
if (algorithm.contains(key) && algorithm.value(key).isDouble())
|
||
spinBox->setValue(algorithm.value(key).toDouble());
|
||
};
|
||
updateDouble(QStringLiteral("scanXScale"), m_scanXScaleSpin);
|
||
updateDouble(QStringLiteral("scanYScale"), m_scanYScaleSpin);
|
||
updateDouble(QStringLiteral("differnceBinTh"), m_differenceBinThresholdSpin);
|
||
updateDouble(QStringLiteral("tearingMinLen"), m_tearingMinLengthSpin);
|
||
updateDouble(QStringLiteral("tearingMinWidthY"), m_tearingMinWidthSpin);
|
||
updateDouble(QStringLiteral("tearingMinDepthZ"), m_tearingMinDepthSpin);
|
||
updateDouble(QStringLiteral("tearingMinGap"), m_tearingMinGapSpin);
|
||
updateDouble(QStringLiteral("sameGapTh"), m_sameGapThresholdSpin);
|
||
if (algorithm.contains(QStringLiteral("gapChkWin")))
|
||
m_gapCheckWindowSpin->setValue(qMax(1, algorithm.value(QStringLiteral("gapChkWin")).toInt(1)));
|
||
|
||
const QJsonObject camera = config.value(QStringLiteral("cameraConfig")).toObject();
|
||
if (camera.contains(QStringLiteral("cameraIP")))
|
||
m_cameraIpEdit->setText(camera.value(QStringLiteral("cameraIP")).toString());
|
||
|
||
const QJsonObject queue = config.value(QStringLiteral("queueProcessParam")).toObject();
|
||
if (queue.contains(QStringLiteral("maxQueueSize")))
|
||
m_maxQueueSizeSpin->setValue(qMax(1, queue.value(QStringLiteral("maxQueueSize")).toInt(1)));
|
||
if (queue.contains(QStringLiteral("generationInterval")))
|
||
m_generationIntervalSpin->setValue(qMax(1, queue.value(QStringLiteral("generationInterval")).toInt(1)));
|
||
if (queue.contains(QStringLiteral("imageLineCount")))
|
||
m_imageLineCountSpin->setValue(qMax(1, queue.value(QStringLiteral("imageLineCount")).toInt(1)));
|
||
|
||
const QJsonObject pointCloud = config.value(QStringLiteral("pointCloudSendParam")).toObject();
|
||
if (!pointCloud.isEmpty()) {
|
||
m_enabledCheck->setChecked(pointCloud.value(QStringLiteral("enabled")).toBool(true));
|
||
m_lineStepSpin->setValue(qMax(1, pointCloud.value(QStringLiteral("lineStep")).toInt(1)));
|
||
m_pointStepSpin->setValue(qMax(1, pointCloud.value(QStringLiteral("pointStep")).toInt(1)));
|
||
}
|
||
}
|
||
|
||
bool MainWindow::parsePointCloud(const QByteArray& payload, PointCloudSummary& summary,
|
||
QVector<PointCloudPoint>& points, QString& error) const
|
||
{
|
||
constexpr int headerSize = 33;
|
||
constexpr int pointSize = 28;
|
||
if (payload.size() < headerSize) {
|
||
error = QStringLiteral("负载过短:%1").arg(payload.size());
|
||
return false;
|
||
}
|
||
|
||
QDataStream stream(payload);
|
||
stream.setByteOrder(QDataStream::BigEndian);
|
||
stream.setFloatingPointPrecision(QDataStream::DoublePrecision);
|
||
quint8 version = 0;
|
||
stream >> version >> summary.frameIndex >> summary.timestamp >> summary.originalCount
|
||
>> summary.sampledCount >> summary.lineStep >> summary.pointStep;
|
||
if (stream.status() != QDataStream::Ok || version != 1) {
|
||
error = QStringLiteral("不支持的点云格式版本:%1").arg(version);
|
||
return false;
|
||
}
|
||
|
||
const quint64 expectedSize = static_cast<quint64>(headerSize)
|
||
+ static_cast<quint64>(summary.sampledCount) * pointSize;
|
||
if (expectedSize != static_cast<quint64>(payload.size())) {
|
||
error = QStringLiteral("长度不匹配,期望 %1,收到 %2").arg(expectedSize).arg(payload.size());
|
||
return false;
|
||
}
|
||
|
||
points.reserve(static_cast<int>(summary.sampledCount));
|
||
for (quint32 i = 0; i < summary.sampledCount; ++i) {
|
||
qint32 pointIndex = 0;
|
||
double x = 0.0;
|
||
double y = 0.0;
|
||
double z = 0.0;
|
||
stream >> pointIndex >> x >> y >> z;
|
||
if (stream.status() != QDataStream::Ok) {
|
||
error = QStringLiteral("点数据读取失败");
|
||
return false;
|
||
}
|
||
points.append(PointCloudPoint{pointIndex, x, y, z});
|
||
if (i == 0) {
|
||
summary.xMin = summary.xMax = x;
|
||
summary.yMin = summary.yMax = y;
|
||
summary.zMin = summary.zMax = z;
|
||
} else {
|
||
summary.xMin = qMin(summary.xMin, x);
|
||
summary.xMax = qMax(summary.xMax, x);
|
||
summary.yMin = qMin(summary.yMin, y);
|
||
summary.yMax = qMax(summary.yMax, y);
|
||
summary.zMin = qMin(summary.zMin, z);
|
||
summary.zMax = qMax(summary.zMax, z);
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
void MainWindow::appendPoints(const PointCloudSummary& summary, const QVector<PointCloudPoint>& points)
|
||
{
|
||
const QString directory = ensureOutputDir();
|
||
if (directory.isEmpty())
|
||
return;
|
||
const QString path = QDir(directory).filePath(QStringLiteral("point_cloud.csv"));
|
||
QFile file(path);
|
||
if (!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
|
||
recordError(QStringLiteral("无法写入 CSV:%1").arg(file.errorString()));
|
||
return;
|
||
}
|
||
QTextStream output(&file);
|
||
if (file.size() == 0)
|
||
output << "frame_index,timestamp,point_index,x,y,z\n";
|
||
for (const PointCloudPoint& point : points)
|
||
output << summary.frameIndex << ',' << summary.timestamp << ','
|
||
<< point.index << ',' << point.x << ',' << point.y << ',' << point.z << '\n';
|
||
}
|
||
|
||
void MainWindow::saveImage(const QByteArray& payload)
|
||
{
|
||
const QString directory = ensureOutputDir();
|
||
if (directory.isEmpty())
|
||
return;
|
||
const QString path = QDir(directory).filePath(
|
||
QStringLiteral("image_%1.jpg").arg(m_imageCount, 6, 10, QLatin1Char('0')));
|
||
QFile file(path);
|
||
if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size())
|
||
recordError(QStringLiteral("无法保存图像:%1").arg(file.errorString()));
|
||
}
|
||
|
||
void MainWindow::saveCloudUtilsFrame(const PointCloudSummary& summary,
|
||
const QVector<PointCloudPoint>& points)
|
||
{
|
||
const QString directory = ensureOutputDir();
|
||
if (directory.isEmpty())
|
||
return;
|
||
|
||
std::vector<std::vector<SVzNL3DPosition>> scanLines(1);
|
||
scanLines.front().reserve(static_cast<size_t>(points.size()));
|
||
for (const PointCloudPoint& point : points) {
|
||
SVzNL3DPosition value{};
|
||
value.nPointIdx = point.index;
|
||
value.pt3D.x = point.x;
|
||
value.pt3D.y = point.y;
|
||
value.pt3D.z = point.z;
|
||
scanLines.front().push_back(value);
|
||
}
|
||
|
||
const QString path = QDir(directory).filePath(
|
||
QStringLiteral("point_cloud_%1.txt").arg(summary.frameIndex, 12, 10, QLatin1Char('0')));
|
||
LaserDataLoader loader;
|
||
const int result = loader.DebugSaveLaser(path.toStdString(), std::move(scanLines));
|
||
if (result != 0) {
|
||
recordError(QStringLiteral("CloudUtils 保存点云失败:%1").arg(
|
||
QString::fromStdString(loader.GetLastError())));
|
||
return;
|
||
}
|
||
}
|
||
|
||
QString MainWindow::ensureOutputDir()
|
||
{
|
||
const QString directory = m_outputDirEdit->text().trimmed();
|
||
if (directory.isEmpty()) {
|
||
recordError(QStringLiteral("输出目录为空"));
|
||
return QString();
|
||
}
|
||
QDir dir(directory);
|
||
if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) {
|
||
recordError(QStringLiteral("无法创建输出目录:%1").arg(directory));
|
||
return QString();
|
||
}
|
||
return dir.absolutePath();
|
||
}
|
||
|
||
void MainWindow::logMessage(const QString& message)
|
||
{
|
||
if (!m_logEdit)
|
||
return;
|
||
|
||
if (m_lastLogRepeatCount > 0 && message == m_lastLogKey) {
|
||
++m_lastLogRepeatCount;
|
||
QTextCursor cursor(m_logEdit->document());
|
||
cursor.movePosition(QTextCursor::End);
|
||
cursor.select(QTextCursor::BlockUnderCursor);
|
||
cursor.insertText(QStringLiteral("[%1] %2 x%3")
|
||
.arg(m_lastLogTime, m_lastLogMessage)
|
||
.arg(m_lastLogRepeatCount));
|
||
m_logEdit->ensureCursorVisible();
|
||
return;
|
||
}
|
||
|
||
m_lastLogKey = message;
|
||
m_lastLogMessage = message;
|
||
m_lastLogTime = QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss"));
|
||
m_lastLogRepeatCount = 1;
|
||
m_logEdit->appendPlainText(QStringLiteral("[%1] %2").arg(m_lastLogTime, message));
|
||
}
|
||
|
||
void MainWindow::logResultMessage(const QString& resultType, const QString& message,
|
||
const QString& repeatKey)
|
||
{
|
||
if (!m_logEdit)
|
||
return;
|
||
|
||
if (m_resultLogKeys.value(resultType) == repeatKey) {
|
||
const int repeatCount = m_resultLogRepeatCounts.value(resultType, 1) + 1;
|
||
const QString renderedLine = QStringLiteral("[%1] %2 x%3")
|
||
.arg(m_resultLogTimes.value(resultType),
|
||
m_resultLogMessages.value(resultType))
|
||
.arg(repeatCount);
|
||
const QTextBlock block = m_logEdit->document()->findBlockByNumber(
|
||
m_resultLogBlocks.value(resultType, -1));
|
||
if (block.isValid()) {
|
||
QTextCursor cursor(block);
|
||
cursor.select(QTextCursor::BlockUnderCursor);
|
||
cursor.insertText(renderedLine);
|
||
m_resultLogRepeatCounts[resultType] = repeatCount;
|
||
m_logEdit->ensureCursorVisible();
|
||
m_lastLogRepeatCount = 0;
|
||
return;
|
||
}
|
||
}
|
||
|
||
const QString logTime = QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss"));
|
||
const QString renderedLine = QStringLiteral("[%1] %2").arg(logTime, message);
|
||
m_logEdit->appendPlainText(renderedLine);
|
||
m_resultLogKeys[resultType] = repeatKey;
|
||
m_resultLogMessages[resultType] = message;
|
||
m_resultLogTimes[resultType] = logTime;
|
||
m_resultLogBlocks[resultType] = m_logEdit->document()->lastBlock().blockNumber();
|
||
m_resultLogRepeatCounts[resultType] = 1;
|
||
m_lastLogRepeatCount = 0;
|
||
}
|
||
|
||
void MainWindow::recordError(const QString& message)
|
||
{
|
||
++m_errorCount;
|
||
if (m_errorsValue)
|
||
m_errorsValue->setText(QString::number(m_errorCount));
|
||
logMessage(QStringLiteral("错误:%1").arg(message));
|
||
}
|
||
|
||
void MainWindow::updatePointCloudStats(const PointCloudSummary& summary)
|
||
{
|
||
m_frameValue->setText(QStringLiteral("%1 (ts=%2)").arg(summary.frameIndex).arg(summary.timestamp));
|
||
m_countValue->setText(QStringLiteral("%1 / %2").arg(summary.originalCount).arg(summary.sampledCount));
|
||
m_stepsValue->setText(QStringLiteral("%1 / %2").arg(summary.lineStep).arg(summary.pointStep));
|
||
m_xRangeValue->setText(formatRange(summary.xMin, summary.xMax));
|
||
m_yRangeValue->setText(formatRange(summary.yMin, summary.yMax));
|
||
m_zRangeValue->setText(formatRange(summary.zMin, summary.zMax));
|
||
}
|