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

456
plotwidget.cpp Normal file
View File

@@ -0,0 +1,456 @@
#include "plotwidget.h"
#include <QPainter>
#include <QPainterPath>
#include <QtMath>
#include <QFontMetrics>
#include <algorithm>
PlotWidget::PlotWidget(QWidget *parent)
: QWidget(parent)
, m_totalPoints(0)
, m_displayPoints(500)
, m_yMin(0)
, m_yMax(65535)
, m_autoYRange(true)
, m_autoScroll(true)
, m_dragging(false)
, m_marginLeft(70)
, m_marginRight(160)
, m_marginTop(30)
, m_marginBottom(50)
, m_bgColor("#1a1a2e")
, m_gridColor("#333355")
, m_gridColorMinor("#252545")
, m_textColor("#cccccc")
, m_axisColor("#888888")
, m_dirty(true)
{
m_data.resize(16);
m_channelVisible.fill(true, 16);
m_colors = defaultColors();
setMouseTracking(true);
setMinimumSize(400, 300);
// 定时刷新约30fps
m_refreshTimer = new QTimer(this);
connect(m_refreshTimer, &QTimer::timeout, this, [this]() {
if (m_dirty) {
m_dirty = false;
update();
}
});
m_refreshTimer->start(33);
}
QList<QColor> PlotWidget::defaultColors()
{
return {
QColor("#FF4444"), QColor("#44FF44"), QColor("#4488FF"), QColor("#FFAA00"),
QColor("#FF44FF"), QColor("#44FFFF"), QColor("#FFFF44"), QColor("#FF8888"),
QColor("#88FF88"), QColor("#8888FF"), QColor("#CC6600"), QColor("#FF66CC"),
QColor("#66FFCC"), QColor("#CCFF66"), QColor("#AA66FF"), QColor("#FF6666"),
};
}
void PlotWidget::addDataPoint(const QVector<double> &values)
{
if (values.size() < 16) return;
for (int i = 0; i < 16; ++i) {
m_data[i].append(values[i]);
}
m_totalPoints++;
m_dirty = true;
if (m_autoYRange && m_totalPoints % 50 == 0) {
autoRangeY();
}
}
void PlotWidget::setAllData(const QVector<QVector<double>> &channels)
{
clear();
if (channels.size() >= 16) {
for (int i = 0; i < 16; ++i) {
m_data[i] = channels[i];
}
m_totalPoints = channels.isEmpty() ? 0 : channels[0].size();
}
m_autoScroll = true;
m_autoYRange = true;
autoRangeY();
m_dirty = true;
update();
}
void PlotWidget::clear()
{
for (auto &ch : m_data) {
ch.clear();
}
m_totalPoints = 0;
m_autoScroll = true;
m_autoYRange = true;
m_dirty = true;
update();
}
void PlotWidget::setChannelVisible(int channel, bool visible)
{
if (channel >= 0 && channel < 16) {
m_channelVisible[channel] = visible;
m_dirty = true;
}
}
bool PlotWidget::isChannelVisible(int channel) const
{
return channel >= 0 && channel < 16 ? m_channelVisible[channel] : false;
}
void PlotWidget::setDisplayPointCount(int count)
{
m_displayPoints = qMax(10, count);
m_dirty = true;
}
int PlotWidget::displayPointCount() const
{
return m_displayPoints;
}
void PlotWidget::setYRange(double min, double max)
{
m_yMin = min;
m_yMax = max;
m_autoYRange = false;
m_dirty = true;
}
void PlotWidget::autoRangeY()
{
if (m_totalPoints == 0) {
m_yMin = 0;
m_yMax = 65535;
return;
}
double globalMin = 1e18, globalMax = -1e18;
int startIdx = qMax(0, m_totalPoints - m_displayPoints);
for (int ch = 0; ch < 16; ++ch) {
if (!m_channelVisible[ch]) continue;
const auto &channel = m_data[ch];
for (int i = startIdx; i < channel.size(); ++i) {
double v = channel[i];
if (v < globalMin) globalMin = v;
if (v > globalMax) globalMax = v;
}
}
if (globalMax - globalMin < 1.0) {
globalMin -= 50;
globalMax += 50;
}
double margin = (globalMax - globalMin) * 0.1;
m_yMin = globalMin - margin;
m_yMax = globalMax + margin;
}
void PlotWidget::setTitle(const QString &title)
{
m_title = title;
m_dirty = true;
}
int PlotWidget::totalPoints() const
{
return m_totalPoints;
}
QRectF PlotWidget::plotArea() const
{
return QRectF(m_marginLeft, m_marginTop,
width() - m_marginLeft - m_marginRight,
height() - m_marginTop - m_marginBottom);
}
QPointF PlotWidget::dataToWidget(double x, double y) const
{
QRectF area = plotArea();
double px = area.left() + x * area.width();
double py = area.bottom() - ((y - m_yMin) / (m_yMax - m_yMin)) * area.height();
return QPointF(px, py);
}
// ================ Paint ================
void PlotWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
drawBackground(painter);
drawGrid(painter);
drawCurves(painter);
drawAxes(painter);
drawLegend(painter);
// 标题
if (!m_title.isEmpty()) {
painter.setPen(m_textColor);
QFont titleFont = font();
titleFont.setPointSize(12);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(QRect(0, 2, width(), m_marginTop - 4), Qt::AlignCenter, m_title);
}
// 数据点计数
painter.setPen(QColor("#666666"));
QFont smallFont = font();
smallFont.setPointSize(8);
painter.setFont(smallFont);
QString info = QString("总点数: %1 显示: %2 范围: [%3, %4]")
.arg(m_totalPoints)
.arg(m_displayPoints)
.arg(m_yMin, 0, 'f', 0)
.arg(m_yMax, 0, 'f', 0);
painter.drawText(QRect(m_marginLeft, height() - m_marginBottom + 30,
width() - m_marginLeft - m_marginRight, 20),
Qt::AlignLeft | Qt::AlignVCenter, info);
}
void PlotWidget::drawBackground(QPainter &painter)
{
painter.fillRect(rect(), m_bgColor);
QRectF area = plotArea();
painter.fillRect(area, QColor("#16213e"));
}
void PlotWidget::drawGrid(QPainter &painter)
{
QRectF area = plotArea();
if (area.width() <= 0 || area.height() <= 0) return;
// 计算合适的刻度
double range = m_yMax - m_yMin;
if (range <= 0) return;
double roughStep = range / 8.0;
double exponent = qFloor(log10(roughStep));
double mantissa = roughStep / qPow(10, exponent);
double gridStep;
if (mantissa < 1.5) gridStep = qPow(10, exponent);
else if (mantissa < 3.5) gridStep = 2 * qPow(10, exponent);
else if (mantissa < 7.5) gridStep = 5 * qPow(10, exponent);
else gridStep = 10 * qPow(10, exponent);
if (gridStep <= 0) gridStep = 1;
double firstGrid = qCeil(m_yMin / gridStep) * gridStep;
QPen majorPen(m_gridColor, 1, Qt::SolidLine);
QPen minorPen(m_gridColorMinor, 1, Qt::DotLine);
// Y轴网格
for (double y = firstGrid; y <= m_yMax; y += gridStep) {
painter.setPen(majorPen);
QPointF p = dataToWidget(0, y);
painter.drawLine(QPointF(area.left(), p.y()), QPointF(area.right(), p.y()));
// 标签
painter.setPen(m_textColor);
QFont gridFont = font();
gridFont.setPointSize(8);
painter.setFont(gridFont);
painter.drawText(QRectF(0, p.y() - 10, m_marginLeft - 5, 20),
Qt::AlignRight | Qt::AlignVCenter,
QString::number(y, 'f', 0));
}
// X轴网格时间/采样点)
int startIdx = qMax(0, m_totalPoints - m_displayPoints);
if (m_autoScroll) {
startIdx = qMax(0, m_totalPoints - m_displayPoints);
}
// 画几条竖线
int xStep = m_displayPoints / 5;
if (xStep < 1) xStep = 1;
// 竖网格线(基于显示的采样点)
for (int i = 0; i <= m_displayPoints; i += xStep) {
double t = (double)i / m_displayPoints;
QPointF p = dataToWidget(t, 0);
painter.setPen(minorPen);
painter.drawLine(QPointF(p.x(), area.top()), QPointF(p.x(), area.bottom()));
painter.setPen(m_textColor);
QFont gridFont = font();
gridFont.setPointSize(8);
painter.setFont(gridFont);
int sampleIdx = startIdx + i;
painter.drawText(QRectF(p.x() - 30, area.bottom() + 3, 60, 15),
Qt::AlignCenter, QString::number(sampleIdx));
}
}
void PlotWidget::drawCurves(QPainter &painter)
{
QRectF area = plotArea();
if (area.width() <= 0 || area.height() <= 0) return;
if (m_totalPoints == 0) return;
int startIdx = qMax(0, m_totalPoints - m_displayPoints);
int visibleCount = m_totalPoints - startIdx;
if (visibleCount < 2) return;
for (int ch = 0; ch < 16; ++ch) {
if (!m_channelVisible[ch]) continue;
const auto &channel = m_data[ch];
if (channel.size() <= startIdx) continue;
QPen pen(m_colors[ch], 1.2);
painter.setPen(pen);
QPainterPath path;
bool first = true;
int endIdx = channel.size();
for (int i = startIdx; i < endIdx; ++i) {
double y = channel[i];
double t = (double)(i - startIdx) / (visibleCount - 1);
QPointF pt = dataToWidget(t, y);
if (first) {
path.moveTo(pt);
first = false;
} else {
path.lineTo(pt);
}
}
painter.drawPath(path);
}
}
void PlotWidget::drawAxes(QPainter &painter)
{
QRectF area = plotArea();
// Y轴
painter.setPen(QPen(m_axisColor, 1.5));
painter.drawLine(area.topLeft(), area.bottomLeft());
// X轴
painter.drawLine(area.bottomLeft(), area.bottomRight());
// Y轴标签
painter.setPen(m_textColor);
QFont labelFont = font();
labelFont.setPointSize(9);
painter.setFont(labelFont);
painter.save();
painter.translate(12, area.center().y());
painter.rotate(-90);
painter.drawText(QRectF(-50, -10, 100, 20), Qt::AlignCenter, "ADC Value");
painter.restore();
}
void PlotWidget::drawLegend(QPainter &painter)
{
QRectF area = plotArea();
int legendX = area.right() + 10;
int legendY = area.top() + 5;
int itemHeight = 18;
painter.setPen(m_textColor);
QFont legendFont = font();
legendFont.setPointSize(8);
painter.setFont(legendFont);
for (int ch = 0; ch < 16; ++ch) {
int y = legendY + ch * itemHeight;
// 颜色方块
QColor color = m_channelVisible[ch] ? m_colors[ch] : QColor("#444444");
painter.fillRect(QRect(legendX, y + 2, 12, 12), color);
// 通道名
QString label = QString("ch%1").arg(ch);
painter.setPen(m_channelVisible[ch] ? m_textColor : QColor("#666666"));
painter.drawText(QRect(legendX + 16, y, 50, itemHeight),
Qt::AlignLeft | Qt::AlignVCenter, label);
}
}
// ================ Mouse Events ================
void PlotWidget::wheelEvent(QWheelEvent *event)
{
QRectF area = plotArea();
if (!area.contains(event->position())) return;
double zoomFactor = 1.15;
if (event->angleDelta().y() < 0) {
zoomFactor = 1.0 / zoomFactor;
}
double centerY = m_yMin + (m_yMax - m_yMin) / 2.0;
double halfRange = (m_yMax - m_yMin) / 2.0 * zoomFactor;
m_yMin = centerY - halfRange;
m_yMax = centerY + halfRange;
m_autoYRange = false;
m_dirty = true;
}
void PlotWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton) {
m_dragging = true;
m_dragStart = event->pos(); // mousePressEvent still uses pos()
m_dragYMinStart = m_yMin;
m_dragYMaxStart = m_yMax;
setCursor(Qt::ClosedHandCursor);
}
}
void PlotWidget::mouseMoveEvent(QMouseEvent *event)
{
if (m_dragging) {
QRectF area = plotArea();
double dy = (event->pos().y() - m_dragStart.y()) / area.height() * (m_yMax - m_yMin);
m_yMin = m_dragYMinStart - dy;
m_yMax = m_dragYMaxStart - dy;
m_autoYRange = false;
m_dirty = true;
}
}
void PlotWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::RightButton) {
m_dragging = false;
setCursor(Qt::ArrowCursor);
}
}
void PlotWidget::mouseDoubleClickEvent(QMouseEvent *)
{
// 双击恢复自动范围
m_autoYRange = true;
m_autoScroll = true;
autoRangeY();
m_dirty = true;
}
void PlotWidget::resizeEvent(QResizeEvent *)
{
m_dirty = true;
}