feat: AiAnalysis - 16通道AI数据UDP实时采集与分析

功能:
- UDP端口监听,实时接收数据
- 解析16通道逗号分隔的ADC数据
- 实时波形绘制(支持滚轮缩放、右键拖拽、双击恢复)
- 数据自动保存到 data_端口号/ 目录
- 支持打开历史CSV文件回放查看
- 16通道独立颜色,可切换显示/隐藏
- 深色主题界面
This commit is contained in:
iorebuild
2026-04-30 12:46:49 +08:00
commit 7e3593c898
11 changed files with 1232 additions and 0 deletions

332
mainwindow.cpp Normal file
View File

@@ -0,0 +1,332 @@
#include "mainwindow.h"
#include "plotwidget.h"
#include "udpreceiver.h"
#include "datamanager.h"
#include <QApplication>
#include <QStatusBar>
#include <QSplitter>
#include <QScrollArea>
#include <QDebug>
#include <QDir>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_udpReceiver(new UdpReceiver(this))
, m_dataManager(new DataManager(this))
, m_isReceiving(false)
, m_packetCount(0)
{
setWindowTitle("AiAnalysis - AI数据实时分析");
setupUi();
// UDP数据接收
connect(m_udpReceiver, &UdpReceiver::dataReceived, this, &MainWindow::onDataReceived);
connect(m_udpReceiver, &UdpReceiver::errorOccurred, this, [this](const QString &err) {
QMessageBox::warning(this, "错误", err);
});
}
MainWindow::~MainWindow()
{
stopReceiving();
}
void MainWindow::setupUi()
{
QWidget *centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->setContentsMargins(8, 8, 8, 8);
mainLayout->setSpacing(6);
// ========== 顶部控制栏 ==========
QHBoxLayout *controlLayout = new QHBoxLayout();
QLabel *portLabel = new QLabel("UDP端口:");
portLabel->setStyleSheet("color: #ccc; font-size: 13px;");
m_portEdit = new QLineEdit();
m_portEdit->setPlaceholderText("输入端口号");
m_portEdit->setMaximumWidth(120);
m_portEdit->setStyleSheet(
"QLineEdit { background: #2a2a3e; color: #fff; border: 1px solid #555; "
"border-radius: 4px; padding: 4px 8px; font-size: 13px; }"
);
m_startStopBtn = new QPushButton("开始监听");
m_startStopBtn->setStyleSheet(
"QPushButton { background: #2ecc71; color: #fff; border: none; "
"border-radius: 4px; padding: 6px 20px; font-size: 13px; font-weight: bold; }"
"QPushButton:hover { background: #27ae60; }"
);
connect(m_startStopBtn, &QPushButton::clicked, this, &MainWindow::onStartStop);
m_openFileBtn = new QPushButton("打开文件");
m_openFileBtn->setStyleSheet(
"QPushButton { background: #3498db; color: #fff; border: none; "
"border-radius: 4px; padding: 6px 16px; font-size: 13px; }"
"QPushButton:hover { background: #2980b9; }"
);
connect(m_openFileBtn, &QPushButton::clicked, this, &MainWindow::onOpenFile);
m_clearBtn = new QPushButton("清除");
m_clearBtn->setStyleSheet(
"QPushButton { background: #e74c3c; color: #fff; border: none; "
"border-radius: 4px; padding: 6px 16px; font-size: 13px; }"
"QPushButton:hover { background: #c0392b; }"
);
connect(m_clearBtn, &QPushButton::clicked, this, &MainWindow::onClear);
QLabel *displayLabel = new QLabel("显示点数:");
displayLabel->setStyleSheet("color: #ccc; font-size: 13px;");
m_displaySpinBox = new QSpinBox();
m_displaySpinBox->setRange(50, 10000);
m_displaySpinBox->setValue(500);
m_displaySpinBox->setSingleStep(100);
m_displaySpinBox->setStyleSheet(
"QSpinBox { background: #2a2a3e; color: #fff; border: 1px solid #555; "
"border-radius: 4px; padding: 4px; font-size: 13px; }"
);
connect(m_displaySpinBox, QOverload<int>::of(&QSpinBox::valueChanged),
this, &MainWindow::onDisplayPointsChanged);
controlLayout->addWidget(portLabel);
controlLayout->addWidget(m_portEdit);
controlLayout->addWidget(m_startStopBtn);
controlLayout->addSpacing(20);
controlLayout->addWidget(m_openFileBtn);
controlLayout->addWidget(m_clearBtn);
controlLayout->addSpacing(20);
controlLayout->addWidget(displayLabel);
controlLayout->addWidget(m_displaySpinBox);
controlLayout->addStretch();
mainLayout->addLayout(controlLayout);
// ========== 中间区域: 绘图 + 通道选择 ==========
QHBoxLayout *centerLayout = new QHBoxLayout();
// 绘图区域
m_plotWidget = new PlotWidget(this);
m_plotWidget->setTitle("AI 数据实时波形");
centerLayout->addWidget(m_plotWidget, 1);
// 右侧通道选择面板
QGroupBox *channelGroup = new QGroupBox("通道选择");
channelGroup->setStyleSheet(
"QGroupBox { color: #ccc; border: 1px solid #444; border-radius: 4px; "
"margin-top: 12px; padding-top: 16px; font-size: 12px; }"
"QGroupBox::title { subcontrol-origin: margin; left: 10px; }"
);
QVBoxLayout *channelLayout = new QVBoxLayout(channelGroup);
channelLayout->setSpacing(2);
for (int i = 0; i < 16; ++i) {
QCheckBox *check = new QCheckBox(QString("ch%1").arg(i));
check->setChecked(true);
check->setStyleSheet(
"QCheckBox { color: #aaa; font-size: 11px; spacing: 4px; }"
"QCheckBox::indicator { width: 12px; height: 12px; }"
"QCheckBox::indicator:checked { background: #3498db; }"
);
connect(check, &QCheckBox::toggled, this, [this, i](bool checked) {
onToggleChannel(i, checked);
});
channelLayout->addWidget(check);
m_channelChecks.append(check);
}
channelLayout->addStretch();
QScrollArea *scrollArea = new QScrollArea();
scrollArea->setWidget(channelGroup);
scrollArea->setWidgetResizable(true);
scrollArea->setMaximumWidth(120);
scrollArea->setStyleSheet("QScrollArea { border: none; background: transparent; }");
centerLayout->addWidget(scrollArea);
mainLayout->addLayout(centerLayout, 1);
// ========== 底部状态栏 ==========
QHBoxLayout *statusLayout = new QHBoxLayout();
m_statusLabel = new QLabel("就绪 - 等待开始监听");
m_statusLabel->setStyleSheet("color: #888; font-size: 12px;");
m_packetLabel = new QLabel("收到: 0 包");
m_packetLabel->setStyleSheet("color: #888; font-size: 12px;");
QLabel *hintLabel = new QLabel("提示: 滚轮缩放 | 右键拖拽平移 | 双击恢复自动范围");
hintLabel->setStyleSheet("color: #555; font-size: 11px;");
statusLayout->addWidget(m_statusLabel);
statusLayout->addStretch();
statusLayout->addWidget(m_packetLabel);
statusLayout->addSpacing(20);
statusLayout->addWidget(hintLabel);
mainLayout->addLayout(statusLayout);
// 全局样式
setStyleSheet("QMainWindow { background-color: #1a1a2e; }");
}
void MainWindow::onStartStop()
{
if (m_isReceiving) {
stopReceiving();
} else {
startReceiving();
}
}
void MainWindow::startReceiving()
{
bool ok;
quint16 port = m_portEdit->text().toUShort(&ok);
if (!ok || port == 0) {
QMessageBox::warning(this, "错误", "请输入有效的端口号 (1-65535)");
return;
}
if (!m_udpReceiver->start(port)) {
return;
}
// 清除旧数据
m_plotWidget->clear();
m_packetCount = 0;
m_packetLabel->setText("收到: 0 包");
// 创建数据保存目录
m_dataDir = QApplication::applicationDirPath() + "/data_" + QString::number(port);
m_dataManager->startRecording(m_dataDir);
m_isReceiving = true;
m_portEdit->setEnabled(false);
m_startStopBtn->setText("停止监听");
m_startStopBtn->setStyleSheet(
"QPushButton { background: #e74c3c; color: #fff; border: none; "
"border-radius: 4px; padding: 6px 20px; font-size: 13px; font-weight: bold; }"
"QPushButton:hover { background: #c0392b; }"
);
m_statusLabel->setText(QString("监听中 - 端口 %1 | 保存至 %2").arg(port).arg(m_dataDir));
m_statusLabel->setStyleSheet("color: #2ecc71; font-size: 12px;");
}
void MainWindow::stopReceiving()
{
m_udpReceiver->stop();
m_dataManager->stopRecording();
m_isReceiving = false;
m_portEdit->setEnabled(true);
m_startStopBtn->setText("开始监听");
m_startStopBtn->setStyleSheet(
"QPushButton { background: #2ecc71; color: #fff; border: none; "
"border-radius: 4px; padding: 6px 20px; font-size: 13px; font-weight: bold; }"
"QPushButton:hover { background: #27ae60; }"
);
m_statusLabel->setText(QString("已停止 - 共收到 %1 包").arg(m_packetCount));
m_statusLabel->setStyleSheet("color: #e74c3c; font-size: 12px;");
}
void MainWindow::onOpenFile()
{
QString filePath = QFileDialog::getOpenFileName(
this, "打开数据文件", m_dataDir, "CSV文件 (*.csv);;所有文件 (*)");
if (filePath.isEmpty()) return;
stopReceiving();
loadAndDisplayFile(filePath);
}
void MainWindow::loadAndDisplayFile(const QString &filePath)
{
QString error;
auto channels = DataManager::loadFile(filePath, error);
if (!error.isEmpty() && channels[0].isEmpty()) {
QMessageBox::warning(this, "错误", error);
return;
}
m_plotWidget->setAllData(channels);
m_plotWidget->setTitle(QString("回放: %1").arg(QFileInfo(filePath).fileName()));
m_statusLabel->setText(QString("回放模式 - %1 | %2 个数据点")
.arg(QFileInfo(filePath).fileName())
.arg(m_plotWidget->totalPoints()));
m_statusLabel->setStyleSheet("color: #3498db; font-size: 12px;");
m_packetLabel->setText(QString("已加载: %1 点").arg(m_plotWidget->totalPoints()));
}
void MainWindow::onClear()
{
m_plotWidget->clear();
m_packetCount = 0;
m_packetLabel->setText("收到: 0 包");
m_statusLabel->setText("已清除");
m_statusLabel->setStyleSheet("color: #888; font-size: 12px;");
}
void MainWindow::onDataReceived(const QByteArray &data)
{
QString raw = QString::fromUtf8(data).trimmed();
if (raw.isEmpty()) return;
// 可能一个UDP包包含多行
QStringList lines = raw.split('\n', Qt::SkipEmptyParts);
for (const QString &line : lines) {
processDataLine(line.trimmed());
}
}
void MainWindow::processDataLine(const QString &line)
{
QStringList parts = line.split(',');
if (parts.size() < 16) return;
QVector<int> values(16);
bool allOk = true;
for (int i = 0; i < 16; ++i) {
bool ok;
values[i] = parts[i].toInt(&ok);
if (!ok) {
allOk = false;
break;
}
}
if (!allOk) return;
// 转换为double给绘图
QVector<double> dvalues(16);
for (int i = 0; i < 16; ++i) {
dvalues[i] = static_cast<double>(values[i]);
}
m_plotWidget->addDataPoint(dvalues);
m_dataManager->appendDataPoint(values);
m_packetCount++;
m_packetLabel->setText(QString("收到: %1 包").arg(m_packetCount));
}
void MainWindow::onDisplayPointsChanged(int count)
{
m_plotWidget->setDisplayPointCount(count);
}
void MainWindow::onToggleChannel(int channel, bool visible)
{
m_plotWidget->setChannelVisible(channel, visible);
}