From 343732919356bff3a344742cbde8a4c09880f876 Mon Sep 17 00:00:00 2001 From: Misaki Date: Sat, 25 Apr 2026 08:53:34 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=B5=8C?= =?UTF-8?q?=E5=85=A5=E5=BC=8Frpc=E7=9A=84dto=E5=B0=81=E8=A3=85=EF=BC=8C?= =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=B8=AD=E8=BD=AC=E6=9D=A5=E8=87=AA=E5=B5=8C?= =?UTF-8?q?=E5=85=A5=E5=BC=8F=E8=AE=BE=E5=A4=87=E7=9A=84=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81tcp,websocket,serial=E5=B8=B8?= =?UTF-8?q?=E8=A7=81=E7=9A=84=E9=80=9A=E4=BF=A1=E6=96=B9=E5=BC=8F=E3=80=82?= =?UTF-8?q?=E5=B5=8C=E5=85=A5=E5=BC=8Frpc=E6=A1=86=E6=9E=B6=E5=8F=AF?= =?UTF-8?q?=E8=A7=81Yosuga=5Fembedded.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/Core/Inc/AppCore.h | 32 ++-- src/Core/Src/AppCore.cpp | 51 ++++- src/DAO/Inc/DeviceDataTransferObject.h | 37 ++++ src/DAO/Inc/NetWorkDO.h | 14 +- src/DAO/Src/DeviceDataTransferObject.cpp | 48 +++++ src/DAO/Src/NetWorkDO.cpp | 3 + .../DataObjectHandle/Inc/DeviceDataHandle.h | 77 ++++++++ .../DataObjectHandle/Src/DeviceDataHandle.cpp | 141 ++++++++++++++ .../NetWorkHandle/Inc/DeviceTcpServer.h | 56 ++++++ .../NetWorkHandle/Inc/DeviceWebSocketServer.h | 52 +++++ .../NetWorkHandle/Src/DeviceTcpServer.cpp | 179 ++++++++++++++++++ .../Src/DeviceWebSocketServer.cpp | 165 ++++++++++++++++ src/UI/Menu/Src/menu.cpp | 1 + src/UI/Setting/Src/ModelPage.cpp | 2 +- 15 files changed, 834 insertions(+), 26 deletions(-) create mode 100644 src/DAO/Inc/DeviceDataTransferObject.h create mode 100644 src/DAO/Src/DeviceDataTransferObject.cpp create mode 100644 src/Handle/DataObjectHandle/Inc/DeviceDataHandle.h create mode 100644 src/Handle/DataObjectHandle/Src/DeviceDataHandle.cpp create mode 100644 src/Handle/NetWorkHandle/Inc/DeviceTcpServer.h create mode 100644 src/Handle/NetWorkHandle/Inc/DeviceWebSocketServer.h create mode 100644 src/Handle/NetWorkHandle/Src/DeviceTcpServer.cpp create mode 100644 src/Handle/NetWorkHandle/Src/DeviceWebSocketServer.cpp diff --git a/README.md b/README.md index 2da6032..2da134c 100644 --- a/README.md +++ b/README.md @@ -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) 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台) 环境为: diff --git a/src/Core/Inc/AppCore.h b/src/Core/Inc/AppCore.h index a6e763b..106d238 100644 --- a/src/Core/Inc/AppCore.h +++ b/src/Core/Inc/AppCore.h @@ -11,32 +11,38 @@ */ #include #include +#include +#include "serialportmanager.h" + +class DeviceTcpServer; +class DeviceWebSocketServer; class AppCore final : public QObject { -Q_OBJECT -Q_DISABLE_COPY(AppCore) // 禁用拷贝 + Q_OBJECT + Q_DISABLE_COPY(AppCore) -private: - /** - * 构造函数私有化 - * @param parent - */ - explicit AppCore(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理 + private: + explicit AppCore(QObject *parent = nullptr); - static QScopedPointer m_instance; // 单例类 + static QScopedPointer 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; }; }; \ No newline at end of file diff --git a/src/Core/Src/AppCore.cpp b/src/Core/Src/AppCore.cpp index 6c74305..9a62528 100644 --- a/src/Core/Src/AppCore.cpp +++ b/src/Core/Src/AppCore.cpp @@ -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::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); diff --git a/src/DAO/Inc/DeviceDataTransferObject.h b/src/DAO/Inc/DeviceDataTransferObject.h new file mode 100644 index 0000000..8641a17 --- /dev/null +++ b/src/DAO/Inc/DeviceDataTransferObject.h @@ -0,0 +1,37 @@ +// +// Created by Yosuga on 2026/4/25. +// + +#pragma once +#include +#include +#include +#include +#include +#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; +}; diff --git a/src/DAO/Inc/NetWorkDO.h b/src/DAO/Inc/NetWorkDO.h index 46e958a..c6f0c32 100644 --- a/src/DAO/Inc/NetWorkDO.h +++ b/src/DAO/Inc/NetWorkDO.h @@ -29,15 +29,16 @@ #include "AudioDataTransferObject.h" #include "AutoAgentDataObject.h" #include "ScreenShotDataTransferObject.h" +#include "DeviceDataTransferObject.h" /** * NetworkDO */ class NetworkDO final : public QObject { -Q_OBJECT -Q_DISABLE_COPY(NetworkDO) // 禁用拷贝 + Q_OBJECT + Q_DISABLE_COPY(NetworkDO) // 禁用拷贝 -public: + public: // 单例访问点 static NetworkDO* getInstance(); // 显式销毁 @@ -53,11 +54,12 @@ public: // 业务发送函数 void sendPacket(const DataTransferObjectBase& packet); -signals: - // 业务接收信号 - void audioPacketReceived(const AudioDataTransferObject& packet); // 音频数据准备完成信号 + 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); // 错误信号 diff --git a/src/DAO/Src/DeviceDataTransferObject.cpp b/src/DAO/Src/DeviceDataTransferObject.cpp new file mode 100644 index 0000000..cd0ed47 --- /dev/null +++ b/src/DAO/Src/DeviceDataTransferObject.cpp @@ -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; +} diff --git a/src/DAO/Src/NetWorkDO.cpp b/src/DAO/Src/NetWorkDO.cpp index 526f695..b5404a4 100644 --- a/src/DAO/Src/NetWorkDO.cpp +++ b/src/DAO/Src/NetWorkDO.cpp @@ -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; } diff --git a/src/Handle/DataObjectHandle/Inc/DeviceDataHandle.h b/src/Handle/DataObjectHandle/Inc/DeviceDataHandle.h new file mode 100644 index 0000000..b677869 --- /dev/null +++ b/src/Handle/DataObjectHandle/Inc/DeviceDataHandle.h @@ -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 +#include +#include +#include +#include +#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 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 m_devices; +}; diff --git a/src/Handle/DataObjectHandle/Src/DeviceDataHandle.cpp b/src/Handle/DataObjectHandle/Src/DeviceDataHandle.cpp new file mode 100644 index 0000000..b5468af --- /dev/null +++ b/src/Handle/DataObjectHandle/Src/DeviceDataHandle.cpp @@ -0,0 +1,141 @@ +// +// Created by Yosuga on 2026/4/25. +// + +#include "DeviceDataHandle.h" +#include "NetWorkDO.h" +#include +#include + +QScopedPointer 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"); + + 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(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; +} diff --git a/src/Handle/NetWorkHandle/Inc/DeviceTcpServer.h b/src/Handle/NetWorkHandle/Inc/DeviceTcpServer.h new file mode 100644 index 0000000..c1a0886 --- /dev/null +++ b/src/Handle/NetWorkHandle/Inc/DeviceTcpServer.h @@ -0,0 +1,56 @@ +// +// Created by misaki on 2026/4/25. +// + +#pragma once +#include +#include +#include +#include +#include +#include +#include + +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 m_deviceSessions; // deviceId -> session + QHash m_socketSessions; // socket -> session + + void parseIncomingData(DeviceSession *session); + void removeSession(QTcpSocket *socket); +}; diff --git a/src/Handle/NetWorkHandle/Inc/DeviceWebSocketServer.h b/src/Handle/NetWorkHandle/Inc/DeviceWebSocketServer.h new file mode 100644 index 0000000..988673a --- /dev/null +++ b/src/Handle/NetWorkHandle/Inc/DeviceWebSocketServer.h @@ -0,0 +1,52 @@ +// +// Created by misaki on 2026/4/25. +// + +#pragma once +#include +#include +#include +#include +#include + +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 m_deviceSessions; + QHash m_socketSessions; + + void removeSession(QWebSocket *socket); +}; diff --git a/src/Handle/NetWorkHandle/Src/DeviceTcpServer.cpp b/src/Handle/NetWorkHandle/Src/DeviceTcpServer.cpp new file mode 100644 index 0000000..4febfcb --- /dev/null +++ b/src/Handle/NetWorkHandle/Src/DeviceTcpServer.cpp @@ -0,0 +1,179 @@ +// +// Created by misaki on 2026/4/25. +// + +#include "DeviceTcpServer.h" +#include +#include +#include +#include +#include + +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(sender()); + if (socket) { + removeSession(socket); + } +} + +void DeviceTcpServer::onReadyRead() +{ + auto *socket = qobject_cast(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; +} diff --git a/src/Handle/NetWorkHandle/Src/DeviceWebSocketServer.cpp b/src/Handle/NetWorkHandle/Src/DeviceWebSocketServer.cpp new file mode 100644 index 0000000..2591e29 --- /dev/null +++ b/src/Handle/NetWorkHandle/Src/DeviceWebSocketServer.cpp @@ -0,0 +1,165 @@ +// +// Created by misaki on 2026/4/25. +// + +#include "DeviceWebSocketServer.h" +#include +#include +#include +#include +#include + +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(sender()); + if (socket) { + removeSession(socket); + } +} + +void DeviceWebSocketServer::onTextMessageReceived(const QString &message) +{ + auto *socket = qobject_cast(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; +} diff --git a/src/UI/Menu/Src/menu.cpp b/src/UI/Menu/Src/menu.cpp index aab21c6..26ced23 100644 --- a/src/UI/Menu/Src/menu.cpp +++ b/src/UI/Menu/Src/menu.cpp @@ -56,6 +56,7 @@ void Menu::createMenu() // 设置按钮 connect(settingsAction, &QAction::triggered, this, [this]() { qDebug() << "Settings triggered"; + AppCore::getInstance()->tryToInit(); // 打开设置窗口 // 如果 Setting 窗口已经存在,则不再创建 diff --git a/src/UI/Setting/Src/ModelPage.cpp b/src/UI/Setting/Src/ModelPage.cpp index 0f0fade..3a51068 100644 --- a/src/UI/Setting/Src/ModelPage.cpp +++ b/src/UI/Setting/Src/ModelPage.cpp @@ -131,7 +131,7 @@ ModelPage::ModelPage(QWidget *parent) return nullptr; } // 执行真正的耗时加载 - // 调用我们在 Manager 里新写的函数 + // 调用在 Manager 里新写的函数 LAppModel *model = LAppLive2DManager::GetInstance()->LoadModelInstance(dir, filename); // 清理子线程资源 context->doneCurrent();