1. 增加了嵌入式rpc的dto封装,负责中转来自嵌入式设备的信息,支持tcp,websocket,serial常见的通信方式。嵌入式rpc框架可见Yosuga_embedded.
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
|
||||
_**本项目为Yosuga.**_
|
||||
|
||||
本项目使用CMake构建,基于C++Qt6.6.3以及Live2D官方SDK(CubismSdkForNative-5-r.4.1)实现Live2D桌面宠物
|
||||
本项目使用CMake构建,基于C++Qt6.6.3以及Live2D官方SDK(CubismSdkForNative-5-r.4.1)实现Live2D桌面助手
|
||||
(本项目由[Yosuga-qt5](https://github.com/Misakityan/Yosuga-qt5) 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台)
|
||||
|
||||
环境为:
|
||||
|
||||
+17
-11
@@ -11,32 +11,38 @@
|
||||
*/
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include "serialportmanager.h"
|
||||
|
||||
class DeviceTcpServer;
|
||||
class DeviceWebSocketServer;
|
||||
|
||||
class AppCore final : public QObject {
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(AppCore) // 禁用拷贝
|
||||
Q_DISABLE_COPY(AppCore)
|
||||
|
||||
private:
|
||||
/**
|
||||
* 构造函数私有化
|
||||
* @param parent
|
||||
*/
|
||||
explicit AppCore(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
|
||||
explicit AppCore(QObject *parent = nullptr);
|
||||
|
||||
static QScopedPointer<AppCore> m_instance; // 单例类
|
||||
static QScopedPointer<AppCore> m_instance;
|
||||
static QMutex m_mutex;
|
||||
|
||||
DeviceTcpServer *m_deviceTcpServer = nullptr;
|
||||
DeviceWebSocketServer *m_deviceWsServer = nullptr;
|
||||
|
||||
private slots:
|
||||
// 业务接收槽函数
|
||||
void onRecordingFinished_Byte(const QByteArray &wavData);
|
||||
|
||||
public:
|
||||
// 单例访问点
|
||||
static AppCore *getInstance();
|
||||
// 显式销毁
|
||||
static void destroy();
|
||||
|
||||
~AppCore() override;
|
||||
|
||||
void registerEmbeddedDevice(const QString &deviceId, SerialPortClient *client);
|
||||
void unregisterEmbeddedDevice(const QString &deviceId);
|
||||
|
||||
public:
|
||||
// 单次对话
|
||||
void SingleExchange();
|
||||
void tryToInit() { return; };
|
||||
};
|
||||
@@ -8,10 +8,13 @@
|
||||
#include "AudioDataHandle.h"
|
||||
#include "AutoAgentHandle.h"
|
||||
#include "ScreenShotReqDataHandle.h"
|
||||
#include "DeviceDataHandle.h"
|
||||
|
||||
#include "AudioInput.h"
|
||||
#include "NetWorkDO.h"
|
||||
#include "websocketmanager.h"
|
||||
#include "DeviceTcpServer.h"
|
||||
#include "DeviceWebSocketServer.h"
|
||||
// 初始化静态成员
|
||||
QScopedPointer<AppCore> AppCore::m_instance;
|
||||
QMutex AppCore::m_mutex;
|
||||
@@ -39,6 +42,32 @@ void AppCore::destroy()
|
||||
|
||||
AppCore::AppCore(QObject *parent) : QObject(parent)
|
||||
{
|
||||
DeviceDataHandle *deviceHandle = DeviceDataHandle::getInstance();
|
||||
|
||||
// 启动嵌入式设备 TCP 服务器
|
||||
m_deviceTcpServer = new DeviceTcpServer(10001, this);
|
||||
connect(m_deviceTcpServer, &DeviceTcpServer::deviceConnected,
|
||||
deviceHandle, [=](const QString &deviceId, const QString &) {
|
||||
deviceHandle->registerDevice(deviceId, "tcp", m_deviceTcpServer);
|
||||
});
|
||||
connect(m_deviceTcpServer, &DeviceTcpServer::deviceDisconnected,
|
||||
deviceHandle, &DeviceDataHandle::unregisterDevice);
|
||||
connect(m_deviceTcpServer, &DeviceTcpServer::jsonReceived,
|
||||
deviceHandle, &DeviceDataHandle::onTcpDeviceData);
|
||||
m_deviceTcpServer->start();
|
||||
|
||||
// 启动嵌入式设备 WebSocket 服务器
|
||||
m_deviceWsServer = new DeviceWebSocketServer(10002, this);
|
||||
connect(m_deviceWsServer, &DeviceWebSocketServer::deviceConnected,
|
||||
deviceHandle, [=](const QString &deviceId, const QString &) {
|
||||
deviceHandle->registerDevice(deviceId, "websocket", m_deviceWsServer);
|
||||
});
|
||||
connect(m_deviceWsServer, &DeviceWebSocketServer::deviceDisconnected,
|
||||
deviceHandle, &DeviceDataHandle::unregisterDevice);
|
||||
connect(m_deviceWsServer, &DeviceWebSocketServer::jsonReceived,
|
||||
deviceHandle, &DeviceDataHandle::onWsDeviceData);
|
||||
m_deviceWsServer->start();
|
||||
|
||||
// 初始化业务解析单例
|
||||
AudioDataHandle::getInstance();
|
||||
AutoAgentHandle::getInstance();
|
||||
@@ -51,20 +80,32 @@ AppCore::AppCore(QObject *parent) : QObject(parent)
|
||||
AudioInput::getInstance()->setAudioPath(QDir::currentPath(), "/temp.wav");
|
||||
// 连接必要的信号
|
||||
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte,
|
||||
this, &AppCore::onRecordingFinished_Byte); // 录音完成信号
|
||||
this, &AppCore::onRecordingFinished_Byte);
|
||||
}
|
||||
|
||||
AppCore::~AppCore()
|
||||
{
|
||||
// 析构业务解析单例
|
||||
ScreenShotReqDataHandle::destroy();
|
||||
AutoAgentHandle::destroy(); // 显式销毁
|
||||
AudioDataHandle::destroy();
|
||||
if (m_deviceTcpServer) m_deviceTcpServer->stop();
|
||||
if (m_deviceWsServer) m_deviceWsServer->stop();
|
||||
|
||||
ScreenShotReqDataHandle::destroy();
|
||||
AutoAgentHandle::destroy();
|
||||
AudioDataHandle::destroy();
|
||||
DeviceDataHandle::destroy();
|
||||
|
||||
qDebug() << "AppCore destroyed";
|
||||
}
|
||||
|
||||
void AppCore::registerEmbeddedDevice(const QString &deviceId, SerialPortClient *client)
|
||||
{
|
||||
DeviceDataHandle::getInstance()->registerDevice(deviceId, QStringLiteral("serial"), client);
|
||||
}
|
||||
|
||||
void AppCore::unregisterEmbeddedDevice(const QString &deviceId)
|
||||
{
|
||||
DeviceDataHandle::getInstance()->unregisterDevice(deviceId);
|
||||
}
|
||||
|
||||
void AppCore::SingleExchange() {
|
||||
// 开始录音,录音结束后会触发录音完成信号
|
||||
AudioInput::getInstance()->startAutoStopAudio(AudioInput::getInstance()->getSilenceThreshold(), 800);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// Created by Yosuga on 2026/4/25.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include "DataTransferObjectBase.h"
|
||||
|
||||
class DeviceDataTransferObject final : public DataTransferObjectBase {
|
||||
public:
|
||||
explicit DeviceDataTransferObject(
|
||||
QString action = "",
|
||||
QString deviceId = "",
|
||||
QJsonObject payload = {}
|
||||
);
|
||||
|
||||
static DeviceDataTransferObject fromJson(const QJsonObject& json);
|
||||
|
||||
// WebSocket 类型固定为 "device_data"
|
||||
[[nodiscard]] QString type() const override { return QStringLiteral("device_data"); }
|
||||
|
||||
[[nodiscard]] QJsonObject toJson() const override;
|
||||
DeviceDataTransferObject& setData(const QString& key, const QJsonValue& value) override;
|
||||
|
||||
[[nodiscard]] QString action() const { return m_action; }
|
||||
[[nodiscard]] QString deviceId() const { return m_deviceId; }
|
||||
[[nodiscard]] QJsonObject payload() const { return m_payload; }
|
||||
|
||||
private:
|
||||
QString m_action;
|
||||
QString m_deviceId;
|
||||
QJsonObject m_payload;
|
||||
};
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "AudioDataTransferObject.h"
|
||||
#include "AutoAgentDataObject.h"
|
||||
#include "ScreenShotDataTransferObject.h"
|
||||
#include "DeviceDataTransferObject.h"
|
||||
/**
|
||||
* NetworkDO
|
||||
*/
|
||||
@@ -58,6 +59,7 @@ signals:
|
||||
void audioPacketReceived(const AudioDataTransferObject& packet); // 音频数据准备完成信号
|
||||
void autoAgentPacketReceived(const AutoAgentDataObject& packet); // 自动代理数据包接收信号
|
||||
void screenShotPacketReceived(const ScreenShotDataTransferObject& packet); // 截图数据包接收信号
|
||||
void deviceCommandReceived(const DeviceDataTransferObject& packet); // 设备控制命令(服务端→客户端)
|
||||
|
||||
void errorOccurred(const QString& errorMsg); // 错误信号
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// Created by Yosuga on 2026/4/25.
|
||||
//
|
||||
|
||||
#include "DeviceDataTransferObject.h"
|
||||
|
||||
DeviceDataTransferObject::DeviceDataTransferObject(
|
||||
QString action,
|
||||
QString deviceId,
|
||||
QJsonObject payload
|
||||
) : m_action(std::move(action))
|
||||
, m_deviceId(std::move(deviceId))
|
||||
, m_payload(std::move(payload))
|
||||
{
|
||||
}
|
||||
|
||||
DeviceDataTransferObject DeviceDataTransferObject::fromJson(const QJsonObject& json)
|
||||
{
|
||||
DeviceDataTransferObject obj;
|
||||
obj.m_action = json.value("action").toString("device_command");
|
||||
obj.m_deviceId = json.value("device_id").toString("");
|
||||
QJsonValue payloadVal = json.value("payload");
|
||||
if (payloadVal.isString()) {
|
||||
obj.m_payload["rpc_call"] = payloadVal.toString();
|
||||
} else if (payloadVal.isObject()) {
|
||||
obj.m_payload = payloadVal.toObject();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
QJsonObject DeviceDataTransferObject::toJson() const
|
||||
{
|
||||
QJsonObject json;
|
||||
json["action"] = m_action;
|
||||
if (!m_deviceId.isEmpty()) {
|
||||
json["device_id"] = m_deviceId;
|
||||
}
|
||||
json["payload"] = m_payload;
|
||||
return json;
|
||||
}
|
||||
|
||||
DeviceDataTransferObject& DeviceDataTransferObject::setData(const QString& key, const QJsonValue& value)
|
||||
{
|
||||
if (key == "action") m_action = value.toString();
|
||||
else if (key == "device_id") m_deviceId = value.toString();
|
||||
else if (key == "payload") m_payload = value.toObject();
|
||||
return *this;
|
||||
}
|
||||
@@ -75,6 +75,9 @@ void NetworkDO::onDataReceived(const QString& type, const QJsonObject& data)
|
||||
else if (type == "screenshot_data") {
|
||||
emit screenShotPacketReceived(ScreenShotDataTransferObject::fromJson(data));
|
||||
}
|
||||
else if (type == "device_command") {
|
||||
emit deviceCommandReceived(DeviceDataTransferObject::fromJson(data));
|
||||
}
|
||||
else {
|
||||
qWarning() << "[NetworkDO] Received unknown type:" << type;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// Created by Yosuga on 2026/4/25.
|
||||
//
|
||||
|
||||
/**
|
||||
* 设备数据处理模块 — 统一路由器
|
||||
*
|
||||
* 管理三种设备连接通道:
|
||||
* - TCP (RK3566 等通过 TCP 接入)
|
||||
* - WebSocket (ESP32 等通过 WebSocket 接入)
|
||||
* - 串口 (STM32 等通过串口接入)
|
||||
*
|
||||
* 数据流向:
|
||||
* Device →(TCP/WS/Serial)→ DeviceDataHandle → NetworkDO → WebSocket → YosugaServer
|
||||
* YosugaServer → WebSocket → NetworkDO → DeviceDataHandle →(TCP/WS/Serial)→ Device
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include <QScopedPointer>
|
||||
#include <QHash>
|
||||
#include <functional>
|
||||
#include "DeviceDataTransferObject.h"
|
||||
#include "serialportmanager.h"
|
||||
|
||||
class DeviceDataHandle final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(DeviceDataHandle)
|
||||
|
||||
private:
|
||||
explicit DeviceDataHandle(QObject *parent = nullptr);
|
||||
static QScopedPointer<DeviceDataHandle> m_instance;
|
||||
static QMutex m_mutex;
|
||||
|
||||
public:
|
||||
static DeviceDataHandle *getInstance();
|
||||
static void destroy();
|
||||
~DeviceDataHandle() override;
|
||||
|
||||
// 注册设备到路由表(由各 Server/Client 在设备握手完成后调用)
|
||||
void registerDevice(const QString &deviceId, const QString &deviceType, QObject *connection);
|
||||
|
||||
// 移除设备
|
||||
void unregisterDevice(const QString &deviceId);
|
||||
|
||||
// 向设备发送数据(自动选择正确的通道)
|
||||
void sendToDevice(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
// 获取设备连接类型
|
||||
[[nodiscard]] QString deviceConnectionType(const QString &deviceId) const;
|
||||
|
||||
public slots:
|
||||
// 收到来自 YosugaServer 的设备命令(通过 device_command 信号)
|
||||
void onDeviceCommandReceived(const DeviceDataTransferObject &packet);
|
||||
|
||||
// 收到来自 TCP 设备的 JSON 数据
|
||||
void onTcpDeviceData(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
// 收到来自 WebSocket 设备的 JSON 数据
|
||||
void onWsDeviceData(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
// 收到来自串口设备的 JSON 数据
|
||||
void onSerialDeviceData(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
private:
|
||||
// 转发设备数据到 YosugaServer
|
||||
void forwardToServer(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
struct DeviceEntry {
|
||||
QString deviceId;
|
||||
QString deviceType; // "tcp", "websocket", "serial"
|
||||
QObject *connection; // DeviceTcpServer / DeviceWebSocketServer / SerialPortClient
|
||||
};
|
||||
QHash<QString, DeviceEntry> m_devices;
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// Created by Yosuga on 2026/4/25.
|
||||
//
|
||||
|
||||
#include "DeviceDataHandle.h"
|
||||
#include "NetWorkDO.h"
|
||||
#include <QDebug>
|
||||
#include <QMetaMethod>
|
||||
|
||||
QScopedPointer<DeviceDataHandle> DeviceDataHandle::m_instance;
|
||||
QMutex DeviceDataHandle::m_mutex;
|
||||
|
||||
DeviceDataHandle *DeviceDataHandle::getInstance()
|
||||
{
|
||||
if (m_instance.isNull()) {
|
||||
QMutexLocker locker(&m_mutex);
|
||||
if (m_instance.isNull()) {
|
||||
m_instance.reset(new DeviceDataHandle());
|
||||
}
|
||||
}
|
||||
return m_instance.data();
|
||||
}
|
||||
|
||||
void DeviceDataHandle::destroy()
|
||||
{
|
||||
QMutexLocker locker(&m_mutex);
|
||||
m_instance.reset();
|
||||
}
|
||||
|
||||
DeviceDataHandle::DeviceDataHandle(QObject *parent) : QObject(parent)
|
||||
{
|
||||
qRegisterMetaType<QJsonObject>("QJsonObject");
|
||||
|
||||
connect(NetworkDO::getInstance(), &NetworkDO::deviceCommandReceived,
|
||||
this, &DeviceDataHandle::onDeviceCommandReceived);
|
||||
}
|
||||
|
||||
DeviceDataHandle::~DeviceDataHandle()
|
||||
{
|
||||
qDebug() << "[DeviceDataHandle] destroyed";
|
||||
}
|
||||
|
||||
void DeviceDataHandle::registerDevice(const QString &deviceId, const QString &deviceType, QObject *connection)
|
||||
{
|
||||
DeviceEntry entry;
|
||||
entry.deviceId = deviceId;
|
||||
entry.deviceType = deviceType;
|
||||
entry.connection = connection;
|
||||
m_devices.insert(deviceId, entry);
|
||||
|
||||
qDebug() << "[DeviceDataHandle] 设备已注册:" << deviceId << "类型:" << deviceType;
|
||||
}
|
||||
|
||||
void DeviceDataHandle::unregisterDevice(const QString &deviceId)
|
||||
{
|
||||
m_devices.remove(deviceId);
|
||||
qDebug() << "[DeviceDataHandle] 设备已移除:" << deviceId;
|
||||
}
|
||||
|
||||
void DeviceDataHandle::sendToDevice(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
DeviceEntry entry = m_devices.value(deviceId);
|
||||
if (entry.connection == nullptr) {
|
||||
qWarning() << "[DeviceDataHandle] 未知设备:" << deviceId;
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过 QMetaObject::invokeMethod 动态调用对应 Server 的 sendToDevice
|
||||
bool ok = QMetaObject::invokeMethod(
|
||||
entry.connection,
|
||||
"sendToDevice",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(QString, deviceId),
|
||||
Q_ARG(QString, type),
|
||||
Q_ARG(QJsonObject, data)
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
// 兜底:如果是串口设备,直接调用 SerialPortClient::sendJson
|
||||
auto *serialClient = qobject_cast<SerialPortClient*>(entry.connection);
|
||||
if (serialClient) {
|
||||
serialClient->sendJson(type, data);
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
qWarning() << "[DeviceDataHandle] 发送失败到设备:" << deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
QString DeviceDataHandle::deviceConnectionType(const QString &deviceId) const
|
||||
{
|
||||
return m_devices.value(deviceId).deviceType;
|
||||
}
|
||||
|
||||
void DeviceDataHandle::onDeviceCommandReceived(const DeviceDataTransferObject &packet)
|
||||
{
|
||||
const QString deviceId = packet.deviceId();
|
||||
if (deviceId.isEmpty()) {
|
||||
qWarning() << "[DeviceDataHandle] device_command 缺少 device_id";
|
||||
return;
|
||||
}
|
||||
|
||||
// 提取 RPC 调用字符串
|
||||
QJsonObject payload = packet.payload();
|
||||
QString rpcCall;
|
||||
if (payload.contains("rpc_call")) {
|
||||
rpcCall = payload.value("rpc_call").toString();
|
||||
} else {
|
||||
rpcCall = QString::fromUtf8(QJsonDocument(payload).toJson(QJsonDocument::Compact));
|
||||
}
|
||||
|
||||
QJsonObject forwardPayload;
|
||||
forwardPayload["rpc_call"] = rpcCall;
|
||||
sendToDevice(deviceId, "rpc_call", forwardPayload);
|
||||
qDebug() << "[DeviceDataHandle] 已转发命令到设备:" << deviceId;
|
||||
}
|
||||
|
||||
void DeviceDataHandle::onTcpDeviceData(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
forwardToServer(deviceId, type, data);
|
||||
}
|
||||
|
||||
void DeviceDataHandle::onWsDeviceData(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
forwardToServer(deviceId, type, data);
|
||||
}
|
||||
|
||||
void DeviceDataHandle::onSerialDeviceData(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
forwardToServer(deviceId, type, data);
|
||||
}
|
||||
|
||||
void DeviceDataHandle::forwardToServer(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
// 所有设备数据统一封装为 DeviceDataTransferObject 发往 YosugaServer
|
||||
DeviceDataTransferObject packet(type, deviceId, data);
|
||||
NetworkDO::getInstance()->sendPacket(packet);
|
||||
qDebug() << "[DeviceDataHandle] 设备数据已转发到服务端:" << deviceId << type;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Created by misaki on 2026/4/25.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QByteArray>
|
||||
|
||||
class DeviceTcpServer final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(DeviceTcpServer)
|
||||
|
||||
public:
|
||||
explicit DeviceTcpServer(quint16 port = 10001, QObject *parent = nullptr);
|
||||
~DeviceTcpServer() override;
|
||||
|
||||
bool start();
|
||||
void stop();
|
||||
[[nodiscard]] bool isListening() const { return m_server && m_server->isListening(); }
|
||||
[[nodiscard]] quint16 serverPort() const { return m_port; }
|
||||
|
||||
Q_INVOKABLE void sendToDevice(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
signals:
|
||||
void deviceConnected(const QString &deviceId, const QString &deviceName);
|
||||
void deviceDisconnected(const QString &deviceId);
|
||||
void jsonReceived(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
void serverError(const QString &errorMsg);
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
void onClientDisconnected();
|
||||
void onReadyRead();
|
||||
|
||||
private:
|
||||
struct DeviceSession {
|
||||
QString deviceId;
|
||||
QString deviceName;
|
||||
QTcpSocket *socket;
|
||||
QByteArray buffer;
|
||||
};
|
||||
|
||||
QTcpServer *m_server;
|
||||
quint16 m_port;
|
||||
QHash<QString, DeviceSession*> m_deviceSessions; // deviceId -> session
|
||||
QHash<QTcpSocket*, DeviceSession*> m_socketSessions; // socket -> session
|
||||
|
||||
void parseIncomingData(DeviceSession *session);
|
||||
void removeSession(QTcpSocket *socket);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// Created by misaki on 2026/4/25.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
#include <QObject>
|
||||
#include <QWebSocketServer>
|
||||
#include <QWebSocket>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
|
||||
class DeviceWebSocketServer final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(DeviceWebSocketServer)
|
||||
|
||||
public:
|
||||
explicit DeviceWebSocketServer(quint16 port = 10002, QObject *parent = nullptr);
|
||||
~DeviceWebSocketServer() override;
|
||||
|
||||
bool start();
|
||||
void stop();
|
||||
[[nodiscard]] bool isListening() const { return m_server && m_server->isListening(); }
|
||||
[[nodiscard]] quint16 serverPort() const { return m_port; }
|
||||
|
||||
Q_INVOKABLE void sendToDevice(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
|
||||
signals:
|
||||
void deviceConnected(const QString &deviceId, const QString &deviceName);
|
||||
void deviceDisconnected(const QString &deviceId);
|
||||
void jsonReceived(const QString &deviceId, const QString &type, const QJsonObject &data);
|
||||
void serverError(const QString &errorMsg);
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
void onClientDisconnected();
|
||||
void onTextMessageReceived(const QString &message);
|
||||
|
||||
private:
|
||||
struct DeviceSession {
|
||||
QString deviceId;
|
||||
QString deviceName;
|
||||
QWebSocket *socket;
|
||||
};
|
||||
|
||||
QWebSocketServer *m_server;
|
||||
quint16 m_port;
|
||||
QHash<QString, DeviceSession*> m_deviceSessions;
|
||||
QHash<QWebSocket*, DeviceSession*> m_socketSessions;
|
||||
|
||||
void removeSession(QWebSocket *socket);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// Created by misaki on 2026/4/25.
|
||||
//
|
||||
|
||||
#include "DeviceTcpServer.h"
|
||||
#include <QDebug>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QDateTime>
|
||||
|
||||
DeviceTcpServer::DeviceTcpServer(quint16 port, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_server(new QTcpServer(this))
|
||||
, m_port(port)
|
||||
{
|
||||
connect(m_server, &QTcpServer::newConnection,
|
||||
this, &DeviceTcpServer::onNewConnection);
|
||||
}
|
||||
|
||||
DeviceTcpServer::~DeviceTcpServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool DeviceTcpServer::start()
|
||||
{
|
||||
if (m_server->isListening()) {
|
||||
qDebug() << "[DeviceTcpServer] Already listening on port" << m_port;
|
||||
return true;
|
||||
}
|
||||
if (!m_server->listen(QHostAddress::Any, m_port)) {
|
||||
QString err = QString("Failed to listen on TCP port %1: %2")
|
||||
.arg(m_port).arg(m_server->errorString());
|
||||
qWarning() << "[DeviceTcpServer]" << err;
|
||||
emit serverError(err);
|
||||
return false;
|
||||
}
|
||||
qDebug() << "[DeviceTcpServer] Listening for embedded devices on TCP port" << m_port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeviceTcpServer::stop()
|
||||
{
|
||||
for (auto it = m_socketSessions.begin(); it != m_socketSessions.end(); ++it) {
|
||||
DeviceSession *session = it.value();
|
||||
if (session->socket->state() == QAbstractSocket::ConnectedState) {
|
||||
session->socket->disconnectFromHost();
|
||||
}
|
||||
delete session;
|
||||
}
|
||||
m_deviceSessions.clear();
|
||||
m_socketSessions.clear();
|
||||
|
||||
if (m_server->isListening()) {
|
||||
m_server->close();
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceTcpServer::sendToDevice(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
DeviceSession *session = m_deviceSessions.value(deviceId);
|
||||
if (!session || !session->socket) {
|
||||
qWarning() << "[DeviceTcpServer] Cannot send to unknown device:" << deviceId;
|
||||
return;
|
||||
}
|
||||
QJsonObject msg;
|
||||
msg["type"] = type;
|
||||
msg["data"] = data;
|
||||
msg["timestamp"] = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
QByteArray payload = QJsonDocument(msg).toJson(QJsonDocument::Compact);
|
||||
session->socket->write(payload);
|
||||
session->socket->write("\n");
|
||||
session->socket->flush();
|
||||
}
|
||||
|
||||
void DeviceTcpServer::onNewConnection()
|
||||
{
|
||||
while (m_server->hasPendingConnections()) {
|
||||
QTcpSocket *socket = m_server->nextPendingConnection();
|
||||
if (!socket) continue;
|
||||
|
||||
auto *session = new DeviceSession{};
|
||||
session->socket = socket;
|
||||
m_socketSessions.insert(socket, session);
|
||||
|
||||
connect(socket, &QTcpSocket::disconnected,
|
||||
this, &DeviceTcpServer::onClientDisconnected);
|
||||
connect(socket, &QTcpSocket::readyRead,
|
||||
this, &DeviceTcpServer::onReadyRead);
|
||||
|
||||
qDebug() << "[DeviceTcpServer] New TCP connection from"
|
||||
<< socket->peerAddress().toString() << ":" << socket->peerPort();
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceTcpServer::onClientDisconnected()
|
||||
{
|
||||
auto *socket = qobject_cast<QTcpSocket*>(sender());
|
||||
if (socket) {
|
||||
removeSession(socket);
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceTcpServer::onReadyRead()
|
||||
{
|
||||
auto *socket = qobject_cast<QTcpSocket*>(sender());
|
||||
if (!socket) return;
|
||||
|
||||
DeviceSession *session = m_socketSessions.value(socket);
|
||||
if (!session) return;
|
||||
|
||||
session->buffer.append(socket->readAll());
|
||||
parseIncomingData(session);
|
||||
}
|
||||
|
||||
void DeviceTcpServer::parseIncomingData(DeviceSession *session)
|
||||
{
|
||||
// TCP: messages are newline-delimited JSON
|
||||
int newlinePos;
|
||||
while ((newlinePos = session->buffer.indexOf('\n')) >= 0) {
|
||||
QByteArray line = session->buffer.left(newlinePos).trimmed();
|
||||
session->buffer.remove(0, newlinePos + 1);
|
||||
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(line, &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "[DeviceTcpServer] Invalid JSON from device:" << err.errorString();
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject msg = doc.object();
|
||||
QString type = msg.value("type").toString();
|
||||
QString deviceId = msg.value("device_id").toString();
|
||||
QJsonObject payload = msg.value("payload").toObject();
|
||||
|
||||
// If this is a registration message, process it
|
||||
if (type == "register" || !session->deviceId.isEmpty()) {
|
||||
if (session->deviceId.isEmpty() && type == "register") {
|
||||
session->deviceId = deviceId;
|
||||
session->deviceName = payload.value("device").toObject().value("name").toString(deviceId);
|
||||
|
||||
m_deviceSessions.insert(session->deviceId, session);
|
||||
qDebug() << "[DeviceTcpServer] Device registered:"
|
||||
<< session->deviceId << "(" << session->deviceName << ")";
|
||||
|
||||
// Send ack
|
||||
QJsonObject ack;
|
||||
ack["status"] = "ok";
|
||||
ack["device_id"] = session->deviceId;
|
||||
sendToDevice(session->deviceId, "register_ack", ack);
|
||||
|
||||
emit deviceConnected(session->deviceId, session->deviceName);
|
||||
}
|
||||
|
||||
if (!session->deviceId.isEmpty()) {
|
||||
emit jsonReceived(session->deviceId, type, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceTcpServer::removeSession(QTcpSocket *socket)
|
||||
{
|
||||
DeviceSession *session = m_socketSessions.take(socket);
|
||||
if (!session) return;
|
||||
|
||||
QString deviceId = session->deviceId;
|
||||
if (!deviceId.isEmpty()) {
|
||||
m_deviceSessions.remove(deviceId);
|
||||
emit deviceDisconnected(deviceId);
|
||||
qDebug() << "[DeviceTcpServer] Device disconnected:" << deviceId;
|
||||
}
|
||||
|
||||
delete session;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// Created by misaki on 2026/4/25.
|
||||
//
|
||||
|
||||
#include "DeviceWebSocketServer.h"
|
||||
#include <QDebug>
|
||||
#include <QJsonParseError>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDateTime>
|
||||
|
||||
DeviceWebSocketServer::DeviceWebSocketServer(quint16 port, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_server(nullptr)
|
||||
, m_port(port)
|
||||
{
|
||||
}
|
||||
|
||||
DeviceWebSocketServer::~DeviceWebSocketServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool DeviceWebSocketServer::start()
|
||||
{
|
||||
if (m_server && m_server->isListening()) {
|
||||
qDebug() << "[DeviceWsServer] Already listening on port" << m_port;
|
||||
return true;
|
||||
}
|
||||
|
||||
m_server = new QWebSocketServer("Yosuga-Device-WS", QWebSocketServer::NonSecureMode, this);
|
||||
if (!m_server->listen(QHostAddress::Any, m_port)) {
|
||||
QString err = QString("Failed to listen on WS port %1: %2")
|
||||
.arg(m_port).arg(m_server->errorString());
|
||||
qWarning() << "[DeviceWsServer]" << err;
|
||||
emit serverError(err);
|
||||
m_server->deleteLater();
|
||||
m_server = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
connect(m_server, &QWebSocketServer::newConnection,
|
||||
this, &DeviceWebSocketServer::onNewConnection);
|
||||
|
||||
qDebug() << "[DeviceWsServer] Listening for embedded devices on WS port" << m_port;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeviceWebSocketServer::stop()
|
||||
{
|
||||
for (auto it = m_socketSessions.begin(); it != m_socketSessions.end(); ++it) {
|
||||
DeviceSession *session = it.value();
|
||||
if (session->socket->state() == QAbstractSocket::ConnectedState) {
|
||||
session->socket->close();
|
||||
}
|
||||
delete session;
|
||||
}
|
||||
m_deviceSessions.clear();
|
||||
m_socketSessions.clear();
|
||||
|
||||
if (m_server) {
|
||||
m_server->close();
|
||||
m_server->deleteLater();
|
||||
m_server = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceWebSocketServer::sendToDevice(const QString &deviceId, const QString &type, const QJsonObject &data)
|
||||
{
|
||||
DeviceSession *session = m_deviceSessions.value(deviceId);
|
||||
if (!session || !session->socket) {
|
||||
qWarning() << "[DeviceWsServer] Cannot send to unknown device:" << deviceId;
|
||||
return;
|
||||
}
|
||||
QJsonObject msg;
|
||||
msg["type"] = type;
|
||||
msg["data"] = data;
|
||||
msg["timestamp"] = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
QByteArray payload = QJsonDocument(msg).toJson(QJsonDocument::Compact);
|
||||
session->socket->sendTextMessage(QString::fromUtf8(payload));
|
||||
}
|
||||
|
||||
void DeviceWebSocketServer::onNewConnection()
|
||||
{
|
||||
while (m_server->hasPendingConnections()) {
|
||||
QWebSocket *socket = m_server->nextPendingConnection();
|
||||
if (!socket) continue;
|
||||
|
||||
auto *session = new DeviceSession{};
|
||||
session->socket = socket;
|
||||
m_socketSessions.insert(socket, session);
|
||||
|
||||
connect(socket, &QWebSocket::disconnected,
|
||||
this, &DeviceWebSocketServer::onClientDisconnected);
|
||||
connect(socket, &QWebSocket::textMessageReceived,
|
||||
this, &DeviceWebSocketServer::onTextMessageReceived);
|
||||
|
||||
qDebug() << "[DeviceWsServer] New WS connection from"
|
||||
<< socket->peerAddress().toString() << ":" << socket->peerPort();
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceWebSocketServer::onClientDisconnected()
|
||||
{
|
||||
auto *socket = qobject_cast<QWebSocket*>(sender());
|
||||
if (socket) {
|
||||
removeSession(socket);
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceWebSocketServer::onTextMessageReceived(const QString &message)
|
||||
{
|
||||
auto *socket = qobject_cast<QWebSocket*>(sender());
|
||||
if (!socket) return;
|
||||
|
||||
DeviceSession *session = m_socketSessions.value(socket);
|
||||
if (!session) return;
|
||||
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8(), &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
qWarning() << "[DeviceWsServer] Invalid JSON:" << err.errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject msg = doc.object();
|
||||
QString type = msg.value("type").toString();
|
||||
QString deviceId = msg.value("device_id").toString();
|
||||
QJsonObject payload = msg.value("payload").toObject();
|
||||
|
||||
if (type == "register") {
|
||||
session->deviceId = deviceId;
|
||||
session->deviceName = payload.value("device").toObject().value("name").toString(deviceId);
|
||||
|
||||
m_deviceSessions.insert(session->deviceId, session);
|
||||
qDebug() << "[DeviceWsServer] Device registered:" << session->deviceId;
|
||||
|
||||
QJsonObject ack;
|
||||
ack["status"] = "ok";
|
||||
ack["device_id"] = session->deviceId;
|
||||
sendToDevice(session->deviceId, "register_ack", ack);
|
||||
|
||||
emit deviceConnected(session->deviceId, session->deviceName);
|
||||
}
|
||||
|
||||
if (!session->deviceId.isEmpty()) {
|
||||
emit jsonReceived(session->deviceId, type, payload);
|
||||
}
|
||||
}
|
||||
|
||||
void DeviceWebSocketServer::removeSession(QWebSocket *socket)
|
||||
{
|
||||
DeviceSession *session = m_socketSessions.take(socket);
|
||||
if (!session) return;
|
||||
|
||||
QString deviceId = session->deviceId;
|
||||
if (!deviceId.isEmpty()) {
|
||||
m_deviceSessions.remove(deviceId);
|
||||
emit deviceDisconnected(deviceId);
|
||||
qDebug() << "[DeviceWsServer] Device disconnected:" << deviceId;
|
||||
}
|
||||
|
||||
delete session;
|
||||
}
|
||||
@@ -56,6 +56,7 @@ void Menu::createMenu()
|
||||
// 设置按钮
|
||||
connect(settingsAction, &QAction::triggered, this, [this]() {
|
||||
qDebug() << "Settings triggered";
|
||||
AppCore::getInstance()->tryToInit();
|
||||
// 打开设置窗口
|
||||
|
||||
// 如果 Setting 窗口已经存在,则不再创建
|
||||
|
||||
@@ -131,7 +131,7 @@ ModelPage::ModelPage(QWidget *parent)
|
||||
return nullptr;
|
||||
}
|
||||
// 执行真正的耗时加载
|
||||
// 调用我们在 Manager 里新写的函数
|
||||
// 调用在 Manager 里新写的函数
|
||||
LAppModel *model = LAppLive2DManager::GetInstance()->LoadModelInstance(dir, filename);
|
||||
// 清理子线程资源
|
||||
context->doneCurrent();
|
||||
|
||||
Reference in New Issue
Block a user