一个完整的数据采集系统的实现,使用 Qt 框架和 QSerialPort 模块开发,支持从串口设备采集数据、实时显示、保存到文件,并支持数据可视化

以下是一个完整的数据采集系统的实现,使用 Qt 框架和 QSerialPort 模块开发,支持从串口设备采集数据、实时显示、保存到文件,并支持数据可视化。接着,我会通过一个具体案例(采集温湿度传感器数据)进行详细扩展。代码和说明将使用中文,方便理解。


一、数据采集系统的完整代码实现

1. 系统功能

  • 串口通信:从串口设备采集数据。

  • 实时显示:在界面上实时显示采集到的数据。

  • 数据保存:将数据保存到本地文件(CSV 格式)。

  • 数据可视化:使用折线图展示数据的变化趋势。

  • 用户交互:支持开始/停止采集、设置保存路径等。

2. 开发环境

  • 工具:Qt Creator(推荐 Qt 5.15 或 Qt 6.x)

  • 依赖:Qt 框架(QSerialPort、Qt Charts 模块)

  • 项目类型:Qt Widgets Application

3. 代码实现

(1) 主窗口头文件 (mainwindow.h)

cpp

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include 
#include 
#include 
#include 
#include 
#include 

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_openButton_clicked(); // 打开/关闭串口
    void on_startButton_clicked(); // 开始/停止采集
    void on_savePathButton_clicked(); // 选择保存路径
    void readData(); // 读取串口数据
    void updateChart(); // 更新折线图

private:
    Ui::MainWindow *ui;
    QSerialPort *serial; // 串口对象
    QTimer *updateTimer; // 定时器,用于更新图表
    QFile *dataFile; // 保存数据的文件
    QtCharts::QLineSeries *series; // 折线图数据系列
    QtCharts::QChart *chart; // 图表
    QtCharts::QChartView *chartView; // 图表视图
    bool isCollecting; // 是否正在采集
    int dataCount; // 数据点计数
    void initSerialPort(); // 初始化串口参数
    void updateSerialPorts(); // 更新可用串口列表
    void initChart(); // 初始化图表
};
#endif // MAINWINDOW_H

(2) 主窗口实现文件 (mainwindow.cpp)

cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include 
#include 
#include 
#include 
#include 

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    serial = new QSerialPort(this);
    updateTimer = new QTimer(this);
    dataFile = new QFile(this);
    isCollecting = false;
    dataCount = 0;

    // 初始化串口和图表
    initSerialPort();
    updateSerialPorts();
    initChart();

    // 连接信号和槽
    connect(serial, &QSerialPort::readyRead, this, &MainWindow::readData);
    connect(updateTimer, &QTimer::timeout, this, &MainWindow::updateChart);
}

MainWindow::~MainWindow()
{
    if (serial->isOpen()) serial->close();
    if (dataFile->isOpen()) dataFile->close();
    delete serial;
    delete updateTimer;
    delete dataFile;
    delete ui;
}

void MainWindow::initSerialPort()
{
    // 设置波特率选项
    ui->baudRateComboBox->addItems({"9600", "19200", "38400", "57600", "115200"});
    ui->baudRateComboBox->setCurrentText("9600");

    // 设置数据位选项
    ui->dataBitsComboBox->addItems({"5", "6", "7", "8"});
    ui->dataBitsComboBox->setCurrentText("8");

    // 设置奇偶校验选项
    ui->parityComboBox->addItems({"None", "Even", "Odd"});
    ui->parityComboBox->setCurrentText("None");

    // 设置停止位选项
    ui->stopBitsComboBox->addItems({"1", "1.5", "2"});
    ui->stopBitsComboBox->setCurrentText("1");

    // 默认禁用采集按钮
    ui->startButton->setEnabled(false);
}

void MainWindow::updateSerialPorts()
{
    ui->portComboBox->clear();
    QList ports = QSerialPortInfo::availablePorts();
    for (const QSerialPortInfo &info : ports) {
        ui->portComboBox->addItem(info.portName());
    }
}

void MainWindow::initChart()
{
    series = new QtCharts::QLineSeries();
    chart = new QtCharts::QChart();
    chart->addSeries(series);
    chart->createDefaultAxes();
    chart->setTitle("实时数据采集");
    chartView = new QtCharts::QChartView(chart);
    chartView->setRenderHint(QPainter::Antialiasing);
    ui->chartLayout->addWidget(chartView); // chartLayout 是 UI 中的 QVBoxLayout
}

void MainWindow::on_openButton_clicked()
{
    if (serial->isOpen()) {
        // 关闭串口
        serial->close();
        ui->openButton->setText("打开串口");
        ui->startButton->setEnabled(false);
        ui->portComboBox->setEnabled(true);
        ui->baudRateComboBox->setEnabled(true);
    } else {
        // 配置串口
        serial->setPortName(ui->portComboBox->currentText());
        serial->setBaudRate(ui->baudRateComboBox->currentText().toInt());
        serial->setDataBits(static_cast(ui->dataBitsComboBox->currentText().toInt()));
        serial->setParity(ui->parityComboBox->currentText() == "None" ? QSerialPort::NoParity :
                         ui->parityComboBox->currentText() == "Even" ? QSerialPort::EvenParity :
                         QSerialPort::OddParity);
        serial->setStopBits(ui->stopBitsComboBox->currentText() == "1" ? QSerialPort::OneStop :
                           ui->stopBitsComboBox->currentText() == "1.5" ? QSerialPort::OneAndHalfStop :
                           QSerialPort::TwoStop);

        // 打开串口
        if (serial->open(QIODevice::ReadWrite)) {
            ui->openButton->setText("关闭串口");
            ui->startButton->setEnabled(true);
            ui->portComboBox->setEnabled(false);
            ui->baudRateComboBox->setEnabled(false);
        } else {
            QMessageBox::critical(this, "错误", "无法打开串口!");
        }
    }
}

void MainWindow::on_startButton_clicked()
{
    if (!isCollecting) {
        // 开始采集
        if (!dataFile->isOpen()) {
            QMessageBox::warning(this, "警告", "请先选择保存路径!");
            return;
        }
        isCollecting = true;
        ui->startButton->setText("停止采集");
        updateTimer->start(1000); // 每秒更新一次图表
        // 写入 CSV 文件头
        QTextStream out(dataFile);
        out << "时间,数据\n";
    } else {
        // 停止采集
        isCollecting = false;
        ui->startButton->setText("开始采集");
        updateTimer->stop();
        if (dataFile->isOpen()) {
            dataFile->close();
        }
    }
}

void MainWindow::on_savePathButton_clicked()
{
    QString filePath = QFileDialog::getSaveFileName(this, "选择保存路径", "", "CSV Files (*.csv)");
    if (!filePath.isEmpty()) {
        ui->savePathLineEdit->setText(filePath);
        if (dataFile->isOpen()) dataFile->close();
        dataFile->setFileName(filePath);
        if (!dataFile->open(QIODevice::WriteOnly | QIODevice::Text)) {
            QMessageBox::critical(this, "错误", "无法打开文件!");
        }
    }
}

void MainWindow::readData()
{
    if (!isCollecting) return;

    QByteArray data = serial->readAll();
    QString dataStr = QString(data).trimmed();
    bool ok;
    double value = dataStr.toDouble(&ok); // 假设数据是数值

    if (ok) {
        // 显示数据
        ui->dataTextEdit->append(QString("数据: %1").arg(value));

        // 保存到文件
        if (dataFile->isOpen()) {
            QTextStream out(dataFile);
            out << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss") << "," << value << "\n";
        }

        // 更新数据点计数
        dataCount++;
        series->append(dataCount, value);

        // 限制图表显示的数据点数
        if (series->count() > 50) {
            series->remove(0);
        }
    }
}

void MainWindow::updateChart()
{
    chart->axes(Qt::Horizontal).first()->setRange(dataCount - 50, dataCount);
    chart->axes(Qt::Vertical).first()->setRange(series->points().first().y() - 10, series->points().last().y() + 10);
}

(3) UI 设计文件 (mainwindow.ui)

在 Qt Designer 中设计 UI,包含以下控件:

  • QComboBox:portComboBox(选择串口)、baudRateComboBox(波特率)、dataBitsComboBox(数据位)、parityComboBox(奇偶校验)、stopBitsComboBox(停止位)

  • QPushButton:openButton(打开/关闭串口)、startButton(开始/停止采集)、savePathButton(选择保存路径)

  • QTextEdit:dataTextEdit(显示采集数据)

  • QLineEdit:savePathLineEdit(显示保存路径)

  • QVBoxLayout:chartLayout(用于放置图表)

布局示例:

  • 顶部:串口参数设置(端口、波特率等)

  • 中部:图表区域(chartLayout)

  • 底部:数据显示(dataTextEdit)、保存路径选择(savePathLineEdit 和 savePathButton)

(4) 项目文件 (datacollector.pro)

pro

QT += core gui serialport charts
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

4. 运行和测试

  • 步骤:

    1. 在 Qt Creator 中创建新项目,选择 "Qt Widgets Application"。

    2. 替换上述代码文件。

    3. 编译并运行。

  • 功能测试:

    • 使用虚拟串口工具(如 com0com)创建一对虚拟串口(如 COM1 和 COM2)。

    • 用串口调试工具(如 SecureCRT)连接 COM2,发送模拟数据(如 "25.5\n"),观察程序是否正确采集并显示。

    • 检查 CSV 文件是否正确保存数据。


二、案例详细扩展:采集温湿度传感器数据

1. 案例背景

  • 目标:使用数据采集系统从 Arduino 开发板上的 DHT11 温湿度传感器采集数据,实时显示温度和湿度,并保存到文件,同时绘制温度和湿度的折线图。

  • 硬件:

    • Arduino Uno 开发板

    • DHT11 温湿度传感器

    • USB 转串口线(连接 Arduino 和电脑)

  • 软件:

    • 数据采集系统(上述代码)

    • Arduino IDE(用于编写和上传 Arduino 代码)

2. Arduino 代码实现

以下是 Arduino 的代码,用于读取 DHT11 传感器数据并通过串口发送。

cpp

#include 

#define DHTPIN 2     // DHT11 传感器接在 2 号引脚
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);

void setup() {
    Serial.begin(9600); // 初始化串口,波特率 9600
    dht.begin();
}

void loop() {
    // 读取温湿度数据
    float humidity = dht.readHumidity();
    float temperature = dht.readTemperature();

    // 检查数据是否有效
    if (isnan(humidity) || isnan(temperature)) {
        Serial.println("Failed to read from DHT sensor!");
        return;
    }

    // 通过串口发送数据,格式为 "温度,湿度\n"
    Serial.print(temperature);
    Serial.print(",");
    Serial.println(humidity);

    delay(2000); // 每 2 秒发送一次数据
}

3. 硬件连接

  • DHT11 传感器:

    • VCC 接 Arduino 的 5V

    • GND 接 Arduino 的 GND

    • DATA 接 Arduino 的 2 号引脚(通过一个 4.7kΩ 上拉电阻连接到 5V)

  • 串口连接:

    • 使用 USB 线将 Arduino 连接到电脑,设备管理器中查看 COM 端口号(如 COM3)。

4. 修改数据采集系统代码

为了同时采集温度和湿度,需要修改 mainwindow.cpp 中的 readData() 函数,解析两组数据,并绘制两条折线图。

修改后的 mainwindow.h

cpp

class MainWindow : public QMainWindow
{
    // ... 其他部分不变 ...
private:
    QtCharts::QLineSeries *tempSeries; // 温度折线
    QtCharts::QLineSeries *humidSeries; // 湿度折线
    // ... 其他成员 ...
};

修改后的 mainwindow.cpp

  • 初始化图表:

cpp

void MainWindow::initChart()
{
    tempSeries = new QtCharts::QLineSeries();
    humidSeries = new QtCharts::QLineSeries();
    tempSeries->setName("温度 (°C)");
    humidSeries->setName("湿度 (%)");
    chart = new QtCharts::QChart();
    chart->addSeries(tempSeries);
    chart->addSeries(humidSeries);
    chart->createDefaultAxes();
    chart->setTitle("温湿度数据采集");
    chartView = new QtCharts::QChartView(chart);
    chartView->setRenderHint(QPainter::Antialiasing);
    ui->chartLayout->addWidget(chartView);
}
  • 读取和处理数据:

cpp

void MainWindow::readData()
{
    if (!isCollecting) return;

    QByteArray data = serial->readAll();
    QString dataStr = QString(data).trimmed();
    QStringList values = dataStr.split(",");
    if (values.size() != 2) return;

    bool ok1, ok2;
    double temperature = values[0].toDouble(&ok1);
    double humidity = values[1].toDouble(&ok2);

    if (ok1 && ok2) {
        // 显示数据
        ui->dataTextEdit->append(QString("温度: %1 °C, 湿度: %2 %").arg(temperature).arg(humidity));

        // 保存到文件
        if (dataFile->isOpen()) {
            QTextStream out(dataFile);
            out << QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss") << ","
                << temperature << "," << humidity << "\n";
        }

        // 更新数据点计数
        dataCount++;
        tempSeries->append(dataCount, temperature);
        humidSeries->append(dataCount, humidity);

        // 限制图表显示的数据点数
        if (tempSeries->count() > 50) {
            tempSeries->remove(0);
            humidSeries->remove(0);
        }
    }
}

void MainWindow::updateChart()
{
    chart->axes(Qt::Horizontal).first()->setRange(dataCount - 50, dataCount);
    chart->axes(Qt::Vertical).first()->setRange(qMin(tempSeries->points().first().y(), humidSeries->points().first().y()) - 10,
                                               qMax(tempSeries->points().last().y(), humidSeries->points().last().y()) + 10);
}
  • 修改文件头:

cpp

void MainWindow::on_startButton_clicked()
{
    if (!isCollecting) {
        if (!dataFile->isOpen()) {
            QMessageBox::warning(this, "警告", "请先选择保存路径!");
            return;
        }
        isCollecting = true;
        ui->startButton->setText("停止采集");
        updateTimer->start(1000);
        QTextStream out(dataFile);
        out << "时间,温度 (°C),湿度 (%)\n"; // 修改 CSV 文件头
    } else {
        isCollecting = false;
        ui->startButton->setText("开始采集");
        updateTimer->stop();
        if (dataFile->isOpen()) {
            dataFile->close();
        }
    }
}

5. 测试步骤

  1. 上传 Arduino 代码:

    • 在 Arduino IDE 中打开上述代码,上传到 Arduino 开发板。

  2. 运行数据采集系统:

    • 打开数据采集系统,选择正确的 COM 端口(COM3),波特率设置为 9600。

    • 点击 "打开串口"。

    • 选择保存路径(如 "temp_humidity_data.csv")。

    • 点击 "开始采集"。

  3. 观察结果:

    • 界面会显示实时温湿度数据,如:

      温度: 25.5 °C, 湿度: 60 %
      温度: 25.6 °C, 湿度: 61 %
    • 图表会绘制两条折线:温度(°C)和湿度(%)。

    • CSV 文件内容示例:

      时间,温度 (°C),湿度 (%)
      2025-06-01 13:07:00,25.5,60
      2025-06-01 13:07:02,25.6,61

6. 扩展功能

  • 报警功能:当温度或湿度超过阈值时,弹出警告。

    • 在 readData() 中添加:

      cpp

      if (temperature > 30) {
          QMessageBox::warning(this, "警告", "温度过高!");
      }
  • 数据导出:支持将图表导出为图片。

    • 添加导出按钮和槽函数:

      cpp

      void MainWindow::on_exportButton_clicked()
      {
          QString filePath = QFileDialog::getSaveFileName(this, "导出图表", "", "PNG Files (*.png)");
          if (!filePath.isEmpty()) {
              QPixmap pixmap = chartView->grab();
              pixmap.save(filePath);
          }
      }
  • 远程监控:将数据通过网络发送到远程服务器,使用 Qt 的 QNetworkAccessManager。

7. 调试和优化

  • 调试:

    • 如果无法接收数据,检查波特率和串口号是否正确。

    • 使用串口调试工具(如 PuTTY)单独测试 Arduino 的输出。

  • 优化:

    • 增加数据过滤:忽略异常值(如温度 < 0 或 > 50)。

    • 支持多传感器:扩展代码以采集更多类型的数据(如气压、光照)。


三、总结

  • 数据采集系统:实现了串口数据采集、实时显示、文件保存和数据可视化功能,支持多组数据的折线图展示。

  • 案例扩展:通过采集 DHT11 传感器的温湿度数据,展示了系统的实际应用,并提供了进一步扩展的方向。

  • 进一步学习:

    • 学习数据库存储(如 SQLite)以替代 CSV 文件。

    • 开发更复杂的系统,如支持多设备采集和云端同步。

如果需要更详细的代码解释或进一步扩展(如添加数据库支持),可以告诉我!

你可能感兴趣的:(C++,qt,信息可视化,开发语言)