Q工控仪器程序框架设计详解(工控)
适用场景:测试仪器、实验室设备、工业检测系统、数据采集分析
核心目标:看完即懂、复制即用、扩展无忧
前言:为什么要这样设计?
在设计测试仪器类应用程序时,开发者通常面临以下挑战:
- 模块耦合严重:通讯、计算、存储、界面代码纠缠在一起,改一处动全身
- 难以测试:没有清晰的接口抽象,单元测试无从下手
- 扩展困难:新增一种通讯协议或分析算法,需要改动大量代码
- 状态混乱:多线程环境下的状态管理容易出错
- 配置分散:参数配置分散在代码各处,修改困难
本框架的设计目标 就是解决这些问题。通过模块化分层 、设计模式应用 、接口抽象,让代码结构清晰、职责明确、易于测试和扩展。
设计原则
本框架遵循以下设计原则:
- 单一职责原则(SRP):每个模块只做一件事,且做好
- 依赖倒置原则(DIP):高层模块不依赖低层模块,都依赖抽象
- 开闭原则(OCP):对扩展开放,对修改封闭
- 接口隔离原则(ISP):使用多个专用接口比使用一个通用接口更好
一、整体架构总览
1.1 为什么需要分层架构?
在小型项目中,把所有代码堆在一起也许能工作。但随着项目规模增长,这种方式会变成噩梦:
❌ 混乱的架构(单体式)
┌──────────────────────────────────────┐
│ MainWindow.cpp (5000行) │
│ ┌─────────────────────────────────┐ │
│ │ 通讯代码、参数代码、计算代码、 │ │
│ │ 存储代码、UI代码全部混在一起 │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────┘
✅ 清晰的架构(分层模块化)
┌──────────────────────────────────────┐
│ UI Layer(界面层) │
├──────────────────────────────────────┤
│ Business Layer(业务层) │
│ ┌────────┐ ┌─────────┐ ┌────────┐ │
│ │参数配置│ │分析计算 │ │保存解析│ │
│ └────────┘ └─────────┘ └────────┘ │
├──────────────────────────────────────┤
│ Core Layer(核心层) │
│ ┌────────┐ ┌─────────┐ ┌────────┐ │
│ │命令队列│ │协议解析 │ │数据缓冲│ │
│ └────────┘ └─────────┘ └────────┘ │
├──────────────────────────────────────┤
│ Hardware Layer(硬件层) │
│ 通讯接口(串口/网口/USB) │
└──────────────────────────────────────┘
分层的核心思想:每一层只知道自己上下相邻的层,不知道更远的层。这样修改某一层不会影响其他层。
1.2 分层设计详解
┌──────────────────────────────────────────────────────────────┐
│ UI Layer │
│ Qt Widgets / QML / 自定义控件 │
│ │
│ 职责:展示数据、接收用户输入、协调业务逻辑 │
│ 特点:被动视图,通过观察者模式订阅数据变化 │
├──────────────────────────────────────────────────────────────┤
│ Business Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
│ │ 参数配置 │ │ 分析计算 │ │ 保存解析 │ │
│ │ Module │ │ Module │ │ Module │ │
│ └─────────────┘ └──────────────┘ └─────────────────────┘ │
│ │
│ 职责:处理业务逻辑、管理业务状态、执行业务规则 │
│ 特点:可测试性强,不依赖UI层 │
├──────────────────────────────────────────────────────────────┤
│ Core Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │
│ │ 命令队列 │ │ 协议解析 │ │ 数据缓冲 │ │
│ │ Manager │ │ Parser │ │ Buffer │ │
│ └─────────────┘ └──────────────┘ └─────────────────────┘ │
│ │
│ 职责:提供基础设施服务,被业务层调用 │
│ 特点:通用性强,与具体业务无关 │
├──────────────────────────────────────────────────────────────┤
│ Hardware Layer │
│ 通讯接口 (Serial / TCP / USB / GPIB / VISA) │
│ │
│ 职责:与物理设备通信 │
│ 特点:可能需要平台特定代码 │
└──────────────────────────────────────────────────────────────┘
1.3 模块职责总表
| 模块 | 职责 | 设计模式 | 所在层 | 为什么用这个模式 |
|---|---|---|---|---|
| 通讯模块 | 设备连接、数据收发、心跳检测、断线重连 | 桥接模式、策略模式、命令模式 | Hardware | 通讯方式可能多种多样(串口/网口/USB),需要抽象出统一接口 |
| 参数配置模块 | 参数加载、保存、校验、动态更新 | 单例模式、观察者模式、Memento模式 | Business | 全局配置只需要一份,参数变化时UI自动更新 |
| 分析计算模块 | 数据处理、算法执行、多线程计算 | 责任链模式、模板方法模式 | Business | 算法可能串联执行或替换,责任链模式最合适 |
| UI模块 | 人机交互、数据可视化、实时刷新 | MVC/MVP模式、组合模式 | UI | 界面与业务逻辑分离,方便测试和换肤 |
| 保存解析模块 | 文件/数据库读写、数据导入导出 | 工厂模式、策略模式、DAO模式 | Business | 存储格式可能变化,工厂模式屏蔽细节 |
1.4 模块依赖关系图
┌─────────────┐
│ 通讯模块 │
│ (Serial/TCP)│
└──────┬──────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 参数配置 │ │ 分析计算 │ │ 保存解析 │
│ Module │ │ Module │ │ Module │
└────────────┘ └─────┬──────┘ └────────────┘
│
▼
┌─────────────┐
│ UI模块 │
│MainWindow │
└─────────────┘
依赖规则:箭头指向的方向是被依赖的模块。也就是说:
- 分析计算模块依赖参数配置模块(从参数读取计算配置)
- UI模块依赖所有业务模块(显示数据、发送控制)
二、核心模块基类设计
2.1 为什么要定义统一的模块基类?
想象一下,如果每个模块都有自己的初始化、启动、停止方式,会发生什么?
cpp
// ❌ 没有统一接口
class CommModule {
void init(); void run(); void halt(); // 接口不一致
};
class AnalysisModule {
bool setup(); void start(); void end(); // 又不一样!
};
class StorageModule {
void initialize(); void execute(); void destroy(); // 第三个版本
};
// 调用代码要记住每个模块的不同方法
comm.init(); comm.run();
analysis.setup(); analysis.start();
storage.initialize(); storage.execute();
如果我们定义一个统一的接口:
cpp
// ✅ 统一接口的好处
class ModuleInterface {
virtual bool initialize() = 0; // 初始化
virtual bool start() = 0; // 启动
virtual bool stop() = 0; // 停止
virtual void cleanup() = 0; // 清理资源
};
这样,模块管理器就可以用统一的方式管理所有模块,代码简洁得多。
2.2 设计思路:状态机模式
每个模块都有生命周期,状态机是最适合描述生命周期的方式:
┌───────────────┐
│ Uninitialized │ ← 初始状态
└───────┬───────┘
│ initialize()
▼
┌───────────────┐
│ Initialized │ ← 可以启动了
└───────┬───────┘
│ start()
▼
┌───────────────┐
┌─────│ Running │ ← 正常工作中
│ └───────┬───────┘
│ │
│ stop() │ pause()
▼ ▼
┌─────────────┐ ┌───────────────┐
│ Stopped │ │ Paused │
└─────────────┘ └───────┬───────┘
│
resume()│
▼
(回到Running)
为什么要设计状态机?
- 防止非法操作:例如在未初始化的状态下启动,会被状态机拒绝
- 便于调试:出了问题,看一眼状态就知道卡在哪里了
- 优雅关闭:stop()会等待当前操作完成,而不是强行杀死线程
2.3 完整代码实现(带详细注释)
cpp
// ============================================================
// ModuleInterface.h - 模块接口定义
// ============================================================
// 设计思路:
// 1. 所有业务模块都实现这个接口,保证接口一致性
// 2. 使用信号槽传递状态变化,方便UI层监听
// 3. 状态机管理模块生命周期,防止非法操作
// ============================================================
#pragma once
#include <QObject>
#include <QString>
// --------------------------------------------------------
// 模块状态枚举
// --------------------------------------------------------
// 设计理由:状态机模式让模块的生命周期管理变得清晰。
// 每个状态都有明确的含义,状态转换有规律可循。
// --------------------------------------------------------
enum class ModuleState {
Uninitialized, // 刚创建,还未初始化
Initialized, // 已初始化,可以启动
Starting, // 正在启动(异步启动时用到)
Running, // 运行中
Paused, // 已暂停(可恢复)
Stopping, // 正在停止
Error, // 发生错误
Stopped // 已停止(可重新初始化)
};
// --------------------------------------------------------
// 错误信息结构
// --------------------------------------------------------
// 设计理由:统一错误格式,方便日志记录和错误追踪。
// 包含:错误码、消息、所属模块、时间戳
// --------------------------------------------------------
struct ErrorInfo {
int code; // 错误码:0表示无错误
QString message; // 人类可读的错误描述
QString module; // 出错的模块名称
QDateTime time; // 发生时间
};
// --------------------------------------------------------
// 模块接口抽象基类
// --------------------------------------------------------
// 设计理由:
// 1. 抽象基类定义接口,子类实现具体逻辑
// 2. Q_OBJECT支持信号槽,用于状态变化通知
// 3. virtual destructor保证子类正确析构
// --------------------------------------------------------
class ModuleInterface {
public:
virtual ~ModuleInterface() = default;
// ----- 标识 -----
// 模块名称,用于在管理器中唯一标识
virtual QString name() const = 0;
// ----- 生命周期管理 -----
// initialize(): 分配资源、加载配置、建立连接
// start(): 开始工作(启动线程、开启定时器等)
// stop(): 优雅停止(等待任务完成、保存状态)
// pause()/resume(): 暂停/恢复(可选项)
// cleanup(): 释放所有资源
virtual bool initialize() = 0;
virtual bool start() = 0;
virtual bool stop() = 0;
virtual bool pause() = 0;
virtual bool resume() = 0;
virtual void cleanup() = 0;
// ----- 状态查询 -----
virtual ModuleState state() const = 0;
// ----- 依赖声明 -----
// 返回本模块依赖的其他模块名称
// 模块管理器会根据这个自动排序启动顺序
// 例如:分析模块依赖参数配置模块
virtual QStringList dependencies() const { return {}; }
// ----- 信号定义 -----
// 状态变化信号:通知观察者(UI)状态改变了
// 为什么要传两个状态?方便追踪状态变化历史
signals:
void stateChanged(ModuleState newState, ModuleState oldState);
// 错误信号:通知上层发生了错误
void errorOccurred(const ErrorInfo &error);
// 进度信号:长时间操作时报告进度
void progressChanged(int percent, const QString &message);
protected:
// 状态变量,供子类访问
ModuleState m_state = ModuleState::Uninitialized;
};
2.4 模块管理器实现
cpp
// ============================================================
// ModuleManager.h - 模块管理器
// ============================================================
// 设计思路:
// 1. 单例模式:整个程序只需要一个管理器
// 2. 自动依赖排序:用拓扑排序确定启动顺序
// 3. 批量操作:初始化/启动/停止一次性完成
// ============================================================
#pragma once
#include "ModuleInterface.h"
#include <QHash>
#include <QVector>
#include <QMutex>
class ModuleManager : public QObject {
Q_OBJECT
// Qt单例宏:禁用拷贝构造和赋值运算符
Q_DECLARE_SINGLETON(ModuleManager)
public:
// ----------------------------------------
// 注册模块
// ----------------------------------------
// 设计理由:在初始化前先注册所有模块
// 这样管理器知道有哪些模块需要管理
// ----------------------------------------
bool registerModule(ModuleInterface *module);
// ----------------------------------------
// 批量初始化
// ----------------------------------------
// 设计理由:自动按依赖顺序初始化
// 例如:B依赖A,则A一定在B之前初始化
// 返回值:false表示有模块初始化失败
// ----------------------------------------
bool initializeAll();
// ----------------------------------------
// 批量启动
// ----------------------------------------
// 注意:启动顺序与初始化顺序相同
// ----------------------------------------
bool startAll();
// ----------------------------------------
// 批量停止(逆序)
// ----------------------------------------
// 设计理由:逆序停止很重要
// 例如:UI依赖数据,如果先停数据再停UI会出问题
// 逆序保证依赖者先停止,提供者后停止
// ----------------------------------------
void stopAll();
// ----------------------------------------
// 获取模块
// ----------------------------------------
// 模板方法:通过名称安全获取模块
// 如果类型不匹配返回nullptr
// ----------------------------------------
template<typename T>
T* getModule(const QString &name) const {
auto it = m_modules.find(name);
if (it != m_modules.end()) {
return qobject_cast<T*>(it.value());
}
return nullptr;
}
// ----------------------------------------
// 便捷方法:获取启动顺序
// ----------------------------------------
// 用于调试:看看模块按什么顺序启动的
// ----------------------------------------
QVector<QString> startupOrder() const { return m_startupOrder; }
signals:
void moduleRegistered(const QString &name);
void allModulesInitialized();
void allModulesStarted();
void allModulesStopped();
void errorOccurred(const QString &moduleName, const ErrorInfo &error);
private:
explicit ModuleManager(QObject *parent = nullptr) : QObject(parent) {}
~ModuleManager() override { stopAll(); }
// 拓扑排序:计算模块启动顺序
// 图算法:深度优先搜索 + 入度统计
// 保证:如果A依赖B,则B在A之前
QVector<QString> topologicalSort();
QHash<QString, ModuleInterface*> m_modules;
QVector<QString> m_startupOrder;
};
cpp
// ModuleManager.cpp
#include "ModuleManager.h"
#include <QDebug>
// ----------------------------------------
// 模块注册
// ----------------------------------------
bool ModuleManager::registerModule(ModuleInterface *module)
{
if (!module) return false;
const QString name = module->name();
if (m_modules.contains(name)) {
qWarning() << "Module already registered:" << name;
return false;
}
m_modules[name] = module;
// 监听模块的错误信号,统一处理
connect(module, &ModuleInterface::errorOccurred,
this, [this, name](const ErrorInfo &e) {
emit errorOccurred(name, e);
});
qDebug() << "Module registered:" << name;
emit moduleRegistered(name);
return true;
}
// ----------------------------------------
// 拓扑排序实现
// ----------------------------------------
// 算法思路(DFS + 后序遍历):
// 1. 对每个模块进行DFS
// 2. 先处理依赖的模块(递归)
// 3. 最后把自己加入结果
// 这样得到的就是从依赖到被依赖的顺序
// ----------------------------------------
QVector<QString> ModuleManager::topologicalSort()
{
QVector<QString> result;
QSet<QString> visited; // 已完成
QSet<QString> visiting; // 正在访问(用于检测循环依赖)
std::function<bool(const QString&)> dfs = [&](const QString &name) -> bool {
// 检测循环依赖:A依赖B,B依赖A会死循环
if (visiting.contains(name)) {
qFatal("Circular dependency detected: %s", qPrintable(name));
return false;
}
// 已处理过,跳过
if (visited.contains(name)) return true;
// 标记为正在访问
visiting.insert(name);
// 递归处理所有依赖
auto module = m_modules.value(name);
if (module) {
for (const QString &dep : module->dependencies()) {
if (!dfs(dep)) return false;
}
}
// 离开节点
visiting.remove(name);
visited.insert(name);
// 关键:把自己加入结果(在依赖之后)
result.append(name);
return true;
};
// 遍历所有未处理的模块
for (const QString &name : m_modules.keys()) {
if (!visited.contains(name)) {
if (!dfs(name)) return {};
}
}
return result;
}
// ----------------------------------------
// 批量初始化
// ----------------------------------------
bool ModuleManager::initializeAll()
{
// 计算启动顺序(拓扑排序)
m_startupOrder = topologicalSort();
// 排序失败(可能有循环依赖)
if (m_startupOrder.isEmpty() && !m_modules.isEmpty()) {
qCritical() << "Topological sort failed (circular dependency?)";
return false;
}
// 按顺序初始化
for (const QString &name : m_startupOrder) {
auto module = m_modules.value(name);
if (module && module->state() == ModuleState::Uninitialized) {
qDebug() << "Initializing module:" << name;
if (!module->initialize()) {
qCritical() << "Failed to initialize module:" << name;
return false;
}
}
}
emit allModulesInitialized();
qDebug() << "All modules initialized";
return true;
}
// ----------------------------------------
// 批量启动
// ----------------------------------------
bool ModuleManager::startAll()
{
for (const QString &name : m_startupOrder) {
auto module = m_modules.value(name);
if (module && module->state() == ModuleState::Initialized) {
qDebug() << "Starting module:" << name;
if (!module->start()) {
qCritical() << "Failed to start module:" << name;
return false;
}
}
}
emit allModulesStarted();
qDebug() << "All modules started";
return true;
}
// ----------------------------------------
// 批量停止(逆序)
// ----------------------------------------
void ModuleManager::stopAll()
{
qDebug() << "Stopping all modules...";
// 逆序停止:从最后一个启动的模块开始
for (int i = m_startupOrder.size() - 1; i >= 0; --i) {
const QString &name = m_startupOrder[i];
auto module = m_modules.value(name);
if (module && module->state() == ModuleState::Running) {
qDebug() << "Stopping module:" << name;
module->stop();
}
}
emit allModulesStopped();
qDebug() << "All modules stopped";
}
三、通讯模块设计
3.1 为什么通讯模块是框架中最复杂的部分?
通讯模块面临以下挑战:
❌ 问题1:多种通讯方式
┌─────────────────────────────────────┐
│ 仪器A (串口) │ 仪器B (网口) │
│ 仪器C (USB) │ 仪器D (GPIB) │
└─────────────────────────────────────┘
如果每个仪器都写一套代码,那要疯掉
❌ 问题2:连接不稳定
┌─────────────────────────────────────┐
│ 串口松动 │ 网线拔掉 │ USB断开 │
└─────────────────────────────────────┘
需要自动重连,但不能无限重试
❌ 问题3:数据粘包
┌─────────────────────────────────────┐
│ 发送:A B C D 接收:A B C D (理想) │
│ 发送:A B C D 接收:A B │ C D (粘包)│
│ 发送:A B C D 接收:A │ B C D (半包)│
└─────────────────────────────────────┘
需要协议解析器处理
❌ 问题4:跨线程数据传递
┌─────────────────────────────────────┐
│ 通讯线程 ──数据──→ 主线程UI显示 │
└─────────────────────────────────────┘
需要线程安全机制
3.2 设计模式选择
策略模式:为不同的通讯方式(串口、TCP、USB)定义统一接口
桥接模式:将"通讯方式"的抽象与"数据收发"的实现分离
┌─────────────────┐
│ ICommInterface │ ← 抽象部分
│ (接口) │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Serial │ │ TCP │ │ USB │ ← 实现部分
│ Comm │ │ Comm │ │ Comm │
└────────┘ └────────┘ └────────┘
好处:
- 新增通讯方式只需实现接口,不影响其他代码
- UI层只依赖接口,不知道具体是哪种通讯方式
- 方便写单元测试(可以mock接口)
3.3 通讯配置设计
cpp
// ============================================================
// 通讯配置基类
// ============================================================
// 设计思路:
// 1. 所有通讯配置都继承这个基类
// 2. clone()方法用于创建配置副本
// 3. 具体配置类只包含自己需要的参数
// ============================================================
#pragma once
#include <QString>
class CommConfig {
public:
virtual ~CommConfig() = default;
// 深拷贝:创建配置的副本
// 为什么需要?通讯模块会持有配置,
// 但UI层可能随时修改配置,需要复制一份
virtual CommConfig* clone() const = 0;
};
// ============================================================
// 串口配置
// ============================================================
// 设计理由:
// 1. 串口参数很多,用结构体封装更清晰
// 2. 提供合理的默认值(115200是最常用的波特率)
// ============================================================
class SerialConfig : public CommConfig {
public:
QString portName = "COM3"; // 串口名称
qint32 baudRate = 115200; // 波特率(默认115200)
QSerialPort::DataBits dataBits = QSerialPort::Data8; // 数据位(默认8位)
QSerialPort::Parity parity = QSerialPort::NoParity; // 校验(默认无)
QSerialPort::StopBits stopBits = QSerialPort::OneStop; // 停止位(默认1位)
int timeout = 3000; // 超时时间(毫秒)
CommConfig* clone() const override {
return new SerialConfig(*this); // 拷贝构造函数
}
};
3.4 通讯接口抽象
cpp
// ============================================================
// ICommInterface.h - 通讯接口抽象基类
// ============================================================
// 设计思路:
// 1. 定义统一的通讯接口,所有通讯方式都实现这个接口
// 2. 使用信号槽处理异步数据到达
// 3. 连接状态变化通过信号通知
// ============================================================
#pragma once
#include "../Core/ModuleInterface.h"
#include <QByteArray>
class ICommInterface : public ModuleInterface {
Q_OBJECT
public:
// ----------------------------------------
// 连接状态枚举
// ----------------------------------------
// 设计理由:连接状态是通讯模块最重要的状态
// 枚举所有可能的状态,让状态机可以正确管理
// ----------------------------------------
enum class ConnectionState {
Disconnected, // 未连接
Connecting, // 正在连接
Connected, // 已连接
Reconnecting, // 正在重连
Error // 连接错误
};
Q_ENUM(ConnectionState) // 让Qt的信号槽能传递这个枚举
explicit ICommInterface(QObject *parent = nullptr)
: ModuleInterface(parent) {}
~ICommInterface() override = default;
// ----- 连接管理 -----
// connect(): 根据配置连接设备
// disconnect(): 断开连接
virtual bool connect(const CommConfig *config) = 0;
virtual void disconnect() = 0;
// ----- 数据收发 -----
// send(): 发送数据,返回是否发送成功
virtual bool send(const QByteArray &data) = 0;
// ----- 状态查询 -----
virtual ConnectionState connectionState() const = 0;
virtual CommConfig* currentConfig() const = 0;
// ----- 信号定义 -----
// 连接状态变化信号
// UI层可以监听这个信号来更新连接按钮状态
signals:
void connectionStateChanged(ConnectionState state);
// 数据到达信号
// 这是通讯模块最重要的信号
// 接收到的原始数据通过这个信号发出
// 后续的解析由上层负责
void dataReceived(const QByteArray &data);
// 数据发送成功信号(用于统计发送速率)
void bytesSent(qint64 bytes);
// 错误信号
void errorOccurred(const QString &error);
};
3.5 串口通讯实现(带详细注释)
cpp
// SerialComm.h - 串口通讯实现
#pragma once
#include "ICommInterface.h"
#include <QSerialPort>
#include <QSerialPortInfo>
class SerialConfig;
class SerialComm : public ICommInterface {
Q_OBJECT
public:
explicit SerialComm(QObject *parent = nullptr);
~SerialComm() override;
// ----- ModuleInterface 实现 -----
QString name() const override { return "SerialComm"; }
bool initialize() override;
void cleanup() override;
bool stop() override { disconnect(); return true; }
bool pause() override { return true; } // 串口不支持暂停
// ----- ICommInterface 实现 -----
bool connect(const CommConfig *config) override;
void disconnect() override;
bool send(const QByteArray &data) override;
ConnectionState connectionState() const override { return m_connectionState; }
CommConfig* currentConfig() const override { return m_config; }
// ----- 便捷方法 -----
// 扫描电脑上可用的串口
static QStringList scanAvailablePorts();
private slots:
// QSerialPort的readyRead信号处理
// 当串口缓冲区有数据时自动调用
// 注意:这个槽在串口自己的线程中执行
void onReadyRead();
// 错误处理
void onErrorOccurred(QSerialPort::SerialPortError error);
private:
// 状态更新辅助方法
void setConnectionState(ConnectionState state);
// 成员变量
QSerialPort *m_port = nullptr; // Qt串口对象
SerialConfig *m_config = nullptr; // 当前配置(持有所有权)
ConnectionState m_connectionState = ConnectionState::Disconnected;
};
cpp
// SerialComm.cpp
#include "SerialComm.h"
#include <QDebug>
// ----------------------------------------
// 构造函数
// ----------------------------------------
// 设计理由:在构造函数中创建QSerialPort对象
// QSerialPort是QObject,需要指定parent
// 这样析构时能自动清理
// ----------------------------------------
SerialComm::SerialComm(QObject *parent)
: ICommInterface(parent)
, m_port(new QSerialPort(this)) // 父对象为this,自动管理生命周期
{
// 连接Qt SerialPort的信号到我们的槽
// readyRead: 有数据可读时触发
connect(m_port, &QSerialPort::readyRead,
this, &SerialComm::onReadyRead);
// errorOccurred: 发生错误时触发
connect(m_port, &QSerialPort::errorOccurred,
this, &SerialComm::onErrorOccurred);
}
SerialComm::~SerialComm()
{
// 确保在析构时断开连接并清理
disconnect();
cleanup();
}
// ----------------------------------------
// 模块初始化
// ----------------------------------------
bool SerialComm::initialize()
{
// 串口模块的初始化很简单,只需设置状态
// 真正的连接在connect()时进行
m_state = ModuleState::Initialized;
qDebug() << "SerialComm initialized";
return true;
}
void SerialComm::cleanup()
{
disconnect();
m_state = ModuleState::Uninitialized;
}
// ----------------------------------------
// 连接设备
// ----------------------------------------
// 设计思路:
// 1. 先断开现有连接(如果存在)
// 2. 类型转换配置
// 3. 配置串口参数
// 4. 打开串口
// 5. 更新状态
// ----------------------------------------
bool SerialComm::connect(const CommConfig *config)
{
// 如果已连接,先断开
if (m_connectionState == ConnectionState::Connected) {
disconnect();
}
// 类型转换:基类指针转派生类指针
// dynamic_cast在转换失败时返回nullptr
const SerialConfig *sc = dynamic_cast<const SerialConfig*>(config);
if (!sc) {
emit errorOccurred("Invalid config type: expected SerialConfig");
return false;
}
// 保存配置副本
// 为什么复制一份?因为原配置可能在其他线程被修改
delete m_config;
m_config = new SerialConfig(*sc);
// 更新连接状态
setConnectionState(ConnectionState::Connecting);
// 配置串口参数
m_port->setPortName(m_config->portName); // 串口名
m_port->setBaudRate(m_config->baudRate); // 波特率
m_port->setDataBits(m_config->dataBits); // 数据位
m_port->setParity(m_config->parity); // 校验位
m_port->setStopBits(m_config->stopBits); // 停止位
// 打开串口(读写模式)
if (!m_port->open(QIODevice::ReadWrite)) {
setConnectionState(ConnectionState::Error);
emit errorOccurred("Failed to open port: " + m_port->errorString());
return false;
}
// 连接成功
setConnectionState(ConnectionState::Connected);
m_state = ModuleState::Running;
qDebug() << "SerialComm connected to" << m_config->portName
<< "at" << m_config->baudRate << "bps";
return true;
}
// ----------------------------------------
// 断开连接
// ----------------------------------------
void SerialComm::disconnect()
{
// 如果串口是打开的,就关闭它
if (m_port->isOpen()) {
m_port->close();
}
setConnectionState(ConnectionState::Disconnected);
m_state = ModuleState::Stopped;
}
// ----------------------------------------
// 发送数据
// ----------------------------------------
bool SerialComm::send(const QByteArray &data)
{
// 检查连接状态
if (m_connectionState != ConnectionState::Connected) {
emit errorOccurred("Cannot send: not connected");
return false;
}
// 写入数据
// 返回写入的字节数,如果与输入不匹配说明有问题
qint64 written = m_port->write(data);
if (written != data.size()) {
emit errorOccurred("Send incomplete: wrote " +
QString::number(written) + " of " +
QString::number(data.size()) + " bytes");
return false;
}
// 发送成功信号(用于统计)
emit bytesSent(written);
return true;
}
// ----------------------------------------
// 数据接收槽函数
// ----------------------------------------
// 设计理由:
// 1. Qt SerialPort会在自己的线程中触发readyRead
// 2. 但我们的信号dataReceived会在接收者的线程中处理
// 3. Qt::QueuedConnection自动处理跨线程信号传递
// ----------------------------------------
void SerialComm::onReadyRead()
{
// 读取所有可用数据
QByteArray data = m_port->readAll();
if (!data.isEmpty()) {
// 发出数据到达信号
// 接收者(通常是协议解析器)会处理这个数据
emit dataReceived(data);
}
}
// ----------------------------------------
// 错误处理槽函数
// ----------------------------------------
void SerialComm::onErrorOccurred(QSerialPort::SerialPortError error)
{
// 忽略"无错误"和"超时"(超时不算错误)
if (error == QSerialPort::NoError ||
error == QSerialPort::TimeoutError) {
return;
}
// 其他错误需要处理
setConnectionState(ConnectionState::Error);
emit this->errorOccurred(m_port->errorString());
}
// ----------------------------------------
// 状态更新辅助函数
// ----------------------------------------
void SerialComm::setConnectionState(ConnectionState state)
{
if (m_connectionState != state) {
m_connectionState = state;
emit connectionStateChanged(state);
}
}
// ----------------------------------------
// 扫描可用串口
// ----------------------------------------
// 便捷方法:列出电脑上所有可用的串口
// 用于填充UI中的串口选择下拉框
// ----------------------------------------
QStringList SerialComm::scanAvailablePorts()
{
QStringList ports;
const QList<QSerialPortInfo> infos = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : infos) {
// qDebug() << "Found port:" << info.portName()
// << "Description:" << info.description();
ports.append(info.portName());
}
return ports;
}
3.6 TCP通讯实现
cpp
// TcpComm.h - TCP通讯实现
#pragma once
#include "ICommInterface.h"
#include <QTcpSocket>
#include <QTimer>
// TCP客户端配置
class TcpClientConfig : public CommConfig {
public:
QString hostAddress = "192.168.1.100"; // 服务器地址
quint16 port = 5025; // 端口号
int timeout = 5000; // 连接超时(毫秒)
int reconnectInterval = 3000; // 重连间隔(毫秒)
bool autoReconnect = true; // 是否自动重连
CommConfig* clone() const override {
return new TcpClientConfig(*this);
}
};
class TcpComm : public ICommInterface {
Q_OBJECT
public:
explicit TcpComm(QObject *parent = nullptr);
~TcpComm() override;
QString name() const override { return "TcpComm"; }
bool initialize() override;
void cleanup() override;
bool stop() override { disconnect(); return true; }
bool pause() override {
m_heartbeatTimer->stop(); // 停止心跳
return true;
}
bool connect(const CommConfig *config) override;
void disconnect() override;
bool send(const QByteArray &data) override;
ConnectionState connectionState() const override { return m_connectionState; }
CommConfig* currentConfig() const override { return m_config; }
private slots:
void onConnected(); // 连接成功
void onDisconnected(); // 连接断开
void onReadyRead(); // 数据可读
void onError(QAbstractSocket::SocketError error); // 错误
void onReconnect(); // 重连定时器触发
void onHeartbeat(); // 定时心跳
private:
void setConnectionState(ConnectionState state);
void scheduleReconnect(); // 计划重连
QTcpSocket *m_socket = nullptr; // TCP套接字
TcpClientConfig *m_config = nullptr; // 当前配置
ConnectionState m_connectionState = ConnectionState::Disconnected;
// 定时器
QTimer *m_reconnectTimer = nullptr; // 重连定时器
Q
QTimer *m_heartbeatTimer = nullptr; // 心跳定时器
};
cpp
// TcpComm.cpp
#include "TcpComm.h"
#include <QDebug>
TcpComm::TcpComm(QObject *parent)
: ICommInterface(parent)
, m_socket(new QTcpSocket(this))
, m_reconnectTimer(new QTimer(this))
, m_heartbeatTimer(new QTimer(this))
{
// 重连定时器设为单次触发
m_reconnectTimer->setSingleShot(true);
// 连接信号槽
connect(m_socket, &QTcpSocket::connected, this, &TcpComm::onConnected);
connect(m_socket, &QTcpSocket::disconnected, this, &TcpComm::onDisconnected);
connect(m_socket, &QTcpSocket::readyRead, this, &TcpComm::onReadyRead);
// Qt5兼容的错误信号连接
// QOverload用于选择正确的重载版本
connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
this, &TcpComm::onError);
// 重连定时器
connect(m_reconnectTimer, &QTimer::timeout, this, &TcpComm::onReconnect);
// 心跳定时器(每30秒触发一次)
connect(m_heartbeatTimer, &QTimer::timeout, this, &TcpComm::onHeartbeat);
}
TcpComm::~TcpComm()
{
cleanup();
}
bool TcpComm::initialize()
{
m_state = ModuleState::Initialized;
return true;
}
void TcpComm::cleanup()
{
disconnect();
m_state = ModuleState::Uninitialized;
}
// ----------------------------------------
// 连接设备
// ----------------------------------------
bool TcpComm::connect(const CommConfig *config)
{
const TcpClientConfig *tc = dynamic_cast<const TcpClientConfig*>(config);
if (!tc) {
emit errorOccurred("Invalid config type: expected TcpClientConfig");
return false;
}
// 保存配置副本
delete m_config;
m_config = new TcpClientConfig(*tc);
setConnectionState(ConnectionState::Connecting);
// 异步连接(不会阻塞)
m_socket->connectToHost(m_config->hostAddress, m_config->port);
// 等待连接完成或超时
// 注意:这里会阻塞主线程,对于GUI程序不太好
// 更好的做法是用信号,但这里简化了
if (!m_socket->waitForConnected(m_config->timeout)) {
setConnectionState(ConnectionState::Error);
emit errorOccurred("Connection timeout: " + m_socket->errorString());
scheduleReconnect();
return false;
}
return true;
}
void TcpComm::disconnect()
{
// 停止所有定时器
m_reconnectTimer->stop();
m_heartbeatTimer->stop();
// 断开连接
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->disconnectFromHost();
}
setConnectionState(ConnectionState::Disconnected);
m_state = ModuleState::Stopped;
}
bool TcpComm::send(const QByteArray &data)
{
if (m_connectionState != ConnectionState::Connected) {
emit errorOccurred("Cannot send: not connected");
return false;
}
qint64 written = m_socket->write(data);
if (written != data.size()) {
emit errorOccurred("Send incomplete");
return false;
}
emit bytesSent(written);
return true;
}
void TcpComm::onConnected()
{
setConnectionState(ConnectionState::Connected);
m_state = ModuleState::Running;
// 启动心跳
m_heartbeatTimer->start(30000);
qDebug() << "TCP connected to" << m_config->hostAddress << ":" << m_config->port;
}
void TcpComm::onDisconnected()
{
// 停止心跳
m_heartbeatTimer->stop();
setConnectionState(ConnectionState::Disconnected);
// 自动重连
scheduleReconnect();
}
void TcpComm::onReadyRead()
{
QByteArray data = m_socket->readAll();
if (!data.isEmpty()) {
emit dataReceived(data);
}
}
void TcpComm::onError(QAbstractSocket::SocketError)
{
setConnectionState(ConnectionState::Error);
emit errorOccurred(m_socket->errorString());
// 自动重连
scheduleReconnect();
}
// ----------------------------------------
// 自动重连机制
// ----------------------------------------
// 设计思路:
// 1. 连接断开或出错时,不立即重连
// 2. 等一小段时间(m_reconnectInterval)再重连
// 3. 这样避免疯狂重连消耗资源
// 4. 可以通过配置关闭自动重连
// ----------------------------------------
void TcpComm::onReconnect()
{
if (m_config && m_config->autoReconnect) {
qDebug() << "Attempting to reconnect...";
setConnectionState(ConnectionState::Reconnecting);
connect(m_config);
}
}
void TcpComm::scheduleReconnect()
{
if (m_config && m_config->autoReconnect) {
// 启动重连定时器
// 单次定时器,时间到了触发onReconnect
m_reconnectTimer->start(m_config->reconnectInterval);
}
}
void TcpComm::setConnectionState(ConnectionState state)
{
if (m_connectionState != state) {
m_connectionState = state;
emit connectionStateChanged(state);
}
}
3.7 协议解析器设计
为什么需要协议解析器?
串口/网络收到的数据是原始字节流 ,但业务需要的是有意义的数据帧。
原始数据流:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│A│A│0│5│D│A│T│A│0│0│1│2│3│4│5│...│ ← 字节流
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
需要解析成:
┌───────────────────┐ ┌───────────────────┐
│ 帧头 | 长度 | 数据 │ │ 帧头 | 长度 | 数据 │
│ 0xAA 0x05 DAT A00 │ │ 0xAA 0x05 DAT A01 │
└───────────────────┘ └───────────────────┘
协议解析器的职责:
- 从原始数据流中识别完整的数据帧(处理粘包和半包)
- 校验数据完整性(CRC校验)
- 将数据帧转换为业务可用的格式
变长协议解析器实现
cpp
// ============================================================
// ProtocolParser.h - 协议解析器
// ============================================================
// 设计思路:
// 1. 抽象基类定义接口,具体协议继承实现
// 2. 缓冲区设计:可能一次收到半帧数据,需要缓存
// 3. 线程安全:数据可能在通讯线程收到,但解析在另一个线程
// ============================================================
#pragma once
#include <QObject>
#include <QByteArray>
#include <QMutex>
// ----------------------------------------
// 协议解析器抽象基类
// ----------------------------------------
class IProtocolParser {
public:
virtual ~IProtocolParser() = default;
// 解析数据,返回完整的帧列表
// 输入:可能是任意长度的原始数据
// 输出:解析出的完整数据帧列表
virtual QList<QByteArray> parse(const QByteArray &data) = 0;
// 封包:将业务数据编码为协议格式
virtual QByteArray package(const QByteArray &data) = 0;
// 添加原始数据到缓冲区
virtual void appendData(const QByteArray &data) = 0;
// 清空缓冲区
virtual void clear() = 0;
};
// ============================================================
// 变长协议解析器
// ============================================================
// 协议格式:
// ┌──────┬───────────┬──────────┬──────┐
// │ 帧头 │ 长度(2B) │ 数据 │ CRC │
// │ 1字节 │ 高低字节 │ N字节 │ 1字节 │
// └──────┴───────────┴──────────┴──────┘
//
// 设计理由:
// 1. 变长协议是仪器控制中最常见的格式
// 2. 帧头帮助识别帧的起始
// 3. 长度字段指明数据长度
// 4. CRC校验保证数据完整性
// ============================================================
class VariableLengthParser : public IProtocolParser {
public:
// 构造函数:指定帧头值
// 默认使用0xAA作为帧头(易于识别)
explicit VariableLengthParser(quint8 header = 0xAA)
: m_header(header)
, m_lengthOffset(1) // 长度字段在帧头的下一个字节
, m_lengthSize(2) // 长度字段占2字节
, m_crcOffset(-1) // -1表示没有CRC
, m_mutex(QMutex::Recursive) // 递归锁,允许同线程嵌套锁定
{}
// ----------------------------------------
// 解析数据帧
// ----------------------------------------
// 算法:
// 1. 查找帧头(如果不是帧头就丢弃)
// 2. 读取长度字段
// 3. 等待收完整帧(包括CRC)
// 4. 返回完整帧,清空已处理的数据
// ----------------------------------------
QList<QByteArray> parse(const QByteArray &data) override
{
QMutexLocker locker(&m_mutex);
QList<QByteArray> result;
// 第一步:将新数据追加到缓冲区
m_buffer.append(data);
// 第二步:循环处理,直到缓冲区数据不足一帧
while (m_buffer.size() >= 4) { // 最小帧 = 帧头(1) + 长度(2) + 至少1字节数据
// 查找帧头
// 如果不是帧头,说明之前的数据是垃圾,丢弃
if (static_cast<quint8>(m_buffer[0]) != m_header) {
m_buffer.remove(0, 1); // 丢弃一个字节
continue;
}
// 提取长度字段
// 支持1字节或2字节长度
int dataLength = 0;
if (m_lengthSize == 2) {
// 大端序:高字节在前
dataLength = (static_cast<quint8>(m_buffer[1]) << 8) |
static_cast<quint8>(m_buffer[2]);
} else {
dataLength = static_cast<quint8>(m_buffer[1]);
}
// 计算完整帧的长度
int frameLength = 1 + m_lengthSize + dataLength;
if (m_crcOffset >= 0) {
frameLength += 1; // 加上CRC字节
}
// 第三步:判断是否收完整
if (m_buffer.size() < frameLength) {
// 数据不完整,等待更多数据
// 这是TCP通讯中常见的情况
break;
}
// 第四步:提取完整帧
QByteArray frame = m_buffer.left(frameLength);
// 第五步:如果是CRC校验模式,验证CRC
if (m_crcOffset >= 0) {
quint8 receivedCrc = static_cast<quint8>(frame[frameLength - 1]);
quint8 calculatedCrc = calculateCrc(frame, 0, frameLength - 1);
if (receivedCrc != calculatedCrc) {
// CRC校验失败,丢弃这帧
qWarning() << "CRC mismatch:" << receivedCrc << "!="
<< calculatedCrc;
m_buffer.remove(0, frameLength);
continue;
}
}
// 验证通过,加入结果
result.append(frame);
// 第六步:从缓冲区移除已处理的数据
m_buffer.remove(0, frameLength);
}
return result;
}
// ----------------------------------------
// 封包
// ----------------------------------------
// 将业务数据编码为协议格式
// ----------------------------------------
QByteArray package(const QByteArray &data) override
{
QMutexLocker locker(&m_mutex);
QByteArray packet;
// 添加帧头
packet.append(m_header);
// 添加长度字段(大端序)
int len = data.size();
packet.append(static_cast<char>((len >> 8) & 0xFF)); // 高字节
packet.append(static_cast<char>(len & 0xFF)); // 低字节
// 添加数据
packet.append(data);
// 添加CRC(如果启用)
if (m_crcOffset >= 0) {
packet.append(calculateCrc(packet, 0, packet.size()));
}
return packet;
}
void appendData(const QByteArray &data) override {
QMutexLocker locker(&m_mutex);
m_buffer.append(data);
}
void clear() override {
QMutexLocker locker(&m_mutex);
m_buffer.clear();
}
// 配置方法
void setHeader(quint8 header) { m_header = header; }
void enableCrc(bool enable) { m_crcOffset = enable ? 0 : -1; }
private:
// CRC校验(简单累加和)
// 实际应用中可能用CRC16或CRC32
quint8 calculateCrc(const QByteArray &data, int start, int len)
{
quint8 crc = 0;
for (int i = start; i < start + len; ++i) {
crc += static_cast<quint8>(data[i]);
}
return crc;
}
quint8 m_header; // 帧头值
int m_lengthOffset; // 长度字段偏移
int m_lengthSize; // 长度字段字节数
int m_crcOffset; // CRC偏移,-1表示无CRC
QByteArray m_buffer; // 接收缓冲区
mutable QMutex m_mutex; // 互斥锁,保证线程安全
};
四、参数配置模块设计
4.1 为什么需要专门的参数配置模块?
cpp
❌ 混乱的配置管理
// A.cpp
int g_timeout = 3000;
// B.cpp
extern int g_timeout;
if (g_timeout > 5000) { ... }
// C.cpp
QSettings settings("config.ini", QSettings::IniFormat);
int timeout = settings.value("device/timeout", 3000).toInt();
问题:
- 配置分散在各处,难以统一管理
- 没有校验:可以设置任何值
- 没有变更通知:改了配置,UI不会自动刷新
- 没有历史:想回滚到之前的配置?做梦
cpp
✅ 统一的配置管理
// 一个地方定义所有参数
class ParameterManager {
// 注册参数(带范围校验)
void registerParameter("device.timeout", 3000, "通讯超时", 100, 60000);
// 统一的读写接口
QVariant get("device.timeout");
bool set("device.timeout", 5000); // 自动校验范围
// 配置变更通知
signals:
void parameterChanged(key, oldValue, newValue);
};
4.2 设计模式分析
单例模式:
- 为什么?整个程序只需要一个配置管理器
- 全局访问点:通过
Instance()获取 - 好处:不用到处传配置对象
观察者模式:
- 为什么?配置变了,所有依赖配置的组件都需要知道
- 实现:参数变化时发出信号
- 应用:
- UI层监听:配置变了,界面自动更新
- 通讯模块监听:超时时间变了,立即生效
- 存储模块监听:存储路径变了,切换存储位置
Memento模式:
- 为什么?能保存和恢复配置快照
- 应用:用户可以保存"我喜欢这个配置",随时恢复
- 实现:序列化配置到JSON
4.3 完整代码实现
cpp
// ParameterManager.h
#pragma once
#include "../Core/ModuleInterface.h"
#include <QVariant>
#include <QMutex>
#include <QJsonObject>
// ----------------------------------------
// 参数项定义
// ----------------------------------------
// 每个参数都有完整的元信息:默认值、范围、描述等
// 这些信息用于:
// 1. UI生成配置界面(范围 -> 滑块/输入框)
// 2. 校验用户输入(是否在范围内)
// 3. 生成配置文档(描述字段)
// ----------------------------------------
struct ParameterItem {
QString key; // 参数键(如"device.timeout")
QVariant defaultValue; // 默认值
QVariant currentValue; // 当前值
QString description; // 人类可读描述
QVariant minValue; // 最小值(数值参数用)
QVariant maxValue; // 最大值(数值参数用)
QStringList enumValues; // 枚举值列表(如{"auto", "manual"})
bool readonly = false; // 是否只读
};
// ----------------------------------------
// 参数管理器
// ----------------------------------------
// 职责:
// 1. 管理所有配置参数
// 2. 提供类型安全的读写接口
// 3. 校验参数值
// 4. 通知变更
// 5. 保存/加载配置
// 6. 配置快照
// ----------------------------------------
class ParameterManager : public ModuleInterface {
Q_OBJECT
Q_DECLARE_SINGLETON(ParameterManager)
public:
// ----------------------------------------
// 注册参数
// ----------------------------------------
// 必须在initialize()之前调用
// 设计理由:初始化时注册所有参数,清晰明了
// ----------------------------------------
void registerParameter(const QString &key,
const QVariant &defaultVal,
const QString &desc = "",
const QVariant &min = {},
const QVariant &max = {},
const QStringList &enums = {});
// ----------------------------------------
// 读取参数
// ----------------------------------------
// 返回参数值,如果不存在返回默认值
// ----------------------------------------
QVariant get(const QString &key,
const QVariant &def = {}) const;
// ----------------------------------------
// 设置参数
// ----------------------------------------
// 返回是否成功(失败原因:不存在/只读/校验失败)
// 成功时会发出parameterChanged信号
// ----------------------------------------
bool set(const QString &key,
const QVariant &value,
const QString &who = "System");
// ----------------------------------------
// 批量设置
// ----------------------------------------
// 原子操作:要么全成功,要么全失败
// 用于加载配置文件等场景
// ----------------------------------------
void setBatch(const QHash<QString, QVariant> &vals,
const QString &who = "System");
// ----------------------------------------
// 配置持久化
// ----------------------------------------
bool load(const QString &path); // 从文件加载
bool save(const QString &path) const; // 保存到文件
// ----------------------------------------
// 配置快照
// ----------------------------------------
// 保存当前配置为快照,可以随时恢复
// 用于:用户保存多个预设配置
// ----------------------------------------
QString saveSnapshot() const; // 返回快照ID
bool restoreSnapshot(const QString &id); // 恢复快照
// ----------------------------------------
// 重置
// ----------------------------------------
void resetToDefaults(); // 恢复到所有参数的默认值
// ----------------------------------------
// ModuleInterface 实现
// ----------------------------------------
QString name() const override { return "ParameterManager"; }
bool initialize() override;
bool start() override { return true; }
bool stop() override { return true; }
void cleanup() override;
QStringList dependencies() const override { return {"CommModule"}; }
signals:
// 参数变化信号
// 谁改的、原来的值、新值都传过去
void parameterChanged(const QString &key,
const QVariant &oldVal,
const QVariant &newVal);
private:
explicit ParameterManager(QObject *parent = nullptr)
: ModuleInterface(parent) {}
~ParameterManager() override { cleanup(); }
// 校验参数值
// 检查类型、范围、枚举是否合法
bool validate(const QString &key, const QVariant &val) const;
mutable QMutex m_mutex;
QHash<QString, ParameterItem> m_params;
QHash<QString, QJsonObject> m_snapshots; // 快照存储
};
cpp
// ParameterManager.cpp
#include "ParameterManager.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
#include <QUuid>
// ----------------------------------------
// 初始化:注册所有参数
// ----------------------------------------
bool ParameterManager::initialize()
{
// 注意:这里注册的是参数定义,不是值
// 值会在第一次读取时使用默认值
// === 通讯配置 ===
registerParameter(
"device.timeout", // 键
3000, // 默认值
"通讯超时时间(毫秒)", // 描述
100, // 最小值
60000 // 最大值
);
registerParameter(
"device.retryCount",
3,
"通讯失败重试次数",
1,
10
);
registerParameter(
"device.baudRate",
115200,
"串口波特率"
);
// === 采集配置 ===
registerParameter(
"acquisition.rate",
1000,
"采样率(Hz)",
1,
100000
);
registerParameter(
"acquisition.duration",
1.0,
"单次采集时长(秒)",
0.001,
3600
);
// 枚举参数:只能从给定值中选择
registerParameter(
"acquisition.triggerMode",
"auto",
"触发模式",
{}, // 无最小值
{}, // 无最大值
{"auto", "manual", "external"} // 枚举值
);
// === 显示配置 ===
registerParameter(
"display.refreshRate",
60,
"UI刷新率(Hz)",
10,
120
);
registerParameter(
"display.showGrid",
true,
"显示网格线"
);
registerParameter(
"display.showLegend",
true,
"显示图例"
);
// === 存储配置 ===
registerParameter(
"storage.enable",
true,
"启用数据存储"
);
registerParameter(
"storage.format",
"binary",
"存储格式",
{},
{},
{"binary", "csv", "hdf5"}
);
registerParameter(
"storage.directory",
"",
"存储目录路径"
);
m_state = ModuleState::Initialized;
qDebug() << "ParameterManager initialized with"
<< m_params.size() << "parameters";
return true;
}
void ParameterManager::cleanup()
{
QMutexLocker locker(&m_mutex);
m_params.clear();
m_snapshots.clear();
m_state = ModuleState::Uninitialized;
}
// ----------------------------------------
// 读取参数
// ----------------------------------------
QVariant ParameterManager::get(const QString &key,
const QVariant &def) const
{
QMutexLocker locker(&m_mutex);
auto it = m_params.find(key);
if (it != m_params.end()) {
return it.value().currentValue;
}
// 参数不存在,返回调用者提供的默认值
return def;
}
// ----------------------------------------
// 设置参数(核心方法)
// ----------------------------------------
bool ParameterManager::set(const QString &key,
const QVariant &value,
const QString &who)
{
QMutexLocker locker(&m_mutex);
// 查找参数
auto it = m_params.find(key);
if (it == m_params.end()) {
qWarning() << "Parameter not found:" << key;
return false;
}
ParameterItem &item = it.value();
// 检查是否只读
if (item.readonly) {
qWarning() << "Cannot modify readonly parameter:" << key;
return false;
}
// 校验值
if (!validate(key, value)) {
qWarning() << "Invalid value for parameter:" << key
<< "=" << value;
return false;
}
// 记录旧值
QVariant oldValue = item.currentValue;
// 更新值
item.currentValue = value;
// 记录历史(可选:可以限制历史长度防止内存泄漏)
// m_changeHistory.append({key, oldValue, value, ...});
locker.unlock();
// 发出变更信号
// 注意:信号在锁释放后发出,防止死锁
emit parameterChanged(key, oldValue, value);
qDebug() << "Parameter changed by" << who << ":"
<< key << "=" << oldValue << "->" << value;
return true;
}
// ----------------------------------------
// 批量设置
// ----------------------------------------
void ParameterManager::setBatch(const QHash<QString, QVariant> &vals,
const QString &who)
{
QMutexLocker locker(&m_mutex);
for (auto it = vals.begin(); it != vals.end(); ++it) {
const QString &key = it.key();
const QVariant &value = it.value();
auto pit = m_params.find(key);
if (pit == m_params.end() || pit.value().readonly) {
continue; // 跳过不存在的或只读的
}
if (!validate(key, value)) {
continue; // 跳过校验失败的
}
pit.value().currentValue = value;
}
// 注意:批量设置不发出单个信号的变更
// 调用者应该负责发出批量变更信号或逐个发出
}
// ----------------------------------------
// 参数校验
// ----------------------------------------
bool ParameterManager::validate(const QString &key,
const QVariant &value) const
{
auto it = m_params.find(key);
if (it == m_params.end()) return false;
const ParameterItem &item = it.value();
// 枚举检查:如果定义了枚举值,新值必须在枚举中
if (!item.enumValues.isEmpty()) {
if (!item.enumValues.contains(value.toString())) {
return false;
}
}
// 范围检查:数值必须在最小值和最大值之间
// 注意:QVariant的比较会自动类型转换
if (item.minValue.isValid() && value < item.minValue) {
return false;
}
if (item.maxValue.isValid() && value > item.maxValue) {
return false;
}
return true;
}
// ----------------------------------------
// 加载配置
// ----------------------------------------
bool ParameterManager::load(const QString &path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open config file:" << path;
return false;
}
QByteArray data = file.readAll();
file.close();
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(data, &error);
if (error.error != QJsonParseError::NoError) {
qWarning() << "JSON parse error:" << error.errorString();
return false;
}
QMutexLocker locker(&m_mutex);
QJsonObject json = doc.object();
if (json.contains("parameters")) {
QJsonObject params = json["parameters"].toObject();
// 逐个应用参数
for (auto it = params.begin(); it != params.end(); ++it) {
const QString &key = it.key();
QVariant value = it.value().toVariant();
// 验证并设置
auto pit = m_params.find(key);
if (pit == m_params.end() || pit.value().readonly) {
continue;
}
if (validate(key, value)) {
pit.value().currentValue = value;
}
}
}
qDebug() << "Configuration loaded from" << path;
return true;
}
// ----------------------------------------
// 保存配置
// ----------------------------------------
bool ParameterManager::save(const QString &path) const
{
QFile file(path);
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Failed to open config file for write:" << path;
return false;
}
QJsonObject json;
json["version"] = "1.0";
json["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
// 序列化所有参数
QJsonObject params;
{
QMutexLocker locker(&m_mutex);
for (auto it = m_params.begin(); it != m_params.end(); ++it) {
params[it.key()] = QJsonValue::fromVariant(it.value().currentValue);
}
}
json["parameters"] = params;
// 写入文件(格式化输出,便于人工阅读)
file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
file.close();
qDebug() << "Configuration saved to" << path;
return true;
}
// ----------------------------------------
// 配置快照
// ----------------------------------------
QString ParameterManager::saveSnapshot() const
{
QMutexLocker locker(&m_mutex);
// 生成唯一ID
QString snapshotId = QUuid::createUuid().toString();
// 创
// 创建快照对象
QJsonObject snapshot;
snapshot["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
// 序列化所有参数
QJsonObject params;
for (auto it = m_params.begin(); it != m_params.end(); ++it) {
params[it.key()] = QJsonValue::fromVariant(it.value().currentValue);
}
snapshot["parameters"] = params;
// 存储快照
m_snapshots[snapshotId] = snapshot;
qDebug() << "Snapshot saved:" << snapshotId;
return snapshotId;
}
bool ParameterManager::restoreSnapshot(const QString &id)
{
QMutexLocker locker(&m_mutex);
// 查找快照
auto it = m_snapshots.find(id);
if (it == m_snapshots.end()) {
qWarning() << "Snapshot not found:" << id;
return false;
}
// 提取快照中的参数
const QJsonObject &snapshot = it.value();
if (!snapshot.contains("parameters")) {
return false;
}
QJsonObject params = snapshot["parameters"].toObject();
// 批量应用参数
locker.unlock();
QHash<QString, QVariant> values;
for (auto it2 = params.begin(); it2 != params.end(); ++it2) {
values[it2.key()] = it2.value().toVariant();
}
setBatch(values, "SnapshotRestore");
qDebug() << "Snapshot restored:" << id;
return true;
}
void ParameterManager::resetToDefaults()
{
QMutexLocker locker(&m_mutex);
for (auto it = m_params.begin(); it != m_params.end(); ++it) {
// 只读参数不能重置
if (!it.value().readonly) {
it.value().currentValue = it.value().defaultValue;
}
}
qDebug() << "Parameters reset to defaults";
}
---
## 五、分析计算模块设计
### 5.1 为什么选择责任链模式?
分析计算模块面临的问题是:**数据处理流程可能很复杂,而且会变化**。
典型的数据处理流程:
原始数据
↓
滤波(去噪)
↓
变换(FFT)
↓
特征提取(峰值检测)
↓
结果
**需求分析**:
1. 多个处理步骤串联执行
2. 每个步骤可独立启用/禁用
3. 可以添加新的处理步骤
4. 步骤的顺序可能变化
5. 有些步骤可能需要不同参数
**设计模式选择**:
| 模式 | 适用性 | 原因 |
|------|--------|------|
| 策略模式 | 一般 | 可以切换算法,但不能串联 |
| 装饰器模式 | 一般 | 可以动态添加功能,但顺序固定 |
| **责任链模式** | ✅ 最适合 | 节点串联、可增删改顺序、可独立启用 |
### 5.2 责任链模式详解
责任链模式结构:
┌────────────┐ ┌────────────┐ ┌────────────┐
│ FilterNode │ ──→ │ FFTNode │ ──→ │ PeakNode │
└────────────┘ └────────────┘ └────────────┘
↑ ↑
└──────────────────────────────────────┘
head节点指向第一个处理器
工作原理:
-
数据从head进入
-
每个节点处理后,交给下一个节点
-
节点可以修改数据、添加元数据、或终止处理
-
处理结果从最后一个节点输出
扩展性演示:
cpp// 添加新节点只需要: class MyCustomNode : public IAnalysisNode { QString name() const override { return "MyCustom"; } AnalysisResult process(const AnalysisData &input) override { // 自己的处理逻辑 return result; } }; // 注册到链中 analysisModule->registerNode(new MyCustomNode()); // 链表自动扩展,不需要改其他代码
5.3 完整代码实现
cpp
// AnalysisModule.h
#pragma once
#include "../Core/ModuleInterface.h"
#include <QVector>
#include <QMutex>
#include <QThreadPool>
#include <QtConcurrent>
// ----------------------------------------
// 数据结构定义
// ----------------------------------------
// 分析数据:输入到分析链的原始数据
struct AnalysisData {
QVector<double> rawData; // 原始数据(输入)
QVector<double> processedData; // 处理后数据(中间/输出)
QVariantMap metadata; // 元数据(时间戳、通道信息等)
// QString source; // 来源(可选)
};
// 分析结果:处理节点的返回值
struct AnalysisResult {
bool success = false; // 是否成功
QString message; // 结果描述
QVariantMap results; // 分析结果(如峰值频率、幅度等)
QVector<double> outputData; // 处理后的数据
double elapsedMs = 0; // 处理耗时(毫秒)
};
// ----------------------------------------
// 分析节点基类(责任链)
// ----------------------------------------
// 设计理由:
// 1. 所有分析算法都继承这个基类
// 2. 每个节点可以决定:处理并传递给下家,或自己终止
// 3. 节点之间无依赖,可以自由组合
// ----------------------------------------
class IAnalysisNode {
public:
virtual ~IAnalysisNode() = default;
// 设置下一个处理节点
void setNext(IAnalysisNode *node) { m_next = node; }
IAnalysisNode* next() const { return m_next; }
// 节点名称(用于日志和调试)
virtual QString name() const = 0;
// 处理数据
// 输入:上一节点传来的数据
// 输出:处理结果
// 设计:返回的outputData会成为下一节点的rawData
virtual AnalysisResult process(const AnalysisData &input) = 0;
protected:
IAnalysisNode *m_next = nullptr; // 责任链中的下一个节点
};
// ----------------------------------------
// 分析模块
// ----------------------------------------
class AnalysisModule : public ModuleInterface {
Q_OBJECT
Q_DECLARE_SINGLETON(AnalysisModule)
public:
// 注册分析节点(组成责任链)
void registerNode(IAnalysisNode *node);
// 执行单次分析
AnalysisResult execute(const AnalysisData &data);
// 批量执行(多线程)
QVector<AnalysisResult> executeBatch(const QVector<AnalysisData> &list);
// 设置节点启用状态
void setNodeEnabled(const QString &nodeName, bool enabled);
QString name() const override { return "AnalysisModule"; }
bool initialize() override;
bool start() override { m_state = ModuleState::Running; return true; }
bool stop() override { m_state = ModuleState::Stopped; return true; }
void cleanup() override;
QStringList dependencies() const override { return {"ParameterManager"}; }
signals:
void analysisCompleted(const AnalysisResult &result);
private:
explicit AnalysisModule(QObject *parent = nullptr);
~AnalysisModule() override;
IAnalysisNode *m_head = nullptr; // 责任链头节点
QVector<IAnalysisNode*> m_allNodes; // 所有节点(用于遍历)
QThreadPool *m_threadPool = nullptr; // 线程池(用于批量处理)
};
// ============================================================
// 内置分析节点实现
// ============================================================
// ----------------------------------------
// 1. 滑动平均滤波节点
// ----------------------------------------
// 算法:取窗口内数据的平均值
// 优点:实现简单,计算速度快
// 缺点:会让波形边缘变得平滑(起始N/2个点无法完全平滑)
// ----------------------------------------
class MovingAverageNode : public IAnalysisNode {
Q_OBJECT
public:
// windowSize: 窗口大小,越大越平滑,但延迟越高
explicit MovingAverageNode(int windowSize = 5)
: m_windowSize(windowSize) {}
QString name() const override { return "MovingAverage"; }
AnalysisResult process(const AnalysisData &input) override
{
AnalysisResult r;
QElapsedTimer timer;
timer.start();
QVector<double> output;
output.reserve(input.rawData.size());
// 核心算法:滑动窗口平均
for (int i = 0; i < input.rawData.size(); ++i) {
double sum = 0;
int count = 0;
// 计算窗口内的和
// 窗口从 max(0, i-windowSize+1) 到 i
for (int j = qMax(0, i - m_windowSize + 1); j <= i; ++j) {
sum += input.rawData[j];
++count;
}
output.append(sum / count);
}
// 返回结果
r.success = true;
r.message = "Moving average filter applied";
r.outputData = output;
r.elapsedMs = timer.elapsed();
return r;
}
private:
int m_windowSize;
};
// ----------------------------------------
// 2. 中值滤波节点
// ----------------------------------------
// 算法:取窗口内数据的中值
// 优点:对脉冲噪声(异常值)非常有效
// 缺点:计算量比滑动平均大
// 适用场景:去除传感器突发噪声
// ----------------------------------------
class MedianFilterNode : public IAnalysisNode {
Q_OBJECT
public:
explicit MedianFilterNode(int windowSize = 5)
: m_windowSize(windowSize) {}
QString name() const override { return "MedianFilter"; }
AnalysisResult process(const AnalysisData &input) override
{
AnalysisResult r;
QElapsedTimer timer;
timer.start();
QVector<double> output;
output.reserve(input.rawData.size());
int halfWindow = m_windowSize / 2;
for (int i = 0; i < input.rawData.size(); ++i) {
// 收集窗口内的数据
QVector<double> window;
int start = qMax(0, i - halfWindow);
int end = qMin(input.rawData.size() - 1, i + halfWindow);
for (int j = start; j <= end; ++j) {
window.append(input.rawData[j]);
}
// 排序
std::sort(window.begin(), window.end());
// 取中值
double median = window[window.size() / 2];
output.append(median);
}
r.success = true;
r.message = "Median filter applied";
r.outputData = output;
r.elapsedMs = timer.elapsed();
return r;
}
private:
int m_windowSize;
};
// ----------------------------------------
// 3. FFT频谱分析节点
// ----------------------------------------
// 算法:快速傅里叶变换
// 作用:将时域信号转换到频域
// 输出:频谱幅度、主频、频谱能量等
// 注意:这是简化实现,实际项目建议用FFTW库
// ----------------------------------------
class FFTNode : public IAnalysisNode {
Q_OBJECT
public:
FFTNode(double sampleRate = 1000.0, int fftPoints = 1024)
: m_sampleRate(sampleRate)
, m_fftPoints(fftPoints) {}
QString name() const override { return "FFT"; }
void setSampleRate(double rate) { m_sampleRate = rate; }
void setFFTPoints(int points) { m_fftPoints = points; }
AnalysisResult process(const AnalysisData &input) override
{
AnalysisResult r;
QElapsedTimer timer;
timer.start();
// 限制FFT点数
int n = qMin(m_fftPoints, input.rawData.size());
// 预分配输出数组
QVector<double> magnitude(n / 2);
QVector<double> frequency(n / 2);
double freqResolution = m_sampleRate / n;
// 离散傅里叶变换(DFT)
// 注意:这是O(n^2)的朴素实现
// 实际应用应该用FFT算法或FFTW库
for (int i = 0; i < n / 2; ++i) {
frequency[i] = i * freqResolution;
double real = 0, imag = 0;
for (int k = 0; k < n; ++k) {
double angle = 2 * M_PI * k * i / n;
real += input.rawData[k] * std::cos(angle);
imag += input.rawData[k] * std::sin(angle);
}
// 计算幅度
magnitude[i] = std::sqrt(real * real + imag * imag) * 2.0 / n;
}
// 找主频(幅度最大的频率分量)
int peakIndex = 0;
for (int i = 1; i < magnitude.size(); ++i) {
if (magnitude[i] > magnitude[peakIndex]) {
peakIndex = i;
}
}
// 填充结果
r.success = true;
r.message = "FFT analysis completed";
r.results["peakFrequency"] = frequency[peakIndex];
r.results["peakMagnitude"] = magnitude[peakIndex];
r.results["fftPoints"] = n;
r.results["frequencyResolution"] = freqResolution;
r.outputData = magnitude;
r.elapsedMs = timer.elapsed();
return r;
}
private:
double m_sampleRate; // 采样率 (Hz)
int m_fftPoints; // FFT点数(必须是2的幂)
};
// ----------------------------------------
// 4. 峰值检测节点
// ----------------------------------------
// 算法:找局部最大值
// 用途:找信号中的峰值点(波峰或波谷)
// 参数:
// - threshold: 阈值,只有超过这个值的点才考虑
// - minDistance: 相邻峰值之间的最小距离
// ----------------------------------------
class PeakDetectionNode : public IAnalysisNode {
Q_OBJECT
public:
PeakDetectionNode(double threshold = 0.5, int minDistance = 10)
: m_threshold(threshold)
, m_minDistance(minDistance) {}
QString name() const override { return "PeakDetection"; }
void setThreshold(double t) { m_threshold = t; }
void setMinDistance(int d) { m_minDistance = d; }
AnalysisResult process(const AnalysisData &input) override
{
AnalysisResult r;
QElapsedTimer timer;
timer.start();
// 使用处理后的数据(如果没有就用原始数据)
const QVector<double> &data =
input.processedData.isEmpty() ? input.rawData : input.processedData;
QVector<double> peakValues;
QVector<int> peakIndices;
// 找峰值
for (int i = m_minDistance; i < data.size() - m_minDistance; ++i) {
// 先检查是否超过阈值
if (data[i] < m_threshold) continue;
// 检查是否是局部最大值
bool isPeak = true;
for (int j = i - m_minDistance; j <= i + m_minDistance; ++j) {
if (j != i && data[j] >= data[i]) {
isPeak = false;
break;
}
}
if (isPeak) {
peakValues.append(data[i]);
peakIndices.append(i);
}
}
r.success = true;
r.message = QString("Found %1 peaks").arg(peakValues.size());
r.results["peakCount"] = peakValues.size();
r.results["peakValues"] = QVariant::fromValue(peakValues);
r.results["peakIndices"] = QVariant::fromValue(peakIndices);
r.elapsedMs = timer.elapsed();
return r;
}
private:
double m_threshold; // 阈值
int m_minDistance; // 最小距离
};
cpp
// AnalysisModule.cpp
#include "AnalysisModule.h"
#include <QDebug>
AnalysisModule::AnalysisModule(QObject *parent)
: ModuleInterface(parent)
, m_threadPool(new QThreadPool(this))
{
// 设置线程池大小
// 建议:CPU密集型任务,线程数 = CPU核心数
m_threadPool->setMaxThreadCount(QThread::idealThreadCount());
}
AnalysisModule::~AnalysisModule()
{
cleanup();
}
// ----------------------------------------
// 初始化
// ----------------------------------------
bool AnalysisModule::initialize()
{
// 注册默认的分析节点
// 注意:注册顺序就是执行顺序
// 1. 先滤波去噪
registerNode(new MovingAverageNode(5));
// 2. 再FFT分析
registerNode(new FFTNode(1000.0, 1024));
// 3. 最后峰值检测
registerNode(new PeakDetectionNode(0.5, 10));
m_state = ModuleState::Initialized;
qDebug() << "AnalysisModule initialized with"
<< m_allNodes.size() << "nodes";
return true;
}
void AnalysisModule::cleanup()
{
// 删除所有节点
for (auto node : m_allNodes) {
delete node;
}
m_allNodes.clear();
m_head = nullptr;
m_state = ModuleState::Uninitialized;
}
// ----------------------------------------
// 注册节点
// ----------------------------------------
void AnalysisModule::registerNode(IAnalysisNode *node)
{
if (!node) return;
// 如果链表为空,作为头节点
if (!m_head) {
m_head = node;
} else {
// 否则找到链表末尾,加入
IAnalysisNode *current = m_head;
while (current->next()) {
current = current->next();
}
current->setNext(node);
}
m_allNodes.append(node);
qDebug() << "Analysis node registered:" << node->name();
}
// ----------------------------------------
// 执行分析
// ----------------------------------------
AnalysisResult AnalysisModule::execute(const AnalysisData &data)
{
// 如果没有节点,返回错误
if (!m_head) {
return {false, "No analysis nodes registered", {}, {}, 0};
}
// 沿着责任链处理
AnalysisData currentData = data;
AnalysisResult finalResult;
IAnalysisNode *current = m_head;
while (current) {
// 调用当前节点的处理方法
AnalysisResult result = current->process(currentData);
// 如果节点返回失败,停止处理
if (!result.success) {
return result;
}
// 将输出数据作为下一个节点的输入
currentData.processedData = result.outputData;
// 记录最后一个成功的结果
finalResult = result;
// 移动到下一个节点
current = current->next();
}
// 处理完成,发出信号
emit analysisCompleted(finalResult);
return finalResult;
}
// ----------------------------------------
// 批量执行
// ----------------------------------------
QVector<AnalysisResult> AnalysisModule::executeBatch(
const QVector<AnalysisData> &dataList)
{
QVector<AnalysisResult> results;
results.reserve(dataList.size());
// 使用QtConcurrent进行并行处理
// blockingMap会阻塞直到所有任务完成
QtConcurrent::blockingMap(dataList, [this, &results](const AnalysisData &data) {
results.append(execute(data));
});
return results;
}
六、UI模块设计
6.1 为什么选择MVP模式?
MVP vs MVC 对比:
MVC(Model-View-Controller)
┌────────┐ ┌────────┐ ┌────────┐
│ Model │ ←→ │ View │ ←→ │Controller│
└────────┘ └────────┘ └────────┘
↑ ↑ ↓
└──────────────┴──────────────┘
View直接观察Model
MVP(Model-View-Presenter)
┌────────┐ ┌────────┐ ┌────────┐
│ Model │ ←→ │Presenter│←→ │ View │
└────────┘ └────────┘ └────────┘
↓
View不直接
访问Model
MVP的优势:
- View和Model完全解耦
- View只需要展示数据,不需要知道数据从哪来
- Presenter包含所有业务逻辑,方便测试
- View可以被mock,便于单元测试
为什么对测试仪器UI重要?
- 仪器UI通常很复杂,逻辑多
- 需要频繁与仪器硬件交互(难以测试)
- MVP让我们可以mock掉通讯模块,单独测试UI逻辑
6.2 完整代码实现
cpp
// MainWindow.h
#pragma once
#include <QMainWindow>
#include <QTabWidget>
#include <QTableWidget>
#include <QPushButton>
#include <QComboBox>
#include <QSpinBox>
#include <QLabel>
#include <QDial>
#include <QLCDNumber>
#include <QCustomPlot>
class ParameterManager;
class AnalysisModule;
class ICommInterface;
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
void setupUi();
// ----------------------------------------
// 公共方法(供外部调用)
// ----------------------------------------
// 这些是Presenter暴露给其他模块的接口
private slots:
// === 通讯相关 ===
void onConnectClicked();
void onDisconnectClicked();
// === 参数相关 ===
// 参数变化时自动调用,更新UI显示
void onParameterChanged(const QString &key,
const QVariant &oldVal,
const QVariant &newVal);
// === 数据相关 ===
// 收到新数据时调用,更新显示
void onDataReceived(const QByteArray &data);
// 分析完成时调用
void onAnalysisCompleted(const AnalysisResult &result);
// === 文件操作 ===
void onSaveData();
void onLoadData();
// === 状态变化 ===
void onModuleStateChanged(const QString &module, ModuleState state);
signals:
// 发出请求让其他模块处理
void saveDataRequested(const QString &path);
void loadDataRequested(const QString &path);
private:
// ----------------------------------------
// UI创建方法
// ----------------------------------------
void createMenuBar();
void createToolBar();
void createStatusBar();
// 创建各个Tab页面
QWidget* createMonitorTab(); // 监视面板
QWidget* createConfigTab(); // 参数配置面板
QWidget* createDataTab(); // 数据表格面板
// ----------------------------------------
// 数据更新方法
// ----------------------------------------
// 这些是View的实现细节
void updateWaveform(const QVector<double> &data);
void updateGauge(double value);
void updateDataTable(const DataRecord &record);
// ----------------------------------------
// 成员变量
// ----------------------------------------
// 标签页
QTabWidget *m_tabWidget = nullptr;
// 监视面板组件
QCustomPlot *m_waveformPlot = nullptr; // 波形图
QDial *m_gaugeDial = nullptr; // 仪表盘
QLCDNumber *m_lcdNumber = nullptr; // 数值显示
QLabel *m_valueLabel = nullptr; // 当前值标签
// 连接控制组件
QComboBox *m_portCombo = nullptr; // 串口选择
QSpinBox *m_baudRateSpin = nullptr; // 波特率设置
QPushButton *m_connectBtn = nullptr; // 连接按钮
// 数据表格
QTableWidget *m_dataTable = nullptr; // 数据表格
int m_rowCount = 0; // 行计数器
// 状态栏组件
QLabel *m_connectionLabel = nullptr; // 连接状态标签
QLabel *m_dataRateLabel = nullptr; // 数据率标签
// 数据缓存(用于波形显示)
QVector<double> m_waveformData;
};
cpp
// MainWindow.cpp
#include "MainWindow.h"
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QAction>
#include <QFileDialog>
#include <QSerialPortInfo>
#include <QGridLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QCheckBox>
#include <QDoubleSpinBox>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
// 设置窗口属性
setWindowTitle("测试仪器框架 - Qt Demo");
resize(1280, 800);
// 初始化UI
setupUi();
}
MainWindow::~MainWindow() {}
void MainWindow::setupUi()
{
// 创建各个UI部分
createMenuBar();
createToolBar();
createStatusBar();
// 创建主TabWidget
m_tabWidget = new QTabWidget(this);
m_tabWidget->addTab(createMonitorTab(), "监视面板");
m_tabWidget->addTab(createConfigTab(), "参数配置");
m_tabWidget->addTab(createDataTab(), "数据表格");
setCentralWidget(m_tabWidget);
// 连接信号槽
// 从参数管理器监听参数变化
auto *pm = ModuleManager::instance()->getModule<ParameterManager>("ParameterManager");
if (pm) {
connect(pm, &ParameterManager::parameterChanged,
this, &MainWindow::onParameterChanged);
}
}
// ----------------------------------------
// 创建菜单栏
// ----------------------------------------
void MainWindow::createMenuBar()
{
QMenuBar *menuBar = this->menuBar();
// 文件菜单
QMenu *fileMenu = menuBar->addMenu("文件(&F)");
fileMenu->addAction("新建", this, [](){}, QKeySequence::New);
fileMenu->addAction("打开...", this, &MainWindow::onLoadData,
QKeySequence::Open);
fileMenu->addAction("保存", this, &MainWindow::onSaveData,
QKeySequence::Save);
fileMenu->addSeparator();
fileMenu->addAction("退出", this &QWidget::close,
QKeySequence::Quit);
// 视图菜单
QMenu *viewMenu = menuBar->addMenu("视图(&V)");
viewMenu->addAction("监视面板", this,
[this](){ m_tabWidget->setCurrentIndex(0); });
viewMenu->addAction("参数配置", this,
[this](){ m_tabWidget->setCurrentIndex(1); });
viewMenu->addAction("数据表格", this,
[this](){ m_tabWidget->setCurrentIndex(2); });
}
// ----------------------------------------
// 创建工具栏
// ----------------------------------------
void MainWindow::createToolBar()
{
QToolBar *toolBar = addToolBar("主工具栏");
// 连接按钮(可切换)
QAction *connectAct = new QAction("连接", this);
connectAct->setCheckable(true);
connect(connectAct, &QAction::toggled, this, [this](bool checked) {
if (checked) {
onConnectClicked();
} else {
onDisconnectClicked();
}
});
toolBar->addAction(connectAct);
toolBar->addSeparator();
toolBar->addAction(new QAction("开始采集", this));
toolBar->addAction(new QAction("停止采集", this));
}
// ----------------------------------------
// 创建状态栏
// ----------------------------------------
void MainWindow::createStatusBar()
{
QStatusBar *statusBar = this->statusBar();
// 连接状态
m_connectionLabel = new QLabel("未连接");
m_connectionLabel->setStyleSheet(
"QLabel { color: red; font-weight: bold; }"
);
// 数据率
m_dataRateLabel = new QLabel("数据率: 0 kS/s");
statusBar->addWidget(m_connectionLabel);
statusBar->addPermanentWidget(m_dataRateLabel);
}
// ----------------------------------------
// 创建监视面板
// ----------------------------------------
QWidget* MainWindow::createMonitorTab()
{
QWidget *tab = new QWidget(this);
QHBoxLayout *layout = new QHBoxLayout(tab);
// === 左侧:波形显示 ===
QWidget *waveWidget = new QWidget(this);
QVBoxLayout *waveLayout = new QVBoxLayout(waveWidget);
waveLayout->addWidget(new QLabel("实时波形", this));
// 创建波形图
m_waveformPlot = new QCustomPlot(this);
m_waveformPlot->addGraph(); // 添加一条曲线
m_waveformPlot->graph(0)->setPen(QPen(Qt::blue)); // 蓝色曲线
m_waveformPlot->xAxis->setLabel("样本点");
m_waveformPlot->yAxis->setLabel("幅度");
m_waveformPlot->setMinimumSize(700, 350);
// 启用拖拽和缩放
m_waveformPlot->setInteraction(QCP::iRangeZoom, true);
m_waveformPlot->setInteraction(QCP::iRangeDrag, true);
waveLayout->addWidget(m_waveformPlot);
// === 右侧:仪表盘 ===
QWidget *gaugeWidget = new QWidget(this);
QVBoxLayout *gaugeLayout = new QVBoxLayout(gaugeWidget);
gaugeLayout->addStretch(); // 顶部留空
// 圆形仪表
m_gaugeDial = new QDial(this);
m_gaugeDial->setMinimum(0);
m_gaugeDial->setMaximum(100);
m_gaugeDial->setValue(50);
m_gaugeDial->setMinimumSize(150, 150);
gaugeLayout->addWidget(m_gaugeDial, 0, Qt::AlignHCenter);
// 数值显示
m_lcdNumber = new QLCDNumber(this);
m_lcdNumber->setDigitCount(8);
m_lcdNumber->display("0000.000");
gaugeLayout->addWidget(m_lcdNumber);
// 当前值标签
m_valueLabel = new QLabel("当前值: 0.00");
m_valueLabel->setAlignment(Qt::AlignCenter);
gaugeLayout->addWidget(m_valueLabel);
gaugeLayout->addStretch(); // 底部留空
// 添加到主布局
layout->addWidget(waveWidget, 2); // 波形占2/3宽度
layout->addWidget(gaugeWidget, 1); // 仪表占1/3宽度
return tab;
}
// ----------------------------------------
// 创建参数配置面板
// ----------------------------------------
QWidget* MainWindow::createConfigTab()
{
QWidget *tab = new QWidget(this);
QGridLayout *grid = new QGridLayout(tab);
int row = 0;
// === 通讯配置 ===
QGroupBox *commGroup = new QGroupBox("通讯配置", tab);
QGridLayout *commLayout = new QGridLayout(commGroup);
commLayout->addWidget(new QLabel("串口:", tab), 0, 0);
m_portCombo = new QComboBox(tab);
// 扫描可用串口
for (const QSerialPortInfo &info : QSerialPortInfo::availablePorts()) {
m_portCombo->addItem(info.portName());
}
commLayout->addWidget(m_portCombo, 0, 1);
commLayout->addWidget(new QLabel("波特率:", tab), 1, 0);
m_baudRateSpin = new QSpinBox(tab);
m_baudRateSpin->setRange(9600, 921600);
m_baudRateSpin->setSingleStep(9600);
m_baudRateSpin->setValue(115200);
commLayout->addWidget(m_baudRateSpin, 1, 1);
m_connectBtn = new QPushButton("连接", tab);
connect(m_connectBtn, &QPushButton::clicked,
this, &MainWindow::onConnectClicked);
commLayout->addWidget(m_connectBtn, 2, 0, 1, 2);
grid->addWidget(commGroup, row++, 0, 1, 2);
// === 采集配置 ===
QGroupBox *acqGroup = new QGroupBox("采集配置", tab);
QGridLayout *acqLayout = new QGridLayout(acqGroup);
acqLayout->addWidget(new QLabel("采样率(Hz):", tab), 0, 0);
QSpinBox *rateSpin = new QSpinBox(tab);
rateSpin->setObjectName("acquisitionRate");
rateSpin->setRange(1, 100000);
rateSpin->setValue(1000);
acqLayout->addWidget(rateSpin, 0, 1);
acqLayout->addWidget(new QLabel("采集时长(s):", tab), 1, 0);
QDoubleSpinBox *durSpin = new QDoubleSpinBox(tab);
durSpin->setObjectName("acquisitionDuration");
durSpin->setRange(0.001, 3600);
durSpin->setValue(1.0);
acqLayout->addWidget(durSpin, 1, 1);
acqLayout->addWidget(new QLabel("触发模式:", tab), 2, 0);
QComboBox *trigCombo = new QComboBox(tab);
trigCombo->setObjectName("triggerMode");
trigCombo->addItems({"auto", "manual", "external"});
acqLayout->addWidget(trigCombo, 2, 1);
grid->addWidget(acqGroup, row++, 0, 1, 2);
// === 显示配置 ===
QGroupBox *dispGroup = new QGroupBox("显示配置", tab);
QGridLayout *dispLayout = new QGridLayout(dispGroup);
dispLayout->addWidget(new QLabel("刷新率(Hz):", tab), 0, 0);
QSpinBox *refSpin = new QSpinBox(tab);
refSpin->setObjectName("displayRefreshRate");
refSpin->setRange(10, 120);
refSpin->setValue(60);
dispLayout->addWidget(refSpin, 0, 1);
QCheckBox *gridCheck = new QCheckBox("显示网格", tab);
gridCheck->setObjectName("displayShowGrid");
gridCheck->setChecked(true);
dispLayout->addWidget(gridCheck, 1, 0, 1, 2);
QCheckBox *legendCheck = new QCheckBox("显示图例", tab);
legendCheck->setObjectName("displayShowLegend");
legendCheck->setChecked(true);
dispLayout->addWidget(legendCheck, 2, 0, 1, 2);
grid->addWidget(dispGroup, row++, 0, 1, 2);
// === 参数绑定 ===
// 将UI控件与参数管理器绑定
// 当控件值变化时,自动更新参数
// 当参数变化时,自动更新控件
auto *pm = ModuleManager::instance()->getModule<ParameterManager>("ParameterManager");
if (pm) {
// 查找所有有objectName的控件并绑定
QList<QWidget*> widgets = tab->findChildren<QWidget*>();
for (QWidget *w : widgets) {
QString objName = w->objectName();
if (objName.isEmpty()) continue;
// SpinBox绑定
if (auto *s = qobject_cast<QSpinBox*>(w)) {
s->setValue(pm->get(objName).toInt());
connect(s, &QSpinBox::valueChanged, this,
[pm, objName](int v) { pm->set(objName, v); });
}
// DoubleSpinBox绑定
else if (auto *ds = qobject_cast<QDoubleSpinBox*>(w)) {
ds->setValue(pm->get(objName).toDouble());
connect(ds, &QDoubleSpinBox::valueChanged, this,
[pm, objName](double v) { pm->set(objName, v); });
}
// ComboBox绑定
else if (auto *c = qobject_cast<QComboBox*>(w)) {
connect(c, &QComboBox::currentTextChanged, this,
[pm, objName](const QString &v) { pm->set(objName, v); });
}
// CheckBox绑定
else if (auto *cb = qobject_cast<QCheckBox*>(w)) {
cb->setChecked(pm->get(objName).toBool());
connect(cb, &QCheckBox::toggled, this,
[pm, objName](bool v) { pm->set(objName, v); });
}
}
}
return tab;
}
// ----------------------------------------
// 创建数据表格面板
// ----------------------------------------
QWidget* MainWindow::createDataTab()
{
QWidget *tab = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout(tab);
// 工具栏
QHBoxLayout *toolbar = new QHBoxLayout();
QPushButton *clearBtn = new QPushButton("清空", tab);
QPushButton *exportCsvBtn = new QPushButton("导出CSV", tab);
toolbar->addWidget(clearBtn);
toolbar->
toolbar->addWidget(exportCsvBtn);
toolbar->addStretch();
layout->addLayout(toolbar);
// 数据表格
m_dataTable = new QTableWidget(tab);
m_dataTable->setColumnCount(5);
m_dataTable->setHorizontalHeaderLabels(
{"序号", "时间戳", "通道1", "通道2", "状态"}
);
m_dataTable->setAlternatingRowColors(true);
m_dataTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_dataTable->setEditTriggers(QAbstractItemView::NoEditTriggers); // 只读
m_dataTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
layout->addWidget(m_dataTable);
// 清空按钮
connect(clearBtn, &QPushButton::clicked, this, [this]() {
m_dataTable->setRowCount(0);
m_rowCount = 0;
});
return tab;
}
// ----------------------------------------
// 连接处理
// ----------------------------------------
void MainWindow::onConnectClicked()
{
auto *comm = ModuleManager::instance()->getModule<ICommInterface>("SerialComm");
if (!comm) return;
// 获取UI配置
SerialConfig cfg;
cfg.portName = m_portCombo->currentText();
cfg.baudRate = m_baudRateSpin->value();
// 连接
if (comm->connect(&cfg)) {
// 更新UI
m_connectionLabel->setText("已连接: " + cfg.portName);
m_connectionLabel->setStyleSheet(
"QLabel { color: green; font-weight: bold; }"
);
m_connectBtn->setText("断开");
}
}
void MainWindow::onDisconnectClicked()
{
auto *comm = ModuleManager::instance()->getModule<ICommInterface>("SerialComm");
if (comm) comm->disconnect();
m_connectionLabel->setText("未连接");
m_connectionLabel->setStyleSheet(
"QLabel { color: red; }"
);
m_connectBtn->setText("连接");
}
// ----------------------------------------
// 参数变化处理
// ----------------------------------------
void MainWindow::onParameterChanged(const QString &key,
const QVariant &,
const QVariant &)
{
// 当参数变化时,更新对应的UI控件
// 这里可以扩展:根据参数名更新特定控件
Q_UNUSED(key)
// 实际实现中可以:
// if (key == "device.baudRate") { ... }
}
// ----------------------------------------
// 数据处理
// ----------------------------------------
void MainWindow::onDataReceived(const QByteArray &data)
{
// 模拟数据处理
// 实际项目中,这里应该解析原始数据
static int index = 0;
index++;
// 简单模拟:生成一个正弦波
double value = static_cast<double>(data.size()) * 0.1 +
std::sin(index * 0.1) * 50;
// 更新波形数据
m_waveformData.append(value);
if (m_waveformData.size() > 1000) {
// 保持最新1000个点
m_waveformData.removeFirst();
}
updateWaveform(m_waveformData);
// 更新仪表
int percent = qBound(0, static_cast<int>((value + 100) / 2), 100);
m_gaugeDial->setValue(percent);
m_lcdNumber->display(QString::number(value, 'f', 3));
m_valueLabel->setText(QString("当前值: %1").arg(value, 8, 'f', 2));
// 添加到数据表格
int row = m_dataTable->rowCount();
m_dataTable->insertRow(row);
m_dataTable->setItem(row, 0, new QTableWidgetItem(QString::number(++m_rowCount)));
m_dataTable->setItem(row, 1, new QTableWidgetItem(
QTime::currentTime().toString("hh:mm:ss.zzz")));
m_dataTable->setItem(row, 2, new QTableWidgetItem(
QString::number(value, 'f', 4)));
m_dataTable->setItem(row, 3, new QTableWidgetItem(
QString::number(value * 1.1, 'f', 4)));
m_dataTable->setItem(row, 4, new QTableWidgetItem("OK"));
// 自动滚动到最后一行
m_dataTable->scrollToBottom();
}
void MainWindow::onAnalysisCompleted(const AnalysisResult &result)
{
if (result.success) {
// 可以在这里更新分析结果显示
qDebug() << "Analysis done in" << result.elapsedMs << "ms"
<< ":" << result.message;
}
}
// ----------------------------------------
// 数据更新方法
// ----------------------------------------
void MainWindow::updateWaveform(const QVector<double> &data)
{
// 生成X轴数据
QVector<double> x(data.size());
for (int i = 0; i < data.size(); ++i) {
x[i] = i;
}
// 更新曲线数据
m_waveformPlot->graph(0)->setData(x, data);
// 设置坐标轴范围
m_waveformPlot->xAxis->setRange(0, data.size());
m_waveformPlot->yAxis->setRange(-120, 120);
// 重绘
m_waveformPlot->replot();
}
void MainWindow::updateGauge(double value)
{
int percent = qBound(0, static_cast<int>((value + 100) / 2), 100);
m_gaugeDial->setValue(percent);
m_lcdNumber->display(QString::number(value, 'f', 3));
}
void MainWindow::updateDataTable(const DataRecord &record)
{
int row = m_dataTable->rowCount();
m_dataTable->insertRow(row);
m_dataTable->setItem(row, 0, new QTableWidgetItem(
QString::number(++m_rowCount)));
m_dataTable->setItem(row, 1, new QTableWidgetItem(
QString::number(record.timestamp)));
// ... 其他字段
}
// ----------------------------------------
// 文件操作
// ----------------------------------------
void MainWindow::onSaveData()
{
QString path = QFileDialog::getSaveFileName(
this, "保存数据", "",
"二进制 (*.bin);;CSV (*.csv)"
);
if (!path.isEmpty()) {
emit saveDataRequested(path);
}
}
void MainWindow::onLoadData()
{
QString path = QFileDialog::getOpenFileName(
this, "加载数据", "",
"所有文件 (*.*)"
);
if (!path.isEmpty()) {
emit loadDataRequested(path);
}
}
七、保存解析模块设计
7.1 存储方案选择
存储需求分析:
┌─────────────────────────────────────────────────────┐
│ 数据特点 │
├─────────────────────────────────────────────────────┤
│ • 采样率高(可达MHz) │
│ • 数据量大(长时间采集可达GB级) │
│ • 需要随机访问(回放特定时间段) │
│ • 可能需要与其他软件共享数据 │
└─────────────────────────────────────────────────────┘
存储格式对比:
┌──────────┬──────────┬──────────┬──────────────────┐
│ 格式 │ 优点 │ 缺点 │ 适用场景 │
├──────────┼──────────┼──────────┼──────────────────┤
│ 二进制 │ 速度快 │ 不通用 │ 高速采集、内部 │
│ │ 体积小 │ 无法直接 │ │
├──────────┼──────────┼──────────┼──────────────────┤
│ CSV │ 通用 │ 速度慢 │ 数据交换、调试 │
│ │ 可用Excel│ 体积大 │ │
├──────────┼──────────┼──────────┼──────────────────┤
│ HDF5 │ 高速 │ 依赖库 │ 科学计算、大数据 │
│ │ 可压缩 │ 学习成本 │ │
│ │ 自描述 │ │ │
├──────────┼──────────┼──────────┼──────────────────┤
│ SQLite │ 通用 │ 不适合 │ 配置、元数据 │
│ │ 查询方便 │ 高速写入 │ │
└──────────┴──────────┴──────────┴──────────────────┘
设计决策:
1. 提供多种格式支持(策略模式)
2. 默认使用二进制格式(速度优先)
3. 用户可切换格式
4. 使用工厂模式创建处理器
7.2 完整代码实现
cpp
// StorageModule.h
#pragma once
#include "../Core/ModuleInterface.h"
#include <QString>
#include <QVariantMap>
#include <QJsonObject>
// ----------------------------------------
// 数据结构
// ----------------------------------------
// 单条数据记录
struct DataRecord {
qint64 timestamp; // 毫秒时间戳
QVector<double> channels; // 各通道数据
QVariantMap metadata; // 附加信息(如状态码、标志位)
int status = 0; // 状态码
};
// 一段时间的数据块
struct DataBlock {
qint64 startTime; // 起始时间
qint64 endTime; // 结束时间
QVector<DataRecord> records; // 记录列表
QVariantMap metadata; // 块的元数据
};
// 存储格式枚举
enum class StorageFormat {
Binary, // 自定义二进制格式(高速)
Csv, // CSV格式(通用)
Json, // JSON格式(可读性好)
HDF5 // HDF5格式(需要HDF5库)
};
// ----------------------------------------
// 存储处理器接口(策略模式)
// ----------------------------------------
class IStorageHandler {
public:
virtual ~IStorageHandler() = default;
// 打开文件
// append=true表示追加模式,false表示覆盖
virtual bool open(const QString &path, bool append = false) = 0;
// 关闭文件
virtual void close() = 0;
// 写入数据块
virtual bool write(const DataBlock &block) = 0;
// 读取数据块
virtual bool read(DataBlock &block) = 0;
// 检查是否打开
virtual bool isOpen() const = 0;
};
// ----------------------------------------
// 二进制存储处理器
// ----------------------------------------
class BinaryStorageHandler : public IStorageHandler {
public:
bool open(const QString &path, bool append) override;
void close() override;
bool write(const DataBlock &block) override;
bool read(DataBlock &block) override;
bool isOpen() const override { return m_file && m_file->isOpen(); }
private:
QFile *m_file = nullptr;
QDataStream *m_stream = nullptr;
};
// ----------------------------------------
// CSV存储处理器
// ----------------------------------------
class CsvStorageHandler : public IStorageHandler {
public:
bool open(const QString &path, bool append) override;
void close() override;
bool write(const DataBlock &block) override;
bool read(DataBlock &block) override;
bool isOpen() const override { return m_file && m_file->isOpen(); }
private:
QFile *m_file = nullptr;
QTextStream *m_stream = nullptr;
};
// ----------------------------------------
// 存储模块
// ----------------------------------------
class StorageModule : public ModuleInterface {
Q_OBJECT
Q_DECLARE_SINGLETON(StorageModule)
public:
// 设置存储格式
void setStorageFormat(StorageFormat format);
// 获取当前格式
StorageFormat currentFormat() const { return m_currentFormat; }
// 写入数据块
bool writeBlock(const DataBlock &block);
// 追加单条记录(带缓冲)
bool appendRecord(const DataRecord &record);
// 冲刷缓冲区(强制写入)
void flush();
// 获取文件信息
QVariantMap getFileInfo(const QString &path) const;
QString name() const override { return "StorageModule"; }
bool initialize() override;
bool start() override {
m_state = ModuleState::Running;
return true;
}
bool stop() override {
// 关闭时自动冲刷缓冲区
if (m_handler && m_handler->isOpen()) {
flush();
m_handler->close();
}
m_state = ModuleState::Stopped;
return true;
}
void cleanup() override;
QStringList dependencies() const override { return {"ParameterManager"}; }
signals:
void storageOpened(const QString &path);
void storageClosed(const QString &path);
void recordWritten(int count);
private:
explicit StorageModule(QObject *parent = nullptr);
~StorageModule() override;
// 工厂方法:根据格式创建处理器
IStorageHandler* createHandler(StorageFormat format);
QString m_currentPath; // 当前文件路径
IStorageHandler *m_handler = nullptr; // 当前处理器
StorageFormat m_currentFormat = StorageFormat::Binary;
// 写入缓冲
DataBlock m_buffer; // 缓冲数据块
int m_bufferSize = 100; // 缓冲记录数
};
cpp
// StorageModule.cpp
#include "StorageModule.h"
#include <QFile>
#include <QDataStream>
#include <QTextStream>
#include <QDebug>
// ============================================================
// BinaryStorageHandler 实现
// ============================================================
bool BinaryStorageHandler::open(const QString &path, bool append)
{
close();
m_file = new QFile(path);
auto mode = append ? QIODevice::Append : (QIODevice::WriteOnly | QIODevice::Truncate);
if (!m_file->open(mode)) {
qWarning() << "Failed to open file:" << path;
delete m_file;
m_file = nullptr;
return false;
}
m_stream = new QDataStream(m_file);
// 设置字节序和数据流版本(保证跨平台兼容性)
m_stream->setByteOrder(QDataStream::LittleEndian);
m_stream->setVersion(QDataStream::Qt_5_15);
return true;
}
void BinaryStorageHandler::close()
{
if (m_stream) {
delete m_stream;
m_stream = nullptr;
}
if (m_file) {
m_file->close();
delete m_file;
m_file = nullptr;
}
}
bool BinaryStorageHandler::write(const DataBlock &block)
{
if (!m_stream) return false;
// 写入块头
*m_stream << block.startTime
<< block.endTime
<< block.metadata;
// 写入记录数
*m_stream << static_cast<quint32>(block.records.size());
// 逐条写入记录
for (const DataRecord &r : block.records) {
*m_stream << r.timestamp;
// 写入通道数和数据
*m_stream << static_cast<quint32>(r.channels.size());
for (double v : r.channels) {
*m_stream << v;
}
// 写入状态和元数据
*m_stream << r.status << r.metadata;
}
// 确保写入磁盘
m_file->flush();
return true;
}
bool BinaryStorageHandler::read(DataBlock &block)
{
if (!m_stream || m_stream->atEnd()) return false;
quint32 recordCount;
*m_stream >> block.startTime
>> block.endTime
>> block.metadata;
*m_stream >> recordCount;
block.records.resize(recordCount);
for (quint32 i = 0; i < recordCount; ++i) {
quint32 channelCount;
*m_stream >> block.records[i].timestamp
>> channelCount;
block.records[i].channels.resize(channelCount);
for (quint32 j = 0; j < channelCount; ++j) {
*m_stream >> block.records[i].channels[j];
}
*m_stream >> block.records[i].status
>> block.records[i].metadata;
}
return true;
}
// ============================================================
// CsvStorageHandler 实现
// ============================================================
bool CsvStorageHandler::open(const QString &path, bool append)
{
close();
m_file = new QFile(path);
auto mode = append ? QIODevice::Append : (QIODevice::WriteOnly | QIODevice::Truncate);
if (!m_file->open(mode)) {
delete m_file;
m_file = nullptr;
return false;
}
m_stream = new QTextStream(m_file);
// 如果不是追加模式,写入表头
if (!append) {
*m_stream << "timestamp,channel_1,channel_2,channel_3,status\n";
}
return true;
}
void CsvStorageHandler::close()
{
if (m_stream) {
m_stream->flush();
delete m_stream;
m_stream = nullptr;
}
if (m_file) {
m_file->close();
delete m_file;
m_file = nullptr;
}
}
bool CsvStorageHandler::write(const DataBlock &block)
{
if (!m_stream) return false;
// 逐条写入CSV
for (const DataRecord &r : block.records) {
// 时间戳
*m_stream << r.timestamp;
// 各通道数据
for (double v : r.channels) {
*m_stream << "," << v;
}
// 状态
*m_stream << "," << r.status << "\n";
}
// 确保写入
m_stream->flush();
return true;
}
bool CsvStorageHandler::read(DataBlock &block)
{
// CSV读取实现略(与写入类似但方向相反)
Q_UNUSED(block)
return false;
}
// ============================================================
// StorageModule 实现
// ============================================================
StorageModule::StorageModule(QObject *parent)
: ModuleInterface(parent)
{
}
StorageModule::~StorageModule()
{
cleanup();
}
bool StorageModule::initialize()
{
// 从参数读取默认格式
auto *pm = ModuleManager::instance()->getModule<ParameterManager>("ParameterManager");
if (pm) {
QString fmt = pm->get("storage.format").toString();
if (fmt == "csv") {
m_currentFormat = StorageFormat::Csv;
} else if (fmt == "hdf5") {
m_currentFormat = StorageFormat::HDF5;
} else {
m_currentFormat = StorageFormat::Binary;
}
}
// 创建处理器
m_handler = createHandler(m_currentFormat);
m_state = ModuleState::Initialized;
return true;
}
void StorageModule::cleanup()
{
if (m_handler) {
if (m_handler->isOpen()) {
flush();
m_handler->close();
}
delete m_handler;
m_handler = nullptr;
}
m_state = ModuleState::Uninitialized;
}
IStorageHandler* StorageModule::createHandler(StorageFormat format)
{
switch (format) {
case StorageFormat::Csv:
return new CsvStorageHandler();
case StorageFormat::Binary:
default:
return new BinaryStorageHandler();
}
}
void StorageModule::setStorageFormat(StorageFormat format)
{
if (m_currentFormat == format) return;
// 关闭现有处理器
if (m_handler && m_handler->isOpen()) {
flush();
m_handler->close();
}
delete m_handler;
m_currentFormat = format;
m_handler = createHandler(format);
// 更新参数
auto *pm = ModuleManager::instance()->getModule<ParameterManager>("ParameterManager");
if (pm) {
QString fmtStr;
switch (format) {
case StorageFormat::Csv: fmtStr = "csv"; break;
case StorageFormat::HDF5: fmtStr = "hdf5"; break;
default: fmtStr = "binary";
}
pm->set("storage.format", fmtStr);
}
}
bool StorageModule::writeBlock(const DataBlock &block)
{
if (!m_handler || !m_handler->isOpen()) return false;
return m_handler->write(block);
}
bool StorageModule::appendRecord(const DataRecord &record)
{
// 添加到缓冲区
m_buffer.records.append(record);
// 如果缓冲区满了,冲刷到磁盘
if (m_buffer.records.size() >= m_bufferSize) {
if (!m_handler || !m_handler->isOpen()) return false;
// 更新时间戳
if (m_buffer.records.size() == 1) {
m_buffer.startTime = record.timestamp;
}
m_buffer.endTime = record.timestamp;
// 写入
DataBlock flushBlock = m_buffer;
m_buffer.records.clear();
bool ok = m_handler->write(flushBlock);
if (ok) {
emit recordWritten(flushBlock.records.size());
}
return ok;
}
return true;
}
void StorageModule::flush()
{
// 冲刷剩余数据
if (!m_buffer.records.isEmpty()) {
if (m_handler && m_handler->isOpen()) {
if (!m_buffer.startTime && !m_buffer.endTime) {
if (!m_buffer.records.isEmpty()) {
m_buffer.startTime = m_buffer.records.first().timestamp;
m_buffer.endTime = m_buffer.records.last().timestamp;
}
}
m_handler->write(m_buffer);
emit recordWritten(m_buffer.records.size());
}
m_buffer.records.clear();
}
}
QVariantMap StorageModule::getFileInfo(const QString &path) const
{
QVariantMap info;
QFileInfo fi(path);
info["size"] = fi.size();
info["modified"] = fi.lastModified().toString(Qt::ISODate);
info["exists"] = fi.exists();
// 读取二进制文件的头信息
if (info["exists"].toBool() && m_currentFormat == StorageFormat::Binary) {
QFile f(path);
if (f.open(QIODevice::ReadOnly)) {
QDataStream s(&f);
qint64 start, end;
s >> start >> end;
info["startTime"] = start;
info["endTime"] = end;
info["durationMs"] = end - start;
f.close();
}
}
return info;
}
八、项目结构与入口
8.1 目录结构
instrument-framework/
├── CMakeLists.txt # CMake构建配置
├── src/
│ ├── main.cpp # 程序入口
│ ├── Core/ # 核心基础设施
│ │ ├── ModuleInterface.h # 模块接口定义
│ │ └── ModuleManager.h / .cpp # 模块管理器
│ ├── Comm/ # 通讯模块
│ │ ├── ICommInterface.h # 通讯接口
│ │ ├── SerialComm.h / .cpp # 串口通讯
│ │ ├── TcpComm.h / .cpp # TCP通讯
│ │ └── ProtocolParser.h / .cpp # 协议解析
│ ├── Parameters/ # 参数配置
│ │ └── ParameterManager.h / .cpp # 参数管理器
│ ├── Analysis/ # 分析计算
│ │ └── AnalysisModule.h / .cpp # 分析模块
│ ├── Storage/ # 保存解析
│ │ └── StorageModule.h / .cpp # 存储模块
│ └── UI/ # 用户界面
│ └── MainWindow.h / .cpp # 主窗口
├── tests/ # 单元测试
│ └── test_modules.cpp # 模块测试
└── docs/ # 文档
└── README.md # 项目说明
8.2 main.cpp 完整实现
cpp
// main.cpp - 程序入口
#include <QApplication>
#include <QDebug>
// 包含所有模块的头文件
#include "Core/ModuleManager.h"
#include "Comm/SerialComm.h"
#include "Comm/TcpComm.h"
#include "Parameters/ParameterManager.h"
#include "Analysis/AnalysisModule.h"
#include "Storage/StorageModule.h"
#include "UI/MainWindow.h"
// 便捷类型别名
using CommInterface = ICommInterface;
int main(int argc, char *argv[])
{
// 创建应用
QApplication app(argc, argv);
app.setApplicationName("Instrument Framework");
app.setApplicationVersion("1.0");
app.setOrganizationName("Your Organization");
qDebug() << "==========================================";
qDebug() << "Instrument Framework Starting...";
qDebug() << "==========================================";
// ----------------------------------------
// 步骤1:创建并注册所有模块
// ----------------------------------------
ModuleManager *mgr = ModuleManager::instance();
// 注册通讯模块
// 注意:可以同时注册多个通讯模块,但名称要唯一
mgr->registerModule(new SerialComm());
mgr->registerModule(new TcpComm());
// 注册业务模块
mgr->registerModule(ParameterManager::instance()); // 单例
mgr->registerModule(AnalysisModule::instance()); // 单例
mgr->registerModule(StorageModule::instance()); // 单例
// ----------------------------------------
// 步骤2:初始化所有模块
// ----------------------------------------
// 会自动按照依赖关系排序
if (!mgr->initializeAll()) {
qCritical() << "FATAL: Failed to initialize modules!";
return 1;
}
qDebug() << "All modules initialized successfully";
// ----------------------------------------
// 步骤3:启动所有模块
// ----------------------------------------
if (!mgr->startAll()) {
qCritical() << "FATAL: Failed to start modules!";
return 1;
}
qDebug() << "All modules started successfully";
// ----------------------------------------
// 步骤4:创建并显示主窗口
// ----------------------------------------
MainWindow mainWindow;
mainWindow.show();
// ----------------------------------------
// 步骤5:连接数据流
// ----------------------------------------
// 获取模块实例
auto *comm = mgr->getModule<CommInterface>("SerialComm");
auto *analysis = mgr->getModule<AnalysisModule>("AnalysisModule");
auto *storage = mgr->getModule<StorageModule>("StorageModule");
// 连接数据流:通讯 -> 分析 -> UI/存储
if (comm && analysis) {
QObject::connect(comm, &CommInterface::dataReceived,
analysis, [analysis](const QByteArray &data) {
// 解析原始数据(这里需要根据实际协议实现)
QVector<double> rawData;
// 示例:假设数据是16位有符号整数序列
for (int i = 0; i < data.size() / 2; ++i) {
qint16 val;
memcpy(&val, data.constData() + i * 2, 2);
// 转换为物理量(假设量程为-10V~+10V)
rawData.append(val / 32768.0 * 10.0);
}
// 创建分析数据
AnalysisData ad;
ad.rawData = rawData;
ad.metadata["timestamp"] = QDateTime::currentMSecsSinceEpoch();
// 执行分析
AnalysisResult result = analysis->execute(ad);
if (result.success) {
qDebug() << "Analysis done in" << result.elapsedMs << "ms";
}
});
}
// ----------------------------------------
// 步骤6:设置存储路径
// ----------------------------------------
auto *pm = mgr->getModule<ParameterManager>("ParameterManager");
if (pm) {
// 设置默认存储路径
QString dir = pm->get("storage.directory").toString();
if (dir.isEmpty()) {
dir = QStandardPaths::writableLocation(
QStandardPaths::DocumentsLocation
) + "/InstrumentData";
// 确保目录存在
QDir().mkpath(dir);
// 保存到配置
pm->set("storage.directory", dir);
}
qDebug() << "Storage directory:" << dir;
}
// ----------------------------------------
// 步骤7:进入事件循环
// ----------------------------------------
qDebug() << "Entering main event loop...";
int ret = app.exec();
// ----------------------------------------
// 步骤8:退出前停止所有模块
// ----------------------------------------
qDebug() << "Stopping all modules...";
mgr->stopAll();
qDebug() << "Application exited with code:" << ret;
return ret;
}
九、框架扩展指南
9.1 添加新的通讯接口(以USB为例)
只需要三步:
第一步:创建USB通讯类
cpp
// UsbComm.h
class UsbConfig : public CommConfig {
public:
int vendorId = 0x1234;
int productId = 0x5678;
int timeout = 3000;
CommConfig* clone() const override {
return new UsbConfig(*this);
}
};
class UsbComm : public ICommInterface {
Q_OBJECT
public:
explicit UsbComm(QObject *parent = nullptr);
~UsbComm() override;
QString name() const override { return "UsbComm"; }
bool initialize() override;
void cleanup() override;
bool stop() override { disconnect(); return true; }
bool pause() override { return true; }
bool connect(const CommConfig *config) override;
void disconnect() override;
bool send(const QByteArray &data) override;
ConnectionState connectionState() const override {
return m_state;
}
CommConfig* currentConfig() const override { return m_config; }
private:
UsbConfig *m_config = nullptr;
ConnectionState m_state = ConnectionState::Disconnected;
};
第二步:实现接口
cpp
// UsbComm.cpp
bool UsbComm::connect(const CommConfig *config)
{
const UsbConfig *uc = dynamic_cast<const UsbConfig*>(config);
if (!uc) return false;
delete m_config;
m_config = new UsbConfig(*uc);
// USB连接实现...
// 使用libusb或其他USB库
setConnectionState(ConnectionState::Connected);
m_state = ModuleState::Running;
return true;
}
第三步:在main.cpp中注册
cpp
mgr->registerModule(new UsbComm());
9.2 添加新的分析算法
cpp
// 添加卡尔曼滤波器
class KalmanFilterNode : public IAnalysisNode {
Q_OBJECT
public:
KalmanFilterNode(double processNoise = 0.001,
double measurementNoise = 0.1)
: m_processNoise(processNoise)
, m_measurementNoise(measurementNoise)
, m_estimate(0)
, m_errorCovariance(1) {}
QString name() const override { return "KalmanFilter"; }
AnalysisResult process(const AnalysisData &input) override
{
AnalysisResult r;
QElapsedTimer timer;
timer.start();
QVector<double> output;
output.reserve(input.rawData.size());
for (double measurement : input.rawData) {
// 预测步骤
double predictedEstimate = m_estimate;
double predictedErrorCovariance = m_errorCovariance + m_processNoise;
// 更新步骤
double kalmanGain = predictedErrorCovariance /
(predictedErrorCovariance + m_measurementNoise);
m_estimate = predictedEstimate +
kalmanGain * (measurement - predictedEstimate);
m_errorCovariance = (1 - kalmanGain) * predictedErrorCovariance;
output.append(m_estimate);
}
r.success = true;
r.message = "Kalman filter applied";
r.outputData = output;
r.elapsedMs = timer.elapsed();
return r;
}
private:
double m_processNoise; // 过程噪声
double m_measurementNoise; // 测量噪声
double m_estimate; // 当前估计值
double m_errorCovariance; // 估计误差协方差
};
// 在AnalysisModule::initialize()中注册
registerNode(new KalmanFilterNode(0.001, 0.1));
9.3 添加数据库存储支持
cpp
// DatabaseStorageHandler.h
class DatabaseStorageHandler : public IStorageHandler {
public:
bool open(const QString &path, bool append) override {
m_db = QSqlDatabase::addDatabase("QSQLITE");
m_db.setDatabaseName(path);
if (!m_db.open()) return false;
// 创建表
QSqlQuery q(m_db);
q.exec("CREATE TABLE IF NOT EXISTS records ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"timestamp INTEGER NOT NULL,"
"channel1 REAL,"
"channel2 REAL,"
"channel3 REAL,"
"channel4 REAL,"
"status INTEGER DEFAULT 0"
")");
return true;
}
void close() override { m_db.close(); }
bool write(const DataBlock &block) override {
QSqlQuery q(m_db);
q.exec("BEGIN TRANSACTION");
for (const DataRecord &r : block.records) {
q.prepare("INSERT INTO records (timestamp, channel1, channel2, "
"channel3, channel4, status) VALUES (?, ?, ?, ?, ?, ?)");
q.addBindValue(r.timestamp);
q.addBindValue(r.channels.value(0, 0));
q.addBindValue(r.channels.value(1, 0));
q.addBindValue(r.channels.value(2, 0));
q.addBindValue(r.channels.value(3, 0));
q.addBindValue(r.status);
q.exec();
}
q.exec("COMMIT");
return true;
}
bool read(DataBlock &block) override {
QSqlQuery q(m_db);
if (!q.exec("SELECT * FROM records ORDER BY timestamp")) return false;
while (q.next()) {
DataRecord r;
r.timestamp = q.value(1).toLongLong();
r.channels.append(q.value(2).toDouble());
r.channels.append(q.value(3).toDouble());
r.channels.append(q.value(4).toDouble());
r.channels.append(q.value(5).toDouble());
r.status = q.value(6).toInt();
block.records.append(r);
}
return true;
}
bool isOpen() const override { return m_db.isOpen(); }
private:
QSqlDatabase m_db;
};
十、CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.16)
project(InstrumentFramework LANGUAGES CXX)
# C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Qt自动处理MOC、UIC、RCC
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
# 寻找Qt6
find_package(Qt6 REQUIRED COMPONENTS
Core
Gui
Widgets
SerialPort
Network
Charts
Sql
)
# ============================================================
# 源文件分组
# ============================================================
set(CORE_SOURCES
src/Core/ModuleManager.cpp
)
set(CORE_HEADERS
src/Core/ModuleInterface.h
src/Core/ModuleManager.h
)
set(COMM_SOURCES
src/Comm/SerialComm.cpp
src/Comm/TcpComm.cpp
src/Comm/ProtocolParser.cpp
)
set(COMM_HEADERS
src/Comm/ICommInterface.h
src/Comm/SerialComm.h
src/Comm/TcpComm.h
src/Comm/ProtocolParser.h
)
set(BUSINESS_SOURCES
src/Parameters/ParameterManager.cpp
src/Analysis/AnalysisModule.cpp
src/Storage/StorageModule.cpp
)
set(BUSINESS_HEADERS
src/Parameters/ParameterManager.h
src/Analysis/AnalysisModule.h
src/Storage/StorageModule.h
)
set(UI_SOURCES
src/UI/MainWindow.cpp
)
set(UI_HEADERS
src/UI/MainWindow.h
)
# ============================================================
// 可执行文件
// ============================================================
add_executable(${PROJECT_NAME}
src/main.cpp
${CORE_SOURCES}
${CORE_HEADERS}
${COMM_SOURCES}
${COMM_HEADERS}
${BUSINESS_SOURCES}
${BUSINESS_HEADERS}
${UI_SOURCES}
${UI_HEADERS}
)
# ============================================================
// 链接库
// ============================================================
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::SerialPort
Qt6::Network
Qt6::Charts
Qt6::Sql
)
# ============================================================
// 包含目录
// ============================================================
target_include_directories(${PROJECT_NAME} PRIVATE
src
src/Core
src/Comm
src/Parameters
src/Analysis
src/Storage
src/UI
)
# ============================================================
// 安装配置
// ============================================================
install(TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION
RUNTIME DESTINATION bin
)
install(DIRECTORY src/
DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
---
## 结语:框架的核心价值
### 本框架解决了什么问题?
┌─────────────────────────────────────────────────────────────┐
│ 测试仪器软件常见问题 │
├─────────────────────────────────────────────────────────────┤
│ ❌ 模块耦合严重 → ✅ 分层模块化,接口清晰 │
│ ❌ 难以单元测试 → ✅ 模块可独立测试,接口可Mock │
│ ❌ 扩展困难 → ✅ 新增模块只需实现接口并注册 │
│ ❌ 状态管理混乱 → ✅ 状态机模式,状态转换可追踪 │
│ ❌ 配置分散 → ✅ 统一配置管理,变更自动通知 │
│ ❌ 通讯不稳定 → ✅ 断线重连、数据缓冲、协议解析 │
└─────────────────────────────────────────────────────────────┘
### 设计模式应用总结
| 模式 | 位置 | 作用 |
|------|------|------|
| 单例模式 | ModuleManager, ParameterManager | 全局唯一实例 |
| 工厂模式 | StorageModule | 根据格式创建处理器 |
| 策略模式 | ICommInterface, IStorageHandler | 同一接口,不同实现 |
| 桥接模式 | ICommInterface | 抽象与实现分离 |
| 观察者模式 | ParameterManager | 配置变更自动通知 |
| 责任链模式 | AnalysisModule | 算法串联执行 |
| 模板方法模式 | IAnalysisNode | 算法框架固定,步骤可扩展 |
| Memento模式 | ParameterManager | 配置快照与回滚 |
| MVC/MVP模式 | MainWindow | 界面与逻辑分离 |
### 如何开始使用?
**方式一:直接复制**
1. 复制整个目录结构
2. 复制所有代码文件
3. 使用Qt Creator打开CMakeLists.txt
4. 构建运行
**方式二:选择性使用**
1. 只复制Core层的ModuleInterface和ModuleManager
2. 根据需要选择性地使用通讯模块、分析模块等
3. 替换或扩展不适合的部分
**方式三:从零开始**
1. 先理解分层架构
2. 实现ModuleInterface和ModuleManager
3. 根据需要逐步添加模块
### 下一步可以做什么?
1. **添加单元测试**:每个模块都应该有对应的测试
2. **添加日志系统**:记录模块的初始化、状态变化、错误等信息
3. **添加性能监控**:监控数据处理耗时、内存使用等
4. **添加远程控制**:通过网络或串口实现远程控制
5. **添加报告生成**:自动生成测试报告
---
### 常见问题
**Q: 为什么使用Q_DECLARE_SINGLETON宏?**
A: 这是Qt的线程安全单例实现,比自己写更可靠。
**Q: 能否在多线程中使用ParameterManager?**
A: 可以。ParameterManager内部使用QMutex保护,所有公开方法都是线程安全的。
**Q: 如何添加新的存储格式(如HDF5)?**
A: 创建新的Handler类实现IStorageHandler接口,然后在StorageModule::createHandler中添加创建逻辑。
**Q: 通讯模块支持SCPI协议吗?**
A: 可以。只需要在ProtocolParser中使用ScpiParser实现,或者创建ScpiCommand封装SCPI命令。
**Q: 如何调试多线程问题?**
A: Qt Creator提供了线程调试器。也可以在关键位置添加qDebug()输出,使用ModuleManager::startupOrder()检查模块启动顺序。
---