系统架构设计
02 — 架构设计
版本:v1.0 | 日期:2026-05-30
1. 分层架构
┌─────────────────────────────────────────────────┐
│ UI 层 (ui/) │
│ MainWindow StatusBar RealTimeChart │
│ DataTableView HistoryPlayer ExportDialog │
├─────────────────────────────────────────────────┤
│ 数据管理层 (data/) │
│ DataTableModel DataBuffer DatabaseManager │
├─────────────────────────────────────────────────┤
│ 业务逻辑层 (protocol/) │
│ ProtocolDecoder ProtocolValidator │
├─────────────────────────────────────────────────┤
│ 通信层 (communication/) │
│ IChannel SerialChannel TcpChannel │
│ ChannelManager │
├─────────────────────────────────────────────────┤
│ 工作线程层 (worker/) │
│ SerialWorker TcpWorker ParseWorker │
└─────────────────────────────────────────────────┘
核心原则:上层依赖下层,下层不感知上层。层间通过信号槽或抽象接口通信。
2. 模块职责
2.1 通信层 (communication/)
IChannel (抽象接口)
├── SerialChannel (QSerialPort 封装)
└── TcpChannel (QTcpSocket 封装,客户端模式)
ChannelManager
- 持有 IChannel*,管理连接生命周期
- 提供 connectToDevice() / disconnect() / reconnect()
- 发射信号:connected / disconnected / dataReceived / errorOccurred
关键设计:
IChannel只暴露open/close/write/signal,上层不关心串口还是 TCPChannelManager内置重连定时器,检测到断开后启动指数退避重连
2.2 协议层 (protocol/)
Frame (struct)
- header: uint16 (0xA55A)
- length: uint8 (payload 字节数)
- type: uint8 (0x01=数据帧, 0x02=心跳, 0x03=应答, 0xFF=错误)
- payload: uint8[]
- crc: uint16 (CRC16-CCITT)
ProtocolDecoder (状态机)
状态: WAIT_HEADER → WAIT_LENGTH → WAIT_TYPE → WAIT_PAYLOAD → WAIT_CRC → DONE
ProtocolValidator
- crc16_ccitt(const uint8_t* data, size_t len) -> uint16_t
解码状态机:
┌─────────────┐
│ WAIT_HEADER │ ← 逐字节扫描 0xA5
└──────┬──────┘
│ 找到 0xA5 → 继续找 0x5A
▼
┌─────────────┐
│ WAIT_LENGTH │ ← 读 1 字节
└──────┬──────┘
▼
┌─────────────┐
│ WAIT_TYPE │ ← 读 1 字节
└──────┬──────┘
▼
┌──────────────┐
│ WAIT_PAYLOAD │ ← 读 length 字节
└──────┬───────┘
▼
┌─────────────┐
│ WAIT_CRC │ ← 读 2 字节 → 校验
└──────┬──────┘
│ CRC 通过 → 发射 frameDecoded(frame)
▼
┌─────────────┐
│ DONE │ → 回到 WAIT_HEADER
└─────────────┘
2.3 数据管理层 (data/)
DataPoint (struct)
- timestamp: uint64 (毫秒时间戳)
- channels: QVector<double>
DataBuffer (环形缓冲区,线程安全)
- 底层:QVector<DataPoint> + 头尾指针
- 写入:QMutex 保护
- 读取:返回 const 引用快照
- 信号:bufferUpdated(int count)
DataTableModel : QAbstractTableModel
- 行:数据点
- 列:时间戳 + 各通道值
- 角色:Qt::DisplayRole / Qt::BackgroundRole(阈值告警红色)
- 更新:收到 bufferUpdated 信号后 beginInsertRows / endInsertRows
DatabaseManager
- 使用 QSqlDatabase + QSqlQuery
- 初始化:CREATE TABLE IF NOT EXISTS + PRAGMA journal_mode=WAL
- 批量写入:每 100 条一个事务
- 查询:SELECT * FROM data_points WHERE timestamp BETWEEN ? AND ? ORDER BY timestamp
- 清理:DELETE FROM data_points WHERE timestamp < ? (保留最近 7 天)
2.4 工作线程层 (worker/)
SerialWorker : QObject ← moveToThread 到通信线程
- 持有 QSerialPort*
- readyRead → 读取原始字节 → emit rawDataReceived(QByteArray)
TcpWorker : QObject ← moveToThread 到通信线程
- 持有 QTcpSocket*
- readyRead → 读取原始字节 → emit rawDataReceived(QByteArray)
ParseWorker : QObject ← moveToThread 到解析线程
- 持有 ProtocolDecoder
- rawDataReceived → decoder.feed() → frameDecoded → emit dataPointReady(DataPoint)
2.5 UI 层 (ui/)
MainWindow
├── 菜单栏:文件(导出CSV/退出) | 视图(曲线/表格/暗色主题) | 帮助(关于)
├── 工具栏:连接按钮 | 断开按钮 | 开始采集 | 停止采集
├── 中央区域 (QSplitter 水平分割)
│ ├── RealTimeChart (左侧 70%)
│ └── DataTableView (右侧 30%)
├── 底部:HistoryPlayer (QSlider + 播放/暂停按钮)
└── 状态栏:连接状态 | 帧数 | 帧率 | 运行时长
RealTimeChart : QWidget
- paintEvent 自绘
- QPainterPath 存储曲线路径(增量更新,避免全量重绘)
- 双缓冲:QPixmap 离屏绘制 → QPainter::drawPixmap
- 交互:wheelEvent 缩放 | mousePressEvent/MoveEvent 拖拽
DataTableView : QWidget
- QTableView + DataTableModel
- scrollToBottom() 自动跟随最新数据
- 右键菜单:复制单元格值
HistoryPlayer : QWidget
- QSlider (时间轴) + QLabel (当前时间) + QPushButton (播放/暂停)
- 拖动滑块 → 从 SQLite 查询对应时间的数据 → 更新 DataTableModel → 重绘曲线
3. 线程模型
┌──────────────┐ 队列信号槽 ┌──────────────┐ 队列信号槽 ┌──────────────┐
│ 通信线程 │ ────────────→ │ 解析线程 │ ────────────→ │ UI 线程 │
│ (QThread #1) │ rawDataReady │ (QThread #2) │ dataPointReady│ (主线程) │
│ │ │ │ │ │
│ SerialWorker │ │ ParseWorker │ │ MainWindow │
│ TcpWorker │ │ ProtocolDec. │ │ DataModel │
│ │ │ DataBuffer │ │ Chart │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
│ │ 队列信号槽 │
│ └──────────────────────────────┘
│ DatabaseManager
│ (SQLite 写入)
关键决策:
- 通信和解析分两个线程:防止协议解析阻塞数据接收
- SQLite 写入放在解析线程:避免 UI 线程阻塞,且写入不需要单独线程(SQLite 串行写)
- 所有跨线程通信全部使用队列信号槽(
Qt::QueuedConnection),参数自动深拷贝 - DataBuffer 环形缓冲由互斥锁保护,允许解析线程写入 + UI 线程读取
4. 核心类图
┌─────────────────┐ ┌──────────────────┐
│ IChannel │ │ ChannelManager │
│ (interface) │◄───────│ - channel: ICh* │
│ + open() │ │ - timer: QTimer │
│ + close() │ │ + connect() │
│ + write() │ │ + disconnect() │
│ signal: │ │ + reconnect() │
│ readyRead() │ └──────────────────┘
└────────┬────────┘
│
┌────┴────┐
│ │
┌───┴────┐ ┌──┴──────┐
│SerialCh│ │TcpCh │
│-QSerP. │ │-QTcpSo. │
└────────┘ └─────────┘
┌──────────────────┐ ┌──────────────────┐
│ ProtocolDecoder │ │ProtocolValidator │
│ - state: enum │ │ + crc16() static │
│ - buffer: QByteA │ └──────────────────┘
│ + feed(bytes) │ │
│ signal: │ │ uses
│ frameDecoded() │ │
└──────────────────┘ │
│ │
│ 内部持有 │
└─────────────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ DataPoint │ │ DataBuffer │
│ + timestamp │ │ - mtx: QMutex │
│ + channels[] │ │ - ring: QVector │
└──────────────────┘ │ + push(DataPoint)│
│ │ + snapshot() │
│ └────────┬─────────┘
│ │
┌────┴─────────┐ │
│ │ │
┌───┴──────────┐ ┌─┴───────────┐ │
│DataTableModel│ │DatabaseMgr │ │
│QAbsTableMod. │ │- db: QSqlDb │ │
│+ rowCount() │ │+ initDB() │◄─┘
│+ data() │ │+ batchInsert│
│+ setData() │ │+ query() │
└──────────────┘ │+ cleanup() │
└─────────────┘
┌──────────────┐
│RealTimeChart │
│QWidget │
│- paths: QMap │
│- pixmap: QP │
│+ appendPoint │
│paintEvent() │
│wheelEvent() │
└──────────────┘
5. 数据流(一次完整采集)
1. 设备 → 串口/TCP 发送二进制帧
2. QSerialPort::readyRead / QTcpSocket::readyRead
3. SerialWorker/TcpWorker → emit rawDataReceived(QByteArray) (队列连接)
4. ParseWorker::onRawDataReceived() → decoder.feed(data)
5. ProtocolDecoder 状态机逐字节解析:
WAIT_HEADER → WAIT_LENGTH → WAIT_TYPE → WAIT_PAYLOAD → WAIT_CRC
6. CRC 校验通过 → emit frameDecoded(Frame)
7. ParseWorker::onFrameDecoded() → 转换为 DataPoint
8. DataBuffer::push(dataPoint) (QMutexLocker 保护)
9. → emit bufferUpdated(count) (队列连接到 UI 线程)
10. UI 线程:
a. DataTableModel → beginInsertRows → endInsertRows
b. RealTimeChart → appendPoint() → update() → paintEvent()
11. 异步(解析线程):
DatabaseManager::batchInsert(dataPoint)
→ 累积 100 条 → db.transaction() → 批量 INSERT → db.commit()
6. 关键设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 通信线程模型 | moveToThread | 职责分离清晰,QObject 不能跨线程移动但可以跨线程发信号 |
| 数据传递方式 | 队列信号槽 | 自动深拷贝参数,无需手动管理跨线程数据生命周期 |
| 曲线渲染方案 | QPainter 自绘 | QChart 在 100Hz 下性能不佳,自绘可控性更强 |
| 数据库写入策略 | 解析线程批量事务 | SQLite 串行写,批量事务比逐条快 10-50 倍 |
| 构建系统 | CMake | 跨平台、Qt6 官方推荐、IDE 支持好 |