输入关键词开始搜索

系统架构设计

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,上层不关心串口还是 TCP
  • ChannelManager 内置重连定时器,检测到断开后启动指数退避重连

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 支持好