以下是一个完整的数据采集系统的实现,使用 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. 运行和测试
步骤:
在 Qt Creator 中创建新项目,选择 "Qt Widgets Application"。
替换上述代码文件。
编译并运行。
功能测试:
使用虚拟串口工具(如 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. 测试步骤
上传 Arduino 代码:
在 Arduino IDE 中打开上述代码,上传到 Arduino 开发板。
运行数据采集系统:
打开数据采集系统,选择正确的 COM 端口(COM3),波特率设置为 9600。
点击 "打开串口"。
选择保存路径(如 "temp_humidity_data.csv")。
点击 "开始采集"。
观察结果:
界面会显示实时温湿度数据,如:
温度: 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 文件。
开发更复杂的系统,如支持多设备采集和云端同步。
如果需要更详细的代码解释或进一步扩展(如添加数据库支持),可以告诉我!