1. 集成autogui-cpp库,以支持后续的自动化操作需求

2. 增加了串口设备管理类,以支持无法联网的嵌入式设备接入Yosuga
3. 基于COBS编码以解决串口收发的粘包问题
This commit is contained in:
Misaki
2026-01-26 23:41:55 +08:00
parent 31e71edac0
commit 96b5ed59b7
24 changed files with 3901 additions and 20 deletions
+11
View File
@@ -0,0 +1,11 @@
//
// Created by misaki on 2026/1/24.
//
#pragma once
/**
* 客户端业务核心
* 1. 处理来自服务端的数据,分发并执行
* 2. 完成非阻塞的事件循环处理,构建业务状态机
*/
+5
View File
@@ -0,0 +1,5 @@
//
// Created by misaki on 2026/1/24.
//
#include "AppCore.h"
+1 -1
View File
@@ -81,7 +81,7 @@ GLCore::GLCore(const int width, const int height, QWidget *parent)
this->setMouseTracking(true);
// 连接一些必要的信号与槽
connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL);
connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL); // 关闭窗口信号
// 注册当前实例到中介类
AppContext::RegisterGLCore(this);
+1 -1
View File
@@ -44,7 +44,7 @@ NetworkDO::~NetworkDO()
// 业务逻辑实现
void NetworkDO::registerSender(SenderFunc sender)
{
QMutexLocker locker(&m_mutex); // 简单保护一下赋值
QMutexLocker locker(&m_mutex); // 加个小锁,简单保护一下赋值
m_sender = std::move(sender);
}
+189
View File
@@ -0,0 +1,189 @@
//
// Created by misaki on 2026/1/26.
//
/**
* 串口通信模块 —— 支持多设备并行 + JSON 协议
* 每个 SerialPortClient 实例对应一个物理串口
*/
#pragma once
#include <QSerialPort>
#include <QSerialPortInfo>
#include <QObject>
#include <QThread>
#include <QTimer>
#include <QMutex>
#include <QScopedPointer>
#include <QJsonDocument>
#include <QJsonObject>
#include <utility>
/**
* @brief 串口管理器(工作线程侧)
* @details 负责单一串口的实际 I/O、参数配置、心跳、JSON 自动编解码
* 与 WebSocketManager 对称设计,支持多实例并行
*/
class SerialPortManager final : public QObject {
Q_OBJECT
public:
explicit SerialPortManager(QString deviceName, QObject *parent = nullptr);
~SerialPortManager() override;
// 禁止拷贝
SerialPortManager(const SerialPortManager&) = delete;
SerialPortManager& operator=(const SerialPortManager&) = delete;
// 设备标识(用于多实例区分)
[[nodiscard]] QString deviceName() const { return m_deviceName; }
// 配置结构体
struct SerialPortConfig {
QString portName; // 端口名: "COM3", "/dev/ttyUSB0"
qint32 baudRate; // 波特率: 9600, 115200
QSerialPort::DataBits dataBits;
QSerialPort::Parity parity;
QSerialPort::StopBits stopBits;
QSerialPort::FlowControl flowControl;
QByteArray heartbeatData; // 心跳包内容(为空则禁用)
int maxJsonSize; // JSON 最大长度(防内存溢出)
// 默认配置:115200-N-8-1 + 无流控 + 心跳禁用 + JSON 上限 64KB
explicit SerialPortConfig(
QString port = "",
qint32 baud = 115200,
QSerialPort::DataBits db = QSerialPort::Data8,
QSerialPort::Parity p = QSerialPort::NoParity,
QSerialPort::StopBits sb = QSerialPort::OneStop,
QSerialPort::FlowControl fc = QSerialPort::NoFlowControl,
QByteArray hb = QByteArray(),
int maxJson = 65536
) : portName(std::move(port)), baudRate(baud), dataBits(db), parity(p),
stopBits(sb), flowControl(fc), heartbeatData(std::move(hb)), maxJsonSize(maxJson) {}
};
signals:
// 状态与数据信号
void opened(); // 串口成功打开
void closed(); // 串口关闭
void dataReceived(const QByteArray &data); // 原始二进制数据
void textReceived(const QString &text); // 文本数据(UTF-8 解码)
void hexReceived(const QString &hex); // 十六进制字符串: "AA BB CC"
void jsonReceived(const QString &type, const QJsonObject &data); // JSON 已解析
void error(const QString &errorMsg); // 错误信息
void log(const QString &msg); // 运行日志
void reconnecting(int attempt); // 自动重连中
public slots:
bool setConfig(const SerialPortManager::SerialPortConfig& config); // 设置配置
[[nodiscard]] SerialPortManager::SerialPortConfig currentConfig() const;
bool open(); // 打开串口
void close(); // 关闭串口
// 多层次发送接口(JSON、文本、HEX、原始二进制)
void sendJson(const QString &type, const QJsonObject &data); // 发送 JSON(自动封装)
void sendText(const QString &text); // 发送文本(UTF-8
void sendHex(const QString &hex); // 发送十六进制: "12 AB CD"
void sendRaw(const QByteArray &data); // 发送原始二进制(重命名为 sendRaw 更清晰)
void setAutoReconnect(bool enabled); // 是否开启自动重连
void setHeartbeatInterval(int msecs); // 心跳间隔(毫秒)
[[nodiscard]] bool isOpen() const; // 串口是否已打开
private slots:
void onReadyRead(); // 串口有数据到达
void onErrorOccurred(QSerialPort::SerialPortError error);
void sendHeartbeat(); // 定时发送心跳
void tryReconnect(); // 重连逻辑
void processCOBSBuffer(); // 尝试解析 COBS帧
private:
QSerialPort *m_serial; /// 串口实例
QString m_deviceName; /// 设备标识(如 "STM32_Master", "ESP32_Slave"
SerialPortConfig m_config; /// 当前配置
QTimer *m_heartbeatTimer; /// 心跳定时器
QTimer *m_reconnectTimer; /// 重连定时器
bool m_isAutoReconnect; /// 是否启用自动重连
int m_reconnectAttempts; /// 重连尝试次数
QByteArray m_cobsBuffer; /// COBS 解码缓冲区
bool m_cobsInFrame; /// 帧状态
};
/**
* @brief 串口客户端(主线程接口层)
* @details 每个实例对应一个物理串口,支持构造多个并行工作
* 封装线程迁移、信号转发、生命周期管理
*/
class SerialPortClient final : public QObject {
Q_OBJECT
Q_DISABLE_COPY(SerialPortClient)
public:
// 构造函数:deviceName 为设备标识,用于日志和多实例区分
explicit SerialPortClient(const QString &deviceName, QObject *parent = nullptr);
~SerialPortClient() override;
// 设备标识
[[nodiscard]] QString deviceName() const { return m_deviceName; }
// 配置串口
bool setConfiguration(const SerialPortManager::SerialPortConfig& config);
// 串口操作
void open();
void close();
void reconnect();
// 多层次发送接口
void sendJson(const QString &type, const QJsonObject &data);
void sendText(const QString &text);
void sendHex(const QString &hex);
void sendRaw(const QByteArray &data);
// 状态查询
[[nodiscard]] bool isOpen() const;
[[nodiscard]] bool hasConfiguration() const { return !m_config.portName.isEmpty(); }
[[nodiscard]] SerialPortManager::SerialPortConfig currentConfig() const { return m_config; }
// 高级功能
void setAutoReconnect(bool enabled);
void setHeartbeatInterval(int msecs);
[[nodiscard]] static QStringList availablePorts(); // 枚举系统可用串口
signals:
// 事件信号(与 SerialPortManager 一一对应,转发到主线程)
void opened();
void closed();
void dataReceived(const QByteArray &data);
void textReceived(const QString &text);
void hexReceived(const QString &hex);
void jsonReceived(const QString &type, const QJsonObject &data);
void error(const QString &errorMsg);
void log(const QString &msg);
void reconnecting(int attempt);
// 配置变更
void configurationChanged(const SerialPortManager::SerialPortConfig &oldConfig,
const SerialPortManager::SerialPortConfig &newConfig);
private:
// 内部信号(用于跨线程通信,对标 internal*)
Q_SIGNAL void internalSetConfig(const SerialPortManager::SerialPortConfig& config);
Q_SIGNAL void internalOpen();
Q_SIGNAL void internalClose();
Q_SIGNAL void internalSendJson(const QString &type, const QJsonObject &data);
Q_SIGNAL void internalSendText(const QString &text);
Q_SIGNAL void internalSendHex(const QString &hex);
Q_SIGNAL void internalSendRaw(const QByteArray &data);
Q_SIGNAL void internalSetAutoReconnect(bool enabled);
Q_SIGNAL void internalSetHeartbeatInterval(int msecs);
QString m_deviceName; /// 设备标识
QThread *m_workerThread; /// 工作线程
SerialPortManager *m_serialManager; /// 管理器实例(工作线程侧)
SerialPortManager::SerialPortConfig m_config; /// 配置缓存
};
+583
View File
@@ -0,0 +1,583 @@
//
// Created by misaki on 2026/1/26.
//
#include "serialportmanager.h"
#include <QDebug>
#include <QMutexLocker>
#include <QJsonDocument>
#include <utility>
#include "cobs.hpp"
/// SerialPortManager
SerialPortManager::SerialPortManager(QString deviceName, QObject *parent)
: QObject(parent)
, m_serial(new QSerialPort(this))
, m_deviceName(std::move(deviceName))
, m_heartbeatTimer(new QTimer(this))
, m_reconnectTimer(new QTimer(this))
, m_isAutoReconnect(false)
, m_reconnectAttempts(0)
, m_cobsBuffer()
, m_cobsInFrame(false)
{
// 心跳定时器配置(默认 5 秒)
m_heartbeatTimer->setInterval(5000);
connect(m_heartbeatTimer, &QTimer::timeout,
this, &SerialPortManager::sendHeartbeat);
// 重连定时器(单次触发)
m_reconnectTimer->setSingleShot(true);
connect(m_reconnectTimer, &QTimer::timeout,
this, &SerialPortManager::tryReconnect);
// 串口信号连接
connect(m_serial, &QSerialPort::readyRead,
this, &SerialPortManager::onReadyRead);
connect(m_serial, QOverload<QSerialPort::SerialPortError>::of(&QSerialPort::errorOccurred),
this, &SerialPortManager::onErrorOccurred);
emit log(QString("[%1] SerialPortManager initialized in worker thread").arg(m_deviceName));
}
SerialPortManager::~SerialPortManager() {
if (m_serial->isOpen()) {
m_serial->close();
}
}
bool SerialPortManager::setConfig(const SerialPortConfig &config) {
if (m_serial->isOpen()) {
emit error(QString("[%1] Cannot change config while port is open. Close it first.").arg(m_deviceName));
return false;
}
if (config.portName.isEmpty()) {
emit error(QString("[%1] Port name cannot be empty!").arg(m_deviceName));
return false;
}
m_config = config;
emit log(QString("[%1] Config updated: %2 @ %3 bps, JSON max: %4 bytes")
.arg(m_deviceName, config.portName).arg(config.baudRate).arg(config.maxJsonSize));
return true;
}
SerialPortManager::SerialPortConfig SerialPortManager::currentConfig() const {
return m_config;
}
bool SerialPortManager::open() {
if (m_serial->isOpen()) {
emit log(QString("[%1] Port already opened, closing first...").arg(m_deviceName));
m_serial->close();
}
// 配置串口参数
m_serial->setPortName(m_config.portName);
m_serial->setBaudRate(m_config.baudRate);
m_serial->setDataBits(m_config.dataBits);
m_serial->setParity(m_config.parity);
m_serial->setStopBits(m_config.stopBits);
m_serial->setFlowControl(m_config.flowControl);
emit log(QString("[%1] Opening %2...").arg(m_deviceName, m_config.portName));
if (!m_serial->open(QIODevice::ReadWrite)) {
QString errMsg = QString("[%1] Failed to open %2: %3")
.arg(m_deviceName, m_config.portName, m_serial->errorString());
emit error(errMsg);
if (m_isAutoReconnect) {
m_reconnectTimer->start(3000);
}
return false;
}
m_reconnectAttempts = 0;
m_cobsBuffer.clear(); // 清空 COBS 缓冲区
m_cobsInFrame = false; // 重置帧状态
emit opened();
emit log(QString("[%1] Serial port opened successfully").arg(m_deviceName));
// 启动心跳(如果有配置心跳包)
if (!m_config.heartbeatData.isEmpty()) {
m_heartbeatTimer->start();
}
return true;
}
void SerialPortManager::close() {
m_isAutoReconnect = false;
m_heartbeatTimer->stop();
m_reconnectTimer->stop();
if (m_serial->isOpen()) {
m_serial->close();
m_cobsBuffer.clear(); // 清空 COBS 缓冲区
m_cobsInFrame = false; // 重置帧状态
emit closed();
emit log(QString("[%1] Serial port closed").arg(m_deviceName));
}
}
void SerialPortManager::sendJson(const QString &type, const QJsonObject &data) {
if (!m_serial->isOpen()) {
emit error(QString("[%1] Cannot send JSON: serial port not opened").arg(m_deviceName));
return;
}
// 封装成 { "type": "...", "data": {...}, "timestamp": 123456 }
QJsonObject wrapper;
wrapper["type"] = type;
wrapper["data"] = data;
wrapper["timestamp"] = QDateTime::currentMSecsSinceEpoch();
QJsonDocument doc(wrapper);
QByteArray jsonBytes = doc.toJson(QJsonDocument::Compact);
// COBS 编码
std::vector<uint8_t> encoded;
auto result = cobs::encode(encoded, std::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(jsonBytes.constData()), jsonBytes.size()
));
if (result.status != cobs::Status::OK) { // 编码失败
emit error(QString("[%1] COBS encode failed: status=%2").arg(m_deviceName).arg(static_cast<int>(result.status)));
return;
}
// 发送编码数据 + 0x00
const qint64 written = m_serial->write(reinterpret_cast<const char*>(encoded.data()), static_cast<qint64>(encoded.size()));
if (written == -1) { // 写入失败
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
} else { // 发送成功
m_serial->flush();
m_serial->write("\0", 1); // 帧结束符
emit log(QString("[%1] Sent JSON: type=%2, %3 bytes encoded").arg(m_deviceName, type).arg(encoded.size() + 1));
}
}
void SerialPortManager::sendText(const QString &text) {
if (!m_serial->isOpen()) {
emit error(QString("[%1] Cannot send text: serial port not opened").arg(m_deviceName));
return;
}
QByteArray data = text.toUtf8();
qint64 written = m_serial->write(data);
if (written == -1) {
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
} else if (written != data.size()) {
emit error(QString("[%1] Incomplete write: %2/%3 bytes sent").arg(m_deviceName).arg(written).arg(data.size()));
} else {
m_serial->flush();
emit log(QString("[%1] Sent text: %2 bytes").arg(m_deviceName).arg(written));
}
}
void SerialPortManager::sendHex(const QString &hex) {
if (!m_serial->isOpen()) {
emit error(QString("[%1] Cannot send hex: serial port not opened").arg(m_deviceName));
return;
}
// 解析十六进制字符串: "AA BB 1A" → QByteArray
QString cleaned = hex.simplified().remove(' ');
QByteArray data = QByteArray::fromHex(cleaned.toUtf8());
if (data.isEmpty() && !cleaned.isEmpty()) {
emit error(QString("[%1] Invalid hex format. Use: 'AA BB CC' or 'AABBCC'").arg(m_deviceName));
return;
}
qint64 written = m_serial->write(data);
if (written == -1) {
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
} else {
m_serial->flush();
emit log(QString("[%1] Sent hex: %2").arg(m_deviceName, cleaned.left(50)));
}
}
void SerialPortManager::sendRaw(const QByteArray &data) {
if (!m_serial->isOpen()) {
emit error(QString("[%1] Cannot send raw: serial port not opened").arg(m_deviceName));
return;
}
qint64 written = m_serial->write(data);
if (written == -1) {
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
} else {
m_serial->flush();
emit log(QString("[%1] Sent raw: %2 bytes").arg(m_deviceName).arg(written));
}
}
bool SerialPortManager::isOpen() const {
return m_serial->isOpen();
}
void SerialPortManager::setAutoReconnect(bool enabled) {
m_isAutoReconnect = enabled;
if (!enabled) {
m_reconnectTimer->stop();
}
emit log(QString("[%1] Auto reconnect %2").arg(m_deviceName, enabled ? "enabled" : "disabled"));
}
void SerialPortManager::setHeartbeatInterval(int msecs) {
m_heartbeatTimer->setInterval(msecs);
emit log(QString("[%1] Heartbeat interval: %2 ms").arg(m_deviceName).arg(msecs));
}
void SerialPortManager::onReadyRead() {
// 读取所有可用数据到 COBS 缓冲区
QByteArray chunk = m_serial->readAll(); // 读取原始字节流
m_cobsBuffer.append(chunk); // 追加到cobs缓冲区
// 触发原始数据信号
emit dataReceived(chunk);
emit textReceived(QString::fromUtf8(chunk)); // 尝试 UTF-8 解码
emit hexReceived(chunk.toHex(' ').toUpper()); // 十六进制表示
// 处理 COBS 帧
processCOBSBuffer();
}
void SerialPortManager::processCOBSBuffer() {
while (true) {
// 查找帧结束符 0x00
int zeroPos = m_cobsBuffer.indexOf('\0');
// 未找到完整帧,继续等待
if (zeroPos == -1) {
m_cobsInFrame = true;
// 防溢出
if (m_cobsBuffer.size() > m_config.maxJsonSize * 2) {
emit error(QString("[%1] COBS buffer overflow, clearing").arg(m_deviceName));
m_cobsBuffer.clear();
m_cobsInFrame = false;
}
return;
}
// 提取编码帧(不含 0x00
QByteArray encodedFrame = m_cobsBuffer.left(zeroPos);
m_cobsBuffer.remove(0, zeroPos + 1); // 移除结束符
m_cobsInFrame = false;
// 跳过空帧
if (encodedFrame.isEmpty()) {
emit log(QString("[%1] Empty COBS frame, skipped").arg(m_deviceName));
continue;
}
// COBS 解码
std::vector<uint8_t> decoded;
auto result = cobs::decode(decoded, std::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(encodedFrame.constData()), encodedFrame.size()
));
if (result.status != cobs::Status::OK) { // 解码失败
emit error(QString("[%1] COBS decode failed: status=%2").arg(m_deviceName).arg(static_cast<int>(result.status)));
continue;
}
// 解析 JSON
QByteArray jsonData(reinterpret_cast<const char*>(decoded.data()), static_cast<qint64>(decoded.size()));
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
if (parseError.error != QJsonParseError::NoError) {
emit error(QString("[%1] JSON parse error: %2").arg(m_deviceName, parseError.errorString()));
continue;
}
if (!doc.isObject()) {
emit error(QString("[%1] JSON is not an object").arg(m_deviceName));
continue;
}
QJsonObject obj = doc.object();
const QString type = obj.value("type").toString();
const QJsonObject data = obj.value("data").toObject();
if (type.isEmpty()) {
emit error(QString("[%1] JSON missing 'type' field").arg(m_deviceName));
continue;
}
// 成功,向上层交付
emit jsonReceived(type, data);
emit log(QString("[%1] JSON delivered: type=%2, size=%3").arg(m_deviceName, type).arg(jsonData.size()));
}
}
void SerialPortManager::onErrorOccurred(QSerialPort::SerialPortError error) {
if (error == QSerialPort::NoError) return;
QString errorMsg;
switch (error) {
case QSerialPort::DeviceNotFoundError:
errorMsg = "Device not found";
break;
case QSerialPort::PermissionError:
errorMsg = "Permission denied. Check udev rules or run with sudo";
break;
case QSerialPort::OpenError:
errorMsg = "Already opened or system error";
break;
case QSerialPort::WriteError:
errorMsg = "Write error";
break;
case QSerialPort::ReadError:
errorMsg = "Read error";
break;
case QSerialPort::ResourceError:
errorMsg = "Resource error: device removed or I/O error";
// 设备被拔插,触发重连
if (m_isAutoReconnect) {
m_reconnectTimer->start(2000);
}
break;
case QSerialPort::UnsupportedOperationError:
errorMsg = "Unsupported operation";
break;
case QSerialPort::TimeoutError:
errorMsg = "Operation timed out";
break;
case QSerialPort::NotOpenError:
errorMsg = "Device not open";
break;
default:
errorMsg = m_serial->errorString();
}
emit this->error(QString("[%1] Serial error: %2").arg(m_deviceName, errorMsg));
}
void SerialPortManager::sendHeartbeat() {
if (!m_serial->isOpen() || m_config.heartbeatData.isEmpty()) {
return;
}
qint64 written = m_serial->write(m_config.heartbeatData);
if (written == -1) {
emit error(QString("[%1] Heartbeat write failed: %2").arg(m_deviceName, m_serial->errorString()));
} else {
emit log(QString("[%1] Heartbeat sent").arg(m_deviceName));
}
}
void SerialPortManager::tryReconnect() {
if (!m_isAutoReconnect) return;
m_reconnectAttempts++;
emit reconnecting(m_reconnectAttempts);
emit log(QString("[%1] Reconnecting... (attempt %2)").arg(m_deviceName).arg(m_reconnectAttempts));
// 直接调用 open()
open();
}
/// SerialPortClient
SerialPortClient::SerialPortClient(const QString &deviceName, QObject *parent)
: QObject(parent)
, m_deviceName(deviceName)
, m_workerThread(new QThread(this))
, m_serialManager(new SerialPortManager(deviceName))
, m_config()
{
// 命名线程,方便调试
m_workerThread->setObjectName(QString("SerialPortThread_%1").arg(deviceName));
// 将 Manager 移到工作线程
m_serialManager->moveToThread(m_workerThread);
// 线程结束时清理 Manager
connect(m_workerThread, &QThread::finished,
m_serialManager, &QObject::deleteLater);
// 信号转发:Manager → Client(主线程)
connect(m_serialManager, &SerialPortManager::opened,
this, &SerialPortClient::opened);
connect(m_serialManager, &SerialPortManager::closed,
this, &SerialPortClient::closed);
connect(m_serialManager, &SerialPortManager::dataReceived,
this, &SerialPortClient::dataReceived);
connect(m_serialManager, &SerialPortManager::textReceived,
this, &SerialPortClient::textReceived);
connect(m_serialManager, &SerialPortManager::hexReceived,
this, &SerialPortClient::hexReceived);
connect(m_serialManager, &SerialPortManager::jsonReceived,
this, &SerialPortClient::jsonReceived);
connect(m_serialManager, &SerialPortManager::error,
this, &SerialPortClient::error);
connect(m_serialManager, &SerialPortManager::log,
this, &SerialPortClient::log);
connect(m_serialManager, &SerialPortManager::reconnecting,
this, &SerialPortClient::reconnecting);
// 内部信号:Client → Manager(跨线程调用)
connect(this, &SerialPortClient::internalSetConfig,
m_serialManager, [this](const SerialPortManager::SerialPortConfig& cfg) {
bool ok = m_serialManager->setConfig(cfg);
if (!ok) emit error(QString("[%1] Failed to set config").arg(m_deviceName));
}, Qt::QueuedConnection);
connect(this, &SerialPortClient::internalOpen,
m_serialManager, &SerialPortManager::open,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalClose,
m_serialManager, &SerialPortManager::close,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalSendJson,
m_serialManager, &SerialPortManager::sendJson,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalSendText,
m_serialManager, &SerialPortManager::sendText,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalSendHex,
m_serialManager, &SerialPortManager::sendHex,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalSendRaw,
m_serialManager, &SerialPortManager::sendRaw,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalSetAutoReconnect,
m_serialManager, &SerialPortManager::setAutoReconnect,
Qt::QueuedConnection);
connect(this, &SerialPortClient::internalSetHeartbeatInterval,
m_serialManager, &SerialPortManager::setHeartbeatInterval,
Qt::QueuedConnection);
// 启动工作线程
m_workerThread->start();
emit log(QString("[%1] SerialPortClient initialized, worker thread started").arg(m_deviceName));
}
SerialPortClient::~SerialPortClient() {
emit log(QString("[%1] Shutting down SerialPortClient...").arg(m_deviceName));
// 关闭串口
close();
// 优雅退出工作线程
if (m_workerThread && m_workerThread->isRunning()) {
m_workerThread->quit();
if (!m_workerThread->wait(3000)) {
m_workerThread->terminate();
m_workerThread->wait();
}
delete m_workerThread;
m_workerThread = nullptr;
}
}
bool SerialPortClient::setConfiguration(const SerialPortManager::SerialPortConfig &config) {
if (config.portName.isEmpty()) {
emit error(QString("[%1] Invalid config: port name empty").arg(m_deviceName));
return false;
}
auto oldConfig = m_config;
m_config = config;
// 跨线程设置
emit internalSetConfig(config);
// 通知配置变更
if (oldConfig.portName != config.portName || oldConfig.baudRate != config.baudRate) {
emit configurationChanged(oldConfig, config);
}
emit log(QString("[%1] Configuration updated: %2 @ %3 bps")
.arg(m_deviceName, config.portName).arg(config.baudRate));
return true;
}
void SerialPortClient::open() {
if (!hasConfiguration()) {
emit error(QString("[%1] Not configured. Call setConfiguration() first.").arg(m_deviceName));
return;
}
emit log(QString("[%1] Opening serial port: %2").arg(m_deviceName, m_config.portName));
emit internalOpen();
}
void SerialPortClient::close() {
emit log(QString("[%1] Closing serial port...").arg(m_deviceName));
emit internalClose();
}
void SerialPortClient::reconnect() {
if (!hasConfiguration()) {
emit error(QString("[%1] No configuration. Cannot reconnect.").arg(m_deviceName));
return;
}
emit log(QString("[%1] Attempting to reconnect...").arg(m_deviceName));
close();
QTimer::singleShot(100, this, [this]() { open(); });
}
void SerialPortClient::sendJson(const QString &type, const QJsonObject &data) {
if (!isOpen()) {
emit error(QString("[%1] Cannot send JSON: serial port not opened").arg(m_deviceName));
return;
}
emit internalSendJson(type, data);
}
void SerialPortClient::sendText(const QString &text) {
if (!isOpen()) {
emit error(QString("[%1] Cannot send text: serial port not opened").arg(m_deviceName));
return;
}
emit internalSendText(text);
}
void SerialPortClient::sendHex(const QString &hex) {
if (!isOpen()) {
emit error(QString("[%1] Cannot send hex: serial port not opened").arg(m_deviceName));
return;
}
emit internalSendHex(hex);
}
void SerialPortClient::sendRaw(const QByteArray &data) {
if (!isOpen()) {
emit error(QString("[%1] Cannot send raw: serial port not opened").arg(m_deviceName));
return;
}
emit internalSendRaw(data);
}
bool SerialPortClient::isOpen() const {
if (m_serialManager) {
bool opened = false;
QMetaObject::invokeMethod(const_cast<SerialPortManager*>(m_serialManager),
[&opened, mgr = m_serialManager]() {
opened = mgr->isOpen();
},
Qt::BlockingQueuedConnection);
return opened;
}
return false;
}
void SerialPortClient::setAutoReconnect(bool enabled) {
emit log(QString("[%1] Auto reconnect %2").arg(m_deviceName, enabled ? "enabled" : "disabled"));
emit internalSetAutoReconnect(enabled);
}
void SerialPortClient::setHeartbeatInterval(int msecs) {
emit log(QString("[%1] Heartbeat interval: %2 ms").arg(m_deviceName).arg(msecs));
emit internalSetHeartbeatInterval(msecs);
}
QStringList SerialPortClient::availablePorts() {
QStringList ports;
const auto portList = QSerialPortInfo::availablePorts();
for (const QSerialPortInfo &info : portList) {
QString desc = QString("%1 (%2)").arg(info.portName(), info.description());
ports << desc;
}
return ports;
}
+1 -4
View File
@@ -2,8 +2,7 @@
// Created by Administrator on 2025/1/21.
//
#ifndef AIRI_DESKTOPGRIL_SETTING_H
#define AIRI_DESKTOPGRIL_SETTING_H
#pragma once
#include <ElaWidget.h>
#include <ElaWindow.h>
@@ -64,5 +63,3 @@ private:
};
#endif //AIRI_DESKTOPGRIL_SETTING_H
+1 -4
View File
@@ -2,8 +2,7 @@
// Created by Administrator on 2025/3/2.
//
#ifndef AIRI_DESKTOPGRIL_UISETTING_H
#define AIRI_DESKTOPGRIL_UISETTING_H
#pragma once
#include "BasePage.h"
class ElaRadioButton;
@@ -25,5 +24,3 @@ private:
ElaRadioButton* _maximumButton = nullptr;
ElaRadioButton* _autoButton = nullptr;
};
#endif //AIRI_DESKTOPGRIL_UISETTING_H
+227
View File
@@ -0,0 +1,227 @@
//
// Created by misaki on 2026/1/26.
//
/**
* cobs.hpp
* 所谓COBS,即Consistent Overhead Byte Stuffing(持续开销字节填充)
* 是一种将字节包编码成不包含值为零的字节(0x00)形式的方法。
* 输入的字节包可以包含从 0x00 到 0xFF 的全部范围内的字节。
* COBS 编码的数据包保证生成字节范围 0x01 到 0xFF 的数据包。
* 因此,在通信协议中,数据包边界可以用 0x00 字节可靠地界定。
*
* 在Yosuga项目当中,COBS编码被用于解决Yosuga与嵌入式设备使用串口收发数据时出现的粘包问题。
* 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。
*
* 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。
*/
#pragma once
#include <cstdint>
#include <cstddef>
#include <span>
#include <vector>
#include <string>
namespace cobs {
// 状态码
enum class [[nodiscard]] Status : uint8_t {
OK = 0x00,
NULL_POINTER = 0x01,
OUT_BUFFER_OVERFLOW = 0x02,
ZERO_BYTE_IN_INPUT = 0x04, // 仅 decode
INPUT_TOO_SHORT = 0x08 // 仅 decode
};
// 结果结构体
struct [[nodiscard]] EncodeResult {
size_t out_len = 0;
Status status = Status::OK;
};
struct [[nodiscard]] DecodeResult {
size_t out_len = 0;
Status status = Status::OK;
};
// 缓冲区大小计算
constexpr size_t encode_dst_len_max(const size_t src_len) noexcept {
return (src_len == 0) ? 1 : (src_len + (src_len + 253) / 254);
}
constexpr size_t decode_dst_len_max(const size_t src_len) noexcept {
return (src_len == 0) ? 0 : (src_len - 1);
}
constexpr size_t encode_src_offset(const size_t src_len) noexcept {
return (src_len + 253) / 254;
}
// 底层核心实现
inline EncodeResult encode_core(std::span<uint8_t> dst, const std::span<const uint8_t> src) noexcept {
EncodeResult result;
if (dst.empty() || src.empty()) {
result.status = Status::NULL_POINTER;
return result;
}
const uint8_t* src_read_ptr = src.data();
const uint8_t* src_end_ptr = src_read_ptr + src.size();
uint8_t* dst_start_ptr = dst.data();
const uint8_t* dst_end_ptr = dst_start_ptr + dst.size();
uint8_t* dst_code_write_ptr = dst_start_ptr;
uint8_t* dst_write_ptr = dst_code_write_ptr + 1;
uint8_t search_len = 1;
if (src.empty()) {
*dst_code_write_ptr = search_len;
result.out_len = 1;
return result;
}
for (;;) {
if (dst_write_ptr >= dst_end_ptr) {
result.status = Status::OUT_BUFFER_OVERFLOW;
break;
}
const uint8_t src_byte = *src_read_ptr++;
if (src_byte == 0) {
*dst_code_write_ptr = search_len;
dst_code_write_ptr = dst_write_ptr++;
search_len = 1;
if (src_read_ptr >= src_end_ptr) break;
} else {
*dst_write_ptr++ = src_byte;
search_len++;
if (src_read_ptr >= src_end_ptr) break;
if (search_len == 0xFF) {
*dst_code_write_ptr = search_len;
dst_code_write_ptr = dst_write_ptr++;
search_len = 1;
}
}
}
if (dst_code_write_ptr >= dst_end_ptr) {
result.status = Status::OUT_BUFFER_OVERFLOW;
} else {
*dst_code_write_ptr = search_len;
}
result.out_len = static_cast<size_t>(dst_write_ptr - dst_start_ptr);
return result;
}
inline DecodeResult decode_core(std::span<uint8_t> dst, const std::span<const uint8_t> src) noexcept {
DecodeResult result;
if (dst.empty() || src.empty()) {
result.status = Status::NULL_POINTER;
return result;
}
const uint8_t* src_read_ptr = src.data();
const uint8_t* src_end_ptr = src_read_ptr + src.size();
uint8_t* dst_start_ptr = dst.data();
uint8_t* dst_end_ptr = dst_start_ptr + dst.size();
uint8_t* dst_write_ptr = dst_start_ptr;
if (src.empty()) {
return result; // out_len = 0, status = OK
}
for (;;) {
uint8_t len_code = *src_read_ptr++;
if (len_code == 0) {
result.status = Status::ZERO_BYTE_IN_INPUT;
break;
}
len_code--;
auto remaining = static_cast<size_t>(src_end_ptr - src_read_ptr);
if (len_code > remaining) {
result.status = Status::INPUT_TOO_SHORT;
len_code = static_cast<uint8_t>(remaining);
}
remaining = static_cast<size_t>(dst_end_ptr - dst_write_ptr);
if (len_code > remaining) {
result.status = Status::OUT_BUFFER_OVERFLOW;
len_code = static_cast<uint8_t>(remaining);
}
for (uint8_t i = len_code; i != 0; i--) {
const uint8_t src_byte = *src_read_ptr++;
if (src_byte == 0) {
result.status = Status::ZERO_BYTE_IN_INPUT;
}
*dst_write_ptr++ = src_byte;
}
if (src_read_ptr >= src_end_ptr) break;
if (len_code != 0xFE) {
if (dst_write_ptr >= dst_end_ptr) {
result.status = Status::OUT_BUFFER_OVERFLOW;
break;
}
*dst_write_ptr++ = 0;
}
}
result.out_len = static_cast<size_t>(dst_write_ptr - dst_start_ptr);
return result;
}
// 便捷接口(std::vector
inline EncodeResult encode(std::vector<uint8_t>& dst, const std::span<const uint8_t> src) noexcept {
dst.resize(encode_dst_len_max(src.size()));
const auto result = encode_core(dst, src);
dst.resize(result.out_len);
return result;
}
inline DecodeResult decode(std::vector<uint8_t>& dst, const std::span<const uint8_t> src) noexcept {
dst.resize(decode_dst_len_max(src.size()));
const auto result = decode_core(dst, src);
dst.resize(result.out_len);
return result;
}
// 便捷接口(std::string
inline EncodeResult encode(std::string& dst, const std::string_view src) noexcept {
dst.resize(encode_dst_len_max(src.size()));
const auto result = encode_core(
std::span<uint8_t>(reinterpret_cast<uint8_t*>(dst.data()), dst.size()),
std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(src.data()), src.size())
);
dst.resize(result.out_len);
return result;
}
inline DecodeResult decode(std::string& dst, const std::span<const uint8_t> src) noexcept {
std::vector<uint8_t> temp;
const auto result = decode(temp, src);
if (result.status == Status::OK) {
dst.assign(reinterpret_cast<const char*>(temp.data()), temp.size());
}
return {result.out_len, result.status};
}
// 类型安全辅助函数
template <typename T>
requires std::is_trivially_copyable_v<T>
inline EncodeResult encode(std::vector<uint8_t>& dst, const T& obj) noexcept {
return encode(dst, std::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(&obj), sizeof(T)
));
}
template <typename T>
requires std::is_trivially_copyable_v<T>
inline DecodeResult decode(T& obj, const std::span<const uint8_t> src) noexcept {
return decode_core(std::span<uint8_t>(
reinterpret_cast<uint8_t*>(&obj), sizeof(T)
), src);
}
} // namespace cobs