串口 + TCP + 协议解析都一起纳进来.
一、整体架构概览
先看概念上的分层,不碰代码:
text
┌─────────────────────┐
│ MainWindow │ ← UI 层:界面、按钮、输入框、日志显示
└────────┬────────────┘
│
ITransport* (抽象传输接口)
/ \
/ \
┌────────────────┐ ┌─────────────────┐
│ SerialService │ │ TcpService │ ← 传输层:串口 / TCP,负责收发字节
└────────────────┘ └─────────────────┘
│
▼
┌─────────────────────┐
│ ProtocolParser │ ← 协议层:粘包处理、校验、拆出 CMD + Payload
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ CommandDispatcher │ ← 命令层:按 CMD 分发到不同处理逻辑
└─────────────────────┘
核心思想只有一句话:
"UI 只管交互,传输只管收发字节,协议只管怎么拆,命令只管怎么处理。"
二、工程目录结构
你可以直接照这个建工程(qmake / CMake 都行,下面用 qmake 示例):
text
SerialTool/
SerialTool.pro
main.cpp
MainWindow.h
MainWindow.cpp
MainWindow.ui
ITransport.h // 抽象传输接口(串口/TCP 统一面向这个)
SerialService.h
SerialService.cpp
TcpService.h
TcpService.cpp
ProtocolParser.h
ProtocolParser.cpp
CommandDispatcher.h
CommandDispatcher.cpp
Utils.h // 工具函数:时间戳等
Utils.cpp
SerialTool.pro 示例
pro
QT += core gui widgets serialport network
TARGET = SerialTool
TEMPLATE = app
SOURCES += \
main.cpp \
MainWindow.cpp \
SerialService.cpp \
TcpService.cpp \
ProtocolParser.cpp \
CommandDispatcher.cpp \
Utils.cpp
HEADERS += \
MainWindow.h \
ITransport.h \
SerialService.h \
TcpService.h \
ProtocolParser.h \
CommandDispatcher.h \
Utils.h
FORMS += \
MainWindow.ui
三、公共工具层 Utils(时间戳)
cpp
// Utils.h
#pragma once
#include <QString>
QString withTimestamp(const QString &msg);
cpp
// Utils.cpp
#include "Utils.h"
#include <QDateTime>
QString withTimestamp(const QString &msg)
{
return QDateTime::currentDateTime()
.toString("[yyyy-MM-dd HH:mm:ss.zzz] ") + msg;
}
评价:
这类小工具集中到 Utils,避免到处复制粘贴时间戳代码,后续想加别的通用函数也有家可归。
四、传输接口 ITransport(关键点)
1. 接口设计
cpp
// ITransport.h
#pragma once
#include <QObject>
class ITransport : public QObject
{
Q_OBJECT
public:
explicit ITransport(QObject *parent = nullptr)
: QObject(parent) {}
virtual ~ITransport() = default;
virtual bool open() = 0;
virtual void close() = 0;
virtual bool isOpen() const = 0;
virtual void send(const QByteArray &data) = 0;
signals:
void received(const QByteArray &data);
void log(const QString &msg);
void opened();
void closed();
void sendCountUpdated(quint64 bytes);
void recvCountUpdated(quint64 bytes);
};
设计评价:
- 把"传输"的抽象统一成:
open / close / isOpen / send。 - 上层完全不需要知道"下面是串口还是 TCP":
只要有个ITransport* transport_就能用。 - 信号统一:
received、log、统计信号都在接口里,方便 UI 一次性绑定。
这一层是整个"支持串口 + TCP 的关键"。
五、串口实现 SerialService(实现 ITransport)
只说结构和关键点,细节你可以用前面那版代码:
cpp
// SerialService.h
#pragma once
#include "ITransport.h"
#include <QSerialPort>
#include <QTimer>
class SerialService : public ITransport
{
Q_OBJECT
public:
explicit SerialService(QObject *parent = nullptr);
void setConfig(const QString &portName, int baudRate);
bool open() override;
void close() override;
bool isOpen() const override { return port_.isOpen(); }
void send(const QByteArray &data) override;
private slots:
void onReadyRead();
void onPortError(QSerialPort::SerialPortError error);
void tryReconnect();
private:
QSerialPort port_;
QTimer reconnectTimer_;
QString portName_;
int baudRate_ = 0;
quint64 sentBytes_ = 0;
quint64 recvBytes_ = 0;
};
关键机制:
readyRead信号异步接收,不会阻塞 UI。errorOccurred捕获串口拔掉等情况,配合QTimer做自动重连。- 内部维护
sentBytes_ / recvBytes_,通过接口的sendCountUpdated / recvCountUpdated发给 UI。
评价:
- 职责非常单一:只管"串口"本身,不碰协议、不碰 UI。
- 因为实现了
ITransport,上层可以随时把它换成别的实现(TCP/Udp 等)。
六、TCP 实现 TcpService(实现 ITransport)
cpp
// TcpService.h
#pragma once
#include "ITransport.h"
#include <QTcpSocket>
#include <QTimer>
class TcpService : public ITransport
{
Q_OBJECT
public:
explicit TcpService(QObject *parent = nullptr);
void setConfig(const QString &host, quint16 port);
bool open() override;
void close() override;
bool isOpen() const override {
return socket_.state() == QAbstractSocket::ConnectedState;
}
void send(const QByteArray &data) override;
private slots:
void onConnected();
void onDisconnected();
void onReadyRead();
void onErrorOccurred(QAbstractSocket::SocketError error);
void tryReconnect();
private:
QTcpSocket socket_;
QTimer reconnectTimer_;
QString host_;
quint16 port_ = 0;
quint64 sentBytes_ = 0;
quint64 recvBytes_ = 0;
};
关键点:
- 和串口一样,用
readyRead异步接收。 - 连接状态通过
connected / disconnected / errorOccurred回调。 - 同样用
QTimer做自动重连(可选)。
评价:
- 从上层视角看,它就是另一个实现了
ITransport的"通道",API 一样,信号一样。 - 你只要在 UI 里根据选择"Serial/TCP"切换
transport_的指向,就完成"支持 TCP"。
七、协议解析层 ProtocolParser(粘包 + 校验)
假设协议格式:
text
55 AA LEN CMD PAYLOAD... CHK
LEN = payload 长度
CHK = CMD + payload 全部 XOR
cpp
// ProtocolParser.h
#pragma once
#include <QObject>
class ProtocolParser : public QObject
{
Q_OBJECT
public:
explicit ProtocolParser(QObject *parent = nullptr);
public slots:
void feed(const QByteArray &data); // 传输层喂原始字节
signals:
void frameParsed(quint8 cmd, const QByteArray &payload);
void log(const QString &msg);
private:
QByteArray buffer_;
void process();
};
核心点:
- 内部
buffer_负责处理"半包 / 粘包"。 - 每次
feed把新数据 append 进 buffer,然后循环解析。 - 成功解析完整帧后,发
frameParsed(cmd, payload),上层不用管粘包问题。
评价:
- 只做"字节 → 帧"的工作,不亲自处理业务,不知道串口/TCP。
- 将来你要改协议(比如 JSON、其他头格式),只要动这个类,不影响 UI 和传输层。
八、命令分发层 CommandDispatcher(按 CMD 派发)
cpp
// CommandDispatcher.h
#pragma once
#include <QObject>
#include <QMap>
#include <functional>
class CommandDispatcher : public QObject
{
Q_OBJECT
public:
explicit CommandDispatcher(QObject *parent = nullptr);
void registerHandler(quint8 cmd,
std::function<void(const QByteArray&)> handler);
public slots:
void dispatch(quint8 cmd, const QByteArray &payload);
signals:
void log(const QString &msg);
private:
QMap<quint8, std::function<void(const QByteArray&)>> handlers_;
};
使用方式:
在 MainWindow 里:
cpp
dispatcher_->registerHandler(0x01, [this](const QByteArray &p){
// 例如 JSON 文本
QString s = QString::fromUtf8(p);
ui->textEditRecv->append(withTimestamp("CMD 0x01(JSON): " + s));
});
dispatcher_->registerHandler(0x02, [this](const QByteArray &p){
ui->textEditRecv->append(withTimestamp(
"CMD 0x02(BIN): " + p.toHex(' ')
));
});
解析器连接分发器:
cpp
connect(parser_, &ProtocolParser::frameParsed,
dispatcher_, &CommandDispatcher::dispatch);
评价:
- 把"不同 CMD 的业务逻辑"从解析器里拿出来,放在可以自由扩展的地方。
- 使用
std::function + lambda,不用为了每个命令再建 N 个子类,写起来很轻松。 - Dispatcher 不依赖 UI,只对 cmd + payload 负责,也方便写单元测试。
九、MainWindow(UI 层 + 总装配)
1. 成员大致长这样
cpp
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
ITransport *transport_ = nullptr;
SerialService *serial_ = nullptr;
TcpService *tcp_ = nullptr;
ProtocolParser *parser_ = nullptr;
CommandDispatcher *dispatcher_ = nullptr;
QTimer autoSendTimer_;
void initUi();
void initSignals();
void initDispatcher();
private slots:
void onOpenClicked();
void onCloseClicked();
void onSendClicked();
void onClearRecv();
void onClearLog();
};
2. 核心职责
MainWindow 只做这些事:
- 初始化 UI(串口下拉、TCP 地址/端口、计数标签等)
- 选择当前通道(Serial/TCP),给通道设置参数
- 把
received接到ProtocolParser,把frameParsed接到CommandDispatcher - 绑定一堆 log 信号到
textEditLog - 自动发送用
QTimer调onSendClicked - 用户输入解析(文本 / 十六进制),然后
transport_->send(data)
评价:
- MainWindow 不直接 touch QSerialPort/QTcpSocket,只面向
ITransport,这点非常关键。 - 协议、命令、传输这些复杂逻辑都在专门的类里面,MainWindow 文件不会长到看不下去。
十、整体设计总结(优缺点)
优点
-
解耦干净
- UI ↔ 传输 ↔ 协议 ↔ 命令,每层都有清晰边界。
- 想改协议,只动 ProtocolParser/Dispatcher,不动 UI/传输。
- 想加 UdpService,只要再继承 ITransport 就行。
-
扩展性强
-
支持串口、TCP 只是换一个
ITransport实现。 -
后面可以很轻松加:
- JSON 协议解析(在某个 CMD 里用 QJsonDocument 解析)
- 多协议并存(不同 CMD 对应不同解析方式)
- 更多通信方式(UDP、WebSocket 等)
-
-
可维护性好
- 每个类职责很单一,文件长度可控。
- 逻辑问题可以很快定位属于哪一层。
-
便于测试
- 可以写一个 MockTransport 实现 ITransport,不连真实设备就能调试协议和 UI。
- ProtocolParser、CommandDispatcher 都不依赖 Qt UI,可以单元测试。
潜在缺点 / 注意点
-
文件数量多一点
对比"所有东西全写在 MainWindow",文件多了一些,但这是换来的可维护性,值。
-
初学者入门理解成本稍高
需要理解 Qt 信号槽 + 抽象接口这套东西。不过一旦搞懂,后面开发效率是翻倍的。
-
自动重连逻辑要按实际需求微调
比如:连不上是否一直重试、是否加最大次数、错误提示策略等。