This commit is contained in:
2026-06-14 15:54:35 +08:00
commit 3caeb07e54
2804 changed files with 373231 additions and 0 deletions
+23
View File
@@ -0,0 +1,23 @@
//
// Created by Administrator on 2025/3/30.
//
/**
* @brief 中介类,避免直接让GLCore成为单例类
* 虽然变得方便了,但也带来了危险,如果你肆意通过中介指针去调用GLCore的成员函数
* 可能会导致渲染问题等
*/
#pragma once
#include "GLCore.h"
class AppContext {
public:
// 注册GLCore
static void RegisterGLCore(GLCore* core) { s_glCore = core; }
// 注销GLCore
static void UnregisterGLCore() { s_glCore = nullptr; }
static GLCore* GetGLCore() { return s_glCore; }
private:
static inline GLCore* s_glCore = nullptr; // C++17 内联静态成员
};
+89
View File
@@ -0,0 +1,89 @@
//
// Created by misaki on 2026/1/24.
//
#pragma once
/**
* 客户端业务核心
* 1. 处理来自服务端的数据,分发并执行
* 2. 完成非阻塞的事件循环处理,构建业务状态机
*/
#include <QMutex>
#include <QObject>
#include <QHash>
#ifdef Q_OS_WIN
#include <windows.h>
#endif
#ifdef Q_OS_LINUX
#include <QAbstractNativeEventFilter>
#endif
#include "serialportmanager.h"
class DeviceTcpServer;
class DeviceWebSocketServer;
class AppCore final : public QObject
#if defined(Q_OS_LINUX) && !defined(EMBEDDED_LINUX)
, public QAbstractNativeEventFilter
#endif
{
Q_OBJECT
Q_DISABLE_COPY(AppCore)
private:
explicit AppCore(QObject *parent = nullptr);
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() { }
// PTT 按住说话
void startPttRecording();
void stopPttRecording();
#if !defined(EMBEDDED_LINUX)
void setupGlobalHotkey();
void cleanupGlobalHotkey();
#endif
#if defined(Q_OS_WIN)
private:
static LRESULT CALLBACK lowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam);
HHOOK m_keyboardHook = nullptr;
bool m_isPttDown = false;
UINT m_hotkeyVKey = VK_OEM_3;
#elif defined(Q_OS_LINUX) && !defined(EMBEDDED_LINUX)
private:
bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) override;
void onDebounceStop();
bool m_isPttDown = false;
int m_hotkeyCode = 0;
class QTimer *m_pttDebounce = nullptr;
#elif defined(Q_OS_MACOS)
private:
void *m_eventTap = nullptr;
void *m_runLoopSource = nullptr;
bool m_isPttDown = false;
int m_hotkeyCode = 50;
#endif
};
+97
View File
@@ -0,0 +1,97 @@
#pragma once
#include <QtWidgets/QWidget>
#include <QOpenGLWidget>
#if !defined(EMBEDDED_LINUX)
#include "menu.h"
#endif
#ifdef Q_OS_WIN
#include <windows.h>
#endif
class GLCore final : public QOpenGLWidget
{
Q_OBJECT
public:
GLCore(int width, int height, QWidget* parent = nullptr);
// 删除拷贝构造函数
GLCore(const GLCore&) = delete;
// 删除拷贝运算符
GLCore& operator=(const GLCore&) = delete;
// 删除移动构造函数
GLCore(GLCore&&) = delete;
// 删除移动运算符
GLCore& operator=(GLCore&&) = delete;
~GLCore() override;
// 帧率控制
void setFrameRate(double fps);
[[nodiscard]] double getFrameRate() const;
// 帧率表
static QMap<QString, double> getFrameRateMap();
static QStringList getFrameRateList();
// 安全地设置窗口的实际像素尺寸
void setWindowSize(int w, int h);
void mouseMoveEvent(QMouseEvent* event) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
// 重写函数
void initializeGL() override;
void paintGL() override;
void resizeGL(int w, int h) override;
void closeEvent(QCloseEvent* event) override;
private:
void closeGL(); // 关闭当前窗口
private:
/*
* 在这个地方我遇到一个很抽象的问题,就是私有成员变量顺序的问题
* 出问题的顺序是:
* bool isLeftPressed; /// 鼠标左键是否按下
bool isRightPressed; /// 鼠标右键是否按下
QPoint currentPos; /// 当前鼠标位置
Menu *contextMenu; /// 使用 Menu 类
AudioInput *audioInput; /// 音频录制类
AudioOutput *audioOutput; /// 音频播放类
这样的顺序导致了我的鼠标一放在窗口上,窗口就往右下瞬移
改成现在下面的顺序就正常了
我一开始以为是我音频录制类里面多线程导致的
但想了想我都没new这个对象,哪来的多线程
后面问了问AI,它的解释是:
可能与C++中类成员的初始化顺序有关。
在C++中,类成员变量按照它们在类中声明的顺序进行初始化,
而不是根据它们在构造函数初始化列表中的顺序。
如果某些成员变量的初始化依赖于其他成员变量的状态
,而它们的实际初始化顺序与预期不符,可能会导致未定义行为或其他意外问题。
我感觉这不一定是根本原因,谁能告诉我到底发生了啥???
2025.3.30(Misaki): 上述问题已经解决,原因是isLeftPressed与isRightPressed
这两个成员变量没有初始化,导致其值是随机的,进而产生bug
*/
double frameRate = 60.0; /// 帧率
static QMap<QString, double> frameRateMap; /// 帧率映射表
QTimer* frameTimer; /// 帧控制定时器
#if !defined(EMBEDDED_LINUX)
Menu *contextMenu; /// 使用 Menu 类
#endif
bool isLeftPressed; /// 鼠标左键是否按下
bool isRightPressed; /// 鼠标右键是否按下
QPoint currentPos; /// 当前鼠标位置
#ifdef Q_OS_WIN
private:
HWND hwnd; // Windows窗口句柄
void setWindowTransparentForMouse(bool transparent);
#endif
};
+352
View File
@@ -0,0 +1,352 @@
//
// Created by misaki on 2026/1/24.
//
#include "AppCore.h"
#include <QDebug>
#include <QApplication>
#include <QTimer>
#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"
#if defined(Q_OS_LINUX) && !defined(EMBEDDED_LINUX)
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#endif
#ifdef Q_OS_MACOS
#include <CoreGraphics/CoreGraphics.h>
#include <CoreFoundation/CoreFoundation.h>
#endif
// 初始化静态成员
QScopedPointer<AppCore> AppCore::m_instance;
QMutex AppCore::m_mutex;
#ifdef Q_OS_WIN
static AppCore *g_hotkeyInstance = nullptr;
#endif
// 单例实现 (QScopedPointer + Mutex)
AppCore* AppCore::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new AppCore());
}
}
return m_instance.data();
}
void AppCore::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
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();
ScreenShotReqDataHandle::getInstance();
// 注入发送接口
NetworkDO::getInstance()->registerSender([](const QString& type, const QJsonObject& data){
WebSocketClient::getInstance()->sendJson(type, data);
});
// TODO Test
AudioInput::getInstance()->setAudioPath(QDir::currentPath(), "/temp.wav");
// 连接必要的信号
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte,
this, &AppCore::onRecordingFinished_Byte);
#if !defined(EMBEDDED_LINUX)
setupGlobalHotkey();
#endif
}
AppCore::~AppCore()
{
#if !defined(EMBEDDED_LINUX)
cleanupGlobalHotkey();
#endif
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);
}
void AppCore::onRecordingFinished_Byte(const QByteArray &wavData) {
// 将录音数据发送给服务端
AudioDataTransferObject packet;
packet.setData("isStream", false).setData("data", wavData.toBase64().data());
NetworkDO::getInstance()->sendPacket(packet);
}
void AppCore::startPttRecording() {
qDebug() << "PTT recording started";
AudioInput::getInstance()->startAudio();
}
void AppCore::stopPttRecording() {
qDebug() << "PTT recording stopped";
AudioInput::getInstance()->stopAudio();
}
// ===================== Windows =====================
#ifdef Q_OS_WIN
void AppCore::setupGlobalHotkey() {
g_hotkeyInstance = this;
HMODULE hMod = GetModuleHandle(nullptr);
m_keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, lowLevelKeyboardHook, hMod, 0);
if (m_keyboardHook) {
qDebug() << "[AppCore] 全局 PTT 热键钩子已安装 (键码:" << m_hotkeyVKey << ")";
} else {
qWarning() << "[AppCore] 全局 PTT 热键钩子安装失败:" << GetLastError();
}
}
void AppCore::cleanupGlobalHotkey() {
if (m_keyboardHook) {
UnhookWindowsHookEx(m_keyboardHook);
m_keyboardHook = nullptr;
qDebug() << "[AppCore] 全局 PTT 热键钩子已卸载";
}
g_hotkeyInstance = nullptr;
}
LRESULT CALLBACK AppCore::lowLevelKeyboardHook(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode == HC_ACTION && g_hotkeyInstance) {
KBDLLHOOKSTRUCT *pKb = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
if (pKb->vkCode == g_hotkeyInstance->m_hotkeyVKey) {
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
if (!g_hotkeyInstance->m_isPttDown) {
g_hotkeyInstance->m_isPttDown = true;
QMetaObject::invokeMethod(qApp, []() {
AppCore::getInstance()->startPttRecording();
}, Qt::QueuedConnection);
}
return 1;
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
if (g_hotkeyInstance->m_isPttDown) {
g_hotkeyInstance->m_isPttDown = false;
QMetaObject::invokeMethod(qApp, []() {
AppCore::getInstance()->stopPttRecording();
}, Qt::QueuedConnection);
}
return 1;
}
}
}
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
// ===================== Linux (X11) =====================
#elif defined(Q_OS_LINUX) && !defined(EMBEDDED_LINUX)
void AppCore::setupGlobalHotkey() {
Display *display = XOpenDisplay(nullptr);
if (!display) {
qWarning() << "[AppCore] 无法打开 X11 Display,全局热键不可用";
return;
}
// 尝试开启 detectable auto-repeat(不一定持久影响其他连接,不影响 debounce 方案)
int orig = 0;
XkbSetDetectableAutoRepeat(display, True, &orig);
KeyCode keycode = XKeysymToKeycode(display, XK_grave);
if (!keycode) {
qWarning() << "[AppCore] 未找到波浪号键的键码";
XCloseDisplay(display);
return;
}
m_hotkeyCode = keycode;
XGrabKey(display, keycode, AnyModifier, DefaultRootWindow(display),
True, GrabModeAsync, GrabModeAsync);
XCloseDisplay(display);
// debounce 定时器:KeyRelease 后等待 50ms,若没有新的 KeyPress 再停
m_pttDebounce = new QTimer(this);
m_pttDebounce->setSingleShot(true);
connect(m_pttDebounce, &QTimer::timeout, this, &AppCore::onDebounceStop);
qApp->installNativeEventFilter(this);
qDebug() << "[AppCore] Linux 全局 PTT 热键已注册 (键码:" << m_hotkeyCode << ")";
}
void AppCore::cleanupGlobalHotkey() {
qApp->removeNativeEventFilter(this);
if (m_pttDebounce) {
m_pttDebounce->stop();
delete m_pttDebounce;
m_pttDebounce = nullptr;
}
Display *display = XOpenDisplay(nullptr);
if (display) {
XUngrabKey(display, m_hotkeyCode, AnyModifier, DefaultRootWindow(display));
XCloseDisplay(display);
}
m_hotkeyCode = 0;
qDebug() << "[AppCore] Linux 全局 PTT 热键已卸载";
}
void AppCore::onDebounceStop() {
if (m_isPttDown) {
m_isPttDown = false;
stopPttRecording();
}
}
bool AppCore::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) {
if (eventType != "xcb_generic_event_t")
return false;
auto *data = static_cast<const uint8_t *>(message);
uint8_t type = data[0] & 0x7f;
if ((type == 2 || type == 3) && data[1] == static_cast<uint8_t>(m_hotkeyCode)) {
if (type == 2) { // KeyPress
m_pttDebounce->stop(); // 取消待决的停止
if (!m_isPttDown) {
m_isPttDown = true;
startPttRecording();
}
} else { // KeyRelease
// 不立即停,等待 50ms 确认没有连发 KeyPress
if (m_isPttDown)
m_pttDebounce->start(50);
}
if (result) *result = 1;
return true;
}
return false;
}
// ===================== macOS (CGEventTap) =====================
#elif defined(Q_OS_MACOS)
static CGEventRef pttEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
auto *core = static_cast<AppCore *>(refcon);
if (type == kCGEventTapDisabledByTimeout) {
if (core && core->m_eventTap)
CGEventTapEnable(static_cast<CFMachPortRef>(core->m_eventTap), true);
return event;
}
if (type != kCGEventKeyDown && type != kCGEventKeyUp)
return event;
CGKeyCode kc = static_cast<CGKeyCode>(CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
if (kc != core->m_hotkeyCode)
return event;
if (type == kCGEventKeyDown && !core->m_isPttDown) {
core->m_isPttDown = true;
core->startPttRecording();
} else if (type == kCGEventKeyUp && core->m_isPttDown) {
core->m_isPttDown = false;
core->stopPttRecording();
}
return nullptr;
}
void AppCore::setupGlobalHotkey() {
CFMachPortRef tap = CGEventTapCreate(
kCGHIDEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionDefault,
CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp),
&pttEventTapCallback,
this);
if (!tap) {
qWarning() << "[AppCore] macOS PTT 热键创建失败 (需要辅助功能权限)";
return;
}
m_eventTap = tap;
CFRunLoopSourceRef src = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0);
m_runLoopSource = src;
CFRunLoopAddSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
CGEventTapEnable(tap, true);
qDebug() << "[AppCore] macOS 全局 PTT 热键已注册 (键码:" << m_hotkeyCode << ")";
}
void AppCore::cleanupGlobalHotkey() {
if (m_eventTap) {
auto *tap = static_cast<CFMachPortRef>(m_eventTap);
CGEventTapEnable(tap, false);
if (m_runLoopSource) {
auto *src = static_cast<CFRunLoopSourceRef>(m_runLoopSource);
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
CFRelease(src);
}
CFRelease(tap);
m_eventTap = nullptr;
m_runLoopSource = nullptr;
}
qDebug() << "[AppCore] macOS 全局 PTT 热键已卸载";
}
#endif
+336
View File
@@ -0,0 +1,336 @@
#include "LAppDelegate.hpp" // 必须要放在第一个,否则会出现头文件顺序错误
#include "LAppView.hpp"
#include "LAppPal.hpp"
#include "LAppLive2DManager.hpp"
#include "LAppDefine.hpp"
#include "GLCore.h"
#include <QTimer>
#include <QMouseEvent>
#include <QDebug>
#include <QFont>
#include <QApplication>
#include <QFontDatabase>
#include <algorithm>
#include "TextRenderer.h"
// #include "AudioOutput.h"
#include "AppContext.h"
QMap<QString, double> GLCore::frameRateMap = {
{"30", 30.0},
{"60", 60.0},
{"120", 120.0},
{"144", 144.0},
{"165", 165.0},
{"240", 240.0}
};
GLCore::GLCore(const int width, const int height, QWidget *parent)
: QOpenGLWidget(parent),
isLeftPressed(false), // 显式初始化
isRightPressed(false) // 显式初始化
{
// 启用高分辨率位图(High DPI Pixmaps)支持
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#else
//根据实际屏幕缩放比例更改
qputenv("QT_SCALE_FACTOR", "1.5");
#endif
#endif
// 不为窗口创建额外的兄弟窗口,从而简化窗口管理并可能提高性能
QApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
// 设置字体
QFontDatabase::addApplicationFont("Resources/Font/ElaAwesome.ttf");
QApplication::setFont(QFont("Microsoft YaHei", 13));
// new一些必要的对象
#if !defined(EMBEDDED_LINUX)
contextMenu = new Menu(this);
#endif
// 设置窗口大小
setFixedSize(width, height);
// 设置文本渲染器窗口大小
TextRenderer::getInstance()->setWindowSize(width, height);
TextRenderer::getInstance()->setGlobalFont(QFont("Microsoft YaHei", 14, QFont::Bold));
TextRenderer::getInstance()->setHoldDuration(1.0f); // 停留1.2秒
TextRenderer::getInstance()->setGravity(600.0f); // 更快的下坠速度
TextRenderer::getInstance()->setDampFactor(0.85f); // 更强的弹性效果
this->setWindowFlag(Qt::FramelessWindowHint); // 设置无边框窗口
this->setWindowFlag(Qt::WindowStaysOnTopHint); // 设置窗口始终在顶部
this->setWindowFlag(Qt::Tool); // 隐藏应用程序图标
this->setAttribute(Qt::WA_TranslucentBackground); // 设置窗口背景透明
// 帧率控制初始化
frameTimer = new QTimer(this);
connect(frameTimer, &QTimer::timeout, [&]() {
update();
});
frameTimer->start(static_cast<int>((1.0 / frameRate) * 1000)); // 使用成员变量计算间隔
#ifdef Q_OS_WIN
// 保存窗口句柄
hwnd = reinterpret_cast<HWND>(this->winId());
#endif
// 启用鼠标跟踪,不启用的话鼠标按下才会回调mouseMoveEvent函数
this->setMouseTracking(true);
// 连接一些必要的信号与槽
#ifndef EMBEDDED_LINUX // 如果不是嵌入式Linux系统(注意如果你的嵌入式Linux平台启用了桌面系统,那么可能需要去掉这个条件宏)
connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL); // 关闭窗口信号
#endif
// 注册当前实例到中介类
AppContext::RegisterGLCore(this);
}
GLCore::~GLCore()
{
// 注销实例
AppContext::UnregisterGLCore();
// 释放TextRender单例
TextRenderer::releaseInstance();
// 释放Live2D 单例
LAppDelegate::ReleaseInstance();
}
// 帧率设置
void GLCore::setFrameRate(double fps)
{
if (qFuzzyCompare(fps, frameRate)) // 避免无意义更新
return;
if (fps <= 0.0) {
qWarning() << "Invalid frame rate:" << fps << "using default 60.0";
fps = 60.0;
}
frameRate = fps;
frameTimer->setInterval(static_cast<int>((1.0 / frameRate) * 1000));
}
// 获取当前帧率
double GLCore::getFrameRate() const
{
return frameRate;
}
QMap<QString, double> GLCore::getFrameRateMap()
{
return frameRateMap;
}
QStringList GLCore::getFrameRateList()
{
// 将 frameRateMap中的String部分转换为 QStringList
QStringList frameRateList;
for (auto it = frameRateMap.begin(); it != frameRateMap.end(); ++it) {
frameRateList.append(it.key());
}
// 将frameRateList的数字字符从小到大排序
std::sort(frameRateList.begin(), frameRateList.end(), [](const QString& a, const QString& b) {
return a.toDouble() < b.toDouble();
});
// 将60放在第一个位置
std::swap(frameRateList[0], frameRateList[frameRateList.indexOf("60")]);
return frameRateList;
}
/**
* 关闭窗口
*/
void GLCore::closeGL()
{
this->close();
}
/**
* 主要是为setWindowFlag(Qt::Tool)这段代码擦屁股。
* 在 Qt 中,程序的退出通常依赖于主事件循环(QApplication的事件循环)的退出。当主窗口关闭时,通常会触发QApplication的lastWindowClosed信号,从而退出事件循环,导致程序退出。
然而,当你将窗口设置为工具窗口(Qt::Tool)时,这个窗口可能不会被视为应用程序的“主窗口”,因此关闭它可能不会触发lastWindowClosed信号,导致程序不会正常退出。
* @param event
*/
void GLCore::closeEvent(QCloseEvent* event)
{
QApplication::quit(); // 显式退出事件循环
event->accept(); // 确保关闭事件被接受
}
#ifdef Q_OS_WIN
void GLCore::setWindowTransparentForMouse(const bool transparent) const {
if (!hwnd) return;
LONG exStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
if (transparent) {
// 启用鼠标穿透
exStyle |= WS_EX_TRANSPARENT;
exStyle |= WS_EX_LAYERED;
} else {
// 禁用鼠标穿透
exStyle &= ~WS_EX_TRANSPARENT;
exStyle &= ~WS_EX_LAYERED;
}
SetWindowLong(hwnd, GWL_EXSTYLE, exStyle);
// 刷新窗口
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
#endif
void GLCore::mouseMoveEvent(QMouseEvent* event)
{
const float x = static_cast<float>(event->position().x());
const float y = static_cast<float>(event->position().y());
LAppDelegate::GetInstance()->GetView()->OnTouchesMoved(x, y); // 将当前鼠标位置传递给LAppDelegate
if (isLeftPressed) { // 鼠标左键按下
const QPoint newPos = event->globalPos() - currentPos;
this->move(newPos);
}
}
void GLCore::mousePressEvent(QMouseEvent* event)
{
const float x = static_cast<float>(event->position().x());
const float y = static_cast<float>(event->position().y());
// 检测是否在模型上
bool onModel = false;
if (LAppDelegate::GetInstance() && LAppDelegate::GetInstance()->GetView()) {
onModel = LAppDelegate::GetInstance()->GetView()->IsModelHit(x, y);
}
if (event->button() == Qt::LeftButton) {
LAppDelegate::GetInstance()->GetView()->OnTouchesBegan(x, y);
this->currentPos = event->globalPos() - this->frameGeometry().topLeft();
if (onModel) {
// 窗口拖动
this->isLeftPressed = true;
#ifdef Q_OS_WIN
// 确保窗口不穿透
setWindowTransparentForMouse(false);
#endif
} else {
// 透明区域:透传(只有WIndows完美实现了,Linux由于平台差异,只是简单实现,并没有完美透传功能)
#ifdef Q_OS_WIN
// 设置窗口为鼠标穿透
setWindowTransparentForMouse(true);
// 发送鼠标按下事件到底层窗口
POINT pt = { event->globalPos().x(), event->globalPos().y() };
HWND hWndBelow = WindowFromPoint(pt);
if (hWndBelow && hWndBelow != hwnd) {
// 转换坐标
ScreenToClient(hWndBelow, &pt);
// 发送鼠标按下消息
PostMessage(hWndBelow, WM_LBUTTONDOWN,
MK_LBUTTON, MAKELPARAM(pt.x, pt.y));
PostMessage(hWndBelow, WM_LBUTTONUP,
0, MAKELPARAM(pt.x, pt.y));
}
// 恢复窗口不穿透状态(下一次鼠标移动时会重新检测)
QTimer::singleShot(100, this, [this]() {
setWindowTransparentForMouse(false);
});
#endif
this->isLeftPressed = false;
}
}
// TODO: 右键菜单等
if (event->button() == Qt::RightButton) {
// 在鼠标右键点击的位置创建菜单,显示自定义右键菜单
if (onModel) {
#if !defined(EMBEDDED_LINUX)
contextMenu->showMenu(event->globalPos());
#endif
this->isRightPressed = true;
}
else {
#ifdef Q_OS_WIN
// 设置窗口为鼠标穿透
setWindowTransparentForMouse(true);
// 发送鼠标按下事件到底层窗口
POINT pt = { event->globalPos().x(), event->globalPos().y() };
HWND hWndBelow = WindowFromPoint(pt);
if (hWndBelow && hWndBelow != hwnd) {
// 转换坐标
ScreenToClient(hWndBelow, &pt);
// 发送鼠标按下消息
PostMessage(hWndBelow, WM_LBUTTONDOWN,
MK_LBUTTON, MAKELPARAM(pt.x, pt.y));
PostMessage(hWndBelow, WM_LBUTTONUP,
0, MAKELPARAM(pt.x, pt.y));
}
// 恢复窗口不穿透状态(下一次鼠标移动时会重新检测)
QTimer::singleShot(100, this, [this]() {
setWindowTransparentForMouse(false);
});
#endif
this->isRightPressed = false;
}
}
}
void GLCore::mouseReleaseEvent(QMouseEvent* event)
{
const float x = static_cast<float>(event->position().x());
const float y = static_cast<float>(event->position().y());
LAppDelegate::GetInstance()->GetView()->OnTouchesEnded(x, y);
if (event->button() == Qt::LeftButton) {
isLeftPressed = false;
}
if (event->button() == Qt::RightButton) {
isRightPressed = false;
}
}
void GLCore::initializeGL()
{
LAppDelegate::GetInstance()->Initialize(this);
}
void GLCore::paintGL()
{
LAppDelegate::GetInstance()->update(); // Live2D画面渲染
// 渲染文本
TextRenderer::getInstance()->update();
TextRenderer::getInstance()->render();
}
void GLCore::resizeGL(const int w, const int h)
{
// 设置文本渲染器窗口大小
TextRenderer::getInstance()->setWindowSize(w, h);
LAppDelegate::GetInstance()->resize(w, h);
}
// 设置窗口大小,并触发 resizeGL 事件
void GLCore::setWindowSize(const int w, const int h)
{
// 检查是否需要更新,避免重复调用
if (this->width() == w && this->height() == h) {
return;
}
// 调用 QWidget::resize 或 setFixedSize 来改变窗口的实际尺寸
setFixedSize(w, h);
// 调用 setFixedSize 会自动触发 QOpenGLWidget 的 resizeEvent
// 进而调用 resizeGL(w, h),无需手动调用 resizeGL
}
+68
View File
@@ -0,0 +1,68 @@
//
// Created by misaki on 2026/1/13.
//
/**
* 数据传输对象 (DTO) 定义
* AudioDataTransferObject
* 与Yosuga_server对等
*/
#pragma once
#include <QObject>
#include <QByteArray>
#include <QJsonObject>
#include <QJsonValue>
#include "DataTransferObjectBase.h"
// 前向声明,减少依赖
class QJsonObject;
class AudioDataTransferObject final : public DataTransferObjectBase{
public:
// 构造函数(带默认值)
explicit AudioDataTransferObject(QString owner = "client",
bool isStream = false,
bool isStart = false,
bool isEnd = false,
int sequence = 0,
QByteArray data = {},
int sampleRate = 16000,
int channelCount = 1,
int bitDepth = 16,
double duration = 0.0,
QString text = "");
// 静态工厂方法
static AudioDataTransferObject fromJson(const QJsonObject& json);
[[nodiscard]] QString type() const override { return "audio_data"; }
// 序列化
[[nodiscard]] QJsonObject toJson() const override; // 通过多态即可统一调用方式
// 链式调用设置
AudioDataTransferObject& setData(const QString& key, const QJsonValue& value) override;
[[nodiscard]] QString owner() const { return m_owner; }
[[nodiscard]] bool isStream() const { return m_isStream; }
[[nodiscard]] bool isStart() const { return m_isStart; }
[[nodiscard]] bool isEnd() const { return m_isEnd; }
[[nodiscard]] int sequence() const { return m_sequence; }
[[nodiscard]] QByteArray audioData() const { return m_data; }
[[nodiscard]] int sampleRate() const { return m_sampleRate; }
[[nodiscard]] int channelCount() const { return m_channelCount; }
[[nodiscard]] int bitDepth() const { return m_bitDepth; }
[[nodiscard]] double duration() const { return m_duration; }
[[nodiscard]] QString text() const { return m_text; }
private:
QString m_owner; /// 音频数据的拥有者(server or client)
bool m_isStream; /// 音频数据是否为流式数据
bool m_isStart; /// 音频数据是否开始(流式时有效)
bool m_isEnd; /// 音频数据是否结束(流式时有效)
int m_sequence; /// 音频数据块序列号(流式时有效)
QByteArray m_data; /// 音频数据,流式时为分块数据,base64编码
int m_sampleRate; /// 音频采样率
int m_channelCount; /// 音频通道数
int m_bitDepth; /// 音频采样位数
double m_duration; /// 音频时长
QString m_text; /// 音频对应的文本
};
+56
View File
@@ -0,0 +1,56 @@
//
// Created by misaki on 2026/1/30.
//
/**
* 自动代理数据对象
* 非对等传输对象,只被用于将服务端返回的auto_agent json转换为对象
*/
#pragma once
#include <QObject>
#include <QByteArray>
#include <QJsonObject>
#include <QJsonValue>
#include "DataTransferObjectBase.h"
class QJsonObject;
class AutoAgentDataObject final : public DataTransferObjectBase {
public:
// 构造函数(带默认值)
explicit AutoAgentDataObject(const QString& Action,
int X1,
int Y1,
int X2,
int Y2,
const QString& Key,
const QString& Content,
const QString& Direction);
// 静态工厂方法
static AutoAgentDataObject fromJson(const QJsonObject& json);
[[nodiscard]] QString type() const override { return "auto_agent"; }
[[nodiscard]] QJsonObject toJson() const override; // 通过多态即可统一调用方式
// 链式调用设置
AutoAgentDataObject& setData(const QString& key, const QJsonValue& value) override;
[[nodiscard]] QString getAction() const { return m_action; }
[[nodiscard]] int getX1() const { return m_x1; }
[[nodiscard]] int getY1() const { return m_y1; }
[[nodiscard]] int getX2() const { return m_x2; }
[[nodiscard]] int getY2() const { return m_y2; }
[[nodiscard]] QString getKey() const { return m_key; }
[[nodiscard]] QString getContent() const { return m_content; }
[[nodiscard]] QString getDirection() const { return m_direction; }
private:
QString m_action; /// 自动化动作名称
int m_x1; /// 鼠标起始位置x1
int m_y1; /// 鼠标起始位置y1
int m_x2; /// 鼠标结束位置x2
int m_y2; /// 鼠标结束位置y2
QString m_key; /// 快捷键
QString m_content; /// 输入文本内容
QString m_direction; /// 滚动方向
};
+21
View File
@@ -0,0 +1,21 @@
//
// Created by misaki on 2026/1/13.
//
#pragma once
#include <QObject>
class DataTransferObjectBase
{
public:
virtual ~DataTransferObjectBase() = default;
// 获取类型,用于区分不同的DTO子类对象
[[nodiscard]] virtual QString type() const = 0;
// 序列化
[[nodiscard]] virtual QJsonObject toJson() const = 0;
// 链式调用设置
virtual DataTransferObjectBase& setData(const QString& key, const QJsonValue& value) = 0;
};
+37
View File
@@ -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;
};
+78
View File
@@ -0,0 +1,78 @@
//
// Created by misaki on 2025/12/29.
//
#pragma once
/**
* 本类为网络数据流会使用到的数据访问对象封装
* 目的是便于统一数据访问方式
* 同时屏蔽了端到端数据交换格式,使得上层调用不再需要关心数据格式,而只需要填入数据即可
*/
/**
* 简单描述一下Yosuga客户端所需要使用到的数据
* 主要为音频数据,控制信息,文本信息。
* 其中文本信息与音频数据为捆绑收发,并且其中还包括了一些特别的信息,例如音频时长等
* 控制信息与各种业务逻辑相关,例如模拟点击,模拟输入等
*/
#include <QObject>
#include <QString>
#include <QByteArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QScopedPointer>
#include <QMutex>
#include <functional>
#include "DataTransferObjectBase.h"
#include "AudioDataTransferObject.h"
#include "AutoAgentDataObject.h"
#include "ScreenShotDataTransferObject.h"
#include "DeviceDataTransferObject.h"
/**
* NetworkDO
*/
class NetworkDO final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(NetworkDO) // 禁用拷贝
public:
// 单例访问点
static NetworkDO* getInstance();
// 显式销毁
static void destroy();
// 定义发送回调函数类型
using SenderFunc = std::function<void(const QString& type, const QJsonObject& data)>;
public:
// 注入发送接口
void registerSender(SenderFunc sender);
// 业务发送函数
void sendPacket(const DataTransferObjectBase& 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); // 错误信号
public slots:
// 接收底层 JSON 数据
void onDataReceived(const QString& type, const QJsonObject& data);
public:
~NetworkDO() override;
private:
// 构造/析构函数私有化
explicit NetworkDO(QObject *parent = nullptr);
static QScopedPointer<NetworkDO> m_instance;
static QMutex m_mutex;
SenderFunc m_sender; // 注入的发送器
};
@@ -0,0 +1,55 @@
//
// Created by misaki on 2026/2/1.
//
/**
* 数据传输对象 (DTO) 定义
* ScreenShotDataTransferObject
* 与Yosuga_server中的是对等DTO
*/
#pragma once
#include <QObject>
#include <QByteArray>
#include <QJsonObject>
#include <QJsonValue>
#include "DataTransferObjectBase.h"
// 前向声明,减少依赖
class QJsonObject;
class ScreenShotDataTransferObject final : public DataTransferObjectBase{
public:
// 构造函数(带默认值)
explicit ScreenShotDataTransferObject(QString owner = "client",
bool isSuccess = true,
QString realtimeScreenShot = "",
int width = 0, int height = 0,
QString describeInfo = "", QString LLMResponse = ""
);
// 静态工厂方法
static ScreenShotDataTransferObject fromJson(const QJsonObject& json);
[[nodiscard]] QString type() const override { return "screenshot_data"; }
// 序列化
[[nodiscard]] QJsonObject toJson() const override; // 通过多态即可统一调用方式
// 链式调用设置
ScreenShotDataTransferObject& setData(const QString& key, const QJsonValue& value) override;
[[nodiscard]] QString owner() const { return m_owner; }
[[nodiscard]] bool isSuccess() const { return m_isSuccess; }
[[nodiscard]] QString realtimeScreenShot() const { return m_realtimeScreenShot; }
[[nodiscard]] int width() const { return m_width; }
[[nodiscard]] int height() const { return m_height; }
[[nodiscard]] QString describeInfo() const { return m_describeInfo; }
[[nodiscard]] QString LLMResponse() const { return m_LLMResponse; }
private:
QString m_owner; /// 数据的拥有者(server or client)
bool m_isSuccess; /// 截图是否成功
QString m_realtimeScreenShot; /// 客户端设备的实时截图数据(base64)
int m_width; /// 截图宽度 非必要字段
int m_height; /// 截图高度 非必要字段
QString m_describeInfo; /// 设备的描述信息(告知模型以做出更加准确的判断) 非必要字段
QString m_LLMResponse; /// LLM的响应结果(由服务端发送时携带)
};
+107
View File
@@ -0,0 +1,107 @@
//
// Created by misaki on 2026/1/13.
//
#include "AudioDataTransferObject.h"
#include <QJsonValue>
#include <utility>
// 构造函数实现(初始化列表)
AudioDataTransferObject::AudioDataTransferObject(QString owner,
bool isStream,
bool isStart,
bool isEnd,
int sequence,
QByteArray data,
int sampleRate,
int channelCount,
int bitDepth,
double duration,
QString text)
: m_owner(std::move(owner))
, m_isStream(isStream)
, m_isStart(isStart)
, m_isEnd(isEnd)
, m_sequence(sequence)
, m_data(std::move(data))
, m_sampleRate(sampleRate)
, m_channelCount(channelCount)
, m_bitDepth(bitDepth)
, m_duration(duration)
, m_text(std::move(text)) {
}
// 静态工厂方法:从 JSON 反序列化
AudioDataTransferObject AudioDataTransferObject::fromJson(const QJsonObject& json) {
// 逐个字段读取,不存在则用默认值
QString owner = json.value("Owner").toString("server");
bool isStream = json.value("isStream").toBool(false);
bool isStart = json.value("isStart").toBool(false);
bool isEnd = json.value("isEnd").toBool(false);
int sequence = json.value("sequence").toInt(0);
// 处理 base64 编码的 data 字段
QByteArray data;
if (json.contains("data")) {
const QString base64Str = json.value("data").toString();
data = QByteArray::fromBase64(base64Str.toUtf8());
}
int sampleRate = json.value("sampleRate").toInt(16000);
int channelCount = json.value("channelCount").toInt(1);
int bitDepth = json.value("bitDepth").toInt(16);
double duration = json.value("duration").toDouble(0.0);
QString text = json.value("text").toString();
// 调用构造函数创建对象
return AudioDataTransferObject(owner, isStream, isStart, isEnd,
sequence, data, sampleRate, channelCount,
bitDepth, duration, text);
}
// 序列化为 JSON
QJsonObject AudioDataTransferObject::toJson() const {
QJsonObject json;
json["Owner"] = m_owner;
json["isStream"] = m_isStream;
json["isStart"] = m_isStart;
json["isEnd"] = m_isEnd;
json["sequence"] = m_sequence;
// data 字段 base64 编码
json["data"] = QString(m_data.toBase64());
json["sampleRate"] = m_sampleRate;
json["channelCount"] = m_channelCount;
json["bitDepth"] = m_bitDepth;
json["duration"] = m_duration;
json["text"] = m_text;
return json;
}
// 链式设置
AudioDataTransferObject& AudioDataTransferObject::setData(const QString& key,
const QJsonValue& value) {
if (key == "Owner") {
m_owner = value.toString();
} else if (key == "isStream") {
m_isStream = value.toBool();
} else if (key == "isStart") {
m_isStart = value.toBool();
} else if (key == "isEnd") {
m_isEnd = value.toBool();
} else if (key == "sequence") {
m_sequence = value.toInt();
} else if (key == "data") {
// 这里要求传入的是 base64 字符串
m_data = QByteArray::fromBase64(value.toString().toUtf8());
} else if (key == "sampleRate") {
m_sampleRate = value.toInt();
} else if (key == "channelCount") {
m_channelCount = value.toInt();
} else if (key == "bitDepth") {
m_bitDepth = value.toInt();
} else if (key == "duration") {
m_duration = value.toDouble();
} else if (key == "text") {
m_text = value.toString();
} else {
qWarning() << "Unknown key or invalid value type:" << key << value;
}
return *this; // 返回自身引用,支持链式调用
}
+72
View File
@@ -0,0 +1,72 @@
//
// Created by misaki on 2026/1/30.
//
#include "AutoAgentDataObject.h"
AutoAgentDataObject::AutoAgentDataObject(const QString& Action,
const int X1,
const int Y1,
const int X2,
const int Y2,
const QString& Key,
const QString& Content,
const QString& Direction)
: m_action(Action),
m_x1(X1),
m_y1(Y1),
m_x2(X2),
m_y2(Y2),
m_key(Key),
m_content(Content),
m_direction(Direction) {}
AutoAgentDataObject AutoAgentDataObject::fromJson(const QJsonObject& json) {
// 从JSON对象中提取数据,如果不存在则使用默认值
const QString action = json.value("Action").toString("");
const int x1 = json.value("x1").toInt(-1);
const int y1 = json.value("y1").toInt(-1);
const int x2 = json.value("x2").toInt(-1);
const int y2 = json.value("y2").toInt(-1);
const QString key = json.value("key").toString("");
const QString content = json.value("content").toString("");
const QString direction = json.value("direction").toString("");
return AutoAgentDataObject(action, x1, y1, x2, y2, key, content, direction);
}
QJsonObject AutoAgentDataObject::toJson() const {
QJsonObject json;
json["Action"] = m_action;
json["x1"] = m_x1;
json["y1"] = m_y1;
json["x2"] = m_x2;
json["y2"] = m_y2;
json["key"] = m_key;
json["content"] = m_content;
json["direction"] = m_direction;
return json;
}
AutoAgentDataObject& AutoAgentDataObject::setData(const QString& key, const QJsonValue& value) {
// 根据键名设置对应的成员变量
if (key == "Action" && value.isString()) {
m_action = value.toString();
} else if (key == "x1" && (value.isDouble() || value.isString())) {
m_x1 = value.toInt();
} else if (key == "y1" && (value.isDouble() || value.isString())) {
m_y1 = value.toInt();
} else if (key == "x2" && (value.isDouble() || value.isString())) {
m_x2 = value.toInt();
} else if (key == "y2" && (value.isDouble() || value.isString())) {
m_y2 = value.toInt();
} else if (key == "key" && value.isString()) {
m_key = value.toString();
} else if (key == "content" && value.isString()) {
m_content = value.toString();
} else if (key == "direction" && value.isString()) {
m_direction = value.toString();
} else {
qWarning() << "Unknown key or invalid value type:" << key << value;
}
return *this;
}
+4
View File
@@ -0,0 +1,4 @@
//
// Created by misaki on 2026/1/13.
//
#include "DataTransferObjectBase.h"
+48
View File
@@ -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;
}
+84
View File
@@ -0,0 +1,84 @@
//
// Created by misaki on 2025/12/29.
//
#include "NetWorkDO.h"
#include <QDebug>
#include <QMutexLocker>
// 初始化静态成员
QScopedPointer<NetworkDO> NetworkDO::m_instance;
QMutex NetworkDO::m_mutex;
// 单例实现 (QScopedPointer + Mutex)
NetworkDO* NetworkDO::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new NetworkDO());
}
}
return m_instance.data();
}
void NetworkDO::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
NetworkDO::NetworkDO(QObject *parent) : QObject(parent)
{
qDebug() << "NetworkDO initialized";
}
NetworkDO::~NetworkDO()
{
qDebug() << "NetworkDO destroyed";
}
// 业务逻辑实现
void NetworkDO::registerSender(SenderFunc sender)
{
QMutexLocker locker(&m_mutex); // 加个小锁,简单保护一下赋值
m_sender = std::move(sender);
}
void NetworkDO::sendPacket(const DataTransferObjectBase &packet)
{
// 检查发送器是否已注入
if (!m_sender) {
emit errorOccurred("Sender not registered! Call registerSender() first.");
return;
}
// 依赖注入 + 多态实现完美解耦
m_sender(packet.type(), packet.toJson());
}
// 接受并没有完全解耦
void NetworkDO::onDataReceived(const QString& type, const QJsonObject& data)
{
// 根据类型分发数据包
// 为什么分发做在这里,而不是统一数据再去分发,如果不在这里做分发通知,分开发信号,而使用统一的信号
// 如果有多个观察者,让观察者自动识别数据包,这会导致信号广播,容易引起性能问题(因为这里依赖的是Qt的信号与槽机制)
// TODO: 考虑在此处使用工厂模式,根据type内容快速创建对应的对象
if (type == "audio_data") {
emit audioPacketReceived(AudioDataTransferObject::fromJson(data)); // 构造并发送音频对象
}
else if (type == "auto_agent") {
emit autoAgentPacketReceived(AutoAgentDataObject::fromJson(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,75 @@
//
// Created by misaki on 2026/2/1.
//
#include "ScreenShotDataTransferObject.h"
#include <QJsonValue>
#include <utility>
#include <QDebug>
// 构造函数实现(初始化列表)
ScreenShotDataTransferObject::ScreenShotDataTransferObject(QString owner,
bool isSuccess,
QString realtimeScreenShot,
int width, int height,
QString describeInfo, QString LLMResponse)
: m_owner(std::move(owner))
, m_isSuccess(isSuccess)
, m_realtimeScreenShot(std::move(realtimeScreenShot))
, m_width(width)
, m_height(height)
, m_describeInfo(std::move(describeInfo))
, m_LLMResponse(std::move(LLMResponse))
{}
// 静态工厂方法:从 JSON 反序列化
ScreenShotDataTransferObject ScreenShotDataTransferObject::fromJson(const QJsonObject& json) {
// 逐个字段读取,不存在则用默认值
const QString owner = json.value("Owner").toString("client");
const bool isSuccess = json.value("isSuccess").toBool(false);
const QString realtimeScreenShot = json.value("RealTimeScreenShot").toString();
const int width = json.value("Width").toInt(0);
const int height = json.value("Height").toInt(0);
const QString describeInfo = json.value("DescribeInfo").toString();
const QString LLMResponse = json.value("LLMResponse").toString();
// 调用构造函数创建对象
return ScreenShotDataTransferObject(owner, isSuccess, realtimeScreenShot,
width, height, describeInfo, LLMResponse);
}
// 序列化为 JSON
QJsonObject ScreenShotDataTransferObject::toJson() const {
QJsonObject json;
json["Owner"] = m_owner;
json["isSuccess"] = m_isSuccess;
json["RealTimeScreenShot"] = m_realtimeScreenShot;
json["Width"] = m_width;
json["Height"] = m_height;
json["DescribeInfo"] = m_describeInfo;
json["LLMResponse"] = m_LLMResponse;
return json;
}
// 链式设置
ScreenShotDataTransferObject& ScreenShotDataTransferObject::setData(const QString& key,
const QJsonValue& value) {
if (key == "Owner") {
m_owner = value.toString();
} else if (key == "isSuccess") {
m_isSuccess = value.toBool();
} else if (key == "RealTimeScreenShot") {
m_realtimeScreenShot = value.toString();
} else if (key == "Width") {
m_width = value.toInt();
} else if (key == "Height") {
m_height = value.toInt();
} else if (key == "DescribeInfo") {
m_describeInfo = value.toString();
} else if (key == "LLMResponse") {
m_LLMResponse = value.toString();
} else {
qWarning() << "Unknown key:" << key << "for ScreenShotDataTransferObject";
}
return *this; // 返回自身引用,支持链式调用
}
+154
View File
@@ -0,0 +1,154 @@
//
// Created by Administrator on 2025/1/17.
//
#pragma once
#include <QAudioSource>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include <QTimer>
#include <QDir>
#include <vector>
#include <QScopedPointer>
#include <QMutex>
/**
* @brief 录音模块
* @author Misaki
* @date 2025/1/17(first) 2025/11/30(update)
* 单例类
* 使用 QAudioSource 获取原始 PCM 数据,实现 RMS 计算和 WAV 保存
*/
class AudioInput : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AudioInput) // 禁用拷贝
private:
/**
* @brief 构造函数
* @param parent
*/
explicit AudioInput(QObject *parent = nullptr);
static QScopedPointer<AudioInput> instance;
static QMutex mutex;
public:
/**
* @brief 获取实例
* @return AudioInput*
*/
static AudioInput* getInstance();
/**
* @brief 析构函数
*/
~AudioInput() override;
/**
* @brief 配置音频参数 (Qt6 中推荐使用 float 或 int16)
*/
void setAudioSettings(int rate = 44100, int channels = 2);
/**
* @brief 设置录音文件输出路径与文件名
* @param path 输出路径
* @param fileName 文件名
*/
void setAudioPath(const QString &path, const QString &fileName);
/**
* @brief 开始录音
*/
void startAudio();
/**
* @brief 停止录音
*/
void stopAudio();
/**
* @brief 设置录音时间并开始录音
* @param duration 录音时长,单位为秒
*/
void startAudioWithDuration(int duration);
/**
* @brief 开始自动录音,根据声音判断是否停止
* @param silenceThreshold 静音阈值,低于该值则认为没有声音
* @param silenceDuration 静音持续时间,单位为毫秒
*/
void startAutoStopAudio(qreal silenceThreshold = 1200, int silenceDuration = 1500);
/**
* @brief 开始最佳阈值计算
* @param Duration 持续时间,单位为毫秒
*/
void startAutoThresholdClu(int Duration = 5000);
/**
* @brief 获取当前系统所有的音频输入设备
* @return 音频输入设备名称列表
*/
static QList<QString> getAvailableAudioInputDevices();
/**
* @brief 设置当前录音设备
* @param deviceName 设备名称
*/
void setAudioInputDevice(const QString &deviceName);
/**
* @brief 设置静音阈值
* @param silenceThreshold 阈值
*/
void setSilenceThreshold(qreal silenceThreshold);
[[nodiscard]] qreal getSilenceThreshold() const;
private:
// WAV头生成工具函数
[[nodiscard]] QByteArray generateWavHeader(quint32 dataSize) const;
// 计算RMS值工具函数
static qreal calculateRMS(const QByteArray& buffer);
signals:
// 录音完成信号
void recordingFinished();
void recordingFinished_Byte(const QByteArray &wavData); // 携带音频数据
// 实时RMS值信号
void rmsRealValue(qreal value);
// 阈值计算完成信号
void thresholdCalculated(qreal bestThreshold);
private slots:
void onTimeout(); // 定时器超时槽函数
void thresholdTimeout(); // 阈值超时槽函数
// void processBuffer(const QAudioBuffer& buffer); // 处理缓冲区数据
void onReadyRead(); // 替代原先的 processBuffer,当有音频数据来时触发
private:
QAudioSource *m_audioSource = nullptr; /// Qt6 核心录音对象
QIODevice *m_ioDevice = nullptr; /// 用于读取数据的 IO 设备
QAudioFormat m_format; /// 音频格式
QAudioDevice m_currentDevice; /// 当前选中的输入设备
// 数据缓存
QByteArray m_rawPCMData; /// 存储原始PCM数据
QString m_outputFilePath;
// 逻辑控制变量
bool isAutoRecording = false; /// 是否自动录音状态
bool isAutoThreshold = false; /// 是否自动计算阈值
qreal m_rmsValue = 0.0; /// 实时RMS值
// 定时器
QTimer *m_timer; /// 总时长定时器
QTimer *m_silenceTimer; /// 静音检测定时器
QTimer *m_thresholdTimer; /// 阈值计算定时器
// 阈值算法相关
std::vector<qreal> m_rmsValues; /// RMS值vector
qreal m_silenceThreshold = 1200; /// 静音阈值
int m_silenceDuration = 1500; /// 静音持续时间
qreal m_smoothRms = 0.0; /// 平滑RMS值(用于防止低频杂波突然打断静音检测)
bool m_hasVoiceDetected = false; /// 是否已检测到人声
};
+121
View File
@@ -0,0 +1,121 @@
//
// Created by Administrator on 2025/1/17.
//
#pragma once
#include <QObject>
#include <QMediaPlayer> // 音频播放模块
#include <QAudioOutput> // QMediaPlayer 的音量控制组件
#include <QAudioSink> // 音频输出组件, 用于原始数据播放
#include <QThread>
#include <QMutex>
#include <QQueue>
#include <QUrl>
#include <QBuffer>
#include <QAudioFormat>
/**
* @brief 音频播放模块
* @author Misaki
* 单例类
* 本模块重新基于Qt6重构 2026.1.31第三次重构
* 实现的功能
* 1. 流式wav音频播放
* 2. 根据音频文件路径播放音频
*/
// Worker 类定义 (负责流式音频的底层处理) 注意:此类实例将完全运行在子线程中
class StreamAudioWorker : public QObject {
Q_OBJECT
public:
explicit StreamAudioWorker(QObject* parent = nullptr) : QObject(parent) {}
~StreamAudioWorker() override;
public slots:
// 初始化并启动音频设备
void start(int sampleRate, int channelCount, int bitDepth);
// 处理接收到的音频数据块
void processChunk(const QByteArray& chunk);
// 停止播放并清理资源
void stop();
signals:
void errorOccurred(const QString& msg);
void playbackFinished(); // 流播放结束(通常指队列空了)
private:
QScopedPointer<QAudioSink> m_sink;
QIODevice* m_ioDevice = nullptr; // 由 m_sink->start() 返回,不需要且不能手动 delete
bool m_firstChunk = true; // 标记是否是第一块数据(用于剥离WAV头)
};
// AudioOutput 主类 (单例,线程安全)
class AudioOutput : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AudioOutput)
private:
explicit AudioOutput(QObject *parent = nullptr);
static QScopedPointer<AudioOutput> m_instance;
static QMutex m_mutex;
public:
static AudioOutput *getInstance();
static void destroy(); // 显式销毁
~AudioOutput() override;
// 通用控制接口
// 停止所有播放 (文件和流)
void stopPlayback();
// 设置音量 (0-100)
void setVolume(int volume);
// 获取当前状态
bool isPlaying() const;
// 文件/URL 播放接口 (基于 QMediaPlayer)
void playUrl(const QUrl& url);
void playData(const QByteArray& data); // 播放完整的内存文件
// 流式播放接口 (基于 QAudioSink + Worker Thread)
/**
* @brief 开启流式播放会话
* @param sampleRate 采样率 (默认 32000)
* @param channelCount 通道数 (默认 1)
* @param bitDepth 位深 (默认 16)
*/
void startStream(int sampleRate = 32000, int channelCount = 1, int bitDepth = 16);
/**
* @brief 写入流数据
* @param chunk 音频数据块
*/
void pushStreamData(const QByteArray& chunk);
/**
* @brief 结束流 (停止接收新数据,播放完当前缓冲后停止)
*/
void stopStream();
signals:
// 内部转发给 Worker 的信号
void sigOperateStreamStart(int sampleRate, int channelCount, int bitDepth);
void sigOperateStreamChunk(const QByteArray& chunk);
void sigOperateStreamStop();
// 对外通知信号
void playbackFinished();
void errorOccurred(const QString& error);
private:
// 文件播放组件
QMediaPlayer* m_player = nullptr;
QAudioOutput* m_audioOutput = nullptr;
// 流式播放组件
QThread* m_workerThread = nullptr;
StreamAudioWorker* m_streamWorker = nullptr;
// 状态管理
bool m_isStreaming = false;
};
+366
View File
@@ -0,0 +1,366 @@
//
// Created by Administrator on 2025/1/17.
//
#include "AudioInput.h"
#include <QDebug>
#include <QtMath>
#include <QtEndian> // 用于处理字节序
QScopedPointer<AudioInput> AudioInput::instance;
QMutex AudioInput::mutex;
AudioInput *AudioInput::getInstance()
{
// 懒汉式 依旧单线程无需加锁
if (instance.isNull()) {
QMutexLocker locker(&mutex);
if (instance.isNull()) {
instance.reset(new AudioInput);
}
}
return instance.data();
}
AudioInput::AudioInput(QObject *parent) : QObject(parent)
{
// new一些必要的对象
// 初始化定时器
m_timer = new QTimer(this);
m_silenceTimer = new QTimer(this);
m_thresholdTimer = new QTimer(this);
m_thresholdTimer->setSingleShot(true);
// 连接定时器信号
connect(m_timer, &QTimer::timeout, this, &AudioInput::onTimeout); // 录音超时槽函数
connect(m_silenceTimer, &QTimer::timeout, this, &AudioInput::stopAudio); // 录音超时槽函数
connect(m_thresholdTimer, &QTimer::timeout, this, &AudioInput::thresholdTimeout); // 阈值检测超时槽函数
// 初始化默认设备和格式
m_currentDevice = QMediaDevices::defaultAudioInput();
setAudioSettings(); // 使用默认参数
}
AudioInput::~AudioInput()
{
stopAudio(); // 停止录音
if (m_audioSource) {
delete m_audioSource;
}
}
void AudioInput::setAudioSettings(const int rate, const int channels)
{
m_format.setSampleRate(rate);
m_format.setChannelCount(channels);
// 为了生成标准 WAV 且方便计算 RMS,强制设为 Int16
m_format.setSampleFormat(QAudioFormat::Int16);
// 检查设备是否支持该格式,不支持则使用最接近的
if (!m_currentDevice.isFormatSupported(m_format)) {
qWarning() << "Requested format not supported, using preferred format.";
m_format = m_currentDevice.preferredFormat();
}
}
void AudioInput::setAudioPath(const QString &path, const QString &fileName)
{
this->m_outputFilePath = path + fileName;
}
void AudioInput::startAudio()
{
// 每次开始前重新创建 QAudioSource,确保状态重置
if (m_audioSource) {
delete m_audioSource;
m_audioSource = nullptr;
}
m_audioSource = new QAudioSource(m_currentDevice, m_format, this);
// 调大缓冲区以避免溢出
m_audioSource->setBufferSize(128000);
// start() 返回一个 QIODevice,可以从中读取数据
m_ioDevice = m_audioSource->start();
if (m_ioDevice) {
connect(m_ioDevice, &QIODevice::readyRead, this, &AudioInput::onReadyRead);
qDebug() << "Started recording with device:" << m_currentDevice.description();
} else {
qCritical() << "Failed to start audio recording.";
}
}
void AudioInput::stopAudio()
{
if (m_audioSource) {
m_audioSource->stop();
// 注意:不要立即 delete m_audioSource,某些情况下可能导致 crash,停止即可
}
// 停止所有定时器
m_timer->stop();
m_silenceTimer->stop();
m_thresholdTimer->stop();
// 生成 WAV 数据
QByteArray wavData;
if (!m_rawPCMData.isEmpty()) {
wavData = generateWavHeader(m_rawPCMData.size());
wavData.append(m_rawPCMData);
#ifdef QT_DEBUG
// 如果需要保存文件(Debug下启用)
if (!m_outputFilePath.isEmpty()) {
QFile file(m_outputFilePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(wavData);
file.close();
qDebug() << "Saved WAV to:" << m_outputFilePath;
}
}
#endif
m_rawPCMData.clear();
}
isAutoRecording = false;
isAutoThreshold = false;
emit recordingFinished();
emit recordingFinished_Byte(wavData);
qDebug() << "Recording stopped.";
}
// 阈值检测超时槽函数
void AudioInput::onReadyRead()
{
if (!m_ioDevice) return;
// 读取当前所有可用的音频数据
QByteArray data = m_ioDevice->readAll();
if (data.isEmpty()) return;
// 保存原始 PCM 数据
m_rawPCMData.append(data);
// 计算 RMS (仅用于分析,计算当前块的RMS)
const qreal currentRms = calculateRMS(data);
m_rmsValue = currentRms;
// 计算平滑RMS (用于防止低频杂波突然打断静音检测)
constexpr qreal alpha = 0.15; // 85% 历史权重, 15% 当前权重
if (qFuzzyIsNull(m_smoothRms)) {
// 如果是第一帧数据,直接赋值,避免从0开始慢慢爬升
m_smoothRms = currentRms;
} else {
// 新值 = (旧值 * (1 - alpha)) + (当前值 * alpha)
m_smoothRms = (m_smoothRms * (1.0 - alpha)) + (currentRms * alpha);
}
// 自动停止逻辑 (VAD)
if (isAutoRecording) {
// 输出 RMS 用于调试
qDebug() << "Raw:" << currentRms << " Smooth:" << m_smoothRms;
if (m_smoothRms < m_silenceThreshold) {
// [当前是静音]
// 如果之前已经检测到过人声(说明是话说完了,或者是句间停顿)
if (m_hasVoiceDetected) {
// 启动/保持“短时”静音检测 (由 AppCore 传入,例如 500ms 或 1500ms)
if (!m_silenceTimer->isActive()) {
m_silenceTimer->start(m_silenceDuration);
}
// 如果 Timer 正在运行,就让它继续倒计时,超时会自动触发 stopAudio
}
else {
// [还没有检测到过人声] (起始静音)
// 这里不需要做额外操作,startAutoStopAudio 里设置的 5000ms 长定时器在跑
// 允许用户深呼吸或准备
}
} else {
// [当前有声音]
m_hasVoiceDetected = true; // 标记:已经有人说话了
// 重置静音定时器
// 只要有人说话,就不断重置定时器,防止断录
m_silenceTimer->stop();
// 这里可以预设启动,也可以不启动,只要有声音就会一直 stop
// 为了安全,设为 silenceDuration
m_silenceTimer->start(m_silenceDuration);
}
}
// 自动阈值计算逻辑
if (isAutoThreshold) {
m_rmsValues.push_back(m_smoothRms);
emit rmsRealValue(m_smoothRms);
}
}
qreal AudioInput::calculateRMS(const QByteArray& buffer)
{
if (buffer.isEmpty()) return 0;
// 设定为 Int16 格式 (16位深)
// 如果是 Stereo,数据排列是 L R L R...
// 简单的 RMS 计算可以将所有通道数据视为一个长序列
const qint16 *data = reinterpret_cast<const qint16*>(buffer.constData());
const int sampleCount = buffer.size() / sizeof(qint16); // 样本数量
if (sampleCount == 0) return 0;
qreal sumSquared = 0;
for (int i = 0; i < sampleCount; ++i) {
const qreal sample = static_cast<qreal>(data[i]);
sumSquared += sample * sample;
}
return qSqrt(sumSquared / sampleCount);
}
// 启动带时长的录音
void AudioInput::startAudioWithDuration(int duration)
{
startAudio();
m_timer->start(duration * 1000);
}
void AudioInput::onTimeout()
{
stopAudio();
qDebug() << "Recording stopped by duration timeout.";
}
// 获取所有音频输入设备
QList<QString> AudioInput::getAvailableAudioInputDevices()
{
QList<QString> list;
const auto devices = QMediaDevices::audioInputs();
for (const auto &device : devices) {
list.append(device.description());
}
return list;
}
// 设置当前录音设备
void AudioInput::setAudioInputDevice(const QString &deviceName)
{
const auto devices = QMediaDevices::audioInputs();
for (const auto &device : devices) {
if (device.description() == deviceName) {
m_currentDevice = device;
break;
}
}
}
// 启动自动录音 (VAD)
void AudioInput::startAutoStopAudio(const qreal silenceThreshold, const int silenceDuration)
{
isAutoRecording = true;
m_silenceThreshold = silenceThreshold;
m_silenceDuration = silenceDuration;
// 重置状态
m_hasVoiceDetected = false;
m_smoothRms = 0.0;
startAudio();
// 延迟200ms是为了避开硬件启动时的爆音,但不需要立即启动短时倒计时
// 延迟启动静音检测,给一点缓冲时间
QTimer::singleShot(200, this, [this](){
if(isAutoRecording) { // 确保还在录音状态
// 如果还没检测到声音,给5秒的等待时间;如果检测到了,逻辑由onReadyRead接管
if(!m_hasVoiceDetected) {
m_silenceTimer->start(5000); // 5秒没声音就停止
}
}
});
}
// 启动阈值计算
void AudioInput::startAutoThresholdClu(const int Duration)
{
isAutoThreshold = true;
m_rmsValues.clear();
startAudio();
m_thresholdTimer->start(Duration);
}
/**
* 2025.12.30重构 Misaki
* 从均值阈值计算的基础上增加了N倍标准差
* 即阈值 = 均值 + N * 标准差(N取3)
*/
void AudioInput::thresholdTimeout()
{
isAutoThreshold = false;
stopAudio(); // 内部会处理 stop
if (m_rmsValues.empty()) {
emit thresholdCalculated(0);
return;
}
// 计算均值
const double mean = std::accumulate(m_rmsValues.begin(), m_rmsValues.end(), 0.0) / m_rmsValues.size();
// 计算标准差
const double sq_sum = std::inner_product(m_rmsValues.begin(), m_rmsValues.end(), m_rmsValues.begin(), 0.0);
double variance = (sq_sum / m_rmsValues.size()) - (mean * mean);
// 防止浮点误差导致负数
if (variance < 0) variance = 0;
const double stdDev = std::sqrt(variance);
// 增加一个固定的偏移量 offset
// 确保即使环境有轻微波动,也不会触发录音
constexpr double offset = 80.0;
// 阈值 = 均值 + 2 * 标准差
const double bestThreshold = mean + (3 * stdDev) + offset;
m_silenceThreshold = std::max(bestThreshold, 150.0);
m_silenceThreshold = std::min(m_silenceThreshold, 30000.0);
qDebug() << "Auto Threshold Calc -> Mean:" << mean
<< " StdDev:" << stdDev
<< " Result:" << m_silenceThreshold;
emit thresholdCalculated(m_silenceThreshold);
}
QByteArray AudioInput::generateWavHeader(const quint32 dataSize) const {
// WAV头结构定义
struct WavHeader {
char riff[4] = {'R','I','F','F'};
quint32 chunkSize;
char wave[4] = {'W','A','V','E'};
char fmt[4] = {'f','m','t',' '};
quint32 fmtSize = 16;
quint16 audioFormat = 1; // PCM
quint16 numChannels;
quint32 sampleRate;
quint32 byteRate;
quint16 blockAlign;
quint16 bitsPerSample;
char data[4] = {'d','a','t','a'};
quint32 dataSize;
} header;
header.numChannels = static_cast<quint16>(m_format.channelCount());
header.sampleRate = static_cast<quint32>(m_format.sampleRate());
header.bitsPerSample = 16; // 强制使用了 Int16
header.byteRate = header.sampleRate * header.numChannels * (header.bitsPerSample / 8);
header.blockAlign = header.numChannels * (header.bitsPerSample / 8);
header.dataSize = dataSize;
header.chunkSize = 36 + dataSize;
return QByteArray(reinterpret_cast<const char*>(&header), sizeof(WavHeader));
}
void AudioInput::setSilenceThreshold(const qreal silenceThreshold)
{
this->m_silenceThreshold = silenceThreshold;
}
qreal AudioInput::getSilenceThreshold() const
{
return this->m_silenceThreshold;
}
+227
View File
@@ -0,0 +1,227 @@
//
// Created by Administrator on 2025/1/17.
//
#include "AudioOutput.h"
#include <QMediaDevices>
#include <QDebug>
#include <QCoreApplication>
#include <QDataStream>
// Static Helpers (WAV Header Parser)
static bool hasWavHeader(const QByteArray& data) {
if (data.size() < 44) return false;
return data.startsWith("RIFF") && data.mid(8, 4) == "WAVE";
}
// StreamAudioWorker 实现
StreamAudioWorker::~StreamAudioWorker() {
// 确保析构时资源释放
stop();
}
void StreamAudioWorker::start(int sampleRate, int channelCount, int bitDepth) {
if (m_sink) {
m_sink->stop();
m_sink.reset();
}
m_firstChunk = true;
// 配置音频格式
QAudioFormat format;
format.setSampleRate(sampleRate);
format.setChannelCount(channelCount);
if (bitDepth == 8) format.setSampleFormat(QAudioFormat::UInt8);
else if (bitDepth == 16) format.setSampleFormat(QAudioFormat::Int16);
else if (bitDepth == 32) format.setSampleFormat(QAudioFormat::Float); // 32位通常为Float
else format.setSampleFormat(QAudioFormat::Int16); // 默认回退
// 检查设备是否支持
auto device = QMediaDevices::defaultAudioOutput();
if (!device.isFormatSupported(format)) {
qWarning() << "[Worker] Device does not support format, using preferred format.";
format = device.preferredFormat();
}
// 创建 Sink (必须在 Worker 线程中创建)
m_sink.reset(new QAudioSink(device, format));
// 监听状态
connect(m_sink.data(), &QAudioSink::stateChanged, this, [this](QAudio::State state){
if (state == QAudio::IdleState) {
emit playbackFinished();
}
else if (state == QAudio::StoppedState) {
if (m_sink->error() != QAudio::NoError) {
emit errorOccurred("Audio Sink Error: " + QString::number(m_sink->error()));
}
}
});
// 启动,获取 IO 设备
m_ioDevice = m_sink->start();
if (!m_ioDevice) {
emit errorOccurred("Failed to start audio device");
} else {
qDebug() << "[Worker] Stream started:" << sampleRate << "Hz";
}
}
void StreamAudioWorker::processChunk(const QByteArray& chunk) {
if (chunk.isEmpty() || !m_ioDevice || !m_sink) return;
QByteArray dataToWrite = chunk;
// 智能处理 WAV 头
if (m_firstChunk) {
if (hasWavHeader(chunk)) {
qDebug() << "[Worker] Detected WAV header, stripping 44 bytes.";
dataToWrite = chunk.mid(44);
}
m_firstChunk = false;
}
// 写入音频设备 (QAudioSink 内部有缓冲区,这里直接 write 即可)
// 如果数据量过大,write 可能会阻塞,但在独立线程中这是可以接受的
qint64 written = m_ioDevice->write(dataToWrite);
if (written != dataToWrite.size()) {
qWarning() << "[Worker] Incomplete write:" << written << "/" << dataToWrite.size();
}
}
void StreamAudioWorker::stop() {
if (m_sink) {
m_sink->stop();
m_sink.reset(); // 删除对象
}
m_ioDevice = nullptr;
qDebug() << "[Worker] Stream stopped";
}
// AudioOutput 主类实现
QScopedPointer<AudioOutput> AudioOutput::m_instance;
QMutex AudioOutput::m_mutex;
AudioOutput* AudioOutput::getInstance() {
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
m_instance.reset(new AudioOutput());
}
}
return m_instance.data();
}
void AudioOutput::destroy() {
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset();
}
}
AudioOutput::AudioOutput(QObject *parent) : QObject(parent) {
// 初始化文件播放器
m_player = new QMediaPlayer(this);
m_audioOutput = new QAudioOutput(this);
m_player->setAudioOutput(m_audioOutput);
connect(m_player, &QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status){
if (status == QMediaPlayer::EndOfMedia) emit playbackFinished();
});
// 预先初始化流式 Worker 线程
// 保持一个常驻线程Worker,通过信号控制
m_streamWorker = new StreamAudioWorker(); // 不能指定 parent,因为要 moveToThread
m_workerThread = new QThread(this);
m_streamWorker->moveToThread(m_workerThread);
// 连接信号槽
// 主线程 -> Worker
connect(this, &AudioOutput::sigOperateStreamStart, m_streamWorker, &StreamAudioWorker::start);
connect(this, &AudioOutput::sigOperateStreamChunk, m_streamWorker, &StreamAudioWorker::processChunk);
connect(this, &AudioOutput::sigOperateStreamStop, m_streamWorker, &StreamAudioWorker::stop);
// Worker -> 主线程
connect(m_streamWorker, &StreamAudioWorker::errorOccurred, this, &AudioOutput::errorOccurred);
// 线程启动
m_workerThread->start();
}
AudioOutput::~AudioOutput() {
stopPlayback();
// 清理线程
if (m_workerThread) {
m_workerThread->quit();
m_workerThread->wait(3000); // 等待退出
delete m_streamWorker;
}
}
// 对外接口
void AudioOutput::stopPlayback() {
// 停止文件播放
if (m_player->playbackState() != QMediaPlayer::StoppedState) {
m_player->stop();
}
// 停止流播放
if (m_isStreaming) {
stopStream();
}
}
void AudioOutput::setVolume(int volume) {
if (m_audioOutput) m_audioOutput->setVolume(volume / 100.0);
// 注意:流式播放的音量控制需要在 Worker 内单独实现
}
bool AudioOutput::isPlaying() const {
return (m_player->playbackState() == QMediaPlayer::PlayingState) || m_isStreaming;
}
// 文件播放
void AudioOutput::playUrl(const QUrl& url) {
stopPlayback(); // 互斥,播放新文件前停止旧的
m_player->setSource(url);
m_player->play();
}
void AudioOutput::playData(const QByteArray& data) {
// 这个方法对于 QMediaPlayer 比较麻烦,需要自定义 QIODevice
// 建议直接走 stream 接口,或者使用 QBuffer + StreamWorker 的一次性模式
// 为了简单,这里将 buffer 视为 stream 播放
stopPlayback();
startStream(44100, 2, 16); // 假设默认 wav 格式,Worker 会自动解析头
pushStreamData(data);
// 不需要显式 stopStream,让它播完
}
// 流式播放
void AudioOutput::startStream(int sampleRate, int channelCount, int bitDepth) {
stopPlayback(); // 确保干净的状态
m_isStreaming = true;
// 通过信号跨线程调用 Worker 的 start
emit sigOperateStreamStart(sampleRate, channelCount, bitDepth);
}
void AudioOutput::pushStreamData(const QByteArray& chunk) {
if (!m_isStreaming) return;
// 直接发射信号,Qt 会把 chunk copy 到子线程事件队列
emit sigOperateStreamChunk(chunk);
}
void AudioOutput::stopStream() {
if (!m_isStreaming) return;
m_isStreaming = false;
emit sigOperateStreamStop();
}
+1
View File
@@ -0,0 +1 @@
实现了录音和播放的功能
@@ -0,0 +1,35 @@
//
// Created by misaki on 2026/1/30.
//
#pragma once
#include <QObject>
#include <QMutex>
#include "AudioDataTransferObject.h"
class AudioDataHandle final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AudioDataHandle) // 禁用拷贝
private:
/**
* 构造函数私有化
* @param parent
*/
explicit AudioDataHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static QScopedPointer<AudioDataHandle> m_instance; // 单例类
static QMutex m_mutex;
private slots:
// 业务接收槽函数,当获取到音频数据包时,进行解析并播放
void onAudioPacketReceived(const AudioDataTransferObject& packet);
public:
// 单例访问点
static AudioDataHandle *getInstance();
// 显式销毁
static void destroy();
~AudioDataHandle() override;
};
@@ -0,0 +1,40 @@
//
// Created by misaki on 2026/1/30.
//
/**
* 本模块通过解析AutoAgentDataObject的内容并调用 AutoGUI 模块
* 来完成自动化GUI操作
* 对于GUI自动化执行器而言,运行时只需要有一个实例即可,因此采用单例模式,并在AppCore当中进行创建
*/
#pragma once
#include <QObject>
#include <QMutex>
#include "AutoAgentDataObject.h"
class AutoAgentHandle final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(AutoAgentHandle) // 禁用拷贝
private:
/**
* 构造函数私有化
* @param parent
*/
explicit AutoAgentHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static QScopedPointer<AutoAgentHandle> m_instance; // 单例类
static QMutex m_mutex;
private slots:
// 业务接收槽函数,当获取到自动化agent数据包时,进行解析并调用 AutoGUI 模块
void onAutoAgentPacketReceived(const AutoAgentDataObject& packet);
public:
// 单例访问点
static AutoAgentHandle *getInstance();
// 显式销毁
static void destroy();
~AutoAgentHandle() override;
};
@@ -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,38 @@
//
// Created by misaki on 2026/2/1.
//
#pragma once
#include <QObject>
#include <QMutex>
#include "ScreenShotDataTransferObject.h"
class ScreenShotReqDataHandle final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(ScreenShotReqDataHandle) // 禁用拷贝
private:
/**
* 构造函数私有化
* @param parent
*/
explicit ScreenShotReqDataHandle(QObject *parent = nullptr); // 并不将本模块挂在对象树当中,因为本模块为单例类,内存自行管理
static QScopedPointer<ScreenShotReqDataHandle> m_instance; // 单例类
static QMutex m_mutex;
private slots:
// 业务接收槽函数,当获取到截图数据包时,进行解析并处理
void onScreenShotPacketReceived(const ScreenShotDataTransferObject& packet) const;
signals:
// 发送截图处理完成的信号,供界面显示使用
void screenShotProcessed(const QPixmap& screenshot, const QString& description);
public:
// 单例访问点
static ScreenShotReqDataHandle *getInstance();
// 显式销毁
static void destroy();
~ScreenShotReqDataHandle() override;
private:
QString m_systemInfo;
};
@@ -0,0 +1,56 @@
//
// Created by misaki on 2026/1/30.
//
#include "AudioDataHandle.h"
#include "NetWorkDO.h"
#include "AudioOutput.h"
// 初始化静态成员
QScopedPointer<AudioDataHandle> AudioDataHandle::m_instance;
QMutex AudioDataHandle::m_mutex;
// 单例实现 (QScopedPointer + Mutex)
AudioDataHandle* AudioDataHandle::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new AudioDataHandle());
}
}
return m_instance.data();
}
void AudioDataHandle::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
AudioDataHandle::AudioDataHandle(QObject *parent) : QObject(parent)
{
connect(NetworkDO::getInstance(), &NetworkDO::audioPacketReceived, this, &AudioDataHandle::onAudioPacketReceived);
}
AudioDataHandle::~AudioDataHandle()
{
qDebug() << "AutoAgentHandle destroyed";
}
void AudioDataHandle::onAudioPacketReceived(const AudioDataTransferObject &packet) {
// 管理并调用AudioOutput播放流式wav音频
if (packet.isEnd()) { // 如果是结束包(空包)
AudioOutput::getInstance()->stopStream(); // 停止播放
return;
}
if (packet.isStart()) { // 如果是开始包(单wav 44字节头)
AudioOutput::getInstance()->startStream(packet.sampleRate(), packet.channelCount(), packet.bitDepth());; // 播放开始
return;
}
// 否则加入播放队列即可
AudioOutput::getInstance()->pushStreamData(packet.audioData());
}
@@ -0,0 +1,87 @@
//
// Created by misaki on 2026/1/30.
//
#include "AutoAgentHandle.h"
#include "NetWorkDO.h"
#if defined(Q_OS_LINUX) && !defined(EMBEDDED_LINUX)
#include <SimpleAutoGUI.h> // 引入 AutoGUI 头文件
#endif
// 初始化静态成员
QScopedPointer<AutoAgentHandle> AutoAgentHandle::m_instance;
QMutex AutoAgentHandle::m_mutex;
// 单例实现 (QScopedPointer + Mutex)
AutoAgentHandle* AutoAgentHandle::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new AutoAgentHandle());
}
}
return m_instance.data();
}
void AutoAgentHandle::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
AutoAgentHandle::AutoAgentHandle(QObject *parent) : QObject(parent)
{
connect(NetworkDO::getInstance(), &NetworkDO::autoAgentPacketReceived, this, &AutoAgentHandle::onAutoAgentPacketReceived);
}
AutoAgentHandle::~AutoAgentHandle()
{
qDebug() << "AutoAgentHandle destroyed";
}
void AutoAgentHandle::onAutoAgentPacketReceived(const AutoAgentDataObject &packet) {
qDebug() << "Received AutoAgent packet: " << packet.getAction();
#if defined(Q_OS_LINUX) && !defined(EMBEDDED_LINUX)
if (packet.getAction() == "click") { // 单击
qDebug() << "Click: " << packet.getX1() << ", " << packet.getY1();
AutoGUI::moveToOnCurrentScreen(packet.getX1(), packet.getY1(), 0.6);
AutoGUI::click(packet.getX1(), packet.getY1());
}
if (packet.getAction() == "left_double") { // 双击
qDebug() << "Double click: " << packet.getX1() << ", " << packet.getY1();
AutoGUI::moveToOnCurrentScreen(packet.getX1(), packet.getY1(), 0.6);
AutoGUI::leftDouble(packet.getX1(), packet.getY1());
}
if (packet.getAction() == "right_single") { // 右键单击
qDebug() << "Right click: " << packet.getX1() << ", " << packet.getY1();
AutoGUI::moveToOnCurrentScreen(packet.getX1(), packet.getY1(), 0.6);
AutoGUI::rightSingle(packet.getX1(), packet.getY1());
}
if (packet.getAction() == "drag") { // 拖拽
qDebug() << "Drag: " << packet.getX1() << ", " << packet.getY1() << " to " << packet.getX2() << ", " << packet.getY2();
AutoGUI::drag(packet.getX1(), packet.getY1(), packet.getX2(), packet.getY2(), 1.2);
}
// TODO: 快捷键,输入文本,滚动待实现
if (packet.getAction() == "type") { // 输入文本
qDebug() << "Type: " << packet.getContent();
AutoGUI::type(packet.getContent().toStdString(), 0.08);
}
if (packet.getAction() == "scroll") { // 滚动
qDebug() << "Scroll: " << packet.getX1() << ", " << packet.getY1() << packet.getDirection();
if (packet.getDirection() == "up") {
AutoGUI::moveToOnCurrentScreen(packet.getX1(), packet.getY1(), 0.1);
AutoGUI::scroll(40, 1);
}
if (packet.getDirection() == "down") {
AutoGUI::moveToOnCurrentScreen(packet.getX1(), packet.getY1(), 0.1);
AutoGUI::scroll(40, -1);
}
}
#endif
}
@@ -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,69 @@
//
// Created by misaki on 2026/2/1.
//
#include "ScreenShotReqDataHandle.h"
#include "NetWorkDO.h"
#include <QDebug>
#include <QPixmap>
#include "ScreenShotDataTransferObject.h"
#include "ScreenHelperUtil.hpp"
// 初始化静态成员
QScopedPointer<ScreenShotReqDataHandle> ScreenShotReqDataHandle::m_instance;
QMutex ScreenShotReqDataHandle::m_mutex;
// 单例实现 (QScopedPointer + Mutex)
ScreenShotReqDataHandle* ScreenShotReqDataHandle::getInstance()
{
if (m_instance.isNull()) {
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
// 使用 reset 创建实例,因为构造函数是私有的
m_instance.reset(new ScreenShotReqDataHandle());
}
}
return m_instance.data();
}
void ScreenShotReqDataHandle::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset(); // 这会触发析构函数
}
}
ScreenShotReqDataHandle::ScreenShotReqDataHandle(QObject *parent) : QObject(parent)
{
connect(NetworkDO::getInstance(), &NetworkDO::screenShotPacketReceived,
this, &ScreenShotReqDataHandle::onScreenShotPacketReceived);
// 初始化时候就构造好关于当前运行平台的信息
ScreenHelper::SystemInfo sysInfo = ScreenHelper::getSystemInfo();
const QString sysText = QString("System: %1 OS Version: %2 Display Server: %3")
.arg(sysInfo.osType, sysInfo.osVersion, sysInfo.displayServer);
this->m_systemInfo = sysText;
qDebug() << "当前平台信息为: " << sysText;
}
ScreenShotReqDataHandle::~ScreenShotReqDataHandle()
{
qDebug() << "ScreenShotDataHandle destroyed";
}
void ScreenShotReqDataHandle::onScreenShotPacketReceived(const ScreenShotDataTransferObject &packet) const {
qDebug() << "ScreenShot packet request from:" << packet.owner();
// 截图当前画面并构造对等DTO发送
const ScreenHelper::ScreenshotResult result = ScreenHelper::captureFocusedScreen(); // 获取当前屏幕截图
if (!result.success) { // 如果截图失败
// TODO: 考虑失败时候构造一个错误DTO给服务端
qDebug() << "截图失败: " << result.errorMsg;
return;
}
ScreenShotDataTransferObject reback; // 构造返回的DTO
reback.setData("isSuccess", true).setData("RealTimeScreenShot", result.base64Data)
.setData("Width", result.width).setData("Height", result.height)
.setData("DescribeInfo", this->m_systemInfo).setData("LLMResponse", packet.LLMResponse());
// 发送DTO
NetworkDO::getInstance()->sendPacket(reback);
qDebug() << "ScreenShot packet sent to:" << packet.owner();
}
+1
View File
@@ -0,0 +1 @@
### 本模块负责将服务端返回的数据对象做解析并执行相应的动作
@@ -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,95 @@
//
// Created by Administrator on 2025/1/19.
//
/**
* 已废弃
*/
#ifndef AIRI_DESKTOPGRIL_NETWORKMANAGER_H
#define AIRI_DESKTOPGRIL_NETWORKMANAGER_H
#include <QObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QThread>
#include <QTimer>
#include <QMutex>
#include <QWaitCondition>
class NetWorkManager : public QObject
{
Q_OBJECT
public:
explicit NetWorkManager(QObject *parent = nullptr);
~NetWorkManager();
// GET 请求
void get(const QString &url);
// POST 请求(JSON 数据)
void post(const QString &url, const QJsonObject &json);
// 文件下载
void downloadFile(const QString &url, const QString &savePath);
// 文件上传
void uploadFile(const QString &url, const QString &filePath);
// 设置超时时间(毫秒)
void setTimeout(int timeout);
// 设置请求头
void setHeader(const QString &key, const QString &value);
// 清除请求头
void clearHeaders();
signals:
// 请求完成信号,返回响应数据
void requestFinished(const QByteArray &response);
// 下载进度信号
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
// 上传进度信号
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
// 错误信号
void errorOccurred(const QString &errorString);
// 超时信号
void timeoutOccurred();
private slots:
// 请求完成槽函数
void onReplyFinished(QNetworkReply *reply);
// 下载进度槽函数
void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
// 上传进度槽函数
void onUploadProgress(qint64 bytesSent, qint64 bytesTotal);
// 超时槽函数
void onTimeout();
private:
QNetworkAccessManager *manager; /// 网络管理对象
QFile *file; /// 文件对象(用于下载/上传)
QNetworkReply *reply; /// 网络响应对象
QTimer *timer; /// 超时计时器
QMutex mutex; /// 互斥锁
QWaitCondition condition; /// 条件变量
QMap<QString, QString> headers; /// 请求头
int timeout; /// 超时时间
};
#endif //AIRI_DESKTOPGRIL_NETWORKMANAGER_H
@@ -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,106 @@
//
// Created by Administrator on 2025/2/5.
//
#ifndef AIRI_DESKTOPGRIL_SOCKETMANAGER_H
#define AIRI_DESKTOPGRIL_SOCKETMANAGER_H
#include <QTcpSocket>
#include <QObject>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QFile>
#include <QUrl>
#include <QDataStream>
#include <QHostAddress>
#include <QDateTime>
#include <QMutex>
/**
* @author Misaki
* @brief SocketManager类
* 本模块基于QTcpSocket进一步拓展,主要针对音频文件的上传与下载
* 基于CS架构与服务端进行通信
* Socket提供长连接支持,异步通信
*/
class SocketManager : public QTcpSocket {
Q_OBJECT
private:
// 构造函数私有化
explicit SocketManager(QObject *parent = nullptr);
public:
// 删除拷贝构造函数和赋值运算符,禁止复制
SocketManager(const SocketManager&) = delete;
SocketManager& operator=(const SocketManager&) = delete;
~SocketManager();
// 获取单例的静态方法
static SocketManager* getInstance();
void connectToServer();
void disconnectFromServer();
/**
* 发送文件(wav) \n
* 使用前需要先确保连接到服务端,不要调用完connectToServer后就直接调用这个函数 \n
* TCP握手是需要一个很短的时间的,等连接稳定了多次调用这个函数就没有问题了 \n
* 注意:调用这个函数请显示传入QString类型,否则编译器会不知道使用哪一个重载 \n
* 原因如下: \n
* 当调用 sendWavFile 时,如果传递的参数类型可能会被这两种函数参数类型接受或隐式转换,编译器就会报 “ambigous” 错误。例如: \n
如果调用 sendWavFile("audio.wav"),因为 "audio.wav" 是一个 C 风格的字符串(const char*), \n
而 QString 和 QByteArray 都可以接受 const char* 的隐式转换: \n
QString 的构造函数可以接受一个 const char*。 \n
QByteArray 的构造函数也可以接受一个 const char*。 \n
因此,编译器无法确定是要调用 sendWavFile(const QString &filePath) 还是 sendWavFile(const QByteArray &wavData),从而导致歧义。\n
* @author Misaki
* @param filePath
*/
void sendWavFile(const QString &filePath);
/**
* 直接发送二进制数据(wav)
* 使用前需要先确保连接到服务端,不要调用完connectToServer后就直接调用这个函数
* TCP握手是需要一个很短的时间的,等连接稳定了多次调用这个函数就没有问题了
* @author Misaki
* @param wavData
*/
void sendWavFile(const QByteArray &wavData);
// 设置目标服务端ip和端口的get&set方法
void setIp(const QString &ip);
QString getIp();
void setPort(qint16 port);
qint16 getPort();
signals:
void revWavFileFinish(const QString &filePath, const QString &response, const float duration);
void revWavDataFinish(const QByteArray &wavData); // 字节流信号
private slots:
/**
* 处理接收到的数据
* @author Misaki
*/
void handleReadyRead();
private:
// 单例实例指针
static SocketManager* m_instance;
static QMutex m_mutex; // 互斥锁确保线程安全
QString ip = "127.0.0.1"; /// 目标ip
qint16 port = 12345; /// 目标端口
QString filePath = "WavFiles\\"; /// 文件路径
QString receiveBuffer; /// 接收缓冲区
const char *endMarker = "<Eden*>"; /// 结束标记
};
#endif //AIRI_DESKTOPGRIL_SOCKETMANAGER_H
@@ -0,0 +1,150 @@
//
// Created by Administrator on 2025/2/4.
//
#pragma once
#include <QWebSocket>
#include <QObject>
#include <QThread>
#include <QJsonDocument>
#include <QTimer>
#include <QQueue>
#include <QAtomicPointer>
#include <QMutexLocker>
/**
* 2025.12.25重构 Misaki
* 多线程websocket实现
*/
class WebSocketManager final : public QObject
{
Q_OBJECT
public:
// 构造函数
explicit WebSocketManager(QObject *parent = nullptr); // 不携带URL参数
~WebSocketManager() override;
// 删除拷贝构造函数和赋值操作符
WebSocketManager(const WebSocketManager&) = delete;
WebSocketManager& operator=(const WebSocketManager&) = delete;
public:
bool setRequestContent(const QString& requestToken); // 设置自定义websocket首次请求Token(鉴权用) 服务端应保持Token一致
bool setSocketUrl(QUrl url); // 设置URL
signals:
// 发给主线程的信号
void connected(); // 连接
void disconnected(); // 断开
void textReceived(const QString &message); // 接收文本
void jsonReceived(const QString &type, const QJsonObject &data); // 接收JSON
void binaryReceived(const QByteArray &data); // 接收二进制数据
void error(const QString &errorMsg); // 错误
void log(const QString &msg); // 日志
void reconnecting(int attempt); // 重连
public slots:
// 主线程调用的槽
bool connectToServer(); // 连接到服务器
void disconnectFromServer(); // 断开连接
void sendText(const QString &message); // 发送文本
void sendJson(const QString &type, const QJsonObject &data); // 发送JSON
void sendBinary(const QByteArray &data); // 发送二进制数据
void setReconnectEnabled(bool enabled); // 设置重连
void setRequestEnabled(bool enabled); // 设置自定义首次请求
[[nodiscard]] bool isConnected() const; // 是否已连接
private slots:
void onConnected(); // 连接成功
void onDisconnected(); // 断开连接
void onTextMessageReceived(const QString &message); // 接收到文本消息
void onError(QAbstractSocket::SocketError socketError); // 错误
void onSslErrors(const QList<QSslError> &errors); // SSL错误
void onPong(quint64 elapsedTime, const QByteArray &payload); // Pong
void sendPing() const; // 发送Ping
void tryReconnect(); // 尝试重连
private:
QWebSocket *m_socket; /// WebSocket对象
QUrl m_url; /// 服务器地址
QTimer *m_pingTimer; /// Ping定时器
QTimer *m_reconnectTimer; /// 重连定时器
int m_reconnectAttempts; /// 重连尝试次数
bool m_isReconnectEnabled; /// 是否启用重连
QNetworkRequest m_request; /// 自定义websocket首次请求(鉴权用)
bool m_isRequest; /// 是否启用websocket首次请求
};
/**
* WebSocket 客户端单例管理类
* 负责管理 WebSocket 线程和全局访问
*/
class WebSocketClient final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY(WebSocketClient)
public:
static WebSocketClient* getInstance();
static void destroy();
// 初始化/重新配置 WebSocket
bool setConfiguration(const QUrl& url, const QString& authToken = QString());
// WebSocket 操作
void connectToServer();
void disconnectFromServer();
void reconnect();
void sendText(const QString& message);
void sendJson(const QString& type, const QJsonObject& data);
void sendBinary(const QByteArray& data);
[[nodiscard]] bool isConnected() const;
[[nodiscard]] bool hasConfiguration() const { return m_url.isValid(); }
void setAutoReconnect(bool enabled);
void setPingInterval(int milliseconds);
[[nodiscard]] QUrl currentUrl() const { return m_url; }
[[nodiscard]] QString currentToken() const { return m_authToken; }
// 获取内部管理器(仅供高级使用)
[[nodiscard]] WebSocketManager* manager() const { return m_webSocketManager; }
signals:
// WebSocket 事件
void connected();
void disconnected();
void textReceived(const QString &message);
void jsonReceived(const QString &type, const QJsonObject &data);
void binaryReceived(const QByteArray &data);
void error(const QString &errorMsg);
void log(const QString &msg);
void reconnecting(int attempt);
// 配置变更
void configurationChanged(const QUrl& oldUrl, const QUrl& newUrl);
// 内部信号(用于跨线程通信)
void internalSetUrl(const QUrl& url);
void internalSetAuthToken(const QString& token);
void internalConnect();
void internalDisconnect();
void internalSendText(const QString& message);
void internalSendJson(const QString& type, const QJsonObject& data);
void internalSendBinary(const QByteArray& data);
void internalSetAutoReconnect(bool enabled);
void internalSetRequestEnabled(bool enabled);
public:
~WebSocketClient() override;
private:
explicit WebSocketClient(QObject *parent = nullptr);
static QMutex m_mutex;
static QScopedPointer<WebSocketClient> m_instance;
QThread* m_workerThread;
WebSocketManager* m_webSocketManager;
QUrl m_url;
QString m_authToken;
bool m_hasAuthToken;
};
@@ -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;
}
@@ -0,0 +1,216 @@
//
// Created by Administrator on 2025/1/19.
//
#include "networkmanager.h"
#include <QHttpMultiPart>
#include <QHttpPart>
#include <QFileInfo>
#include <QThread>
NetWorkManager::NetWorkManager(QObject *parent) : QObject(parent)
{
this->manager = new QNetworkAccessManager(this);
this->file = nullptr;
this->reply = nullptr;
this->timer = new QTimer(this);
this->timeout = 30000; // 默认超时时间为 30 秒
// 连接请求完成信号
connect(this->manager, &QNetworkAccessManager::finished, this, &NetWorkManager::onReplyFinished);
// 连接超时信号
connect(this->timer, &QTimer::timeout, this, &NetWorkManager::onTimeout);
}
NetWorkManager::~NetWorkManager()
{
if (this->reply) {
this->reply->abort();
this->reply->deleteLater();
}
if (this->file) {
this->file->close();
delete this->file;
}
}
// GET 请求
void NetWorkManager::get(const QString &url)
{
QNetworkRequest request;
request.setUrl(QUrl(url));
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
this->reply = manager->get(request);
// 启动超时计时器
this->timer->start(timeout);
// 连接下载进度信号
connect(this->reply, &QNetworkReply::downloadProgress, this, &NetWorkManager::onDownloadProgress);
}
// POST 请求(JSON 数据)
void NetWorkManager::post(const QString &url, const QJsonObject &json)
{
QNetworkRequest request;
request.setUrl(QUrl(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
QByteArray data = QJsonDocument(json).toJson();
this->reply = manager->post(request, data);
// 启动超时计时器
this->timer->start(timeout);
// 连接上传进度信号
connect(this->reply, &QNetworkReply::uploadProgress, this, &NetWorkManager::onUploadProgress);
}
// 文件下载
void NetWorkManager::downloadFile(const QString &url, const QString &savePath)
{
QNetworkRequest request;
request.setUrl(QUrl(url));
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
this->reply = manager->get(request);
// 启动超时计时器
this->timer->start(timeout);
// 打开文件
this->file = new QFile(savePath);
if (!this->file->open(QIODevice::WriteOnly)) {
emit errorOccurred("Failed to open file for writing");
delete this->file;
this->file = nullptr;
return;
}
// 连接下载进度信号
connect(this->reply, &QNetworkReply::downloadProgress, this, &NetWorkManager::onDownloadProgress);
// 读取数据并写入文件
connect(reply, &QNetworkReply::readyRead, this, [this]() {
if (this->file) {
this->file->write(reply->readAll());
}
});
}
// 文件上传
void NetWorkManager::uploadFile(const QString &url, const QString &filePath)
{
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
// 创建文件部分
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"" + QFileInfo(filePath).fileName() + "\""));
filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream"));
QFile *file = new QFile(filePath);
if (!file->open(QIODevice::ReadOnly)) {
emit errorOccurred("Failed to open file for reading");
delete file;
return;
}
filePart.setBodyDevice(file);
file->setParent(multiPart); // 将文件对象绑定到 multiPart,由 multiPart 负责释放
multiPart->append(filePart);
// 发送请求
QNetworkRequest request;
request.setUrl(QUrl(url));
// 设置请求头
for (auto it = headers.begin(); it != headers.end(); ++it) {
request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
}
this->reply = manager->post(request, multiPart);
multiPart->setParent(reply); // 将 multiPart 绑定到 reply,由 reply 负责释放
// 启动超时计时器
this->timer->start(timeout);
// 连接上传进度信号
connect(reply, &QNetworkReply::uploadProgress, this, &NetWorkManager::onUploadProgress);
}
// 设置超时时间
void NetWorkManager::setTimeout(int timeout)
{
this->timeout = timeout;
}
// 设置请求头
void NetWorkManager::setHeader(const QString &key, const QString &value)
{
this->headers[key] = value;
}
// 清除请求头
void NetWorkManager::clearHeaders()
{
this->headers.clear();
}
// 请求完成槽函数
void NetWorkManager::onReplyFinished(QNetworkReply *reply)
{
this->timer->stop(); // 停止超时计时器
if (reply->error() == QNetworkReply::NoError) {
QByteArray response = reply->readAll();
emit requestFinished(response);
} else {
emit errorOccurred(reply->errorString());
}
// 关闭并释放文件对象
if (file) {
file->close();
delete file;
file = nullptr;
}
reply->deleteLater();
}
// 下载进度槽函数
void NetWorkManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
emit downloadProgress(bytesReceived, bytesTotal);
}
// 上传进度槽函数
void NetWorkManager::onUploadProgress(qint64 bytesSent, qint64 bytesTotal)
{
emit uploadProgress(bytesSent, bytesTotal);
}
// 超时槽函数
void NetWorkManager::onTimeout()
{
if (reply) {
this->reply->abort();
emit timeoutOccurred();
}
}
@@ -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;
}
@@ -0,0 +1,208 @@
//
// Created by Administrator on 2025/2/5.
//
#include "socketmanager.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
// 初始化静态成员变量
SocketManager* SocketManager::m_instance = nullptr;
QMutex SocketManager::m_mutex;
SocketManager* SocketManager::getInstance()
{
QMutexLocker locker(&m_mutex); // 自动加锁,确保线程安全
if (!m_instance) {
m_instance = new SocketManager(); // 延迟初始化,首次调用时创建实例
}
return m_instance;
}
SocketManager::SocketManager(QObject *parent) : QTcpSocket(parent)
{
connect(this, &QTcpSocket::connected, [=]() {
qDebug() << "SocketManager::connected !";
});
connect(this, &QTcpSocket::disconnected, [=]() {
qDebug() << "SocketManager::disconnected !";
});
// 当有数据到达就调用handleReadyRead进行处理数据
connect(this, &QTcpSocket::readyRead, this, &SocketManager::handleReadyRead);
}
SocketManager::~SocketManager()
{
}
void SocketManager::connectToServer()
{
// 如果未连接,就连接
if(this->state() == QAbstractSocket::UnconnectedState){
this->connectToHost(this->ip, this->port);
return;
}
// 已连接则返回
if(this->state() == QAbstractSocket::ConnectedState){
return;
}
}
void SocketManager::disconnectFromServer()
{
this->disconnectFromHost();
}
/**
* 报文形式:先发送数据长度,再发送数据本身
* 数据长度占4字节,大端
* 数据本身压缩发送
* @param filePath
*/
void SocketManager::sendWavFile(const QString &filePath)
{
if(this->state() == QAbstractSocket::ConnectedState){
QFile file(filePath);
if(file.open(QIODevice::ReadOnly)){
// 压缩数据
QByteArray compressedData = qCompress(file.readAll(), 9);
// 发送数据长度(4字节,大端)
quint32 totalSize = compressedData.size();
QByteArray sizeData;
QDataStream sizeStream(&sizeData, QIODevice::WriteOnly);
sizeStream.setByteOrder(QDataStream::BigEndian);
sizeStream << totalSize;
this->write(sizeData);
this->flush();
// 发送压缩数据
this->write(compressedData);
this->flush();
file.close();
}
} else{
qDebug() << "SocketManager::发送失败! 请检查是否连接到服务端!";
}
}
/**
* 发送二进制WAV数据
* 数据长度占4字节,大端
* 数据本身压缩发送
* @param wavData
*/
void SocketManager::sendWavFile(const QByteArray &wavData)
{
if (this->state() == QAbstractSocket::ConnectedState) {
if (!wavData.isEmpty()) {
// 压缩数据(保持与文件发送相同的压缩方式)
QByteArray compressedData = qCompress(wavData, 9);
// 发送数据长度(4字节大端)
quint32 totalSize = compressedData.size();
QByteArray sizeData;
QDataStream sizeStream(&sizeData, QIODevice::WriteOnly);
sizeStream.setByteOrder(QDataStream::BigEndian);
sizeStream << totalSize;
// 分步发送确保可靠性
this->write(sizeData);
this->flush();
this->write(compressedData);
this->flush();
qDebug() << "二进制WAV数据已发送,原始大小:" << wavData.size()
<< "压缩后大小:" << compressedData.size();
} else {
qWarning() << "尝试发送空的WAV数据";
}
} else {
qDebug() << "SocketManager::发送失败! 未连接到服务端!";
}
}
void SocketManager::handleReadyRead()
{
receiveBuffer.append(this->readAll()); // 累积数据到缓冲区
// 检查是否包含结束标记
int endIndex;
while ((endIndex = receiveBuffer.indexOf(endMarker)) != -1) {
// 提取结束标记前的数据
QString data = receiveBuffer.left(endIndex);
receiveBuffer = receiveBuffer.mid(endIndex + strlen(endMarker)); // 移除已处理数据
// 处理数据
// 使用动态文件名
QFile rev(filePath + "revTest_" + QDateTime::currentDateTime().toString("yyyy-MM-dd_hh-mm-ss") +".wav");
if (rev.open(QIODevice::WriteOnly)) {
QJsonDocument jsonDoc = QJsonDocument::fromJson(data.toUtf8());
// 提取JSON对象
QJsonObject jsonObj = jsonDoc.object();
QString response;
// 解析response字段
if (jsonObj.contains("response") && jsonObj["response"].isString()) {
response = jsonObj["response"].toString();
qDebug() << "解析到response:" << response;
} else {
qDebug() << "response字段缺失或类型错误";
}
float duration;
if (jsonObj.contains("wav_duration") && jsonObj["wav_duration"].isDouble()) {
duration = jsonObj["wav_duration"].toDouble();
qDebug() << "解析到duration:" << duration;
} else {
qDebug() << "duration字段缺失或类型错误";
}
QByteArray wavData;
// 解析wav_data_base64字段
if (jsonObj.contains("wav_data_base64") && jsonObj["wav_data_base64"].isString()) {
QString base64Data = jsonObj["wav_data_base64"].toString();
wavData = QByteArray::fromBase64(base64Data.toUtf8());
// 获取当前音频时长
qDebug() << "音频数据大小:" << wavData.size() << "字节";
} else {
qDebug() << "wav_data_base64字段缺失或类型错误";
}
rev.write(wavData);
rev.close();
// 清空缓冲区,这一步很重要,不然会导致下次接收数据时,会保存着上次接收的数据
receiveBuffer.clear();
// 发送信号
emit revWavFileFinish(rev.fileName(), response, duration);
emit revWavDataFinish(wavData);
qDebug() << "文件接收完成并保存: " + rev.fileName();
} else {
qDebug() << "无法保存文件";
}
}
}
void SocketManager::setIp(const QString &ip)
{
this->ip = ip;
}
QString SocketManager::getIp()
{
return this->ip;
}
void SocketManager::setPort(qint16 port)
{
this->port = port;
}
qint16 SocketManager::getPort()
{
return this->port;
}
@@ -0,0 +1,476 @@
//
// Created by Administrator on 2025/2/4.
//
#include "websocketmanager.h"
#include <QDebug>
#include <QJsonObject>
#include <utility>
#include <QMutexLocker>
/// WebSocketManager
WebSocketManager::WebSocketManager(QObject *parent)
: QObject(parent)
, m_socket(new QWebSocket) // 创建 WebSocket 对象
, m_pingTimer(new QTimer(this))
, m_reconnectTimer(new QTimer(this))
, m_reconnectAttempts(0) // 重连尝试次数初始为0
, m_isReconnectEnabled(false) // 默认不启用重连
, m_isRequest(false) // 默认不启用自定义首次请求
{
// 配置 WebSocket
m_socket->setParent(this); // 确保 socket 也在工作线程
m_socket->setMaxAllowedIncomingFrameSize(50 * 1024 * 1024); // 单帧最大 50MB // 防止发送大数据包导致websocket断开
m_socket->setMaxAllowedIncomingMessageSize(50 * 1024 * 1024); // 完整消息最大 50MB
// 连接信号
connect(m_socket, &QWebSocket::connected,
this, &WebSocketManager::onConnected);
connect(m_socket, &QWebSocket::disconnected,
this, &WebSocketManager::onDisconnected);
connect(m_socket, &QWebSocket::textMessageReceived,
this, &WebSocketManager::onTextMessageReceived);
connect(m_socket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred),
this, &WebSocketManager::onError);
connect(m_socket, &QWebSocket::sslErrors,
this, &WebSocketManager::onSslErrors);
connect(m_socket, &QWebSocket::pong,
this, &WebSocketManager::onPong);
// 心跳定时器
m_pingTimer->setInterval(30000); // 30秒
connect(m_pingTimer, &QTimer::timeout, this, &WebSocketManager::sendPing);
// 重连定时器
m_reconnectTimer->setSingleShot(true);
connect(m_reconnectTimer, &QTimer::timeout, this, &WebSocketManager::tryReconnect);
}
WebSocketManager::~WebSocketManager() {
if (m_socket) {
m_socket->close();
m_socket->deleteLater();
}
}
bool WebSocketManager::setRequestContent(const QString& requestToken) {
if (this->m_url.isEmpty()) {
emit error("URL is empty!");
return false;
}
m_request.setUrl(this->m_url);
m_request.setRawHeader("Authorization", requestToken.toUtf8()); // 设置请求头(包含鉴权Token)
return true;
}
bool WebSocketManager::setSocketUrl(QUrl url) {
if (this->m_url == url) {return true;} // 如果 URL 没有变化,直接返回成功
// 判断URL是否合法,即是否符合websocket的格式
if (!url.isValid() || url.scheme() != "ws" && url.scheme() != "wss") {
emit error("Invalid URL!");
return false;
}
this->m_url = std::move(url); // 设置 URL
return true;
}
bool WebSocketManager::connectToServer() {
if (m_socket->state() != QAbstractSocket::UnconnectedState) { // 如果已经连接,则先断开连接重新连接
m_socket->close();
}
if (m_url.isEmpty()) { // 如果 URL 为空
emit error("URL is empty!");
return false;
}
emit log(QString("Connecting to %1...").arg(m_url.toString()));
// SSL 配置
if (m_url.scheme() == "wss") {
QSslConfiguration sslConfig = m_socket->sslConfiguration();
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); // 开发环境
m_socket->setSslConfiguration(sslConfig);
}
if (m_isRequest) { // 如果需要自定义首次请求
m_socket->open(this->m_request); // 使用重载函数
}
else {
m_socket->open(m_url);
}
return true;
}
void WebSocketManager::disconnectFromServer() {
m_isReconnectEnabled = false; // 手动断开不重连
if (m_socket->state() != QAbstractSocket::UnconnectedState) {
m_socket->close(QWebSocketProtocol::CloseCodeNormal, "Client closed");
}
}
void WebSocketManager::sendText(const QString &message) {
if (m_socket->state() == QAbstractSocket::ConnectedState) {
m_socket->sendTextMessage(message);
}
}
void WebSocketManager::sendJson(const QString &type, const QJsonObject &data) {
QJsonObject wrapper;
wrapper["type"] = type;
wrapper["data"] = data;
wrapper["timestamp"] = QDateTime::currentMSecsSinceEpoch();
sendText(QJsonDocument(wrapper).toJson(QJsonDocument::Compact));
}
void WebSocketManager::sendBinary(const QByteArray &data) {
if (m_socket->state() == QAbstractSocket::ConnectedState) {
m_socket->sendBinaryMessage(data);
}
}
bool WebSocketManager::isConnected() const {
return m_socket->state() == QAbstractSocket::ConnectedState;
}
void WebSocketManager::setReconnectEnabled(bool enabled) {
m_isReconnectEnabled = enabled;
if (!enabled) {
m_reconnectTimer->stop();
}
}
void WebSocketManager::setRequestEnabled(const bool enabled) {
m_isRequest = enabled;
}
// 私有槽
void WebSocketManager::onConnected() {
m_reconnectAttempts = 0;
emit log("✅ WebSocket connected");
emit connected();
m_pingTimer->start();
}
void WebSocketManager::onDisconnected() {
emit log("⚠️ WebSocket disconnected");
emit disconnected();
m_pingTimer->stop();
// 自动重连
if (m_isReconnectEnabled) {
m_reconnectTimer->start(3000); // 3秒后重试
}
}
void WebSocketManager::onTextMessageReceived(const QString &message) {
emit textReceived(message);
// 自动解析 JSON
const QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
if (doc.isObject()) {
const QJsonObject obj = doc.object();
const QString type = obj.value("type").toString();
const QJsonObject data = obj.value("data").toObject();
if (!type.isEmpty()) {
emit jsonReceived(type, data);
}
}
}
void WebSocketManager::onError(QAbstractSocket::SocketError socketError) {
QString errorMsg;
switch (socketError) {
case QAbstractSocket::ConnectionRefusedError:
errorMsg = "Connection refused";
break;
case QAbstractSocket::RemoteHostClosedError:
errorMsg = "Remote host closed";
break;
case QAbstractSocket::HostNotFoundError:
errorMsg = "Host not found";
break;
case QAbstractSocket::SocketTimeoutError:
errorMsg = "Socket timeout";
break;
case QAbstractSocket::NetworkError:
errorMsg = "Network error";
break;
case QAbstractSocket::SslHandshakeFailedError:
errorMsg = "SSL handshake failed";
break;
default:
errorMsg = m_socket->errorString();
}
emit error(QString("Socket error: %1").arg(errorMsg));
}
void WebSocketManager::onSslErrors(const QList<QSslError> &errors) {
foreach (const QSslError &err, errors) {
emit log(QString("SSL error: %1").arg(err.errorString()));
}
#ifdef QT_DEBUG
m_socket->ignoreSslErrors(); // 开发环境忽略
#endif
}
void WebSocketManager::sendPing() const {
if (m_socket->state() == QAbstractSocket::ConnectedState) {
m_socket->ping();
}
}
void WebSocketManager::onPong(quint64 elapsedTime, const QByteArray &) {
emit log(QString("Pong received, latency: %1ms").arg(elapsedTime));
}
void WebSocketManager::tryReconnect() {
if (!m_isReconnectEnabled) return;
m_reconnectAttempts++;
emit reconnecting(m_reconnectAttempts);
emit log(QString("🔄 Reconnecting... (attempt %1)").arg(m_reconnectAttempts));
m_socket->open(m_url);
}
#include <QScopedPointer>
QMutex WebSocketClient::m_mutex;
QScopedPointer<WebSocketClient> WebSocketClient::m_instance;
WebSocketClient* WebSocketClient::getInstance()
{
QMutexLocker locker(&m_mutex);
if (m_instance.isNull()) {
m_instance.reset(new WebSocketClient());
}
return m_instance.data();
}
void WebSocketClient::destroy()
{
QMutexLocker locker(&m_mutex);
if (!m_instance.isNull()) {
m_instance.reset();
}
}
WebSocketClient::WebSocketClient(QObject *parent)
: QObject(parent)
, m_workerThread(nullptr)
, m_webSocketManager(nullptr)
, m_hasAuthToken(false)
{
// 创建工作线程
m_workerThread = new QThread(this);
m_workerThread->setObjectName("WebSocketWorkerThread");
// 创建 WebSocketManager
m_webSocketManager = new WebSocketManager();
m_webSocketManager->moveToThread(m_workerThread);
// 连接线程结束信号
connect(m_workerThread, &QThread::finished,
m_webSocketManager, &QObject::deleteLater);
// 连接 WebSocketManager 的信号到本类的信号(转发到主线程)
connect(m_webSocketManager, &WebSocketManager::connected,
this, &WebSocketClient::connected);
connect(m_webSocketManager, &WebSocketManager::disconnected,
this, &WebSocketClient::disconnected);
connect(m_webSocketManager, &WebSocketManager::textReceived,
this, &WebSocketClient::textReceived);
connect(m_webSocketManager, &WebSocketManager::jsonReceived,
this, &WebSocketClient::jsonReceived);
connect(m_webSocketManager, &WebSocketManager::binaryReceived,
this, &WebSocketClient::binaryReceived);
connect(m_webSocketManager, &WebSocketManager::error,
this, &WebSocketClient::error);
connect(m_webSocketManager, &WebSocketManager::log,
this, &WebSocketClient::log);
connect(m_webSocketManager, &WebSocketManager::reconnecting,
this, &WebSocketClient::reconnecting);
// 连接本类的内部信号到 WebSocketManager 的槽(跨线程调用)
// 注意:由于 internal* 信号是私有信号,我们需要使用 lambda 包装器
connect(this, &WebSocketClient::internalSetUrl,
m_webSocketManager, [this](const QUrl& url) {
bool success = m_webSocketManager->setSocketUrl(url);
if (!success) {
emit error("Failed to set WebSocket URL");
}
}, Qt::QueuedConnection);
connect(this, &WebSocketClient::internalSetAuthToken,
m_webSocketManager, [this](const QString& token) {
if (!token.isEmpty()) {
bool success = m_webSocketManager->setRequestContent(token);
if (!success) {
emit error("Failed to set authentication token");
}
m_webSocketManager->setRequestEnabled(true);
} else {
m_webSocketManager->setRequestEnabled(false);
}
}, Qt::QueuedConnection);
connect(this, &WebSocketClient::internalConnect,
m_webSocketManager, &WebSocketManager::connectToServer,
Qt::QueuedConnection);
connect(this, &WebSocketClient::internalDisconnect,
m_webSocketManager, &WebSocketManager::disconnectFromServer,
Qt::QueuedConnection);
connect(this, &WebSocketClient::internalSendText,
m_webSocketManager, &WebSocketManager::sendText,
Qt::QueuedConnection);
connect(this, &WebSocketClient::internalSendJson,
m_webSocketManager, &WebSocketManager::sendJson,
Qt::QueuedConnection);
connect(this, &WebSocketClient::internalSendBinary,
m_webSocketManager, &WebSocketManager::sendBinary,
Qt::QueuedConnection);
connect(this, &WebSocketClient::internalSetAutoReconnect,
m_webSocketManager, &WebSocketManager::setReconnectEnabled,
Qt::QueuedConnection);
// 启动工作线程
m_workerThread->start();
qDebug() << "WebSocketClient initialized, worker thread:" << m_workerThread;
}
WebSocketClient::~WebSocketClient()
{
qDebug() << "Shutting down WebSocketClient...";
// 断开连接
disconnectFromServer();
// 停止工作线程
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 WebSocketClient::setConfiguration(const QUrl& url, const QString& authToken)
{
if (!url.isValid()) {
emit error("Invalid URL provided");
return false;
}
QUrl oldUrl = m_url;
m_url = url;
m_authToken = authToken;
m_hasAuthToken = !authToken.isEmpty();
// 发送到工作线程进行配置
emit internalSetUrl(url);
if (!authToken.isEmpty()) {
emit internalSetAuthToken(authToken);
}
// 通知配置变更
if (oldUrl != url) {
emit configurationChanged(oldUrl, url);
}
emit log(QString("WebSocket configuration updated: %1").arg(url.toString()));
return true;
}
void WebSocketClient::connectToServer()
{
if (!m_url.isValid()) {
emit error("WebSocket URL not configured. Call setConfiguration() first.");
return;
}
emit log(QString("Connecting to server: %1").arg(m_url.toString()));
emit internalConnect();
}
void WebSocketClient::disconnectFromServer()
{
emit log("Disconnecting from server...");
emit internalDisconnect();
}
void WebSocketClient::reconnect()
{
if (!hasConfiguration()) {
emit error("WebSocket not configured. Cannot reconnect.");
return;
}
// 先断开,再连接
disconnectFromServer();
// 短暂延迟后重新连接
QTimer::singleShot(100, this, [this]() {
emit log("Attempting to reconnect...");
connectToServer();
});
}
void WebSocketClient::sendText(const QString& message)
{
if (isConnected()) {
emit internalSendText(message);
} else {
emit error("Cannot send message: WebSocket is not connected");
}
}
void WebSocketClient::sendJson(const QString& type, const QJsonObject& data)
{
if (isConnected()) {
emit internalSendJson(type, data);
} else {
emit error("Cannot send JSON: WebSocket is not connected");
}
}
void WebSocketClient::sendBinary(const QByteArray& data)
{
if (isConnected()) {
emit internalSendBinary(data);
} else {
emit error("Cannot send binary data: WebSocket is not connected");
}
}
bool WebSocketClient::isConnected() const
{
if (m_webSocketManager) {
bool connected = false;
// 使用阻塞调用获取连接状态
QMetaObject::invokeMethod(const_cast<WebSocketManager*>(m_webSocketManager),
[&connected, manager = m_webSocketManager]() {
connected = manager->isConnected();
},
Qt::BlockingQueuedConnection);
return connected;
}
return false;
}
void WebSocketClient::setAutoReconnect(bool enabled)
{
emit log(QString("Auto reconnect %1").arg(enabled ? "enabled" : "disabled"));
emit internalSetAutoReconnect(enabled);
}
void WebSocketClient::setPingInterval(int milliseconds)
{
// 注意:需要在 WebSocketManager 中添加相应的方法才能支持
// 这里暂时记录日志,提醒需要实现
emit log(QString("setPingInterval(%1) called, but not implemented in WebSocketManager")
.arg(milliseconds));
Q_UNUSED(milliseconds)
}
+236
View File
@@ -0,0 +1,236 @@
当前项目中实现了tcp, socket, websocket三种通信方式
项目只用到了了**websocket**方式,其他两种是历史遗留(Yosuga[Qt5]所使用)
并且websocket经过了重构,交互数据也为自定义格式
这边顺便提供下WebSocket类的Mermaid图,帮助理解(将代码丢给AI生成出来的图,审阅了一下还是十分准确的)
## 1. 类图
```mermaid
classDiagram
class WebSocketManager {
-QWebSocket* m_socket
-QUrl m_url
-QTimer* m_pingTimer
-QTimer* m_reconnectTimer
-int m_reconnectAttempts
-bool m_isReconnectEnabled
-QNetworkRequest m_request
-bool m_isRequest
+WebSocketManager(parent)
~WebSocketManager()
+setRequestContent(requestToken) bool
+setSocketUrl(url) bool
+connectToServer() bool
+disconnectFromServer()
+sendText(message)
+sendJson(type, data)
+sendBinary(data)
+setReconnectEnabled(enabled)
+setRequestEnabled(enabled)
+isConnected() bool
-onConnected()
-onDisconnected()
-onTextMessageReceived(message)
-onError(socketError)
-onSslErrors(errors)
-onPong(elapsedTime, payload)
-sendPing()
-tryReconnect()
-- signals --
+connected()
+disconnected()
+textReceived(message)
+jsonReceived(type, data)
+binaryReceived(data)
+error(errorMsg)
+log(msg)
+reconnecting(attempt)
}
class WebSocketClient {
-static QMutex m_mutex
-static QScopedPointer~WebSocketClient~ m_instance
-QThread* m_workerThread
-WebSocketManager* m_webSocketManager
-QUrl m_url
-QString m_authToken
-bool m_hasAuthToken
+getInstance() WebSocketClient*
+destroy()
+setConfiguration(url, authToken) bool
+connectToServer()
+disconnectFromServer()
+reconnect()
+sendText(message)
+sendJson(type, data)
+sendBinary(data)
+isConnected() bool
+hasConfiguration() bool
+setAutoReconnect(enabled)
+setPingInterval(milliseconds)
+currentUrl() QUrl
+currentToken() QString
+manager() WebSocketManager*
-- signals --
+connected()
+disconnected()
+textReceived(message)
+jsonReceived(type, data)
+binaryReceived(data)
+error(errorMsg)
+log(msg)
+reconnecting(attempt)
+configurationChanged(oldUrl, newUrl)
-internalSetUrl(url)
-internalSetAuthToken(token)
-internalConnect()
-internalDisconnect()
-internalSendText(message)
-internalSendJson(type, data)
-internalSendBinary(data)
-internalSetAutoReconnect(enabled)
-internalSetRequestEnabled(enabled)
}
class QWebSocket {
+open(url)
+close()
+sendTextMessage(message)
+sendBinaryMessage(data)
+ping()
+state() QAbstractSocket::SocketState
}
class QThread {
+start()
+quit()
+wait()
+terminate()
+isRunning() bool
}
WebSocketClient "1" --> "1" WebSocketManager : 管理
WebSocketClient "1" --> "1" QThread : 工作线程
WebSocketManager "1" --> "1" QWebSocket : 封装
WebSocketManager "1" --> "2" QTimer : 心跳/重连
```
## 2. 连接时序图
```mermaid
sequenceDiagram
participant MainThread as 主线程
participant Client as WebSocketClient
participant WorkerThread as 工作线程
participant Manager as WebSocketManager
participant Socket as QWebSocket
participant Server as WebSocket服务器
MainThread->>Client: getInstance()
Client-->>MainThread: WebSocketClient实例
MainThread->>Client: setConfiguration(url, token)
Client->>WorkerThread: 创建工作线程
Client->>Manager: 创建WebSocketManager
Manager->>Socket: 创建QWebSocket
Client->>Manager: 信号连接配置
Client->>Manager: 设置URL和Token
MainThread->>Client: connectToServer()
Client->>Manager: emit internalConnect()
Manager->>Socket: open(url/request)
Socket->>Server: WebSocket握手
Server-->>Socket: 连接成功
Socket->>Manager: connected()
Manager->>Manager: 启动心跳定时器
Manager->>Client: emit connected()
Client->>MainThread: emit connected()
```
## 3. 状态图
```mermaid
stateDiagram-v2
[*] --> 未配置
未配置 --> 已配置 : setConfiguration()
已配置 --> 连接中 : connectToServer()
连接中 --> 已连接 : 连接成功
连接中 --> 重连中 : 连接失败
连接中 --> [*] : disconnectFromServer()
已连接 --> 已连接 : 发送/接收数据
已连接 --> 已断开 : 连接断开
已连接 --> [*] : disconnectFromServer()
已断开 --> 重连中 : 自动重连开启
已断开 --> [*] : 自动重连关闭
重连中 --> 已连接 : 重连成功
重连中 --> 重连中 : 重连失败(继续重试)
重连中 --> [*] : disconnectFromServer()
state 重连中 {
[*] --> 等待重试
等待重试 --> 尝试连接 : 定时器触发
尝试连接 --> 等待重试 : 连接失败
尝试连接 --> [*] : 连接成功
}
```
## 4. 消息发送时序图
```mermaid
sequenceDiagram
participant App as 应用程序
participant Client as WebSocketClient
participant Manager as WebSocketManager
participant Socket as QWebSocket
participant Server as WebSocket服务器
App->>Client: sendJson("message", data)
alt 已连接
Client->>Manager: emit internalSendJson("message", data)
Manager->>Socket: sendTextMessage(json)
Socket->>Server: 发送JSON数据
Server-->>Socket: 响应(可选)
Socket->>Manager: textMessageReceived()
Manager->>Manager: 解析JSON
Manager->>Client: emit jsonReceived()
Client->>App: emit jsonReceived()
else 未连接
Client->>App: emit error("未连接")
end
```
## 5. 线程关系图
```mermaid
flowchart TD
subgraph 主线程 [UI/主线程]
direction LR
App[应用程序]
Client[WebSocketClient<br/>单例]
end
subgraph 工作线程 [WebSocket工作线程]
direction LR
Manager[WebSocketManager]
Socket[QWebSocket]
end
subgraph 外部系统
Server[WebSocket服务器]
end
App -- 调用 --> Client
Client -- 跨线程信号 --> Manager
Manager -- Qt信号槽 --> Socket
Socket -- TCP/WebSocket --> Server
Client -- 返回事件信号 --> App
Socket -- 接收数据 --> Manager
Manager -- 跨线程信号 --> Client
```
+47
View File
@@ -0,0 +1,47 @@
#pragma once
/**
* @brief 菜单
* @author Misaki
*
* 基于Ela UI的菜单控件
*/
#include <ElaMenu.h>
#include <QMenu>
#include <QAction>
#include <QPoint>
#include <QScopedPointer> // 智能指针
#include "Setting.h"
#include "networkmanager.h"
#include "socketmanager.h"
class Menu final : public ElaMenu
{
Q_OBJECT
public:
explicit Menu(QWidget *parent = nullptr);
~Menu() override;
void showMenu(const QPoint &pos);
signals:
void closeMainWindow(); // 自定义关闭主窗口的信号
void startPlay(); // 自定义开始播放的信号
private:
void createMenu();
QAction *toggleThe; /// 切换主题(全局)
QAction *startSingleExchangeAction; /// 开启单次对话
QAction *startContinueExchangeAction; /// 开启连续对话
QAction *settingsAction; /// 设置
QAction *closeAction; /// 关闭
QScopedPointer<Setting> settingWindow; // 使用智能指针管理 Setting 窗口
private slots:
void toggleTheme();
// void startExchange();
};
+166
View File
@@ -0,0 +1,166 @@
#include "menu.h"
#include "ElaTheme.h"
#include "LAppLive2DManager.hpp"
#include <QDebug>
#include <QTimer>
#include "TextRenderer.h"
#include "AppCore.h"
Menu::Menu(QWidget *parent)
: ElaMenu(parent)
{
// 设置默认主题
eTheme->setThemeMode(ElaThemeType::Dark);
createMenu();
}
Menu::~Menu() {
AppCore::destroy(); // 显式销毁 AppCore
}
void Menu::createMenu()
{
toggleThe = addAction("切换主题");
// 单次对话功能按钮
startSingleExchangeAction = addAction("单次对话(测试)");
// 连续对话功能按钮
startContinueExchangeAction = addAction("连续对话(测试)");
// 添加设置按钮
settingsAction = addAction("设置");
// 添加关闭按钮
closeAction = addAction("关闭");
// 连接信号与槽
// 切换主题按钮
connect(toggleThe, &QAction::triggered, this, [this]() {
toggleTheme();
});
// 单次对话功能
connect(startSingleExchangeAction, &QAction::triggered, this, []() {
qDebug() << "Start SingleExchange triggered";
AppCore::getInstance()->SingleExchange();
});
// 连续对话功能 TODO: 待开发
connect(startContinueExchangeAction, &QAction::triggered, this, []() {
qDebug() << "Start ContinueExchange triggered";
});
// 设置按钮
connect(settingsAction, &QAction::triggered, this, [this]() {
qDebug() << "Settings triggered";
AppCore::getInstance()->tryToInit();
// 打开设置窗口
// 如果 Setting 窗口已经存在,则不再创建
if (settingWindow) {
settingWindow->show();
return;
}
// 动态创建 Setting 窗口
settingWindow.reset(new Setting()); // 使用智能指针管理
// 显示 Setting 窗口
settingWindow->show();
});
// 关闭
connect(closeAction, &QAction::triggered, this, [this]() {
emit closeMainWindow(); // 发射关闭信号
});
}
void Menu::showMenu(const QPoint &pos)
{
// 在指定位置显示菜单
exec(pos);
}
void Menu::toggleTheme()
{
if (eTheme->getThemeMode() == ElaThemeType::Light) {
eTheme->setThemeMode(ElaThemeType::Dark);
} else {
eTheme->setThemeMode(ElaThemeType::Light);
}
}
/*
void Menu::startExchange()
{
// 列出所有音频输入设备
QList<QString> devices = AudioInput::getAvailableAudioInputDevices();
qDebug() << "可用录音设备:";
for (const QString &device : devices) {
qDebug() << device;
}
// 设置当前录音设备(假设选择第一个设备)
if (!devices.isEmpty()) {
qDebug() << "选择的录音设备是: " << devices.first();
AudioInput::getInstance()->setAudioInputDevice(devices.first());
qDebug() << "当前录音设备: " << AudioInput::getInstance()->audioInput(); // 检查当前录音设备
}
// 第一次需要主动录音来启动 信号与槽状态机(FSM)
qDebug() << "开始录音";
AudioInput::getInstance()->startAutoStopAudio();
// 检查录音状态
if (AudioInput::getInstance()->state() == QMediaRecorder::RecordingState) {
qDebug() << "录音已启动";
} else {
qDebug() << "录音启动失败";
}
// 连接信号和槽
// 播放回答
// 当完整接受wav文件后播放相关的wav文件
connect(SocketManager::getInstance(), &SocketManager::revWavFileFinish, [this](const QString &filePath, const QString &response, const float duration) {
LAppLive2DManager::GetInstance()->StartLipSync(filePath.toUtf8().constData());
AudioOutput::getInstance()->setAudioPath(filePath);
AudioOutput::getInstance()->playAudio();
TextRenderer::getInstance()->addText(response, 40.0f, QColor("#FF69B4"), duration);
});
// 开始录音
// 当播放完成后继续开始录音
connect(AudioOutput::getInstance(), &AudioOutput::playbackFinished, [this]() {
qDebug() << "开始录音";
AudioInput::getInstance()->startAutoStopAudio();
// 检查录音状态
if (AudioInput::getInstance()->state() == QMediaRecorder::RecordingState) {
qDebug() << "录音已启动";
} else {
qDebug() << "录音启动失败";
}
});
// 上传录音
// 当录音完成时,发送wav文件
connect(AudioInput::getInstance(), &AudioInput::recordingFinished_Byte, [this](const QByteArray &wavData) {
qDebug() << "录音完成,开始上传录音文件...";
if (wavData.isEmpty()) {
qWarning() << "录音数据为空!";
return;
}
qDebug() << "准备发送WAV数据,大小:" << wavData.size() << "字节";
SocketManager::getInstance()->sendWavFile(wavData);
});
}
*/
+1
View File
@@ -0,0 +1 @@
本目录下为右键菜单的相关代码
@@ -0,0 +1,99 @@
//
// Created by Administrator on 2025/2/16.
//
#pragma once
#include <QOpenGLWidget>
#include <QVector>
#include <QString>
#include <QColor>
#include <QElapsedTimer>
#include <QVector2D>
#include <QFontMetrics>
#include <QLinearGradient>
class TextRenderer {
public:
// 删除拷贝构造函数和赋值运算符
TextRenderer(const TextRenderer&) = delete;
void operator=(const TextRenderer&) = delete;
// 获取单例实例
static TextRenderer* getInstance()
{
if(instance == nullptr){
instance = new TextRenderer();
}
return instance;
}
struct TextInstance {
QString text; // 文本内容
QVector2D basePosition; // 基础位置(Y轴)
QColor primaryColor; // 主要文字颜色
QColor outlineColor; // 轮廓颜色
float duration; // 显示总时长(秒)
qint64 startTime; // 开始显示时间(毫秒)
bool isDropping; // 是否正在下坠
qint64 dropStartTime; // 下坠开始时间
float dropYVelocity; // Y轴下落速度
float alpha; // 透明度
QList<QPoint> charPositions; // 字符位置
QList<int> charWidths; // 每个字符宽度
int visibleChars; // 可见字符数
bool flowCompleted; // 流式显示是否完成
float holdDuration; // 实际使用的停留时间
qint64 flowEndTime; // 流式完成时间戳
TextInstance() : isDropping(false), dropYVelocity(0.0f),
alpha(1.0f), visibleChars(0),
flowCompleted(false), holdDuration(0.5f),
flowEndTime(0) {}
};
void setWindowSize(int w, int h);
void addText(const QString &text, float yPos,
const QColor &color, float duration);
void update();
void render();
void setGlobalFont(const QFont &newFont);
/**
* 参数建议值:<br>
效果类型 gravity dampFactor holdDuration <br>
柔和下落 600.0f 0.85f 1.0f <br>
快速坠落 1200.0f 0.6f 0.3f <br>
弹性效果 900.0f 0.75f 0.8f <br>
真实物理模拟 980.0f 0.82f 0.5f
*/
void setHoldDuration(const float seconds) { defaultHoldDuration = seconds; }
void setGravity(const float g) { gravity = g; }
void setDampFactor(const float damp) { dampFactor = damp; }
// 释放单例
static void releaseInstance() {
if (instance) {
delete instance;
instance = nullptr;
}
}
private:
explicit TextRenderer(); // 构造函数私有化
void updateFlowPositions(TextInstance &instance);
void updateDropPositions(TextInstance &instance, float deltaTime);
private:
static TextRenderer *instance;
QList<TextInstance> activeTexts; /// 当前显示的文字
QElapsedTimer globalTimer; /// 全局计时器
QFont font; /// 全局字体
int windowWidth; /// 窗口宽度
int windowHeight; /// 窗口高度
qint64 lastFrameTime; /// 上一帧的时间
// 一些自定义参数
float defaultHoldDuration; /// 默认停留时间(秒)
float gravity; /// 重力加速度(像素/秒²)
float dampFactor; /// 碰撞阻尼系数
};
@@ -0,0 +1,248 @@
//
// Created by Administrator on 2025/2/16.
//
/**
* 用于渲染文本显示
*/
#include "TextRenderer.h"
#include <QPainter>
#include <QOpenGLPaintDevice>
#include <cmath>
#include <QRandomGenerator>
TextRenderer *TextRenderer::instance = nullptr;
TextRenderer::TextRenderer() : windowWidth(800), // 默认窗口宽
windowHeight(600), // 默认窗口高
lastFrameTime(0),
defaultHoldDuration(0.5f), // 默认停留0.5秒
gravity(980.0f), // 默认重力
dampFactor(0.82f) // 默认阻尼
{
globalTimer.start();
font.setFamily("Microsoft YaHei");
font.setPixelSize(28);
font.setWeight(QFont::Bold);
}
void TextRenderer::setGlobalFont(const QFont &newFont)
{
font = newFont;
font.setWeight(QFont::Bold);
}
void TextRenderer::setWindowSize(int w, int h)
{
windowWidth = w;
windowHeight = h;
}
void TextRenderer::addText(const QString &text, float yPos,
const QColor &color, float duration)
{
TextInstance instance;
instance.text = text;
instance.basePosition = QVector2D(0, yPos);
instance.primaryColor = color;
instance.outlineColor = QColor(0, 0, 0, 180);
instance.duration = duration;
instance.startTime = globalTimer.elapsed();
instance.holdDuration = defaultHoldDuration; // 应用当前全局设置
QFontMetrics metrics(font);
instance.charWidths.clear();
for (const QChar &ch : text) {
instance.charWidths.append(metrics.horizontalAdvance(ch));
}
// 初始位置计算
updateFlowPositions(instance);
activeTexts.append(instance);
}
void TextRenderer::updateFlowPositions(TextInstance &instance)
{
QFontMetrics metrics(font);
const int rightMargin = 20; // 右侧留白
// 计算可见部分总宽度
int visibleWidth = 0;
for (int i = 0; i < instance.visibleChars; ++i) {
visibleWidth += instance.charWidths[i];
}
// 动态计算起始位置
int startX = qMin(
windowWidth - visibleWidth - rightMargin, // 优先保证右侧空间
(windowWidth - visibleWidth) / 2 // 次选居中显示
);
// 边界保护:至少保留20px左侧边距
startX = qMax(20, startX);
// 更新字符位置
int currentX = startX;
instance.charPositions.clear();
for (int i = 0; i < instance.text.size(); ++i) {
if (i < instance.visibleChars) {
instance.charPositions.append(QPoint(currentX, instance.basePosition.y()));
currentX += instance.charWidths[i];
} else {
instance.charPositions.append(QPoint(-10000, -10000));
}
}
// 自动滚动调整:当文字溢出时整体左移
if (currentX > windowWidth - rightMargin) {
int overflow = currentX - (windowWidth - rightMargin);
for (QPoint &pos : instance.charPositions) {
if (pos.x() != -10000) {
pos.rx() -= overflow;
}
}
}
}
void TextRenderer::updateDropPositions(TextInstance &instance, float deltaTime)
{
const float floorY = windowHeight - 30;
instance.dropYVelocity += gravity * deltaTime; // 使用全局重力值
float deltaY = instance.dropYVelocity * deltaTime;
bool hasCollision = false;
for (QPoint &pos : instance.charPositions) {
float newY = pos.y() + deltaY;
if (newY >= floorY) {
newY = floorY;
instance.dropYVelocity = -qAbs(instance.dropYVelocity) * dampFactor; // 使用全局阻尼系数
hasCollision = true;
pos.rx() += QRandomGenerator::global()->bounded(-3, 4);
}
pos.setY(newY);
}
// 碰撞后处理
if (hasCollision) {
// 速度衰减到临界值时开始加速透明
if (qAbs(instance.dropYVelocity) < 100.0f) {
instance.alpha *= 0.92f; // 加快透明度衰减速度
}
// 完全静止后强制移除
if (qAbs(instance.dropYVelocity) < 5.0f) {
instance.alpha = 0.0f;
}
}
// 常规透明度衰减
instance.alpha = qMax(0.0f, instance.alpha * 0.98f);
// 添加随机水平扰动(只在有速度时)
if (qAbs(instance.dropYVelocity) > 10.0f) {
for (QPoint &pos : instance.charPositions) {
pos.rx() += QRandomGenerator::global()->bounded(-1, 2);
}
}
}
void TextRenderer::update()
{
qint64 currentTime = globalTimer.elapsed();
float deltaTime = (currentTime - lastFrameTime) / 1000.0f;
lastFrameTime = currentTime;
auto it = activeTexts.begin();
while (it != activeTexts.end()) {
TextInstance &instance = *it;
if (instance.isDropping) {
updateDropPositions(instance, deltaTime); // 使用统一的deltaTime
// 强化消失条件:Y轴速度接近零 或 透明度低于阈值
if ((qAbs(instance.dropYVelocity) < 5.0f && instance.alpha < 0.3f)
|| instance.alpha < 0.01f) {
it = activeTexts.erase(it);
continue;
}
} else {
// 流式显示更新
float progress = (currentTime - instance.startTime) / 1000.0f / instance.duration;
progress = qMin(progress, 1.0f);
if (!instance.flowCompleted) {
int newVisible = qMin(instance.text.size(),
static_cast<int>(progress * instance.text.size()));
if (newVisible != instance.visibleChars) {
instance.visibleChars = newVisible;
updateFlowPositions(instance);
}
// 流式显示完成检测
if (progress >= 1.0f) {
instance.flowCompleted = true;
instance.flowEndTime = currentTime; // 记录完成时间
}
}
// 已流式完成但未开始下坠
else if (!instance.isDropping) {
// 检查停留时间是否结束
if (currentTime - instance.flowEndTime >= instance.holdDuration * 1000) {
instance.isDropping = true;
instance.dropStartTime = currentTime;
// 最终居中定位
int totalWidth = 0;
for (const int w : instance.charWidths) totalWidth += w;
const int startX = (windowWidth - totalWidth) / 2;
int currentX = startX;
instance.charPositions.clear();
for (int i = 0; i < instance.text.size(); ++i) {
instance.charPositions.append(QPoint(currentX, instance.basePosition.y()));
currentX += instance.charWidths[i];
}
}
}
}
++it;
}
}
void TextRenderer::render()
{
QOpenGLPaintDevice device(windowWidth, windowHeight);
QPainter painter(&device);
painter.setFont(font);
painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
for (const auto &instance : activeTexts) {
QColor mainColor = instance.primaryColor;
mainColor.setAlphaF(instance.alpha);
QColor outlineColor = instance.outlineColor;
outlineColor.setAlphaF(instance.alpha * 0.7f);
for (int i = 0; i < instance.charPositions.size(); ++i) {
const QPoint &pos = instance.charPositions[i];
if (pos.x() < -9999) continue; // 跳过隐藏字符
// 绘制轮廓
painter.setPen(outlineColor);
for (int dx = -1; dx <= 1; ++dx) {
for (int dy = -1; dy <= 1; ++dy) {
if (dx == 0 && dy == 0) continue;
painter.drawText(pos + QPoint(dx, dy), QString(instance.text[i]));
}
}
// 绘制主体
painter.setPen(mainColor);
painter.drawText(pos, QString(instance.text[i]));
}
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// Created by Administrator on 2025/3/4.
//
#ifndef AIRI_DESKTOPGRIL_AUDIOPAGE_H
#define AIRI_DESKTOPGRIL_AUDIOPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
class ElaComboBox;
class ElaSpinBox;
class ElaProgressBar;
class ElaPushButton;
class AudioPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit AudioPage(QWidget* parent = nullptr);
~AudioPage() override;
private:
ElaComboBox* audioInputDeviceComboBox = nullptr;
ElaSpinBox* audioInputSpinBox = nullptr;
ElaProgressBar* audioInputProgressBar = nullptr;
ElaPushButton* audioAutoThresholdStartButton = nullptr;
ElaPushButton* audioManualThresholdStartButton = nullptr;
ElaPushButton* testAudioPlayButton = nullptr;
};
#endif //AIRI_DESKTOPGRIL_AUDIOPAGE_H
+21
View File
@@ -0,0 +1,21 @@
//
// Created by Administrator on 2025/2/27.
//
#ifndef AIRI_DESKTOPGRIL_BASEPAGE_H
#define AIRI_DESKTOPGRIL_BASEPAGE_H
#include <ElaScrollPage.h>
class QVBoxLayout;
class BasePage : public ElaScrollPage
{
Q_OBJECT
public:
Q_INVOKABLE explicit BasePage(QWidget* parent = nullptr);
~BasePage();
protected:
void createCustomWidget(QString desText);
};
#endif //AIRI_DESKTOPGRIL_BASEPAGE_H
+23
View File
@@ -0,0 +1,23 @@
//
// Created by Administrator on 2025/2/28.
//
#ifndef AIRI_DESKTOPGRIL_HOMEPAGE_H
#define AIRI_DESKTOPGRIL_HOMEPAGE_H
#include "BasePage.h"
class ElaMenu;
class HomePage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit HomePage(QWidget* parent = nullptr);
~HomePage();
Q_SIGNALS:
Q_SIGNAL void audioNavigation();
Q_SIGNAL void modelShopNavigation();
};
#endif //AIRI_DESKTOPGRIL_HOMEPAGE_H
+41
View File
@@ -0,0 +1,41 @@
//
// Created by Administrator on 2025/4/1.
//
/**
* @brief 模型页面
* 暂时只做最简单功能切换模型
*/
#pragma once
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaComboBox.h"
#include "ElaSlider.h"
#include <QUrl>
#include <utility>
class ModelPage final : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit ModelPage(QWidget* parent = nullptr);
std::pair<QString, QString> splitPath(const QString& fullPath);
~ModelPage() override;
private:
// 设置当前模型
ElaLineEdit* modelUrlEdit = nullptr; /// 模型Url 编辑框
ElaPushButton* modelChoosePushButton = nullptr; /// 选择模型按钮
ElaPushButton* modelUsePushButton = nullptr; /// 使用模型按钮
ElaSlider* modelSlider = nullptr; /// 滑块(用于设置模型实时大小)
QUrl modelFileUrl;
QString modelFilePathFirst;
QString modelFilePathSecond;
};
+39
View File
@@ -0,0 +1,39 @@
//
// Created by Administrator on 2025/3/2.
//
#pragma once
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
class ElaPushButton;
class ElaLineEdit;
class NetWorkPage final : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit NetWorkPage(QWidget* parent = nullptr);
~NetWorkPage() override;
private:
void initUI();
void initWebSocketClient();
private:
// websocket 控件
ElaPushButton* websocketPushButton = nullptr;
ElaLineEdit* websocketLineEdit = nullptr;
// 连接测试
ElaPushButton* connectTestPushButton = nullptr;
// 连接
ElaPushButton* connectPushButton = nullptr;
// 断开
ElaPushButton* disconnectPushButton = nullptr;
// 发送测试按钮
ElaPushButton* sendTestPushButton = nullptr;
};
+30
View File
@@ -0,0 +1,30 @@
//
// Created by Administrator on 2025/3/30.
//
#ifndef YOSUGA_RENDERPAGE_H
#define YOSUGA_RENDERPAGE_H
#include "BasePage.h"
#include "ElaPushButton.h"
#include "ElaLineEdit.h"
#include "ElaComboBox.h"
class RenderPage : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit RenderPage(QWidget* parent = nullptr);
~RenderPage();
private:
// 帧率设置
ElaComboBox* frameRateComboBox = nullptr;
};
#endif //YOSUGA_RENDERPAGE_H
+65
View File
@@ -0,0 +1,65 @@
//
// Created by Administrator on 2025/1/21.
//
#pragma once
#include <ElaWidget.h>
#include <ElaWindow.h>
#include <ElaPushButton.h>
#include <ElaScrollPage.h>
#include <QStackedWidget>
#include "HomePage.h"
#include "NetworkPage.h"
#include "UISetting.h"
#include "AudioPage.h"
#include "RenderPage.h"
#include "ModelPage.h"
class Setting : public ElaWindow
{
Q_OBJECT
public:
explicit Setting(QWidget *parent = nullptr);
~Setting();
private:
/**
* 初始化所有页面指针
* @author : Misaki
*/
void initPages();
/**
* 初始化导航栏
* @author : Misaki
*/
void initNavigationBar();
/**
* 初始化上下文切换
* @author : Misaki
*/
void initContent();
private slots:
void toggleTheme();
private:
// 页面指针
HomePage *homePage;
NetWorkPage *networkPage;
UISetting *uiSetting;
AudioPage *audioPage;
RenderPage *renderPage;
ModelPage *modelPage;
// 节点键值
QString basePageKey;
//
ElaPushButton *themeToggleButton;
};
+26
View File
@@ -0,0 +1,26 @@
//
// Created by Administrator on 2025/3/2.
//
#pragma once
#include "BasePage.h"
class ElaRadioButton;
class ElaToggleSwitch;
class ElaComboBox;
class UISetting : public BasePage
{
Q_OBJECT
public:
Q_INVOKABLE explicit UISetting(QWidget* parent = nullptr);
~UISetting();
private:
ElaComboBox* _themeComboBox = nullptr;
ElaToggleSwitch* _micaSwitchButton = nullptr;
ElaToggleSwitch* _logSwitchButton = nullptr;
ElaRadioButton* _minimumButton = nullptr;
ElaRadioButton* _compactButton = nullptr;
ElaRadioButton* _maximumButton = nullptr;
ElaRadioButton* _autoButton = nullptr;
};
+132
View File
@@ -0,0 +1,132 @@
//
// Created by Administrator on 2025/3/4.
//
#include "AudioPage.h"
#include <QHBoxLayout>
#include "ElaComboBox.h"
#include "ElaPlainTextEdit.h"
#include "ElaProgressBar.h"
#include "ElaScrollPageArea.h"
#include "ElaSlider.h"
#include "ElaSpinBox.h"
#include "ElaText.h"
#include "ElaMessageBar.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "TextRenderer.h"
#include "LAppLive2DManager.hpp"
AudioPage::AudioPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("AudioPage");
audioInputDeviceComboBox = new ElaComboBox(this);
audioInputDeviceComboBox->setToolTip("选择可用的录音设备");
QStringList comboList = AudioInput::getAvailableAudioInputDevices();
audioInputDeviceComboBox->addItems(comboList);
ElaScrollPageArea* comboBoxArea = new ElaScrollPageArea(this);
QHBoxLayout* comboBoxLayout = new QHBoxLayout(comboBoxArea);
ElaText* comboBoxText = new ElaText("录音设备", this);
comboBoxText->setTextPixelSize(15);
comboBoxLayout->addWidget(comboBoxText);
comboBoxLayout->addWidget(audioInputDeviceComboBox);
comboBoxLayout->addStretch();
comboBoxLayout->addSpacing(10);
connect(audioInputDeviceComboBox, &ElaComboBox::currentTextChanged, [this](const QString& text) {
AudioInput::getInstance()->setAudioInputDevice(text);
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "成功设置 " + text + " 为当前录音设备", 800.0, this);
});
audioInputSpinBox = new ElaSpinBox(this); // SpinBox
audioInputSpinBox->setRange(0, 10000);
audioInputProgressBar = new ElaProgressBar(this);
audioInputProgressBar->setRange(0, 10000);
// 关闭ProgressBar的百分比显示
audioInputProgressBar->setTextVisible(false);
// 将SpinBox和ProgressBar的数值相互绑定
connect(audioInputSpinBox, QOverload<int>::of(&ElaSpinBox::valueChanged), [this](int value) {
audioInputProgressBar->setValue(value);
});
connect(audioInputProgressBar, QOverload<int>::of(&ElaProgressBar::valueChanged), [this](int value) {
audioInputSpinBox->setValue(value);
});
// 绑定实时录音阈值到ProgressBar,同时归一到0~1000范围内
connect(AudioInput::getInstance(), &AudioInput::rmsRealValue, [this](const qreal value) {
audioInputProgressBar->setValue(value);
});
// 当计算完成最优阈值
connect(AudioInput::getInstance(), &AudioInput::thresholdCalculated, [this](qreal value) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "自动计算出的最优阈值为:" + QString::number(value), 1000, this);
// AudioInput会自动设置计算出的最优阈值
});
audioAutoThresholdStartButton = new ElaPushButton("自动最优阈值", this);
audioAutoThresholdStartButton->setToolTip("点击后保持当前环境音5秒,自动计算出最合适的静音检测阈值");
connect(audioAutoThresholdStartButton, &ElaPushButton::clicked, [=]() {
AudioInput::getInstance()->startAutoThresholdClu(5000);
qDebug("开始计算最优阈值");
});
audioManualThresholdStartButton = new ElaPushButton("手动设置阈值", this);
audioManualThresholdStartButton->setToolTip("如果你觉得自动计算的不准的话");
connect(audioManualThresholdStartButton, &ElaPushButton::clicked, [this]() {
AudioInput::getInstance()->setSilenceThreshold(audioInputSpinBox->value());
ElaMessageBar::success(ElaMessageBarType::TopRight, "音频设置", "手动设置的阈值为:" + QString::number(audioInputSpinBox->value()), 1000, this);
});
ElaScrollPageArea* audioInputProgressBarArea = new ElaScrollPageArea(this);
QHBoxLayout* audioInputProgressBarLayout = new QHBoxLayout(audioInputProgressBarArea);
ElaText* audioInputProgressBarText = new ElaText("静音检测阈值", this);
audioInputProgressBarText->setToolTip("测试当前环境的静音阈值,用于对话中的静音检测");
audioInputProgressBarText->setTextPixelSize(15);
audioInputProgressBarLayout->addWidget(audioInputProgressBarText);
audioInputProgressBarLayout->addWidget(audioInputProgressBar, 1);
audioInputProgressBarLayout->addWidget(audioInputSpinBox);
audioInputProgressBarLayout->addStretch(); // 添加弹性空间将后续控件推到右侧
audioInputProgressBarLayout->addWidget(audioAutoThresholdStartButton);
audioInputProgressBarLayout->addWidget(audioManualThresholdStartButton);
audioInputProgressBarLayout->addStretch();
audioInputProgressBarLayout->addSpacing(10);
testAudioPlayButton = new ElaPushButton("播放测试", this);
testAudioPlayButton->setToolTip("播放一段测试音频来检测播放功能是否正常,注意观察模型嘴唇以及文字下落动画");
ElaScrollPageArea* testAudioArea = new ElaScrollPageArea(this);
QHBoxLayout* testAudioLayout = new QHBoxLayout(testAudioArea);
ElaText* testAudioText = new ElaText("测试", this);
testAudioText->setTextPixelSize(15);
testAudioLayout->addWidget(testAudioText);
testAudioLayout->addStretch(); // 添加弹性空间将后续控件推到右侧
testAudioLayout->addWidget(testAudioPlayButton);
testAudioLayout->addSpacing(10);
connect(testAudioPlayButton, &ElaPushButton::clicked, [this]() {
const QString text = "あれアイリーじゃないよ!急にいなくなるからどこに行ったのかと思えば~";
constexpr float duration = 6.0f; // 音频时长
TextRenderer::getInstance()->addText(text, 40.0f, QColor("#FF69B4"), duration);
LAppLive2DManager::GetInstance()->StartLipSync("Resources/TestFiles/test.wav");
AudioOutput::getInstance()->playUrl(QUrl("Resources/TestFiles/test.wav"));
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("音频设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(comboBoxArea);
centerLayout->addWidget(audioInputProgressBarArea);
centerLayout->addWidget(testAudioArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
AudioPage::~AudioPage()
{
}
+89
View File
@@ -0,0 +1,89 @@
//
// Created by Administrator on 2025/2/27.
//
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "BasePage.h"
#include "ElaMenu.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaToolButton.h"
BasePage::BasePage(QWidget* parent)
: ElaScrollPage(parent)
{
connect(eTheme, &ElaTheme::themeModeChanged, this, [=, this]() {
if (!parent)
{
update();
}
});
}
BasePage::~BasePage()
{
}
void BasePage::createCustomWidget(QString desText)
{
// 顶部元素
QWidget* customWidget = new QWidget(this);
ElaText* subTitleText = new ElaText(this);
subTitleText->setText("https://github.com/Liniyous/ElaWidgetTools");
subTitleText->setTextInteractionFlags(Qt::TextSelectableByMouse);
subTitleText->setTextPixelSize(11);
ElaToolButton* documentationButton = new ElaToolButton(this);
documentationButton->setFixedHeight(35);
documentationButton->setIsTransparent(false);
documentationButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
//_toolButton->setPopupMode(QToolButton::MenuButtonPopup);
documentationButton->setText("Documentation");
documentationButton->setElaIcon(ElaIconType::FileDoc);
ElaMenu* documentationMenu = new ElaMenu(this);
documentationMenu->addElaIconAction(ElaIconType::CardsBlank, "CardsBlank");
documentationMenu->addElaIconAction(ElaIconType::EarthAmericas, "EarthAmericas");
documentationButton->setMenu(documentationMenu);
ElaToolButton* sourceButton = new ElaToolButton(this);
sourceButton->setFixedHeight(35);
sourceButton->setIsTransparent(false);
sourceButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
sourceButton->setText("Source");
sourceButton->setElaIcon(ElaIconType::NfcSymbol);
ElaMenu* sourceMenu = new ElaMenu(this);
sourceMenu->addElaIconAction(ElaIconType::FireBurner, "FireBurner");
sourceMenu->addElaIconAction(ElaIconType::Galaxy, "Galaxy~~~~");
sourceButton->setMenu(sourceMenu);
ElaToolButton* themeButton = new ElaToolButton(this);
themeButton->setFixedSize(35, 35);
themeButton->setIsTransparent(false);
themeButton->setElaIcon(ElaIconType::MoonStars);
connect(themeButton, &ElaToolButton::clicked, this, [=]() {
eTheme->setThemeMode(eTheme->getThemeMode() == ElaThemeType::Light ? ElaThemeType::Dark : ElaThemeType::Light);
});
QHBoxLayout* buttonLayout = new QHBoxLayout();
buttonLayout->addWidget(documentationButton);
buttonLayout->addSpacing(5);
buttonLayout->addWidget(sourceButton);
buttonLayout->addStretch();
buttonLayout->addWidget(themeButton);
buttonLayout->addSpacing(15);
ElaText* descText = new ElaText(this);
descText->setText(desText);
descText->setTextPixelSize(13);
QVBoxLayout* topLayout = new QVBoxLayout(customWidget);
topLayout->setContentsMargins(0, 0, 0, 0);
topLayout->addWidget(subTitleText);
topLayout->addSpacing(5);
topLayout->addLayout(buttonLayout);
topLayout->addSpacing(5);
topLayout->addWidget(descText);
setCustomWidget(customWidget);
}
+154
View File
@@ -0,0 +1,154 @@
//
// Created by Administrator on 2025/2/28.
//
#include "HomePage.h"
#include <QDebug>
#include <QDesktopServices>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QPainter>
#include <QVBoxLayout>
#include "ElaAcrylicUrlCard.h"
#include "ElaFlowLayout.h"
#include "ElaImageCard.h"
#include "ElaMenu.h"
#include "ElaMessageBar.h"
#include "ElaNavigationRouter.h"
#include "ElaPopularCard.h"
#include "ElaScrollArea.h"
#include "ElaText.h"
#include "ElaToolTip.h"
HomePage::HomePage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("Home");
setTitleVisible(false);
setContentsMargins(2, 2, 0, 0);
// 标题卡片区域
ElaText* desText = new ElaText("UI By ElaWidgetTools", this);
desText->setTextPixelSize(18);
ElaText* titleText = new ElaText("Yosuga!", this);
titleText->setTextPixelSize(35);
QVBoxLayout* titleLayout = new QVBoxLayout();
titleLayout->setContentsMargins(30, 60, 0, 0);
titleLayout->addWidget(desText);
titleLayout->addWidget(titleText);
ElaImageCard* backgroundCard = new ElaImageCard(this);
backgroundCard->setBorderRadius(10);
backgroundCard->setFixedHeight(400);
backgroundCard->setMaximumAspectRatio(1.7);
backgroundCard->setCardImage(QImage("Resources/Pic/Airi/Airi_Background.png"));
ElaAcrylicUrlCard* urlCard1 = new ElaAcrylicUrlCard(this);
urlCard1->setCardPixmapSize(QSize(62, 62));
urlCard1->setFixedSize(195, 225);
urlCard1->setTitlePixelSize(17);
urlCard1->setTitleSpacing(25);
urlCard1->setSubTitleSpacing(13);
urlCard1->setUrl("https://github.com/Misakityan/Yosuga");
urlCard1->setCardPixmap(QPixmap("Resources/Pic/Others/img.png"));
urlCard1->setTitle("Yosuga Github");
urlCard1->setSubTitle("Star++!");
ElaToolTip* urlCard1ToolTip = new ElaToolTip(urlCard1);
urlCard1ToolTip->setToolTip("点击前往本项目GitHub");
ElaAcrylicUrlCard* urlCard2 = new ElaAcrylicUrlCard(this);
urlCard2->setCardPixmapSize(QSize(62, 62));
urlCard2->setFixedSize(195, 225);
urlCard2->setTitlePixelSize(17);
urlCard2->setTitleSpacing(25);
urlCard2->setSubTitleSpacing(13);
urlCard2->setUrl("https://space.bilibili.com/140315806");
urlCard2->setCardPixmap(QPixmap("Resources/Pic/Others/Misaki.jpg"));
urlCard2->setTitle("Misaki");
urlCard2->setSubTitle("1841738040@qq.com");
ElaToolTip* urlCard2ToolTip = new ElaToolTip(urlCard2);
urlCard2ToolTip->setToolTip("点击前往 Misaki 的个人主页");
ElaScrollArea* cardScrollArea = new ElaScrollArea(this);
cardScrollArea->setWidgetResizable(true);
cardScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
cardScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
cardScrollArea->setIsGrabGesture(true, 0);
cardScrollArea->setIsOverShoot(Qt::Horizontal, true);
QWidget* cardScrollAreaWidget = new QWidget(this);
cardScrollAreaWidget->setStyleSheet("background-color:transparent;");
cardScrollArea->setWidget(cardScrollAreaWidget);
QHBoxLayout* urlCardLayout = new QHBoxLayout();
urlCardLayout->setSpacing(15);
urlCardLayout->setContentsMargins(30, 0, 0, 6);
urlCardLayout->addWidget(urlCard1);
urlCardLayout->addWidget(urlCard2);
urlCardLayout->addStretch();
QVBoxLayout* cardScrollAreaWidgetLayout = new QVBoxLayout(cardScrollAreaWidget);
cardScrollAreaWidgetLayout->setContentsMargins(0, 0, 0, 0);
cardScrollAreaWidgetLayout->addStretch();
cardScrollAreaWidgetLayout->addLayout(urlCardLayout);
QVBoxLayout* backgroundLayout = new QVBoxLayout(backgroundCard);
backgroundLayout->setContentsMargins(0, 0, 0, 0);
backgroundLayout->addLayout(titleLayout);
backgroundLayout->addWidget(cardScrollArea);
// 推荐卡片
ElaText* flowText = new ElaText("快速转到", this);
flowText->setTextPixelSize(20);
QHBoxLayout* flowTextLayout = new QHBoxLayout();
flowTextLayout->setContentsMargins(33, 0, 0, 0);
flowTextLayout->addWidget(flowText);
// ElaFlowLayout
// 模型商店卡片
ElaPopularCard* ModeShopCard = new ElaPopularCard(this);
connect(ModeShopCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() {
Q_EMIT modelShopNavigation();
});
ModeShopCard->setCardPixmap(QPixmap("Resources/Pic/Others/Live2D.png"));
ModeShopCard->setTitle("模型设置");
ModeShopCard->setSubTitle("属于你的Live2D模型");
ModeShopCard->setInteractiveTips("By Misaki");
ModeShopCard->setDetailedText("选择你喜欢的Live2D模型");
// 音频设置卡片
ElaPopularCard* AudioSettingCard = new ElaPopularCard(this);
connect(AudioSettingCard, &ElaPopularCard::popularCardButtonClicked, this, [=, this]() {
Q_EMIT audioNavigation();
});
AudioSettingCard->setTitle("音频设置");
AudioSettingCard->setSubTitle("录音与播放的设置");
AudioSettingCard->setCardPixmap(QPixmap("Resources/Pic/control/AutomationProperties.png"));
AudioSettingCard->setInteractiveTips("By Misaki");
AudioSettingCard->setDetailedText("自定义音频与播放的相关设定,打造最舒适的交流环境。");
ElaFlowLayout* flowLayout = new ElaFlowLayout(0, 5, 5);
flowLayout->setContentsMargins(30, 0, 0, 0);
flowLayout->setIsAnimation(true);
flowLayout->addWidget(ModeShopCard);
flowLayout->addWidget(AudioSettingCard);
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("Home");
QVBoxLayout* centerVLayout = new QVBoxLayout(centralWidget);
centerVLayout->setSpacing(0);
centerVLayout->setContentsMargins(0, 0, 0, 0);
centerVLayout->addWidget(backgroundCard);
centerVLayout->addSpacing(20);
centerVLayout->addLayout(flowTextLayout);
centerVLayout->addSpacing(10);
centerVLayout->addLayout(flowLayout);
centerVLayout->addStretch();
addCentralWidget(centralWidget);
}
HomePage::~HomePage()
{
}
+191
View File
@@ -0,0 +1,191 @@
//
// Created by Administrator on 2025/4/1.
//
#include "ModelPage.h"
#include <QFileDialog>
#include <QHBoxLayout>
#include "ElaMessageBar.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include <QtConcurrent>
#include <QOpenGLContext>
#include <QOffscreenSurface>
#include <QFutureWatcher>
#include "LAppLive2DManager.hpp"
ModelPage::ModelPage(QWidget *parent)
: BasePage(parent) {
// 预览窗口标题
setWindowTitle("ModelPage");
modelUrlEdit = new ElaLineEdit(this); // 模型Url Edit
modelUrlEdit->setFixedWidth(300);
modelUrlEdit->setPlaceholderText("用于显示当前的模型Url");
modelChoosePushButton = new ElaPushButton("选择模型", this);
modelChoosePushButton->setToolTip("选择.model3.json结尾的文件");
modelUsePushButton = new ElaPushButton("使用模型", this);
modelUsePushButton->setToolTip("使用选择的模型或Url对应的模型");
ElaScrollPageArea *modelSetArea = new ElaScrollPageArea(this);
QHBoxLayout *modelSetLayout = new QHBoxLayout(modelSetArea);
ElaText *modelSetText = new ElaText("模型设置", this);
modelSetText->setTextPixelSize(15);
modelSetLayout->addWidget(modelSetText);
modelSetLayout->addWidget(modelUrlEdit);
modelSetLayout->addStretch();
modelSetLayout->addWidget(modelChoosePushButton);
modelSetLayout->addWidget(modelUsePushButton);
modelSetLayout->addSpacing(10);
// 创建滑块控件和标签
ElaText *modelSizeText = new ElaText("模型大小比例设置", this);
modelSizeText->setToolTip("实时调整模型大小");
modelSizeText->setTextPixelSize(15);
modelSlider = new ElaSlider(this); // 滑块(用于设置模型实时大小)
modelSlider->setRange(0, 99); // 设置范围
modelSlider->setValue(85); // 设置默认值
modelSlider->setOrientation(Qt::Horizontal); // 水平方向
// 创建独立的区域容器
ElaScrollPageArea *modelSliderArea = new ElaScrollPageArea(this);
QHBoxLayout *modelSliderLayout = new QHBoxLayout(modelSliderArea);
// 添加到布局(加一个标签显示数值)
ElaText *modelSliderValueText = new ElaText("85%", this);
modelSliderValueText->setTextPixelSize(14);
connect(modelSlider, &ElaSlider::valueChanged, this, [modelSliderValueText](const int value){
modelSliderValueText->setText(QString("%1%").arg(value + 1));
LAppLive2DManager::GetInstance()->ModelSizeChange(100 - value); // 实时更新模型大小
});
modelSliderLayout->addWidget(modelSizeText);
modelSliderLayout->addWidget(modelSlider);
modelSliderLayout->addWidget(modelSliderValueText);
modelSliderLayout->addStretch(); // 让内容靠左
connect(modelChoosePushButton, &ElaPushButton::clicked, this, [this]() {
// 创建对话框对象(使用 heap 分配,由 Qt 对象树管理内存)
auto *fileDialog = new QFileDialog(this);
fileDialog->setWindowTitle("选择模型文件");
fileDialog->setNameFilter("Live2D Model (*.model3.json)");
// 设置初始目录
const QString exeDir = QCoreApplication::applicationDirPath(); // 获取当前exe所在目录的本地路径
fileDialog->setDirectory(exeDir); // 设置初始目录为当前 exe 所在目录
// 连接信号:当用户选中文件并点击打开时
connect(fileDialog, &QFileDialog::fileSelected, this, [this, fileDialog](const QString &file) {
modelFileUrl = QUrl::fromLocalFile(file);
if (!modelFileUrl.isEmpty()) {
const QString t = modelFileUrl.toLocalFile();
const std::pair<QString, QString> path = this->splitPath(t);
this->modelFilePathFirst = path.first;
this->modelFilePathSecond = path.second;
this->modelUrlEdit->setText(t);
ElaMessageBar::success(ElaMessageBarType::BottomRight, "模型设置", "模型选择成功", 2000, this);
}
// 用完即弃,自动清理内存
fileDialog->deleteLater();
});
// 处理取消的情况(防止内存泄漏)
connect(fileDialog, &QFileDialog::rejected, fileDialog, &QObject::deleteLater);
// 显示对话框(非阻塞,不会卡住主界面)
fileDialog->open();
});
connect(modelUsePushButton, &ElaPushButton::clicked, this, [this]() {
// 模型使用
if (modelFileUrl.isEmpty()) {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "模型设置", "似乎并没有选择模型", 800.0, this);
return;
}
// UI 状态设置为加载中
modelUsePushButton->setEnabled(false); // 禁用使用按钮
modelUsePushButton->setText("加载中"); // 修改按钮文本
modelChoosePushButton->setEnabled(false); // 禁用选择按钮
// 获取路径字符串 (必须按值传递给lambda)
std::string dir = this->modelFilePathFirst.toStdString();
std::string filename = this->modelFilePathSecond.toStdString();
// 启动异步任务
QFuture<LAppModel *> future = QtConcurrent::run([dir, filename]() -> LAppModel * {
// 以下代码在子线程执行
// 创建临时 OpenGL 上下文
auto *context = new QOpenGLContext();
// 关键点:设置与全局共享上下文共享 (这样主线程才能看到纹理)
context->setShareContext(QOpenGLContext::globalShareContext());
if (!context->create()) {
delete context;
return nullptr;
}
// 创建离屏表面 (因为子线程没有窗口,需要一个假的绘制表面)
auto *surface = new QOffscreenSurface();
surface->setFormat(context->format());
surface->create();
// 绑定上下文
if (!context->makeCurrent(surface)) {
delete surface;
delete context;
return nullptr;
}
// 执行真正的耗时加载
// 调用在 Manager 里新写的函数
LAppModel *model = LAppLive2DManager::GetInstance()->LoadModelInstance(dir, filename);
// 清理子线程资源
context->doneCurrent();
delete surface;
delete context;
return model;
});
// 监控任务结束
auto *watcher = new QFutureWatcher<LAppModel *>();
connect(watcher, &QFutureWatcher<LAppModel *>::finished, this, [this, watcher]() {
// 下面的代码在主线程中执行
LAppModel *newModel = watcher->result();
if (newModel) {
// 调用挂载函数,瞬间完成切换
LAppLive2DManager::GetInstance()->MountLoadedModel(newModel);
ElaMessageBar::success(ElaMessageBarType::BottomRight, "成功", "模型加载完成", 2000, this);
} else {
ElaMessageBar::error(ElaMessageBarType::BottomRight, "错误", "模型加载失败 (OpenGL环境异常)", 2000, this);
}
// 恢复 UI
modelUsePushButton->setEnabled(true);
modelUsePushButton->setText("使用模型");
modelChoosePushButton->setEnabled(true);
watcher->deleteLater();
});
// 开始监控
watcher->setFuture(future);
});
QWidget *centralWidget = new QWidget(this);
centralWidget->setWindowTitle("模型设置");
QVBoxLayout *centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(modelSetArea);
centerLayout->addWidget(modelSliderArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
// 返回 pair<目录路径, 文件名>
std::pair<QString, QString> ModelPage::splitPath(const QString &fullPath) {
const QFileInfo fileInfo(fullPath);
// 获取目录部分(自动处理末尾斜杠)
QString dirPath = fileInfo.dir().absolutePath() + "/";
// 获取文件名部分(如果是目录则返回空)
QString fileName = fileInfo.fileName();
return {dirPath, fileName};
}
ModelPage::~ModelPage() {
}
+186
View File
@@ -0,0 +1,186 @@
//
// Created by Administrator on 2025/3/2.
//
#include "NetworkPage.h"
#include <QHBoxLayout>
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "ElaMessageBar.h"
#include "websocketmanager.h"
#include "NetWorkDO.h"
#include <QFile>
NetWorkPage::NetWorkPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("NetworkPage");
this->initUI(); // 初始化UI
this->initWebSocketClient(); // 初始化websocket客户端(主要是相关的信号与槽)
}
NetWorkPage::~NetWorkPage()
{
}
void NetWorkPage::initUI() {
// websocket UI
websocketPushButton = new ElaPushButton("设定",this);
websocketPushButton->setToolTip("设定服务端WebSocket地址");
websocketLineEdit = new ElaLineEdit(this);
websocketLineEdit->setPlaceholderText("请输入服务端WebSocket地址");
websocketLineEdit->setFixedWidth(300); // 设置websocketLineEdit框的宽度
ElaScrollPageArea* websocketToggleSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* websocketToggleSwitchLayout = new QHBoxLayout(websocketToggleSwitchArea);
ElaText* websocketToggleSwitchText = new ElaText("服务端WebSocket地址:", this);
websocketToggleSwitchText->setTextPixelSize(15);
websocketToggleSwitchLayout->addWidget(websocketToggleSwitchText);
websocketToggleSwitchLayout->addWidget(websocketLineEdit);
websocketToggleSwitchLayout->addStretch();
websocketToggleSwitchLayout->addWidget(websocketPushButton);
websocketToggleSwitchLayout->addSpacing(10);
// 连通测试按钮
connectTestPushButton = new ElaPushButton("连通测试",this);
connectTestPushButton->setToolTip("测试与服务器连通性(如果成功连通会自动连上服务器)");
connectPushButton = new ElaPushButton("连接",this);
disconnectPushButton = new ElaPushButton("断开",this);
sendTestPushButton = new ElaPushButton("发送测试",this);
ElaScrollPageArea* connectTestArea = new ElaScrollPageArea(this); // 创建一个滚动页面
QHBoxLayout* connectTestLayout = new QHBoxLayout(connectTestArea);
connectTestLayout->addWidget(connectTestPushButton); // 将连通测试按钮添加到布局中
connectTestLayout->addStretch(); // 添加一个空格
connectTestLayout->addWidget(sendTestPushButton); // 将发送测试按钮添加到布局中
connectTestLayout->addWidget(disconnectPushButton); // 将断开按钮添加到布局中
connectTestLayout->addWidget(connectPushButton); // 将连接按钮添加到布局中
connectTestLayout->addSpacing(10);
// 添加到布局
QWidget* centralWidget = new QWidget(this); // 中心部件
centralWidget->setWindowTitle("连接设置"); // 中心部件标题
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget); // 中心部件布局
centerLayout->addWidget(websocketToggleSwitchArea); // 将websocketToggleSwitchArea添加到布局中
centerLayout->addWidget(connectTestArea); // 将connectTestArea添加到布局中
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0); // 设置布局的边距
addCentralWidget(centralWidget, true, true, 0); // 添加中心部件
}
void NetWorkPage::initWebSocketClient() {
auto* client = WebSocketClient::getInstance(); // 获取单例实例(设置一个默认地址)
auto* netDO = NetworkDO::getInstance();
// 注入:将底层发送能力赋予 NetworkDO
netDO->registerSender([client](const QString& type, const QJsonObject& data){
client->sendJson(type, data);
});
// 监听:底层收到数据 -> NetworkDO 解析
connect(client, &WebSocketClient::jsonReceived,
netDO, &NetworkDO::onDataReceived);
// 连接成功的处理
connect(client, &WebSocketClient::connected, this, [this]() {
ElaMessageBar::success(ElaMessageBarType::TopRight, "WebSocket", "连接成功", 800.0, this);
});
// 连接失败的处理
connect(client, &WebSocketClient::error, this, [this](const QString& errorMsg) {
ElaMessageBar::error(ElaMessageBarType::TopLeft, "WebSocket错误", errorMsg, 1500.0, this);
});
// 断开连接的处理
connect(client, &WebSocketClient::disconnected, this, [this]() {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "WebSocket", "连接已断开", 800.0, this);
});
// 接收数据处理
connect(client, &WebSocketClient::jsonReceived, this, [](const QString &type, const QJsonObject &data) {
qDebug() << "Received JSON data: " << type << " " << data;
});
connect(websocketPushButton, &ElaPushButton::clicked, this, [this, client]() { // 设置服务端websocket地址
const QUrl url(websocketLineEdit->text().trimmed()); // 从LineEdit中获取服务端websocket地址
// 初始化客户端
if (client->setConfiguration(url)) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "连接设置",
QString("服务器地址已设置为: %1").arg(url.toString()), 800.0, this);
return;
}
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连接设置",
QString("服务器地址存在问题"), 800.0, this);
});
connect(connectTestPushButton, &ElaPushButton::clicked, this, [this, client]() {
if (client->isConnected()) {
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试",
"当前已连通", 800.0, this);
return;
}
client->connectToServer(); // 连接
// 使用定时器延迟检查连接状态
QTimer::singleShot(1000, this, [this, client]() {
if (!client->isConnected()) {
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连通测试",
"无法连接到服务器,请检查地址和服务器状态", 1500.0, this);
return;
}
ElaMessageBar::success(ElaMessageBarType::TopRight, "连通测试",
"连通测试成功", 800.0, this);
});
});
connect(connectPushButton, &ElaPushButton::clicked, this, [this, client]() {
if (client->isConnected()) {
ElaMessageBar::information(ElaMessageBarType::TopRight, "连接状态",
"已连接,无需重复连接", 800.0, this);
return;
}
const QString urlStr = websocketLineEdit->text().trimmed();
if (urlStr.isEmpty()) {
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "连接",
"请先设置服务器地址", 800.0, this);
return;
}
// 确保使用正确的地址
client->setConfiguration(QUrl(urlStr));
client->connectToServer();
// 连接结果会在 connected/error 信号中处理
ElaMessageBar::information(ElaMessageBarType::TopRight, "连接",
"正在连接服务器...", 800.0, this);
});
connect(disconnectPushButton, &ElaPushButton::clicked, this, [this, client]() {
if (!client->isConnected()) {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接",
"当前未连接", 800.0, this);
return;
}
client->disconnectFromServer();
ElaMessageBar::success(ElaMessageBarType::TopRight, "断开连接",
"已断开连接", 800.0, this);
});
connect(sendTestPushButton, &ElaPushButton::clicked, this, [this, netDO, client]() {
if (!client->isConnected()) {
ElaMessageBar::information(ElaMessageBarType::BottomRight, "断开连接",
"当前未连接", 800.0, this);
return;
}
// 创建数据包
QFile test_wav_file("Resources/TestFiles/test.wav");
if (!test_wav_file.open(QIODevice::ReadOnly)) {
qDebug() << "Failed to open test.wav";
ElaMessageBar::warning(ElaMessageBarType::TopLeft, "发送测试",
"无法打开测试音频文件", 1500.0, this);
return;
}
QByteArray wavData = test_wav_file.readAll();
QString base64Str = QString::fromLatin1(wavData.toBase64());
AudioDataTransferObject packet;
packet.setData("Owner", "client")
.setData("isStream", true)
.setData("sequence", 42)
.setData("text", "Hello World")
.setData("data", base64Str); // 填入测试音频数据
netDO->sendPacket(packet);
ElaMessageBar::success(ElaMessageBarType::TopRight, "发送测试",
"已成功发送数据包", 1000.0, this);
});
}
+54
View File
@@ -0,0 +1,54 @@
//
// Created by Administrator on 2025/3/30.
//
#include "RenderPage.h"
#include <QHBoxLayout>
#include <QtWidgets>
#include "ElaComboBox.h"
#include "ElaMessageBar.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "GLCore.h"
#include "AppContext.h"
RenderPage::RenderPage(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
setWindowTitle("RenderPage");
frameRateComboBox = new ElaComboBox(this);
QStringList frameRateComboList = GLCore::getFrameRateList();
frameRateComboBox->addItems(frameRateComboList);
ElaScrollPageArea* frameRateComboBoxArea = new ElaScrollPageArea(this);
QHBoxLayout* frameRateComboBoxLayout = new QHBoxLayout(frameRateComboBoxArea);
ElaText* frameRateComboBoxText = new ElaText("帧率设置", this);
frameRateComboBoxText->setTextPixelSize(15);
frameRateComboBoxLayout->addWidget(frameRateComboBoxText);
frameRateComboBoxLayout->addStretch();
frameRateComboBoxLayout->addWidget(frameRateComboBox);
frameRateComboBoxLayout->addSpacing(10);
connect(frameRateComboBox, &ElaComboBox::currentTextChanged, this, [this](const QString& text) {
AppContext::GetGLCore()->setFrameRate(GLCore::getFrameRateMap().value(text));
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("渲染设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addWidget(frameRateComboBoxArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
RenderPage::~RenderPage()
{
}
+114
View File
@@ -0,0 +1,114 @@
//
// Created by Administrator on 2025/1/21.
//
#include <ElaTheme.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSvgRenderer>
#include <QPainter>
#include "Setting.h"
#include "socketmanager.h"
Setting::Setting(QWidget *parent)
: ElaWindow(parent)
{
// 设置窗口标题
setWindowTitle("设置");
// 设置窗口图标
setWindowIcon(QIcon("Resources/Pic/Airi/Airi_s.svg"));
// 初始化窗口
resize(1000, 740);
// 禁用窗口缩放按钮
setWindowButtonFlag(ElaAppBarType::MaximizeButtonHint, false); // 隐藏最大化按钮
setWindowButtonFlag(ElaAppBarType::MinimizeButtonHint, false); // 隐藏最小化按钮
// 移动窗口到屏幕中心
this->moveToCenter();
this->setWindowFlag(Qt::Tool); // 隐藏应用程序图标
this->setWindowFlag(Qt::WindowStaysOnTopHint); // 默认设置窗口始终在顶部
// 设置用户信息卡
this->setUserInfoCardTitle("Yosuga");
this->setUserInfoCardSubTitle("联系维度的桥梁!");
// 加载 SVG 图片
QSvgRenderer renderer(QString("Resources/Pic/Airi/Airi.svg"));
// 创建 QPixmap 并绘制 SVG
QPixmap pixmap(64, 64);
pixmap.fill(Qt::transparent); // 设置透明背景
QPainter painter(&pixmap);
renderer.render(&painter);
this->setUserInfoCardPixmap(pixmap);
// 初始化页面
initPages();
// 创建导航栏和内容区域
initNavigationBar();
// 创建上下文
initContent();
// 设置初始主题
eTheme->setThemeMode(ElaThemeType::Dark);
}
Setting::~Setting()
{
}
void Setting::initPages()
{
homePage = new HomePage(this);
networkPage = new NetWorkPage(this);
uiSetting = new UISetting(this);
audioPage = new AudioPage(this);
renderPage = new RenderPage(this);
modelPage = new ModelPage(this);
}
void Setting::initNavigationBar()
{
// 添加主页节点(顶级节点)
addPageNode("主页", homePage, ElaIconType::House);
// 添加模型商店节点
addPageNode("模型设置", modelPage, ElaIconType::Shop);
// 添加网络连接设置节点
addPageNode("连接设置", networkPage, ElaIconType::NetworkWired);
// 添加音频设置节点
addPageNode("音频设置", audioPage, ElaIconType::MusicNote);
// 添加渲染设置节点
addPageNode("渲染设置", renderPage, ElaIconType::ArrowsRotate);
QString uiSettingKey;
addFooterNode("UI设置", uiSetting, uiSettingKey, 0, ElaIconType::GearComplex);
}
void Setting::initContent()
{
connect(homePage, &HomePage::modelShopNavigation, this, [&](){
this->navigation(modelPage->property("ElaPageKey").toString());
});
connect(homePage, &HomePage::audioNavigation, this, [&](){
this->navigation(audioPage->property("ElaPageKey").toString());
});
}
void Setting::toggleTheme()
{
if (eTheme->getThemeMode() == ElaThemeType::Light) {
eTheme->setThemeMode(ElaThemeType::Dark);
} else {
eTheme->setThemeMode(ElaThemeType::Light);
}
}
+166
View File
@@ -0,0 +1,166 @@
//
// Created by Administrator on 2025/3/2.
//
#include "UISetting.h"
#include <QDebug>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include "ElaApplication.h"
#include "ElaComboBox.h"
#include "ElaLog.h"
#include "ElaRadioButton.h"
#include "ElaScrollPageArea.h"
#include "ElaText.h"
#include "ElaTheme.h"
#include "ElaToggleSwitch.h"
#include "ElaWindow.h"
UISetting::UISetting(QWidget* parent)
: BasePage(parent)
{
// 预览窗口标题
ElaWindow* window = dynamic_cast<ElaWindow*>(parent);
setWindowTitle("Setting");
ElaText* themeText = new ElaText("主题设置", this);
themeText->setWordWrap(false);
themeText->setTextPixelSize(18);
_themeComboBox = new ElaComboBox(this);
_themeComboBox->addItem("日间模式");
_themeComboBox->addItem("夜间模式");
ElaScrollPageArea* themeSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* themeSwitchLayout = new QHBoxLayout(themeSwitchArea);
ElaText* themeSwitchText = new ElaText("主题切换", this);
themeSwitchText->setWordWrap(false);
themeSwitchText->setTextPixelSize(15);
themeSwitchLayout->addWidget(themeSwitchText);
themeSwitchLayout->addStretch();
themeSwitchLayout->addWidget(_themeComboBox);
connect(_themeComboBox, QOverload<int>::of(&ElaComboBox::currentIndexChanged), this, [=](int index) {
if (index == 0)
{
eTheme->setThemeMode(ElaThemeType::Light);
}
else
{
eTheme->setThemeMode(ElaThemeType::Dark);
}
});
connect(eTheme, &ElaTheme::themeModeChanged, this, [=, this](ElaThemeType::ThemeMode themeMode) {
_themeComboBox->blockSignals(true);
if (themeMode == ElaThemeType::Light)
{
_themeComboBox->setCurrentIndex(0);
}
else
{
_themeComboBox->setCurrentIndex(1);
}
_themeComboBox->blockSignals(false);
});
ElaText* helperText = new ElaText("应用程序设置", this);
helperText->setWordWrap(false);
helperText->setTextPixelSize(18);
_micaSwitchButton = new ElaToggleSwitch(this);
ElaScrollPageArea* micaSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* micaSwitchLayout = new QHBoxLayout(micaSwitchArea);
ElaText* micaSwitchText = new ElaText("启用云母效果", this);
micaSwitchText->setWordWrap(false);
micaSwitchText->setTextPixelSize(15);
micaSwitchLayout->addWidget(micaSwitchText);
micaSwitchLayout->addStretch();
micaSwitchLayout->addWidget(_micaSwitchButton);
connect(_micaSwitchButton, &ElaToggleSwitch::toggled, this, [=](bool checked) {
eApp->setIsEnableMica(checked);
});
_logSwitchButton = new ElaToggleSwitch(this);
ElaScrollPageArea* logSwitchArea = new ElaScrollPageArea(this);
QHBoxLayout* logSwitchLayout = new QHBoxLayout(logSwitchArea);
ElaText* logSwitchText = new ElaText("启用日志功能", this);
logSwitchText->setWordWrap(false);
logSwitchText->setTextPixelSize(15);
logSwitchLayout->addWidget(logSwitchText);
logSwitchLayout->addStretch();
logSwitchLayout->addWidget(_logSwitchButton);
connect(_logSwitchButton, &ElaToggleSwitch::toggled, this, [=](bool checked) {
ElaLog::getInstance()->initMessageLog(checked);
if (checked)
{
qDebug() << "日志已启用!";
}
else
{
qDebug() << "日志已关闭!";
}
});
_minimumButton = new ElaRadioButton("Minimum", this);
_compactButton = new ElaRadioButton("Compact", this);
_maximumButton = new ElaRadioButton("Maximum", this);
_autoButton = new ElaRadioButton("Auto", this);
_autoButton->setChecked(true);
ElaScrollPageArea* displayModeArea = new ElaScrollPageArea(this);
QHBoxLayout* displayModeLayout = new QHBoxLayout(displayModeArea);
ElaText* displayModeText = new ElaText("导航栏模式选择", this);
displayModeText->setWordWrap(false);
displayModeText->setTextPixelSize(15);
displayModeLayout->addWidget(displayModeText);
displayModeLayout->addStretch();
displayModeLayout->addWidget(_minimumButton);
displayModeLayout->addWidget(_compactButton);
displayModeLayout->addWidget(_maximumButton);
displayModeLayout->addWidget(_autoButton);
connect(_minimumButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Minimal);
}
});
connect(_compactButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Compact);
}
});
connect(_maximumButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Maximal);
}
});
connect(_autoButton, &ElaRadioButton::toggled, this, [=](bool checked) {
if (checked)
{
window->setNavigationBarDisplayMode(ElaNavigationType::Auto);
}
});
QWidget* centralWidget = new QWidget(this);
centralWidget->setWindowTitle("UI设置");
QVBoxLayout* centerLayout = new QVBoxLayout(centralWidget);
centerLayout->addSpacing(30);
centerLayout->addWidget(themeText);
centerLayout->addSpacing(10);
centerLayout->addWidget(themeSwitchArea);
centerLayout->addSpacing(15);
centerLayout->addWidget(helperText);
centerLayout->addSpacing(10);
centerLayout->addWidget(logSwitchArea);
centerLayout->addWidget(micaSwitchArea);
centerLayout->addWidget(displayModeArea);
centerLayout->addStretch();
centerLayout->setContentsMargins(0, 0, 0, 0);
addCentralWidget(centralWidget, true, true, 0);
}
UISetting::~UISetting()
{
}
+1
View File
@@ -0,0 +1 @@
设置界面,UI使用Ela UI
+53
View File
@@ -0,0 +1,53 @@
//
// Created by misaki on 2026/2/1.
//
/**
* 屏幕截图与系统信息获取工具类
*/
#pragma once
#include <QString>
#include <QSize>
#include <QScreen>
#include <QPixmap>
#include <QBuffer>
#include <QGuiApplication>
#include <QWindow>
#include <QCursor>
#include <QSysInfo>
class ScreenHelper
{
public:
// 系统信息struct
struct SystemInfo {
QString osType; // 例如: "windows", "linux", "macos"
QString osVersion; // 例如: "Windows 11 (10.0)", "Ubuntu 22.04"
QString displayServer; // 例如: "windows", "cocoa", "xcb" (X11), "wayland"
bool isWayland; // 专门标记是否为 Wayland
};
// 截图结果struct
struct ScreenshotResult {
bool success; // 是否成功
QString base64Data; // 图片的Base64字符串 (PNG格式)
int width; // 图片宽度
int height; // 图片高度
QString screenName; // 屏幕名称
QString errorMsg; // 如果失败,返回错误信息
};
public:
/**
* @brief 获取当前焦点屏幕的全屏截图并转换为Base64 \n
* 判定逻辑:优先取有焦点的窗口所在屏幕,若无,取鼠标所在屏幕
*/
static ScreenshotResult captureFocusedScreen();
/**
* @brief 获取当前操作系统和显示服务信息
*/
static SystemInfo getSystemInfo();
private:
// 私有构造,禁止实例化
ScreenHelper() = default;
};
+229
View File
@@ -0,0 +1,229 @@
//
// 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实现。
*
* C++20
*/
#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
+92
View File
@@ -0,0 +1,92 @@
//
// Created by misaki on 2026/2/1.
//
#include <QDebug>
#include "ScreenHelperUtil.hpp"
ScreenHelper::ScreenshotResult ScreenHelper::captureFocusedScreen()
{
ScreenHelper::ScreenshotResult result;
result.success = false;
// 获取目标屏幕
QScreen *targetScreen = nullptr;
// 首先尝试获取当前应用程序拥有焦点的窗口所在的屏幕
QWindow *focusWindow = QGuiApplication::focusWindow();
if (focusWindow) {
targetScreen = focusWindow->screen();
}
// 如果没有窗口焦点或者窗口还没显示,获取鼠标光标所在的屏幕
if (!targetScreen) {
targetScreen = QGuiApplication::screenAt(QCursor::pos());
}
// 如果以上都失败,回退到主屏幕
if (!targetScreen) {
targetScreen = QGuiApplication::primaryScreen();
}
if (!targetScreen) {
result.errorMsg = "Critical Error: No detectible screen found.";
return result;
}
// 获取屏幕基本信息
result.screenName = targetScreen->name();
// 执行截图
// grabWindow(0) 表示截取整个屏幕
// 注:在 Wayland 上,这可能需要系统权限或会弹出确认框,或者在某些安全策略下返回黑色图像
QPixmap pixmap = targetScreen->grabWindow(0);
if (pixmap.isNull()) {
result.errorMsg = "Failed to grab screen content (Permission denied or System restriction).";
return result;
}
result.width = pixmap.width();
result.height = pixmap.height();
// 转换为 Base64
QByteArray byteArray;
QBuffer buffer(&byteArray);
buffer.open(QIODevice::WriteOnly);
// 保存为 PNG 格式,质量默认即可
if (pixmap.save(&buffer, "PNG")) {
result.base64Data = QString::fromLatin1(byteArray.toBase64());
result.success = true;
} else {
result.errorMsg = "Failed to encode image to PNG buffer.";
}
return result;
}
ScreenHelper::SystemInfo ScreenHelper::getSystemInfo()
{
ScreenHelper::SystemInfo info;
// 获取操作系统类型
info.osType = QSysInfo::productType();
// 获取详细版本 (例如 Windows 10/11, Ubuntu 20.04)
// prettyProductName() 通常能区分 Win10 和 Win11
info.osVersion = QSysInfo::prettyProductName();
// 获取显示服务器类型 (Platform Plugin)
// 这里的返回值通常是 QPA 插件的名字
// Windows -> "windows"
// macOS -> "cocoa"
// Linux X11 -> "xcb"
// Linux Wayland -> "wayland"
QString platformName = QGuiApplication::platformName();
info.displayServer = platformName;
// 专门判断 Wayland
info.isWayland = (platformName == "wayland");
// 针对 Linux 做更细致的显示名称优化
if (platformName == "xcb") {
info.displayServer = "X11 (xcb)";
} else if (platformName == "wayland") {
info.displayServer = "Wayland";
}
return info;
}