输入关键词开始搜索

T012 全链路集成

T012 全链路串联 — 业务逻辑与代码分析

里程碑②:首次实现”点连接 → 数据从网络到屏幕 + 数据库”的完整闭环。


一、数据全链路(端到端)

┌──────────┐   TCP     ┌─────────────┐  raw bytes  ┌────────────────┐
│ 模拟器    │ ────────→ │ TcpChannel  │ ──────────→ │ ProtocolDecoder │
│ 100Hz    │  小端二进制 │ (T003)      │             │ (T005)          │
└──────────┘           └─────────────┘             └───────┬────────┘

                                                  frameDecoded(Frame)


                                                  ┌────────────────┐
                                                  │ MainWindow     │
                                                  │ onFrameDecoded │
                                                  └───┬───────┬────┘
                                                      │       │
                                              push()  │       │ insert()
                                                      ▼       ▼
                                                  ┌──────┐ ┌──────────────┐
                                                  │Buffer│ │DatabaseManager│
                                                  │(内存)│ │(data.db)      │
                                                  └──┬───┘ └──────────────┘

                                          bufferUpdated (QueuedConnection)

                                    ┌────────────────┼────────────────┐
                                    ▼                ▼                ▼
                            DataTableModel    RealTimeChart       (后续扩展)
                            → QTableView      → 自绘曲线

二、按钮与状态机

                    ┌─────────────────────────────────┐
                    │           初始状态               │
                    │   m_channelMgr = nullptr        │
                    │   m_collecting  = false         │
                    └───────────┬─────────────────────┘

                    ┌───────────▼───────────┐
                    │    点 "连接"          │
                    │  → onConnect()        │
                    │  创建管道              │
                    │  TCP 连接中...         │
                    └───────────┬───────────┘
                                │ connected 信号
                    ┌───────────▼───────────┐
                    │    点 "开始"          │
                    │  → onStart()          │
                    │  m_collecting = true  │
                    │  数据流入 buffer + DB  │
                    │  状态: "采集中..."     │
                    └───────────┬───────────┘

              ┌─────────────────┼─────────────────┐
              │                 │                 │
    ┌─────────▼──────┐  ┌──────▼───────┐  ┌──────▼───────┐
    │   点 "停止"     │  │  点 "断开"    │  │  意外断线     │
    │ m_collecting   │  │ 销毁管道      │  │ ChannelManager│
    │ = false        │  │ buffer 保留   │  │ 自动重连      │
    │ TCP 不断       │  │ 状态: 已断开  │  │ 状态: 重连中  │
    │ 状态: 已暂停   │  │               │  │               │
    └─────────┬──────┘  └──────┬───────┘  └──────┬───────┘
              │                │                 │
              ▼                ▼                 ▼
        可再点"开始"     可再点"连接"       连上 → "已连接"
        恢复采集          重建管道          失败 → 退避重试

三、核心代码逐段解析

onConnect() — 创建数据管道

void MainWindow::onConnect()
{
    if (m_channelMgr) return;        // 防重入:已连则忽略

为什么:按钮可能被连点两次,不加判断会创建两套管道互相冲突。

    if (!m_buffer) {                 // buffer 只创建一次
        m_buffer = new DataBuffer(10000, this);
        setDataBuffer(m_buffer);     // 注入曲线 + 表格
    }

为什么复用:断开后再连接,旧的历史数据不该丢。m_bufferonDisconnect 中不删除。

    m_decoder    = new ProtocolDecoder(this);
    m_channelMgr = new ChannelManager(this);
    m_dbMgr      = new DatabaseManager(this);
    m_dbMgr->init("data.db");

    TcpChannel *tcp = new TcpChannel("127.0.0.1", 9999, this);
    m_channelMgr->setChannel(tcp);

parent = this:所有对象以 MainWindow 为父。MainWindow 析构时 Qt 自动 delete 所有子对象——不会泄漏。

    connect(m_channelMgr, &ChannelManager::readyRead,
            m_decoder, &ProtocolDecoder::feed);
    connect(m_decoder, &ProtocolDecoder::frameDecoded,
            this, &MainWindow::onFrameDecoded);

两条信号线:字节→解码器,帧→MainWindow。中间不需要传数据副本,Qt 信号槽参数类型匹配就行。

    connect(m_channelMgr, &ChannelManager::connected,
            [this]() { m_statusLabel->setText("已连接"); });
    connect(m_channelMgr, &ChannelManager::disconnected, [this]() {
        if (m_collecting) m_statusLabel->setText("已断开(重连中)");
        else m_statusLabel->setText("已断开");
    });

状态栏联动:采集中断线显示”重连中”,暂停中断线只显示”已断开”。


onDisconnect() — 销毁管道,保留数据

void MainWindow::onDisconnect()
{
    m_collecting = false;           // ① 先停采集
    if (!m_channelMgr) return;

    if (IChannel *ch = m_channelMgr->channel())
        ch->close();                // ② 关 TCP(abort)

    delete m_channelMgr;            // ③ 删管道(顺序很重要)
    delete m_decoder;
    if (m_dbMgr) { m_dbMgr->flush(); delete m_dbMgr; }
    // m_buffer 不删 ← ④ 历史数据保留
}

删除顺序为什么重要

ChannelManager 先删 → 断开内部信号 → Decoder 不再收到数据
Decoder 次删 → 断开 frameDecoded → 不再回调 MainWindow
DB 最后删 → flush 残留的 100 条缓冲 → 关 SQLite

如果先删 Decoder,ChannelManager 的 readyRead 信号还连着 Decoder 的槽→悬空指针→crash。


onFrameDecoded() — 帧 → 数据点 → 双写

void MainWindow::onFrameDecoded(const Frame &frame)
{
    if (!m_collecting) return;      // 暂停中,忽略数据
    if (frame.type != Frame::TYPE_DATA || frame.payload.size() < 6) return;

双重过滤m_collecting 是用户意图(点停止),type != DATA 过滤心跳帧等非数据帧。两者职责不同。

    DataPoint dp;
    dp.timestamp = QDateTime::currentMSecsSinceEpoch();
    auto d = reinterpret_cast<const uint8_t*>(frame.payload.constData());
    dp.channels = {
        double(d[0] | (d[1] << 8)),
        double(d[2] | (d[3] << 8)),
        double(d[4] | (d[5] << 8))
    };

小端序解析d[0] | (d[1] << 8) — 低字节 + 高字节左移 8 位拼成 uint16_t,再转 double。和模拟器 struct.pack('<HHH') 对应。

    m_buffer->push(dp);
    m_dbMgr->insert(dp);
}

双写模式:内存 buffer(UI 可视化)+ SQLite(持久化)。insert 内部缓冲满 100 条才 commit,不卡主线程。


四、关键设计决策

决策选择理由
buffer 断连不删复用重连后历史数据不丢失
DB 断连时 flush最后几十条缓冲数据不浪费
开始/停止只控制采集不控 TCP停止后恢复即时生效,不需要重建连接
parent 统一为 thisQt 父子树自动管理,析构不用逐个 delete
防重入if(m_channelMgr) return连点”连接”不会创建双管道

五、本次遇到的坑

#现象根因修复
1onConnect 连点两次 crash无双管道防护if(m_channelMgr) return
2断开后 UI 清空setDataBuffer(nullptr) 掐断了曲线数据源去掉,buffer 保留
3重连后历史数据消失每次 new 新 buffer 覆盖旧 bufferbuffer 复用,只创建一次
4断开后无法重连delete m_channelMgr 没关底层 TcpChannelch->close() 再 delete
5断开/停止后状态混淆没有采集状态标记m_collecting 标志
6模拟器断连即退出没捕获 ConnectionAbortedError脚本加异常捕获 + 循环 accept