#include "mainwindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(static_cast(data.at(offset))) << 24) | (static_cast(static_cast(data.at(offset + 1))) << 16) | (static_cast(static_cast(data.at(offset + 2))) << 8) | static_cast(static_cast(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(&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(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(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(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(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(kFrameHeaderSize) + payloadSize + kFrameTailSize; if (static_cast(m_receiveBuffer.size()) < frameSize) return; const int tailOffset = kFrameHeaderSize + static_cast(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(payloadSize)); m_receiveBuffer.remove(0, static_cast(frameSize)); handleFrame(dataType, payload, static_cast(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(kJsonFrameHeaderSize) + kJsonFrameLengthSize + payloadSize + kJsonFrameTailSize; if (static_cast(m_receiveBuffer.size()) < frameSize) return; const int tailOffset = kJsonFrameHeaderSize + kJsonFrameLengthSize + static_cast(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(payloadSize)); m_receiveBuffer.remove(0, static_cast(frameSize)); m_receivedBytes += static_cast(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 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(points.size())); for (const PointCloudPoint& point : points) { cloud.push_back(Point3DRGB(static_cast(point.x), static_cast(point.y), static_cast(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& 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(headerSize) + static_cast(summary.sampledCount) * pointSize; if (expectedSize != static_cast(payload.size())) { error = QStringLiteral("长度不匹配,期望 %1,收到 %2").arg(expectedSize).arg(payload.size()); return false; } points.reserve(static_cast(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& 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& points) { const QString directory = ensureOutputDir(); if (directory.isEmpty()) return; std::vector> scanLines(1); scanLines.front().reserve(static_cast(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)); }