1. 集成autogui-cpp库,以支持后续的自动化操作需求
2. 增加了串口设备管理类,以支持无法联网的嵌入式设备接入Yosuga 3. 基于COBS编码以解决串口收发的粘包问题
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/24.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* 客户端业务核心
|
||||
* 1. 处理来自服务端的数据,分发并执行
|
||||
* 2. 完成非阻塞的事件循环处理,构建业务状态机
|
||||
*/
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/24.
|
||||
//
|
||||
|
||||
#include "AppCore.h"
|
||||
@@ -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);
|
||||
|
||||
@@ -44,7 +44,7 @@ NetworkDO::~NetworkDO()
|
||||
// 业务逻辑实现
|
||||
void NetworkDO::registerSender(SenderFunc sender)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex); // 简单保护一下赋值
|
||||
QMutexLocker locker(&m_mutex); // 加个小锁,简单保护一下赋值
|
||||
m_sender = std::move(sender);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; /// 配置缓存
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user