diff --git a/3rdparty/autogui-cpp/.gitignore b/3rdparty/autogui-cpp/.gitignore new file mode 100644 index 0000000..8f93188 --- /dev/null +++ b/3rdparty/autogui-cpp/.gitignore @@ -0,0 +1,12 @@ +# IDEs +/.idea +/example/.idea +.vscode + +# Build files +/cmake-build-debug +/cmake-build-release +/example/cmake-build-debug + + +/build diff --git a/3rdparty/autogui-cpp/CMakeLists.txt b/3rdparty/autogui-cpp/CMakeLists.txt new file mode 100644 index 0000000..e688695 --- /dev/null +++ b/3rdparty/autogui-cpp/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.10) +project(autogui-cpp VERSION 1.0.0 LANGUAGES CXX) + +# 基础配置 +set(CMAKE_CXX_STANDARD 17) # Cpp17 +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# 创建库目标 +add_library(autogui-cpp STATIC + src/Keyboard.cpp + src/Mouse.cpp + src/Utils.cpp + src/Autogui.cpp +) + +# 设置头文件搜索路径 +# 使用生成器表达式支持add_subdirectory和install两种使用方式 +target_include_directories(autogui-cpp PUBLIC + $ + $ +) + +# 平台特定配置 +# macOS平台 +if(APPLE) + # 查找并链接ApplicationServices框架 + # 该框架包含CoreGraphics和Carbon相关API + find_library(APPLICATIONSERVICES_LIBRARY ApplicationServices REQUIRED) + target_link_libraries(autogui-cpp PUBLIC ${APPLICATIONSERVICES_LIBRARY}) + +# Windows平台 +elseif(WIN32) + # 链接User32.lib (提供SendInput, GetCursorPos等API) + target_link_libraries(autogui-cpp PUBLIC User32) + +# Linux/Unix平台 +elseif(UNIX AND NOT APPLE) + # Linux需要X11后端(Wayland尚未支持) + # Linux需要X11后端 + find_package(X11 REQUIRED) + # 查找Xtst库(XTest扩展) + find_library(X11_Xtst_LIB Xtst) + if(NOT X11_Xtst_LIB) + message(FATAL_ERROR "Xtst library not found. Install libxtst-dev.") + endif() + # 查找Xext库(X扩展) + find_library(X11_Xext_LIB Xext) + if(NOT X11_Xext_LIB) + message(FATAL_ERROR "Xext library not found.") + endif() + # 链接所有必需的X11库 + target_link_libraries(autogui-cpp PUBLIC + ${X11_LIBRARIES} + ${X11_Xtst_LIB} + ${X11_Xext_LIB} + ) + target_include_directories(autogui-cpp PUBLIC ${X11_INCLUDE_DIR}) +endif() + +# 集成方式: add_subdirectory +# 在你的项目CMakeLists.txt中: +# add_subdirectory(autogui-cpp) +# target_link_libraries(your_target PRIVATE autogui-cpp) diff --git a/3rdparty/autogui-cpp/README.md b/3rdparty/autogui-cpp/README.md new file mode 100644 index 0000000..2164f0f --- /dev/null +++ b/3rdparty/autogui-cpp/README.md @@ -0,0 +1,118 @@ +# autogui-cpp + +![GitHub last commit](https://img.shields.io/github/last-commit/Misakityan/autogui-cpp) +![GitHub issues](https://img.shields.io/github/issues/Misakityan/autogui-cpp) +![GitHub stars](https://img.shields.io/github/stars/Misakityan/autogui-cpp?style=social) + +本项目继承自[Robot CPP](https://github.com/developer239/robot-cpp),在此感谢该项目的贡献。 + + +原先的项目仅支持`Windows`与`Mac OS`,本项目增加了对`Linux X11`的支持,`Linux Wanland`的支持正在开发中 + +为什么要创建本项目,这是因为我在苦苦寻找`pyautogui`的cpp实现,但很遗憾,我并没有找到这个轮子。 + +本项目在原项目的基础上,增加了以下文件: +1. `Autogui.cpp/.h` +2. `SimpleAutoGUI.h` + +并修复了原项目的bug: +1. 模拟键盘输入的时候,无法处理大小写与符号的转换 + +最终实现了与`pyautogui`类似的功能。 + +但是,本项目不支持以下功能: +1. 模拟键盘输入只支持`ASCII`字符,如果你需要输入中文,或者其他语言的字符,这可能是一个很大的工程量。 +更好的解决方案是将内容copy到剪贴板,然后粘贴到目标位置。 + +同时,本项目裁剪掉了原项目的部分功能: +1. `Hooks` +2. `Screen` +3. `Record` + +主要是因为有着更加完美的上位替代品,例如`Screen`可用`Qt`的`QScreen`替代,并且支持的更加完美。 + +注意:原项目并没有说明代码的开源协议,因此关于`Robot-cpp`部分的代码,解释权归属于原作者` +developer239`。 + +本项目被我直接使用在[Yosuga](https://github.com/Misakityan/Yosuga)这个我自己的项目当中。 + +项目已在以下平台完成测试: +1. Windows 10 +2. kUbuntu 24.04 + +由于我没有Mac PC, 因此并未在mac上进行测试,不过mac部分都是直接使用`Robot-cpp`部分的代码,原作者测试没问题, +大概也是可以使用的。如果遇到bug,尽情提issue即可。 + +## How to use? +想使用本项目十分容易,在你的项目CMakeLists.txt中: +```cmake +add_subdirectory(autogui-cpp) +target_link_libraries(your_target PRIVATE autogui-cpp) # your_target为你项目的构建目标 +# 注意:有时候你还需要根据导入本项目的位置,在autogui-cpp的前面增加一些路径信息 +``` +之后,在你需要的地方`#include "SimpleAutoGUI.h"`,访问AutoGUI当中已经封装好的函数即可使用。 + + +## some examples +```c++ +#include +#include "SimpleAutoGUI.h" +int main() { + Robot::Point screenSize = AutoGUI::size(); + std::cout << "屏幕尺寸: " << screenSize.x << "x" << screenSize.y << std::endl; + + // 测试1: 移动鼠标 + std::cout << "移动鼠标到屏幕中心..." << std::endl; + int centerX = screenSize.x / 2; + int centerY = screenSize.y / 2; + AutoGUI::moveTo(centerX, centerY, 2); + + // 测试2: 单击 + std::cout << "左键单击..." << std::endl; + AutoGUI::click(); + AutoGUI::sleep(0.5); + + // 测试3: 右键单击 + std::cout << "右键单击..." << std::endl; + AutoGUI::rightSingle(); + AutoGUI::sleep(0.5); + + // 测试4: 双击 + std::cout << "左键双击..." << std::endl; + AutoGUI::leftDouble(); + AutoGUI::sleep(0.5); + + // 测试5: 拖拽 + std::cout << "拖拽测试..." << std::endl; + AutoGUI::dragRel(100, 100, 0.5); + AutoGUI::sleep(0.5); + + // 测试6: 输入文本 + std::cout << "输入文本测试..." << std::endl; + AutoGUI::type("Hello, AutoGUI!", 0.05); // 每个字符间隔50ms + AutoGUI::sleep(0.5); + + // 测试7: 快捷键 + std::cout << "快捷键测试 (Ctrl+A)..." << std::endl; + AutoGUI::hotkey({AutoGUI::Keys::CTRL, "a"}); + AutoGUI::sleep(0.5); + + std::cout << "快捷键测试 (Ctrl+C)..." << std::endl; + AutoGUI::hotkey({AutoGUI::Keys::CTRL, "c"}); + AutoGUI::sleep(0.5); + + // 测试8: 滚动 + std::cout << "向上滚动..." << std::endl; + AutoGUI::scroll(5); + AutoGUI::sleep(0.5); + + std::cout << "向下滚动..." << std::endl; + AutoGUI::scroll(-5); + AutoGUI::sleep(0.5); + + // 测试9: 获取鼠标位置 + Robot::Point pos = AutoGUI::position(); + std::cout << "当前鼠标位置: (" << pos.x << ", " << pos.y << ")" << std::endl; + return 0; +} +``` \ No newline at end of file diff --git a/3rdparty/autogui-cpp/src/Autogui.cpp b/3rdparty/autogui-cpp/src/Autogui.cpp new file mode 100644 index 0000000..1ea84e6 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Autogui.cpp @@ -0,0 +1,562 @@ +// +// Created by misaki on 2026/1/25. +// + +#include "Autogui.h" +#include +#include +#include +#include +#include +#include +#include + +namespace AutoGUI { + +// 内部辅助函数 +namespace { + +// 等待指定毫秒数 +void delayMs(const int ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +// 将秒转换为毫秒 +int secondsToMs(double seconds) { return static_cast(seconds * 1000); } + +// 获取当前鼠标位置(内部使用) +Robot::Point getCurrentPosition() { return Robot::Mouse::GetPosition(); } + +} // namespace + +// 实现主要API函数 +void moveTo(int x, int y, double duration) { + Robot::Point target{x, y}; + + if (duration > 0.0) { + Robot::Mouse::MoveSmooth(target); + } else { + Robot::Mouse::Move(target); + } +} + +void moveRel(int xOffset, int yOffset, double duration) { + Robot::Point current = getCurrentPosition(); + Robot::Point target{current.x + xOffset, current.y + yOffset}; + + moveTo(target.x, target.y, duration); +} + +void click(int x, int y, Button button, int clicks, double interval) { + // 如果提供了坐标,先移动到该位置 + if (x >= 0 && y >= 0) { + moveTo(x, y); + delayMs(10); + } + + Robot::MouseButton robotButton = toRobotButton(button); + + for (int i = 0; i < clicks; i++) { + if (clicks == 2 && i == 0) { + // 双击 + Robot::Mouse::DoubleClick(robotButton); + } else { + Robot::Mouse::Click(robotButton); + } + + // 如果不是最后一次点击,并且设置了间隔,则等待 + if (i < clicks - 1 && interval > 0) { + sleep(interval); + } + } +} + +void leftDouble(int x, int y) { click(x, y, Button::LEFT, 2); } + +void rightSingle(int x, int y) { click(x, y, Button::RIGHT, 1); } + +void middleClick(int x, int y) { click(x, y, Button::MIDDLE, 1); } + +void mouseDown(Button button, int x, int y) { + // 如果提供了坐标,先移动到该位置 + if (x >= 0 && y >= 0) { + moveTo(x, y); + delayMs(10); + } + + Robot::Mouse::ToggleButton(true, toRobotButton(button)); +} + +void mouseUp(Button button, int x, int y) { + // 如果提供了坐标,先移动到该位置 + if (x >= 0 && y >= 0) { + moveTo(x, y); + delayMs(10); + } + + Robot::Mouse::ToggleButton(false, toRobotButton(button)); +} + +void drag(const int x1, const int y1, const int x2, const int y2, + const double duration, const Button button) { + // 参数验证 + if (!isValidCoord(x1, y1)) { + throw AutoGUIException("Invalid start coordinates: (" + std::to_string(x1) + + ", " + std::to_string(y1) + ")"); + } + if (!isValidCoord(x2, y2)) { + throw AutoGUIException("Invalid end coordinates: (" + std::to_string(x2) + + ", " + std::to_string(y2) + ")"); + } + if (duration < 0) { + throw AutoGUIException("Duration cannot be negative"); + } + Robot::MouseButton robotButton = toRobotButton(button); + // 移动到起始位置 + Robot::Point start{x1, y1}; + Robot::Mouse::Move(start); + delayMs(10); // 短暂延迟确保移动完成 + // 按下鼠标按钮 + Robot::Mouse::ToggleButton(true, robotButton); + delayMs(10); // 短暂延迟确保按钮按下 + // 拖动到目标位置 + Robot::Point end{x2, y2}; + if (duration > 0.0) { + // 使用平滑拖动 + Robot::Mouse::DragSmooth(end); + } else { + // 立即拖动 + Robot::Mouse::Drag(end); + } + // 释放鼠标按钮 + Robot::Mouse::ToggleButton(false, robotButton); +} + +void dragTo(const int x, const int y, const double duration, + const Button button) { + // 获取当前位置 + Robot::Point current = getCurrentPosition(); + // 如果当前位置就是目标位置,则只做点击操作 + if (current.x == x && current.y == y) { + click(x, y, button); + return; + } + // 调用 drag 函数 + drag(current.x, current.y, x, y, duration, button); +} + +void dragRel(const int xOffset, const int yOffset, const double duration, + const Button button) { + // 获取当前位置 + const Robot::Point current = getCurrentPosition(); + // 计算目标位置 + const int targetX = current.x + xOffset; + const int targetY = current.y + yOffset; + // 如果偏移量为0,则只做点击操作 + if (xOffset == 0 && yOffset == 0) { + click(current.x, current.y, button); + return; + } + // 调用 drag 函数 + drag(current.x, current.y, targetX, targetY, duration, button); +} + +Robot::Point position() { return getCurrentPosition(); } + +void scroll(int clicks, int x) { + // 注意:正数向上滚动,负数向下滚动 + // 与AutoGUI一致 + Robot::Mouse::ScrollBy(clicks, x); +} + +void type(const std::string &text, double interval) { + if (interval > 0.0) { + // 有间隔的输入 + for (char c : text) { + Robot::Keyboard::Click(c); + if (interval > 0) { + sleep(interval); + } + } + } else { + // 无间隔快速输入 + Robot::Keyboard::Type(text); + } +} + +void press(const std::string &key) { + std::string lowerKey = toLower(key); + + if (lowerKey.length() == 1) { + // 单个字符键 + Robot::Keyboard::Click(lowerKey[0]); + } else if (isSpecialKey(lowerKey)) { + // 特殊键 + Robot::Keyboard::SpecialKey specialKey = stringToSpecialKey(lowerKey); + Robot::Keyboard::Click(specialKey); + } else { + throw AutoGUIException("Unknown key: " + key); + } +} + +void keyDown(const std::string &key) { + std::string lowerKey = toLower(key); + + if (lowerKey.length() == 1) { + // 单个字符键 + Robot::Keyboard::Press(lowerKey[0]); + } else if (isSpecialKey(lowerKey)) { + // 特殊键 + Robot::Keyboard::SpecialKey specialKey = stringToSpecialKey(lowerKey); + Robot::Keyboard::Press(specialKey); + } else { + throw AutoGUIException("Unknown key: " + key); + } +} + +void keyUp(const std::string &key) { + std::string lowerKey = toLower(key); + + if (lowerKey.length() == 1) { + // 单个字符键 + Robot::Keyboard::Release(lowerKey[0]); + } else if (isSpecialKey(lowerKey)) { + // 特殊键 + Robot::Keyboard::SpecialKey specialKey = stringToSpecialKey(lowerKey); + Robot::Keyboard::Release(specialKey); + } else { + throw AutoGUIException("Unknown key: " + key); + } +} + +void hotkey(const std::initializer_list &keys) { + // 按下所有键 + for (const auto &key : keys) { + keyDown(key); + } + + delayMs(50); + + // 释放所有键(按相反顺序) + const std::string *end = keys.end(); + const std::string *begin = keys.begin(); + for (const std::string *it = end - 1; it >= begin; --it) { + keyUp(*it); + } +} + +void hotkey(const std::vector &keys) { + // 按下所有键 + for (const auto &key : keys) { + keyDown(key); + } + + delayMs(50); + + // 释放所有键(按相反顺序) + for (auto it = keys.rbegin(); it != keys.rend(); ++it) { + keyUp(*it); + } +} + +void sleep(double seconds) { + if (seconds > 0) { + delayMs(secondsToMs(seconds)); + } +} + +// 平台特定函数实现 +Robot::Point size() { + // 平台特定实现 +#ifdef _WIN32 +// Windows实现 +#include + int width = GetSystemMetrics(SM_CXSCREEN); + int height = GetSystemMetrics(SM_CYSCREEN); + return {width, height}; + +#elif defined(__APPLE__) +// macOS实现 +#include + CGRect mainDisplayBounds = CGDisplayBounds(CGMainDisplayID()); + int width = static_cast(CGRectGetWidth(mainDisplayBounds)); + int height = static_cast(CGRectGetHeight(mainDisplayBounds)); + return {width, height}; + +#elif defined(__linux__) +// Linux实现(X11) +#include + + Display *display = XOpenDisplay(nullptr); + if (display) { + const Screen *screen = DefaultScreenOfDisplay(display); + const int width = WidthOfScreen(screen); + const int height = HeightOfScreen(screen); + XCloseDisplay(display); + return {width, height}; + } + return {1920, 1080}; // 默认值 + +#else + // 其他平台 + return {1920, 1080}; // 默认值 +#endif +} + +// 辅助函数实现 + +bool isValidCoord(int x, int y) { + const Robot::Point screenSize = size(); + return x >= 0 && x < screenSize.x && y >= 0 && y < screenSize.y; +} + +std::string toLower(const std::string &str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), ::tolower); + return result; +} + +namespace ExtendedFunc { +void dragRect(const int x1, const int y1, const int width, const int height, + double const duration, const Button button) { + // 参数验证 + if (width <= 0 || height <= 0) { + throw AutoGUIException("Width and height must be positive"); + } + // 计算矩形右下角坐标 + const int x2 = x1 + width; + const int y2 = y1 + height; + + // 拖动矩形:左上角 -> 右上角 -> 右下角 -> 左下角 -> 左上角 + drag(x1, y1, x2, y1, duration/4, button); // 上边 + drag(x2, y1, x2, y2, duration/4, button); // 右边 + drag(x2, y2, x1, y2, duration/4, button); // 下边 + drag(x1, y2, x1, y1, duration/4, button); // 左边 +} + +void dragCircle(const int centerX, const int centerY, const int radius, + double const duration, const Button button) { + // 参数验证 + if (radius <= 0) { + throw AutoGUIException("Radius must be positive"); + } + // 计算圆上的点数量(越多越平滑) + const int segments = 36; // 每10度一个点 + const double angleStep = 2.0 * M_PI / segments; + // 从3点钟方向开始 + double startX = centerX + radius; + double startY = centerY; + + // 拖动圆形 + for (int i = 1; i <= segments; i++) { + double angle = angleStep * i; + double endX = centerX + radius * cos(angle); + double endY = centerY + radius * sin(angle); + // 拖动到下一个点 + drag(static_cast(startX), static_cast(startY), + static_cast(endX), static_cast(endY), + duration / segments, button); + // 更新起点为终点 + startX = endX; + startY = endY; + } +} + +void dragRandomInArea(const int x1, const int y1, const int x2, const int y2, + const double minDuration, const double maxDuration, + const Button button) { + // 参数验证 + if (x1 >= x2 || y1 >= y2) { + throw AutoGUIException("Invalid area coordinates"); + } + + if (minDuration < 0 || maxDuration < 0 || minDuration > maxDuration) { + throw AutoGUIException("Invalid duration range"); + } + + // 设置随机数生成器 + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution<> distX(x1, x2); + std::uniform_int_distribution<> distY(y1, y2); + std::uniform_real_distribution<> distDuration(minDuration, maxDuration); + + // 生成随机起始点和结束点 + int startX = distX(gen); + int startY = distY(gen); + int endX = distX(gen); + int endY = distY(gen); + double duration = distDuration(gen); + + // 执行随机拖动 + drag(startX, startY, endX, endY, duration, button); +} + +// 模拟人类打字 +void typeHumanLike(const std::string& text, double minDelay, double maxDelay, double errorRate) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> delayDist(minDelay, maxDelay); + std::uniform_real_distribution<> errorDist(0.0, 1.0); + + for (char c : text) { + // 有一定概率打错字 + if (errorDist(gen) < errorRate) { + // 输入一个随机错误字符 + char wrongChar = 'a' + static_cast(errorDist(gen) * 26); + AutoGUI::press(std::string(1, wrongChar)); + + // 退格删除错误 + AutoGUI::press("backspace"); + AutoGUI::sleep(0.1); + } + + // 输入正确字符 + AutoGUI::press(std::string(1, c)); + + // 随机延迟 + double delay = delayDist(gen); + AutoGUI::sleep(delay); + } +} + +void typewriteEnter(const std::string& text, double interval) { + AutoGUI::type(text, interval); + AutoGUI::press("enter"); +} + +void typewriteLines(const std::vector& lines, double interval, double lineDelay) { + for (size_t i = 0; i < lines.size(); i++) { + AutoGUI::type(lines[i], interval); + if (i < lines.size() - 1) { + AutoGUI::press("enter"); + AutoGUI::sleep(lineDelay); + } + } +} + +void typeDateTime(const std::string& format) { + const auto now = std::chrono::system_clock::now(); + const auto time = std::chrono::system_clock::to_time_t(now); + const std::tm tm = *std::localtime(&time); + + std::stringstream ss; + ss << std::put_time(&tm, format.c_str()); + AutoGUI::type(ss.str()); +} + +/// 键盘快捷键扩展实现 +void saveFile() { + AutoGUI::hotkey({"ctrl", "s"}); +} + +void openFile() { + AutoGUI::hotkey({"ctrl", "o"}); +} + +void newFile() { + AutoGUI::hotkey({"ctrl", "n"}); +} + +void copy() { + AutoGUI::hotkey({"ctrl", "c"}); +} + +void paste() { + AutoGUI::hotkey({"ctrl", "v"}); +} + +void cut() { + AutoGUI::hotkey({"ctrl", "x"}); +} + +void selectAll() { + AutoGUI::hotkey({"ctrl", "a"}); +} + +void undo() { + AutoGUI::hotkey({"ctrl", "z"}); +} + +void redo() { +#ifdef _WIN32 + AutoGUI::hotkey({"ctrl", "y"}); +#else + AutoGUI::hotkey({"ctrl", "shift", "z"}); +#endif +} + +void find() { + AutoGUI::hotkey({"ctrl", "f"}); +} + +void replace() { + AutoGUI::hotkey({"ctrl", "h"}); +} + +void print() { + AutoGUI::hotkey({"ctrl", "p"}); +} + +void closeWindow(bool useAltF4) { + if (useAltF4) { + AutoGUI::hotkey({"alt", "f4"}); + } else { + AutoGUI::hotkey({"ctrl", "w"}); + } +} + +void switchWindow(int times) { + for (int i = 0; i < times; i++) { + AutoGUI::hotkey({"alt", "tab"}); + AutoGUI::sleep(0.1); + } +} + +void refresh() { + AutoGUI::press("f5"); +} + +/// 游戏/自动化操作实现 +void autoClicker(int x, int y, int count, double interval, AutoGUI::Button button) { + for (int i = 0; i < count; i++) { + AutoGUI::click(x, y, button); + if (i < count - 1) { + AutoGUI::sleep(interval); + } + } +} + +void rapidClicker(int x, int y, double duration, double clickRate, AutoGUI::Button button) { + int totalClicks = static_cast(duration * clickRate); + double interval = 1.0 / clickRate; + + for (int i = 0; i < totalClicks; i++) { + AutoGUI::click(x, y, button); + AutoGUI::sleep(interval); + } +} + +void rapidKeyPress(const std::string& key, double duration, double pressRate) { + int totalPresses = static_cast(duration * pressRate); + double interval = 1.0 / pressRate; + + for (int i = 0; i < totalPresses; i++) { + AutoGUI::press(key); + AutoGUI::sleep(interval); + } +} + +void rapidHotkey(const std::vector& keys, double duration, double pressRate) { + int totalPresses = static_cast(duration * pressRate); + double interval = 1.0 / pressRate; + + for (int i = 0; i < totalPresses; i++) { + AutoGUI::hotkey(keys); + AutoGUI::sleep(interval); + } +} + +} // namespace ExtendedFunc + +} // namespace AutoGUI \ No newline at end of file diff --git a/3rdparty/autogui-cpp/src/Autogui.h b/3rdparty/autogui-cpp/src/Autogui.h new file mode 100644 index 0000000..bbd8b35 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Autogui.h @@ -0,0 +1,500 @@ +// +// Created by misaki on 2026/1/25. +// +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Keyboard.h" +#include "Mouse.h" +#include "types.h" + +namespace AutoGUI { + +// 错误类型 +class AutoGUIException : public std::exception { +private: + std::string message; + +public: + explicit AutoGUIException(std::string msg) : message(std::move(msg)) {} + + [[nodiscard]] const char* what() const noexcept override { + return message.c_str(); + } +}; + +// 鼠标按钮枚举 +enum class Button { + LEFT = 0, + RIGHT = 1, + MIDDLE = 2 +}; + +// 将 Button 转换为 Robot::MouseButton +inline Robot::MouseButton toRobotButton(Button button) { + switch (button) { + case Button::LEFT: return Robot::MouseButton::LEFT_BUTTON; + case Button::RIGHT: return Robot::MouseButton::RIGHT_BUTTON; + case Button::MIDDLE: return Robot::MouseButton::CENTER_BUTTON; + default: return Robot::MouseButton::LEFT_BUTTON; + } +} + +// 将 Button 枚举转换为字符串 +inline std::string buttonToString(Button button) { + switch (button) { + case Button::LEFT: return "left"; + case Button::RIGHT: return "right"; + case Button::MIDDLE: return "middle"; + default: return "left"; + } +} + +// 键名字符串到 Keyboard::SpecialKey 的映射 +inline Robot::Keyboard::SpecialKey stringToSpecialKey(const std::string& key) { + static std::map keyMap = { + {"backspace", Robot::Keyboard::BACKSPACE}, + {"enter", Robot::Keyboard::ENTER}, + {"return", Robot::Keyboard::ENTER}, + {"tab", Robot::Keyboard::TAB}, + {"escape", Robot::Keyboard::ESCAPE}, + {"esc", Robot::Keyboard::ESCAPE}, + {"up", Robot::Keyboard::UP}, + {"down", Robot::Keyboard::DOWN}, + {"right", Robot::Keyboard::RIGHT}, + {"left", Robot::Keyboard::LEFT}, + {"win", Robot::Keyboard::META}, + {"command", Robot::Keyboard::META}, + {"cmd", Robot::Keyboard::META}, + {"alt", Robot::Keyboard::ALT}, + {"ctrl", Robot::Keyboard::CONTROL}, + {"control", Robot::Keyboard::CONTROL}, + {"shift", Robot::Keyboard::SHIFT}, + {"capslock", Robot::Keyboard::CAPSLOCK}, + {"f1", Robot::Keyboard::F1}, + {"f2", Robot::Keyboard::F2}, + {"f3", Robot::Keyboard::F3}, + {"f4", Robot::Keyboard::F4}, + {"f5", Robot::Keyboard::F5}, + {"f6", Robot::Keyboard::F6}, + {"f7", Robot::Keyboard::F7}, + {"f8", Robot::Keyboard::F8}, + {"f9", Robot::Keyboard::F9}, + {"f10", Robot::Keyboard::F10}, + {"f11", Robot::Keyboard::F11}, + {"f12", Robot::Keyboard::F12}, + {"space", Robot::Keyboard::BACKSPACE} // 特殊处理空格 + }; + + std::string lower = key; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + + auto it = keyMap.find(lower); + if (it != keyMap.end()) { + return it->second; + } + + // 如果是单个字符,直接返回(会在其他地方处理) + if (lower.length() == 1) { + // 这是单个字符键,不是特殊键 + throw AutoGUIException("Unknown key: " + key); + } + + throw AutoGUIException("Unknown key: " + key); +} + +// 检查字符串是否是特殊键 +inline bool isSpecialKey(const std::string& key) { + static std::vector specialKeys = { + "backspace", "enter", "return", "tab", "escape", "esc", + "up", "down", "right", "left", "win", "command", "cmd", + "alt", "ctrl", "control", "shift", "capslock", + "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", + "space" + }; + + std::string lower = key; + std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); + + return std::find(specialKeys.begin(), specialKeys.end(), lower) != specialKeys.end(); +} + +// 主要 API 函数 +/** + * @brief 移动鼠标到指定位置 + * @param x 目标位置的X坐标 + * @param y 目标位置的Y坐标 + * @param duration 移动持续时间(秒),0表示立即移动 + */ +void moveTo(int x, int y, double duration = 0.0); + +/** + * @brief 相对移动鼠标 + * @param xOffset X方向偏移量 + * @param yOffset Y方向偏移量 + * @param duration 移动持续时间(秒),0表示立即移动 + */ +void moveRel(int xOffset, int yOffset, double duration = 0.0); + +/** + * @brief 单击鼠标 + * @param x 点击位置的X坐标,-1表示当前位置 + * @param y 点击位置的Y坐标,-1表示当前位置 + * @param button 鼠标按钮:left, right, middle + * @param clicks 点击次数 + * @param interval 多次点击之间的间隔(秒) + */ +void click(int x = -1, int y = -1, + Button button = Button::LEFT, + int clicks = 1, + double interval = 0.0); + +/** + * @brief 左键双击 + * @param x 双击位置的X坐标,-1表示当前位置 + * @param y 双击位置的Y坐标,-1表示当前位置 + */ +void leftDouble(int x = -1, int y = -1); + +/** + * @brief 右键单击 + * @param x 点击位置的X坐标,-1表示当前位置 + * @param y 点击位置的Y坐标,-1表示当前位置 + */ +void rightSingle(int x = -1, int y = -1); + +/** + * @brief 中键单击 + * @param x 点击位置的X坐标,-1表示当前位置 + * @param y 点击位置的Y坐标,-1表示当前位置 + */ +void middleClick(int x = -1, int y = -1); + +/** + * @brief 按下鼠标按钮 + * @param button 要按下的鼠标按钮 + * @param x 按下位置的X坐标,-1表示当前位置 + * @param y 按下位置的Y坐标,-1表示当前位置 + */ +void mouseDown(Button button = Button::LEFT, int x = -1, int y = -1); + +/** + * @brief 释放鼠标按钮 + * @param button 要释放的鼠标按钮 + * @param x 释放位置的X坐标,-1表示当前位置 + * @param y 释放位置的Y坐标,-1表示当前位置 + */ +void mouseUp(Button button = Button::LEFT, int x = -1, int y = -1); + +/** + * @brief 从指定位置拖动到另一个位置 + * @param x1 起始位置的X坐标 + * @param y1 起始位置的Y坐标 + * @param x2 目标位置的X坐标 + * @param y2 目标位置的Y坐标 + * @param duration 拖动持续时间(秒),0表示立即拖动 + * @param button 拖动时按住的鼠标按钮 + */ +void drag(int x1, int y1, int x2, int y2, + double duration = 0.0, + Button button = Button::LEFT); + +/** + * @brief 拖拽鼠标到指定位置 + * @param x 目标位置的X坐标 + * @param y 目标位置的Y坐标 + * @param duration 拖拽持续时间(秒),0表示立即拖拽 + * @param button 拖拽时按住的鼠标按钮 + */ +void dragTo(int x, int y, double duration = 0.0, Button button = Button::LEFT); + +/** + * @brief 相对拖拽鼠标 + * @param xOffset X方向偏移量 + * @param yOffset Y方向偏移量 + * @param duration 拖拽持续时间(秒),0表示立即拖拽 + * @param button 拖拽时按住的鼠标按钮 + */ +void dragRel(int xOffset, int yOffset, double duration = 0.0, Button button = Button::LEFT); + +/** + * @brief 获取当前鼠标位置 + * @return 包含x,y坐标的Point结构体 + */ +Robot::Point position(); + +/** + * @brief 滚动鼠标滚轮 + * @param clicks 滚动量,正数向上滚动,负数向下滚动 + * @param x 水平滚动量(Linux/macOS支持) + */ +void scroll(int clicks, int x = 0); + +/** + * @brief 输入文本 + * @param text 要输入的文本 + * @param interval 字符之间的间隔时间(秒),0表示无间隔 + * 注意:只支持ASCII字符 + */ +void type(const std::string& text, double interval = 0.0); + +/** + * @brief 按下并释放一个键 + * @param key 键名(如:"a", "enter", "ctrl"等) + */ +void press(const std::string& key); + +/** + * @brief 按下键(不释放) + * @param key 键名 + */ +void keyDown(const std::string& key); + +/** + * @brief 释放键 + * @param key 键名 + */ +void keyUp(const std::string& key); + +/** + * @brief 按下组合键 + * @param keys 键名列表,如 {"ctrl", "c"} + */ +void hotkey(const std::initializer_list& keys); + +/** + * @brief 按下组合键(向量版本) + * @param keys 键名向量 + */ +void hotkey(const std::vector& keys); + +/** + * @brief 睡眠/等待 + * @param seconds 等待的秒数 + */ +void sleep(double seconds); + +/** + * @brief 获取屏幕尺寸 + * @return 包含屏幕宽度和高度的Point结构体 + * @note 需要平台特定实现 + * @note 注意:不提供屏幕管理功能,如果你有多个屏幕,那么返回的size可能是错误的,对于单个屏幕是没有影响的,对于获取屏幕尺寸,更加推荐使用Qt当中的接口 + */ +Robot::Point size(); + +// 辅助函数 +/** + * @brief 检查坐标是否有效 + * @param x X坐标 + * @param y Y坐标 + * @return 如果坐标有效返回true + */ +bool isValidCoord(int x, int y); + +/** + * @brief 将字符串转换为小写 + * @param str 输入字符串 + * @return 小写字符串 + */ +std::string toLower(const std::string& str); + +namespace ExtendedFunc { +/// drag拓展功能 +/** +* @brief 从指定位置开始拖动一个矩形区域 +* @param x1 矩形左上角X坐标 +* @param y1 矩形左上角Y坐标 +* @param width 矩形宽度 +* @param height 矩形高度 +* @param duration 拖动持续时间(秒) +* @param button 拖动时按住的鼠标按钮 +*/ +void dragRect(int x1, int y1, int width, int height, + double duration = 0.5, Button button = Button::LEFT); + +/** + * @brief 拖动一个圆形区域(从中心开始到边缘) + * @param centerX 圆心X坐标 + * @param centerY 圆心Y坐标 + * @param radius 圆半径 + * @param duration 拖动持续时间(秒) + * @param button 拖动时按住的鼠标按钮 + */ +void dragCircle(int centerX, int centerY, int radius, + double duration = 0.5, Button button = Button::LEFT); + +/** + * @brief 在区域内随机拖动(用于测试或自动化) + * @param x1 区域左上角X坐标 + * @param y1 区域左上角Y坐标 + * @param x2 区域右下角X坐标 + * @param y2 区域右下角Y坐标 + * @param minDuration 最小拖动持续时间(秒) + * @param maxDuration 最大拖动持续时间(秒) + * @param button 拖动时按住的鼠标按钮 + */ +void dragRandomInArea(int x1, int y1, int x2, int y2, + double minDuration = 0.1, double maxDuration = 0.5, + Button button = Button::LEFT); + +/// 文字输入扩展功能 +/** + * @brief 模拟人类打字(带随机延迟和可能的错误) + * @param text 要输入的文本 + * @param minDelay 最小延迟(秒) + * @param maxDelay 最大延迟(秒) + * @param errorRate 错误率(0.0-1.0) + */ +void typeHumanLike(const std::string& text, double minDelay = 0.05, + double maxDelay = 0.2, double errorRate = 0.0); + +/** + * @brief 输入文本并回车 + * @param text 要输入的文本 + * @param interval 字符间隔 + */ +void typewriteEnter(const std::string& text, double interval = 0.0); + +/** + * @brief 输入多行文本 + * @param lines 文本行数组 + * @param interval 字符间隔 + * @param lineDelay 行间延迟 + */ +void typewriteLines(const std::vector& lines, + double interval = 0.0, double lineDelay = 0.1); + +/** + * @brief 输入当前日期时间 + * @param format 时间格式(如:"%Y-%m-%d %H:%M:%S") + */ +void typeDateTime(const std::string& format = "%Y-%m-%d %H:%M:%S"); + +/// 键盘快捷键扩展功能 +/** + * @brief 保存文件 (Ctrl+S) + */ +void saveFile(); + +/** + * @brief 打开文件 (Ctrl+O) + */ +void openFile(); + +/** + * @brief 新建文件 (Ctrl+N) + */ +void newFile(); + +/** + * @brief 复制 (Ctrl+C) + */ +void copy(); + +/** + * @brief 粘贴 (Ctrl+V) + */ +void paste(); + +/** + * @brief 剪切 (Ctrl+X) + */ +void cut(); + +/** + * @brief 全选 (Ctrl+A) + */ +void selectAll(); + +/** + * @brief 撤销 (Ctrl+Z) + */ +void undo(); + +/** + * @brief 重做 (Ctrl+Y 或 Ctrl+Shift+Z) + */ +void redo(); + +/** + * @brief 查找 (Ctrl+F) + */ +void find(); + +/** + * @brief 替换 (Ctrl+H) + */ +void replace(); + +/** + * @brief 打印 (Ctrl+P) + */ +void print(); + +/** + * @brief 关闭窗口 (Alt+F4 或 Ctrl+W) + */ +void closeWindow(bool useAltF4 = true); + +/** + * @brief 切换窗口 (Alt+Tab) + * @param times 切换次数 + */ +void switchWindow(int times = 1); + +/** + * @brief 刷新 (F5) + */ +void refresh(); + +/// 游戏/自动化操作功能拓展 +/** + * @brief 自动点击器(连续点击) + * @param x 点击位置X坐标 + * @param y 点击位置Y坐标 + * @param count 点击次数 + * @param interval 点击间隔(秒) + * @param button 鼠标按钮 + */ +void autoClicker(int x, int y, int count = 10, double interval = 0.1, + Button button = Button::LEFT); + +/** + * @brief 连点器(按住连点) + * @param x 点击位置X坐标 + * @param y 点击位置Y坐标 + * @param duration 持续时间(秒) + * @param clickRate 点击频率(次/秒) + * @param button 鼠标按钮 + */ +void rapidClicker(int x, int y, double duration = 1.0, + double clickRate = 10.0, + Button button = Button::LEFT); + +/** + * @brief 按键连发(按住按键) + * @param key 按键名称 + * @param duration 持续时间(秒) + * @param pressRate 按键频率(次/秒) + */ +void rapidKeyPress(const std::string& key, double duration = 1.0, + double pressRate = 10.0); + +/** + * @brief 组合键连发 + * @param keys 按键组合 + * @param duration 持续时间 + * @param pressRate 按键频率 + */ +void rapidHotkey(const std::vector& keys, double duration = 1.0, + double pressRate = 5.0); + +} // namespace ExtendedFunc + +} // namespace AutoGUI diff --git a/3rdparty/autogui-cpp/src/Keyboard.cpp b/3rdparty/autogui-cpp/src/Keyboard.cpp new file mode 100644 index 0000000..27e065c --- /dev/null +++ b/3rdparty/autogui-cpp/src/Keyboard.cpp @@ -0,0 +1,898 @@ +#ifdef __APPLE__ +#include +#endif +#ifdef __linux__ +#include +#include +#include +#include +#endif +#include +#include +#include +#include + +#include "./Keyboard.h" +#include "./Utils.h" + +namespace Robot { + +int Keyboard::delay = 1; + +const char Keyboard::INVALID_ASCII = static_cast(0xFF); + +std::thread Keyboard::keyPressThread; +std::atomic Keyboard::continueHolding(false); +std::set Keyboard::heldAsciiChars; +std::set Keyboard::heldSpecialKeys; + +#ifdef __linux__ + Display* Keyboard::display = nullptr; + Window Keyboard::rootWindow = 0; + + void Keyboard::InitializeX11() { + if (display == nullptr) { + display = XOpenDisplay(nullptr); + if (display == nullptr) { + throw std::runtime_error("Cannot open X11 display"); + } + rootWindow = DefaultRootWindow(display); + } + } + + void Keyboard::CleanupX11() { + if (display != nullptr) { + XCloseDisplay(display); + display = nullptr; + } + } +#endif +#ifdef __linux__ + static bool NeedShiftForKeySym(KeySym keysym) { + // 检查这个键是否需要 Shift 键配合 + switch (keysym) { + case XK_exclam: // ! + case XK_at: // @ + case XK_numbersign: // # + case XK_dollar: // $ + case XK_percent: // % + case XK_asciicircum: // ^ + case XK_ampersand: // & + case XK_asterisk: // * + case XK_parenleft: // ( + case XK_parenright: // ) + case XK_underscore: // _ + case XK_plus: // + + case XK_braceleft: // { + case XK_braceright: // } + case XK_bar: // | + case XK_colon: // : + case XK_quotedbl: // " + case XK_less: // < + case XK_greater: // > + case XK_question: // ? + case XK_asciitilde: // ~ + return true; + + // 大写字母需要 Shift 键 + case XK_A: + case XK_B: + case XK_C: + case XK_D: + case XK_E: + case XK_F: + case XK_G: + case XK_H: + case XK_I: + case XK_J: + case XK_K: + case XK_L: + case XK_M: + case XK_N: + case XK_O: + case XK_P: + case XK_Q: + case XK_R: + case XK_S: + case XK_T: + case XK_U: + case XK_V: + case XK_W: + case XK_X: + case XK_Y: + case XK_Z: + return true; + + default: + return false; + } + } + + static bool NeedShiftForChar(const char c) { + // 简单判断:大写字母和特殊符号需要 Shift + if (std::isupper(c)) { + return true; + } + + // 特殊符号需要 Shift + const char* shiftChars = "!@#$%^&*()_+{}|:\"<>?~"; + return (strchr(shiftChars, c) != nullptr); + } +#endif + + +void Keyboard::HoldStart(char asciiChar) { + if (heldAsciiChars.empty() && heldSpecialKeys.empty()) { + continueHolding = true; + keyPressThread = std::thread(KeyHoldThread); + } + heldAsciiChars.insert(asciiChar); +} + +void Keyboard::HoldStart(SpecialKey specialKey) { + if (heldAsciiChars.empty() && heldSpecialKeys.empty()) { + continueHolding = true; + keyPressThread = std::thread(KeyHoldThread); + } + heldSpecialKeys.insert(specialKey); +} + +void Keyboard::HoldStop(char asciiChar) { + heldAsciiChars.erase(asciiChar); + if (heldAsciiChars.empty() && heldSpecialKeys.empty()) { + continueHolding = false; + if (keyPressThread.joinable()) { + keyPressThread.join(); + } + } + Release(asciiChar); +} + +void Keyboard::HoldStop(SpecialKey specialKey) { + heldSpecialKeys.erase(specialKey); + if (heldAsciiChars.empty() && heldSpecialKeys.empty()) { + continueHolding = false; + if (keyPressThread.joinable()) { + keyPressThread.join(); + } + } + Release(specialKey); +} + +void Keyboard::KeyHoldThread() { + while (continueHolding) { + for (char asciiChar : heldAsciiChars) { + Press(asciiChar); + } + for (SpecialKey specialKey : heldSpecialKeys) { + Press(specialKey); + } + Robot::delay(50); + } + + for (char asciiChar : heldAsciiChars) { + Release(asciiChar); + } + for (SpecialKey specialKey : heldSpecialKeys) { + Release(specialKey); + } +} + +void Keyboard::Type(const std::string &query) { + for (const char c : query) { + if (!KeyUtils::IsValidAscii(c)) { + std::cerr << "Warning: Skipping invalid ASCII character: " << static_cast(c) << std::endl; + continue; + } + Click(c); + } +} + +void Keyboard::TypeHumanLike(const std::string &query) { + std::normal_distribution distribution(75, 25); + std::random_device rd; + std::mt19937 engine(rd()); + + for (char c : query) { + if (!KeyUtils::IsValidAscii(c)) { + std::cerr << "Warning: Skipping invalid ASCII character: " << static_cast(c) << std::endl; + continue; + } + + Click(c); + Robot::delay((int)distribution(engine)); + } +} + +void Keyboard::Click(char asciiChar) { +#ifdef __linux__ + InitializeX11(); + // Linux 特殊处理:对于需要 Shift 的字符,先按下 Shift + bool needShift = NeedShiftForChar(asciiChar); + + if (needShift) { + // 获取 Shift 键的键码 + KeyCode shiftKeyCode = XKeysymToKeycode(display, XK_Shift_L); + if (shiftKeyCode != 0) { + XTestFakeKeyEvent(display, shiftKeyCode, True, CurrentTime); + XFlush(display); + Robot::delay(delay); + } + } + Press(asciiChar); + Release(asciiChar); + if (needShift) { + // 释放 Shift 键 + KeyCode shiftKeyCode = XKeysymToKeycode(display, XK_Shift_L); + if (shiftKeyCode != 0) { + XTestFakeKeyEvent(display, shiftKeyCode, False, CurrentTime); + XFlush(display); + Robot::delay(delay); + } + } +#endif + +#ifdef __APPLE__ + // macOS实现 + bool needShift = KeyUtils::NeedsShift(asciiChar);~ + char baseKey = KeyUtils::GetBaseKey(asciiChar); + auto it = asciiToVirtualKeyMap.find(baseKey); + if (it == asciiToVirtualKeyMap.end()) { + std::cerr << "Warning: Character not found in key map: " << asciiChar << std::endl; + return; + } + + CGKeyCode keycode = static_cast(it->second); + CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + + if (needShift) { + // 按下Shift键 + CGEventRef shiftDown = CGEventCreateKeyboardEvent(source, kVK_Shift, true); + CGEventPost(kCGHIDEventTap, shiftDown); + CFRelease(shiftDown); + Robot::delay(delay); + } + + // 按下并释放目标键 + CGEventRef keyDown = CGEventCreateKeyboardEvent(source, keycode, true); + CGEventRef keyUp = CGEventCreateKeyboardEvent(source, keycode, false); + + CGEventPost(kCGHIDEventTap, keyDown); + Robot::delay(delay); + CGEventPost(kCGHIDEventTap, keyUp); + Robot::delay(delay); + + if (needShift) { + // 释放Shift键 + CGEventRef shiftUp = CGEventCreateKeyboardEvent(source, kVK_Shift, false); + CGEventPost(kCGHIDEventTap, shiftUp); + CFRelease(shiftUp); + Robot::delay(delay); + } + + CFRelease(keyDown); + CFRelease(keyUp); + CFRelease(source); + +#endif + +#ifdef _WIN32 + // Windows实现 + bool needShift = KeyUtils::NeedsShift(asciiChar); + char baseKey = KeyUtils::GetBaseKey(asciiChar); + + // 获取基础键的虚拟键码 + SHORT vkAndShift = VkKeyScan(baseKey); + if (vkAndShift == -1) { + std::cerr << "Warning: Cannot get virtual key for character: " << asciiChar << std::endl; + return; + } + + WORD keycode = static_cast(vkAndShift & 0xFF); + + std::vector inputs; + + if (needShift) { + // Shift键按下 + INPUT shiftDown = {0}; + shiftDown.type = INPUT_KEYBOARD; + shiftDown.ki.wVk = VK_SHIFT; + inputs.push_back(shiftDown); + } + + // 目标键按下 + INPUT keyDown = {0}; + keyDown.type = INPUT_KEYBOARD; + keyDown.ki.wVk = keycode; + inputs.push_back(keyDown); + + // 目标键释放 + INPUT keyUp = {0}; + keyUp.type = INPUT_KEYBOARD; + keyUp.ki.wVk = keycode; + keyUp.ki.dwFlags = KEYEVENTF_KEYUP; + inputs.push_back(keyUp); + + if (needShift) { + // Shift键释放 + INPUT shiftUp = {0}; + shiftUp.type = INPUT_KEYBOARD; + shiftUp.ki.wVk = VK_SHIFT; + shiftUp.ki.dwFlags = KEYEVENTF_KEYUP; + inputs.push_back(shiftUp); + } + + // 发送所有输入 + SendInput(static_cast(inputs.size()), inputs.data(), sizeof(INPUT)); +#endif +} + +void Keyboard::Click(SpecialKey specialKey) { + Press(specialKey); + Release(specialKey); +} + +void Keyboard::Press(char asciiChar) { + KeyCode keycode = AsciiToVirtualKey(asciiChar); +#ifdef __APPLE__ + CGEventSourceRef source = + CGEventSourceCreate(kCGEventSourceStateHIDSystemState); + CGEventRef event = CGEventCreateKeyboardEvent(source, keycode, true); + CGEventPost(kCGHIDEventTap, event); + + CFRelease(event); + CFRelease(source); +#endif + +#ifdef _WIN32 + INPUT input = {0}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = keycode; + SendInput(1, &input, sizeof(INPUT)); +#endif + +#ifdef __linux__ + InitializeX11(); + KeyCode xkeycode = XKeysymToKeycode(display, keycode); + if (xkeycode != 0) { + XTestFakeKeyEvent(display, xkeycode, True, CurrentTime); + XFlush(display); + } +#endif + Robot::delay(delay); +} + +void Keyboard::Press(SpecialKey specialKey) { + KeyCode keycode = SpecialKeyToVirtualKey(specialKey); +#ifdef __APPLE__ + CGEventRef event = CGEventCreateKeyboardEvent(nullptr, keycode, true); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); +#endif + +#ifdef _WIN32 + INPUT input = {0}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = keycode; + SendInput(1, &input, sizeof(INPUT)); +#endif + +#ifdef __linux__ + InitializeX11(); + KeyCode xkeycode = XKeysymToKeycode(display, keycode); + if (xkeycode != 0) { + XTestFakeKeyEvent(display, xkeycode, True, CurrentTime); + XFlush(display); + } +#endif + Robot::delay(delay); +} + +void Keyboard::Release(char asciiChar) { + KeyCode keycode = AsciiToVirtualKey(asciiChar); +#ifdef __APPLE__ + CGEventRef event = + CGEventCreateKeyboardEvent(nullptr, (CGKeyCode)keycode, false); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); +#endif + +#ifdef _WIN32 + INPUT input = {0}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = keycode; + input.ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(1, &input, sizeof(INPUT)); +#endif + +#ifdef __linux__ + InitializeX11(); + KeyCode xkeycode = XKeysymToKeycode(display, keycode); + if (xkeycode != 0) { + XTestFakeKeyEvent(display, xkeycode, False, CurrentTime); + XFlush(display); + } +#endif + Robot::delay(delay); +} + +void Keyboard::Release(SpecialKey specialKey) { + KeyCode keycode = SpecialKeyToVirtualKey(specialKey); +#ifdef __APPLE__ + CGEventRef event = + CGEventCreateKeyboardEvent(nullptr, (CGKeyCode)keycode, false); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); +#endif + +#ifdef _WIN32 + INPUT input = {0}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = keycode; + input.ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(1, &input, sizeof(INPUT)); +#endif + +#ifdef __linux__ + InitializeX11(); + KeyCode xkeycode = XKeysymToKeycode(display, keycode); + if (xkeycode != 0) { + XTestFakeKeyEvent(display, xkeycode, False, CurrentTime); + XFlush(display); + } +#endif + Robot::delay(delay); +} + +KeyCode Keyboard::SpecialKeyToVirtualKey(SpecialKey specialKey) { + return specialKeyToVirtualKeyMap.at(specialKey); +} + +KeyCode Keyboard::AsciiToVirtualKey(char asciiChar) { +#ifdef __APPLE__ + auto it = asciiToVirtualKeyMap.find(asciiChar); + if (it == asciiToVirtualKeyMap.end()) { + std::cerr + << "Warning: Character not found in the virtual key map. Ignoring..." + << std::endl; + return 0xFFFF; // Return an invalid keycode + } + return static_cast(it->second); +#endif + +#ifdef _WIN32 + // Windows: VkKeyScan 函数返回一个short,高字节是shift状态,低字节是虚拟键码 + SHORT vkAndShift = VkKeyScan(asciiChar); + if (vkAndShift == -1) { + std::cerr + << "Warning: Character not found in the virtual key map. Ignoring..." + << std::endl; + return 0xFFFF; // Return an invalid keycode + } + // 返回虚拟键码(低字节) + return static_cast(vkAndShift & 0xFF); +#endif + +#ifdef __linux__ + // Convert ASCII to X11 KeySym + // For Linux, we need to handle case sensitivity properly + if (std::isalpha(asciiChar)) { + // For letters, use lowercase KeySym + switch (char lowerChar = std::tolower(asciiChar)) { + case 'a': return XK_a; + case 'b': return XK_b; + case 'c': return XK_c; + case 'd': return XK_d; + case 'e': return XK_e; + case 'f': return XK_f; + case 'g': return XK_g; + case 'h': return XK_h; + case 'i': return XK_i; + case 'j': return XK_j; + case 'k': return XK_k; + case 'l': return XK_l; + case 'm': return XK_m; + case 'n': return XK_n; + case 'o': return XK_o; + case 'p': return XK_p; + case 'q': return XK_q; + case 'r': return XK_r; + case 's': return XK_s; + case 't': return XK_t; + case 'u': return XK_u; + case 'v': return XK_v; + case 'w': return XK_w; + case 'x': return XK_x; + case 'y': return XK_y; + case 'z': return XK_z; + } + } + + // For numbers and special characters + switch (asciiChar) { + // Numbers + case '0': return XK_0; + case '1': return XK_1; + case '2': return XK_2; + case '3': return XK_3; + case '4': return XK_4; + case '5': return XK_5; + case '6': return XK_6; + case '7': return XK_7; + case '8': return XK_8; + case '9': return XK_9; + + // Special characters that need shift + case '!': return XK_exclam; // Shift+1 + case '@': return XK_at; // Shift+2 + case '#': return XK_numbersign; // Shift+3 + case '$': return XK_dollar; // Shift+4 + case '%': return XK_percent; // Shift+5 + case '^': return XK_asciicircum; // Shift+6 + case '&': return XK_ampersand; // Shift+7 + case '*': return XK_asterisk; // Shift+8 + case '(': return XK_parenleft; // Shift+9 + case ')': return XK_parenright; // Shift+0 + case '_': return XK_underscore; // Shift+- + case '+': return XK_plus; // Shift+= + case '{': return XK_braceleft; // Shift+[ + case '}': return XK_braceright; // Shift+] + case '|': return XK_bar; // Shift+\ + case ':': return XK_colon; // Shift+; + case '"': return XK_quotedbl; // Shift+' + case '<': return XK_less; // Shift+, + case '>': return XK_greater; // Shift+. + case '?': return XK_question; // Shift+/ + case '~': return XK_asciitilde; // Shift+` + + // Special characters without shift + case ' ': return XK_space; + case '\t': return XK_Tab; + case '\n': return XK_Return; + case '\b': return XK_BackSpace; + case 27: return XK_Escape; // ESC + + case '-': return XK_minus; + case '=': return XK_equal; + case '[': return XK_bracketleft; + case ']': return XK_bracketright; + case '\\': return XK_backslash; + case ';': return XK_semicolon; + case '\'': return XK_apostrophe; + case ',': return XK_comma; + case '.': return XK_period; + case '/': return XK_slash; + case '`': return XK_grave; + + case ':': return XK_colon; + + default: + std::cerr << "Warning: Character " << static_cast(asciiChar) + << " not mapped to X11 KeySym. Using XK_space instead." + << std::endl; + return XK_space; + } +#endif +} + +#ifdef __APPLE__ +std::map Keyboard::specialKeyToVirtualKeyMap = { + {Keyboard::BACKSPACE, kVK_Delete}, + {Keyboard::ENTER, kVK_Return}, + {Keyboard::TAB, kVK_Tab}, + {Keyboard::ESCAPE, kVK_Escape}, + {Keyboard::UP, kVK_UpArrow}, + {Keyboard::DOWN, kVK_DownArrow}, + {Keyboard::RIGHT, kVK_RightArrow}, + {Keyboard::LEFT, kVK_LeftArrow}, + {Keyboard::META, kVK_Command}, + {Keyboard::ALT, kVK_Option}, + {Keyboard::CONTROL, kVK_Control}, + {Keyboard::SHIFT, kVK_Shift}, + {Keyboard::CAPSLOCK, kVK_CapsLock}, + {Keyboard::F1, kVK_F1}, + {Keyboard::F2, kVK_F2}, + {Keyboard::F3, kVK_F3}, + {Keyboard::F4, kVK_F4}, + {Keyboard::F5, kVK_F5}, + {Keyboard::F6, kVK_F6}, + {Keyboard::F7, kVK_F7}, + {Keyboard::F8, kVK_F8}, + {Keyboard::F9, kVK_F9}, + {Keyboard::F10, kVK_F10}, + {Keyboard::F11, kVK_F11}, + {Keyboard::F12, kVK_F12}}; +#endif + + + +#ifdef __linux__ + std::map Keyboard::specialKeyToVirtualKeyMap = { + {Keyboard::BACKSPACE, XK_BackSpace}, + {Keyboard::ENTER, XK_Return}, + {Keyboard::TAB, XK_Tab}, + {Keyboard::ESCAPE, XK_Escape}, + {Keyboard::UP, XK_Up}, + {Keyboard::DOWN, XK_Down}, + {Keyboard::RIGHT, XK_Right}, + {Keyboard::LEFT, XK_Left}, + {Keyboard::META, XK_Super_L}, // Super/Windows key + {Keyboard::ALT, XK_Alt_L}, + {Keyboard::CONTROL, XK_Control_L}, + {Keyboard::SHIFT, XK_Shift_L}, + {Keyboard::CAPSLOCK, XK_Caps_Lock}, + {Keyboard::F1, XK_F1}, + {Keyboard::F2, XK_F2}, + {Keyboard::F3, XK_F3}, + {Keyboard::F4, XK_F4}, + {Keyboard::F5, XK_F5}, + {Keyboard::F6, XK_F6}, + {Keyboard::F7, XK_F7}, + {Keyboard::F8, XK_F8}, + {Keyboard::F9, XK_F9}, + {Keyboard::F10, XK_F10}, + {Keyboard::F11, XK_F11}, + {Keyboard::F12, XK_F12}}; +#endif + + char Keyboard::VirtualKeyToAscii(KeyCode virtualKey) { +#ifdef __APPLE__ + auto map = asciiToVirtualKeyMap; + auto it = std::find_if( + map.begin(), + map.end(), + [virtualKey](const std::pair &p) { + return p.second == virtualKey; + } + ); + + if (it == map.end()) { + return INVALID_ASCII; + } + + return it->first; +#endif + +#ifdef _WIN32 + // Convert the virtual key code to a scan code + UINT scanCode = MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC); + + // Convert the scan code to the corresponding character + char character = 0; + BYTE keyboardState[256] = {0}; + GetKeyboardState(keyboardState); + wchar_t buffer[2]; + if (ToUnicode(virtualKey, scanCode, keyboardState, buffer, 2, 0) == 1) { + character = static_cast(buffer[0]); + } + return character; +#endif + +#ifdef __linux__ + InitializeX11(); + + // Get the keycode from keysym + KeyCode keycode = XKeysymToKeycode(display, virtualKey); + if (keycode == 0) { + return INVALID_ASCII; + } + + // Get the current keyboard state + char keys[32]; + XQueryKeymap(display, keys); + + // Get the current modifier state + XKeyEvent event; + event.display = display; + event.window = rootWindow; + event.root = rootWindow; + event.subwindow = None; + event.time = CurrentTime; + event.x = event.y = event.x_root = event.y_root = 0; + event.same_screen = True; + event.keycode = keycode; + event.state = 0; + + // Try to get the character with different modifier states + KeySym keysym; + XLookupString(&event, nullptr, 0, &keysym, nullptr); + + // Convert keysym to ASCII if possible + if (keysym >= 0x20 && keysym <= 0x7E) { // Printable ASCII range + return static_cast(keysym); + } + + return INVALID_ASCII; +#endif + + } + + Keyboard::SpecialKey Keyboard::VirtualKeyToSpecialKey(KeyCode virtualKey) { +#ifdef __APPLE__ + switch (virtualKey) { + case 123: + return Keyboard::LEFT; + case 124: + return Keyboard::RIGHT; + case 125: + return Keyboard::DOWN; + case 126: + return Keyboard::UP; + case 36: + return Keyboard::ENTER; + case 48: + return Keyboard::TAB; + case 51: + return Keyboard::BACKSPACE; + case 53: + return Keyboard::ESCAPE; + case 55: + return Keyboard::META; + case 56: + return Keyboard::SHIFT; + case 57: + return Keyboard::CAPSLOCK; + case 58: + return Keyboard::ALT; + case 59: + return Keyboard::CONTROL; + } +#endif + +#ifdef _WIN32 + switch (virtualKey) { + case VK_LEFT: + return Keyboard::LEFT; + case VK_RIGHT: + return Keyboard::RIGHT; + case VK_DOWN: + return Keyboard::DOWN; + case VK_UP: + return Keyboard::UP; + case VK_RETURN: + return Keyboard::ENTER; + case VK_TAB: + return Keyboard::TAB; + case VK_BACK: + return Keyboard::BACKSPACE; + case VK_ESCAPE: + return Keyboard::ESCAPE; + case VK_LWIN: + case VK_RWIN: + return Keyboard::META; + case VK_SHIFT: + return Keyboard::SHIFT; + case VK_CAPITAL: + return Keyboard::CAPSLOCK; + case VK_MENU: + return Keyboard::ALT; + case VK_CONTROL: + return Keyboard::CONTROL; + } +#endif + +#ifdef __linux__ + switch (virtualKey) { + case XK_Left: + return Keyboard::LEFT; + case XK_Right: + return Keyboard::RIGHT; + case XK_Down: + return Keyboard::DOWN; + case XK_Up: + return Keyboard::UP; + case XK_Return: + return Keyboard::ENTER; + case XK_Tab: + return Keyboard::TAB; + case XK_BackSpace: + return Keyboard::BACKSPACE; + case XK_Escape: + return Keyboard::ESCAPE; + case XK_Super_L: + case XK_Super_R: + return Keyboard::META; + case XK_Shift_L: + case XK_Shift_R: + return Keyboard::SHIFT; + case XK_Caps_Lock: + return Keyboard::CAPSLOCK; + case XK_Alt_L: + case XK_Alt_R: + return Keyboard::ALT; + case XK_Control_L: + case XK_Control_R: + return Keyboard::CONTROL; + } +#endif + // Default case for all platforms + return static_cast(0); + } + +#ifdef _WIN32 +std::map Keyboard::specialKeyToVirtualKeyMap = { + {Keyboard::BACKSPACE, VK_BACK}, + {Keyboard::ENTER, VK_RETURN}, + {Keyboard::TAB, VK_TAB}, + {Keyboard::ESCAPE, VK_ESCAPE}, + {Keyboard::UP, VK_UP}, + {Keyboard::DOWN, VK_DOWN}, + {Keyboard::RIGHT, VK_RIGHT}, + {Keyboard::LEFT, VK_LEFT}, + {Keyboard::META, VK_LWIN}, + {Keyboard::ALT, VK_MENU}, + {Keyboard::CONTROL, VK_CONTROL}, + {Keyboard::SHIFT, VK_SHIFT}, + {Keyboard::CAPSLOCK, VK_CAPITAL}, + {Keyboard::F1, VK_F1}, + {Keyboard::F2, VK_F2}, + {Keyboard::F3, VK_F3}, + {Keyboard::F4, VK_F4}, + {Keyboard::F5, VK_F5}, + {Keyboard::F6, VK_F6}, + {Keyboard::F7, VK_F7}, + {Keyboard::F8, VK_F8}, + {Keyboard::F9, VK_F9}, + {Keyboard::F10, VK_F10}, + {Keyboard::F11, VK_F11}, + {Keyboard::F12, VK_F12}}; +#endif + +#ifdef __APPLE__ +std::map Keyboard::asciiToVirtualKeyMap = { + // 数字 + {'0', kVK_ANSI_0}, {'1', kVK_ANSI_1}, {'2', kVK_ANSI_2}, + {'3', kVK_ANSI_3}, {'4', kVK_ANSI_4}, {'5', kVK_ANSI_5}, + {'6', kVK_ANSI_6}, {'7', kVK_ANSI_7}, {'8', kVK_ANSI_8}, + {'9', kVK_ANSI_9}, + + // 字母(只使用小写) + {'a', kVK_ANSI_A}, {'b', kVK_ANSI_B}, {'c', kVK_ANSI_C}, + {'d', kVK_ANSI_D}, {'e', kVK_ANSI_E}, {'f', kVK_ANSI_F}, + {'g', kVK_ANSI_G}, {'h', kVK_ANSI_H}, {'i', kVK_ANSI_I}, + {'j', kVK_ANSI_J}, {'k', kVK_ANSI_K}, {'l', kVK_ANSI_L}, + {'m', kVK_ANSI_M}, {'n', kVK_ANSI_N}, {'o', kVK_ANSI_O}, + {'p', kVK_ANSI_P}, {'q', kVK_ANSI_Q}, {'r', kVK_ANSI_R}, + {'s', kVK_ANSI_S}, {'t', kVK_ANSI_T}, {'u', kVK_ANSI_U}, + {'v', kVK_ANSI_V}, {'w', kVK_ANSI_W}, {'x', kVK_ANSI_X}, + {'y', kVK_ANSI_Y}, {'z', kVK_ANSI_Z}, + + // 特殊字符(不需要Shift的) + {' ', kVK_Space}, + {'-', kVK_ANSI_Minus}, + {'=', kVK_ANSI_Equal}, + {'[', kVK_ANSI_LeftBracket}, + {']', kVK_ANSI_RightBracket}, + {'\\', kVK_ANSI_Backslash}, + {';', kVK_ANSI_Semicolon}, + {'\'', kVK_ANSI_Quote}, + {',', kVK_ANSI_Comma}, + {'.', kVK_ANSI_Period}, + {'/', kVK_ANSI_Slash}, + {'`', kVK_ANSI_Grave}, + + // 需要Shift的字符(映射到对应数字或符号键) + {'!', kVK_ANSI_1}, // Shift+1 + {'@', kVK_ANSI_2}, // Shift+2 + {'#', kVK_ANSI_3}, // Shift+3 + {'$', kVK_ANSI_4}, // Shift+4 + {'%', kVK_ANSI_5}, // Shift+5 + {'^', kVK_ANSI_6}, // Shift+6 + {'&', kVK_ANSI_7}, // Shift+7 + {'*', kVK_ANSI_8}, // Shift+8 + {'(', kVK_ANSI_9}, // Shift+9 + {')', kVK_ANSI_0}, // Shift+0 + {'_', kVK_ANSI_Minus}, // Shift+- + {'+', kVK_ANSI_Equal}, // Shift+= + {'{', kVK_ANSI_LeftBracket}, // Shift+[ + {'}', kVK_ANSI_RightBracket}, // Shift+] + {'|', kVK_ANSI_Backslash}, // Shift+\ + {':', kVK_ANSI_Semicolon}, // Shift+; + {'"', kVK_ANSI_Quote}, // Shift+' + {'<', kVK_ANSI_Comma}, // Shift+, + {'>', kVK_ANSI_Period}, // Shift+. + {'?', kVK_ANSI_Slash}, // Shift+/ + {'~', kVK_ANSI_Grave} // Shift+` +}; +#endif +} // namespace Robot diff --git a/3rdparty/autogui-cpp/src/Keyboard.h b/3rdparty/autogui-cpp/src/Keyboard.h new file mode 100644 index 0000000..58fe9a9 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Keyboard.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#ifdef __APPLE__ +#import +#endif + +#ifdef __linux__ +#include +#include +#include +#endif + +#include +#include +#include +#include + +namespace Robot { + +#ifdef __APPLE__ +typedef CGKeyCode KeyCode; +#endif + +#ifdef _WIN32 +typedef WORD KeyCode; +#endif + +#ifdef __linux__ + typedef KeySym KeyCode; +#endif + +class Keyboard { + public: + enum SpecialKey { + BACKSPACE, + ENTER, + TAB, + ESCAPE, + UP, + DOWN, + RIGHT, + LEFT, + META, + ALT, + CONTROL, + SHIFT, + CAPSLOCK, + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12 + }; + + static const char INVALID_ASCII; + + Keyboard() = delete; + virtual ~Keyboard() = default; + + static void Type(const std::string& query); + + static void TypeHumanLike(const std::string& query); + + static void Click(char asciiChar); + static void Click(SpecialKey specialKey); + + static void HoldStart(char asciiChar); + static void HoldStart(SpecialKey specialKey); + static void HoldStop(char asciiChar); + static void HoldStop(SpecialKey specialKey); + + static void Press(char asciiChar); + static void Press(SpecialKey specialKey); + + static void Release(char asciiChar); + static void Release(SpecialKey specialKey); + + static char VirtualKeyToAscii(KeyCode virtualKey); + static SpecialKey VirtualKeyToSpecialKey(KeyCode virtualKey); + + private: + static std::thread keyPressThread; + static std::atomic continueHolding; + static std::set heldAsciiChars; + static std::set heldSpecialKeys; + + static void KeyHoldThread(); + + static int delay; + + static KeyCode AsciiToVirtualKey(char asciiChar); + + static KeyCode SpecialKeyToVirtualKey(SpecialKey specialKey); + + static std::map specialKeyToVirtualKeyMap; + // Platform-specific implementations +#ifdef __linux__ + static Display* display; + static Window rootWindow; + static void InitializeX11(); + static void CleanupX11(); +#endif + // note: windows alternative doesn't use map +#ifdef __APPLE__ + static std::map asciiToVirtualKeyMap; +#endif +}; + +} // namespace Robot diff --git a/3rdparty/autogui-cpp/src/Mouse.cpp b/3rdparty/autogui-cpp/src/Mouse.cpp new file mode 100644 index 0000000..7102582 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Mouse.cpp @@ -0,0 +1,375 @@ +#include "./Mouse.h" +#include "./Utils.h" + +#ifdef _WIN32 +#include +#elif __APPLE__ +#include +#elif __linux__ +#include +#include +#include +#include +#include +#endif + +namespace Robot { + +unsigned int Mouse::delay = 16; +bool Mouse::isPressed = false; +MouseButton Mouse::pressedButton = MouseButton::LEFT_BUTTON; + +#ifdef __linux__ + Display* Mouse::display = nullptr; + Window Mouse::rootWindow = 0; + + void Mouse::InitializeX11() { + if (display == nullptr) { + display = XOpenDisplay(nullptr); + if (display == nullptr) { + throw std::runtime_error("Cannot open X11 display"); + } + rootWindow = DefaultRootWindow(display); + } + } + + void Mouse::CleanupX11() { + if (display != nullptr) { + XCloseDisplay(display); + display = nullptr; + } + } +#endif + +#ifdef _WIN32 +POINT Mouse::getCurrentPosition() { + POINT winPoint; + GetCursorPos(&winPoint); + return winPoint; +} +#elif __APPLE__ +CGPoint Mouse::getCurrentPosition() { + CGEventRef event = CGEventCreate(nullptr); + CGPoint cursor = CGEventGetLocation(event); + CFRelease(event); + return cursor; +} +#elif __linux__ + Robot::Point Mouse::getCurrentPosition() { + InitializeX11(); + + Robot::Point point; + Window root_return, child_return; + int root_x, root_y; + int win_x, win_y; + unsigned int mask_return; + + XQueryPointer(display, rootWindow, &root_return, &child_return, + &root_x, &root_y, &win_x, &win_y, &mask_return); + + point.x = root_x; + point.y = root_y; + + return point; + } +#endif + +void Mouse::Move(Robot::Point point) { +#ifdef _WIN32 + SetCursorPos(point.x, point.y); +#elif __APPLE__ + CGPoint target = CGPointMake(point.x, point.y); + CGEventRef event = CGEventCreateMouseEvent( + nullptr, + kCGEventMouseMoved, + target, + kCGMouseButtonLeft + ); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + + if (Mouse::isPressed) { + Mouse::MoveWithButtonPressed(point, Mouse::pressedButton); + } +#elif __linux__ + InitializeX11(); + + // Move the mouse using XTest + XTestFakeMotionEvent(display, -1, point.x, point.y, CurrentTime); + XFlush(display); + + if (Mouse::isPressed) { + Mouse::MoveWithButtonPressed(point, Mouse::pressedButton); + } +#endif +} + +Robot::Point Mouse::GetPosition() { + // TODO: how long exactly should we wait? + Robot::delay(16); + + Robot::Point point; +#ifdef _WIN32 + POINT cursor = getCurrentPosition(); +#elif __APPLE__ + CGPoint cursor = getCurrentPosition(); +#elif __linux__ + Robot::Point cursor = getCurrentPosition(); + point.x = cursor.x; + point.y = cursor.y; +#endif + point.x = cursor.x; + point.y = cursor.y; + + return point; +} + +void Mouse::ToggleButton(bool down, MouseButton button, bool doubleClick) { +#ifdef _WIN32 + INPUT input = {0}; + input.type = INPUT_MOUSE; + input.mi.dwFlags = + (button == MouseButton::LEFT_BUTTON + ? (down ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP) + : button == MouseButton::RIGHT_BUTTON + ? (down ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP) + : (down ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP)); + SendInput(1, &input, sizeof(INPUT)); +#elif __APPLE__ + CGPoint currentPosition = getCurrentPosition(); + + CGEventType buttonType; + switch (button) { + case MouseButton::LEFT_BUTTON: + buttonType = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp; + break; + case MouseButton::RIGHT_BUTTON: + buttonType = down ? kCGEventRightMouseDown : kCGEventRightMouseUp; + break; + case MouseButton::CENTER_BUTTON: + buttonType = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp; + break; + } + + CGEventRef buttonEvent = CGEventCreateMouseEvent( + nullptr, + buttonType, + currentPosition, + (button == MouseButton::CENTER_BUTTON) ? kCGMouseButtonCenter + : kCGMouseButtonLeft + ); + + if (doubleClick) { + CGEventSetIntegerValueField(buttonEvent, kCGMouseEventClickState, 2); + } + + CGEventPost(kCGHIDEventTap, buttonEvent); + CFRelease(buttonEvent); +#elif __linux__ + InitializeX11(); + + unsigned int buttonCode; + switch (button) { + case MouseButton::LEFT_BUTTON: + buttonCode = 1; + break; + case MouseButton::RIGHT_BUTTON: + buttonCode = 3; + break; + case MouseButton::CENTER_BUTTON: + buttonCode = 2; + break; + default: + buttonCode = 1; + } + + if (down) { + XTestFakeButtonEvent(display, buttonCode, True, CurrentTime); + } else { + XTestFakeButtonEvent(display, buttonCode, False, CurrentTime); + } + + XFlush(display); + + // Handle double click + if (doubleClick && !down) { + Robot::delay(10); + XTestFakeButtonEvent(display, buttonCode, True, CurrentTime); + XTestFakeButtonEvent(display, buttonCode, False, CurrentTime); + XFlush(display); + } +#endif + + if (down) { + Mouse::isPressed = true; + Mouse::pressedButton = button; + } else { + Mouse::isPressed = false; + } +} + +void Mouse::MoveWithButtonPressed(Robot::Point point, MouseButton button) { +#ifdef _WIN32 + // On Windows, just calling Move is enough as it will keep the button state. + Mouse::Move(point); +#elif __APPLE__ + CGPoint target = CGPointMake(point.x, point.y); + + CGEventType dragEventType; + CGMouseButton cgButton; + switch (button) { + case MouseButton::LEFT_BUTTON: + dragEventType = kCGEventLeftMouseDragged; + cgButton = kCGMouseButtonLeft; + break; + case MouseButton::RIGHT_BUTTON: + dragEventType = kCGEventRightMouseDragged; + cgButton = kCGMouseButtonRight; + break; + case MouseButton::CENTER_BUTTON: + dragEventType = kCGEventOtherMouseDragged; + cgButton = kCGMouseButtonCenter; + break; + } + + CGEventRef mouseDragEvent = + CGEventCreateMouseEvent(nullptr, dragEventType, target, cgButton); + CGEventPost(kCGHIDEventTap, mouseDragEvent); + CFRelease(mouseDragEvent); +#elif __linux__ + InitializeX11(); + + // For dragging, we simulate mouse motion with button pressed + unsigned int buttonState = 0; + switch (button) { + case MouseButton::LEFT_BUTTON: + buttonState = Button1Mask; + break; + case MouseButton::RIGHT_BUTTON: + buttonState = Button3Mask; + break; + case MouseButton::CENTER_BUTTON: + buttonState = Button2Mask; + break; + } + + // Create a motion event with the appropriate button state + XTestFakeMotionEvent(display, -1, point.x, point.y, CurrentTime); + + // Ensure the button is held down + unsigned int buttonCode; + switch (button) { + case MouseButton::LEFT_BUTTON: + buttonCode = 1; + break; + case MouseButton::RIGHT_BUTTON: + buttonCode = 3; + break; + case MouseButton::CENTER_BUTTON: + buttonCode = 2; + break; + } + + XFlush(display); +#endif +} + +void Mouse::Click(MouseButton button) { + ToggleButton(true, button); + Robot::delay(10); // add a little delay + ToggleButton(false, button); +} + +void Mouse::DoubleClick(MouseButton button) { + Click(button); + Robot::delay(80); + Click(button); +} + +void Mouse::ScrollBy(int y, int x) { +#ifdef _WIN32 + INPUT input = {0}; + input.type = INPUT_MOUSE; + + input.mi.dwFlags = MOUSEEVENTF_WHEEL; + input.mi.mouseData = static_cast(WHEEL_DELTA * y); + SendInput(1, &input, sizeof(INPUT)); + + input.mi.dwFlags = MOUSEEVENTF_HWHEEL; + input.mi.mouseData = static_cast(WHEEL_DELTA * x); + SendInput(1, &input, sizeof(INPUT)); +#elif __APPLE__ + CGEventRef scrollEvent = + CGEventCreateScrollWheelEvent(nullptr, kCGScrollEventUnitPixel, 2, y, x); + CGEventPost(kCGHIDEventTap, scrollEvent); + CFRelease(scrollEvent); +#elif __linux__ + InitializeX11(); + + // Vertical scrolling + if (y != 0) { + unsigned int buttonCode = (y > 0) ? 4 : 5; // 4=up, 5=down + for (int i = 0; i < std::abs(y); i++) { + XTestFakeButtonEvent(display, buttonCode, True, CurrentTime); + XTestFakeButtonEvent(display, buttonCode, False, CurrentTime); + XFlush(display); + Robot::delay(10); + } + } + + // Horizontal scrolling + if (x != 0) { + unsigned int buttonCode = (x > 0) ? 7 : 6; // 6=left, 7=right + for (int i = 0; i < std::abs(x); i++) { + XTestFakeButtonEvent(display, buttonCode, True, CurrentTime); + XTestFakeButtonEvent(display, buttonCode, False, CurrentTime); + XFlush(display); + Robot::delay(10); + } + } +#endif +} + +void Mouse::Drag(Robot::Point toPoint) { + Robot::Mouse::ToggleButton(true, Robot::MouseButton::LEFT_BUTTON); + Robot::delay(10); + Mouse::Move(toPoint); + Robot::delay(10); + Mouse::ToggleButton(false, MouseButton::LEFT_BUTTON); +} + +void Mouse::MoveSmooth(Robot::Point point) { + Robot::Point currentPosition = GetPosition(); + + int dx = point.x - currentPosition.x; + int dy = point.y - currentPosition.y; + + int steps = (std::abs(dx) > std::abs(dy)) ? std::abs(dx) : std::abs(dy); + + float deltaX = static_cast(dx) / steps; + float deltaY = static_cast(dy) / steps; + + for (int i = 1; i <= steps; i++) { + Robot::Point stepPosition; + stepPosition.x = currentPosition.x + static_cast(deltaX * i); + stepPosition.y = currentPosition.y + static_cast(deltaY * i); + + if (Mouse::isPressed) { + MoveWithButtonPressed(stepPosition, Mouse::pressedButton); + } else { + Move(stepPosition); + } + + Robot::delay(1); + } +} + +void Mouse::DragSmooth(Robot::Point toPoint) { + Robot::Mouse::ToggleButton(true, Robot::MouseButton::LEFT_BUTTON); + Robot::delay(10); + Mouse::MoveSmooth(toPoint); + Robot::delay(10); + Mouse::ToggleButton(false, MouseButton::LEFT_BUTTON); +} + +} // namespace Robot diff --git a/3rdparty/autogui-cpp/src/Mouse.h b/3rdparty/autogui-cpp/src/Mouse.h new file mode 100644 index 0000000..e2a0104 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Mouse.h @@ -0,0 +1,72 @@ +#pragma once + +#include "./types.h" + +#include +#include + +#ifdef _WIN32 +#include +#elif __APPLE__ +#include +#elif __linux__ +#include +#include +#endif +namespace Robot { + +enum class MouseButton : uint8_t { + LEFT_BUTTON = 0, + RIGHT_BUTTON = 1, + CENTER_BUTTON = 2 +}; + +class Mouse { + public: + static unsigned int delay; + + static bool isPressed; + static MouseButton pressedButton; + + Mouse() = delete; + + static void Move(Robot::Point point); + + static void MoveSmooth(Robot::Point point); + + static void Drag(Robot::Point toPoint); + + static void DragSmooth(Robot::Point toPoint); + + static Robot::Point GetPosition(); + + static void ToggleButton( + bool down, MouseButton button, bool doubleClick = false + ); + + static void Click(MouseButton button); + + static void DoubleClick(MouseButton button); + + static void ScrollBy(int y, int x = 0); + + private: + static void MoveWithButtonPressed(Robot::Point point, MouseButton button); + // Platform-specific implementations +#ifdef __linux__ + static Display* display; + static Window rootWindow; + static void InitializeX11(); + static void CleanupX11(); +#endif +#ifdef _WIN32 + static POINT getCurrentPosition(); +#elif __APPLE__ + static CGPoint getCurrentPosition(); +#elif __linux__ + static Robot::Point getCurrentPosition(); +#endif + +}; + +} // namespace Robot diff --git a/3rdparty/autogui-cpp/src/SimpleAutoGUI.h b/3rdparty/autogui-cpp/src/SimpleAutoGUI.h new file mode 100644 index 0000000..b8d4f8a --- /dev/null +++ b/3rdparty/autogui-cpp/src/SimpleAutoGUI.h @@ -0,0 +1,47 @@ +// +// Created by misaki on 2026/1/25. +// +#pragma once + +// 包含所有必要头文件 +#include "Autogui.h" + +// 常用常量 +namespace AutoGUI { + // 鼠标按钮常量 + const Button LEFT = Button::LEFT; + const Button RIGHT = Button::RIGHT; + const Button MIDDLE = Button::MIDDLE; + + // 常用键常量(字符串形式,用于press、keyDown等函数) + namespace Keys { + const std::string BACKSPACE = "backspace"; + const std::string ENTER = "enter"; + const std::string TAB = "tab"; + const std::string ESCAPE = "escape"; + const std::string ESC = "esc"; + const std::string UP = "up"; + const std::string DOWN = "down"; + const std::string RIGHT = "right"; + const std::string LEFT = "left"; + const std::string WIN = "win"; + const std::string COMMAND = "command"; + const std::string ALT = "alt"; + const std::string CTRL = "ctrl"; + const std::string SHIFT = "shift"; + const std::string CAPSLOCK = "capslock"; + const std::string F1 = "f1"; + const std::string F2 = "f2"; + const std::string F3 = "f3"; + const std::string F4 = "f4"; + const std::string F5 = "f5"; + const std::string F6 = "f6"; + const std::string F7 = "f7"; + const std::string F8 = "f8"; + const std::string F9 = "f9"; + const std::string F10 = "f10"; + const std::string F11 = "f11"; + const std::string F12 = "f12"; + const std::string SPACE = "space"; + } +} \ No newline at end of file diff --git a/3rdparty/autogui-cpp/src/Utils.cpp b/3rdparty/autogui-cpp/src/Utils.cpp new file mode 100644 index 0000000..ddafec7 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Utils.cpp @@ -0,0 +1,9 @@ +#include "./Utils.h" + +namespace Robot { + +void delay(unsigned int ms) { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); +} + +} // namespace Robot diff --git a/3rdparty/autogui-cpp/src/Utils.h b/3rdparty/autogui-cpp/src/Utils.h new file mode 100644 index 0000000..8755541 --- /dev/null +++ b/3rdparty/autogui-cpp/src/Utils.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include + +namespace Robot { + +void delay(unsigned int ms); + +namespace KeyUtils { +// 检查字符是否需要Shift键 +inline bool NeedsShift(char c) { + // 大写字母需要Shift + if (c >= 'A' && c <= 'Z') { + return true; + } + + // 特殊符号需要Shift + const char* shiftChars = "!@#$%^&*()_+{}|:\"<>?~"; + return (strchr(shiftChars, c) != nullptr); +} + +// 获取字符的基础键(物理键位) +inline char GetBaseKey(char c) { + // 对于字母,返回小写形式 + if (c >= 'A' && c <= 'Z') { + return std::tolower(c); + } + + // 对于数字,保持不变 + if (c >= '0' && c <= '9') { + return c; + } + + // 对于需要Shift的符号,返回对应的基础键 + switch (c) { + case '!': return '1'; + case '@': return '2'; + case '#': return '3'; + case '$': return '4'; + case '%': return '5'; + case '^': return '6'; + case '&': return '7'; + case '*': return '8'; + case '(': return '9'; + case ')': return '0'; + case '_': return '-'; + case '+': return '='; + case '{': return '['; + case '}': return ']'; + case '|': return '\\'; + case ':': return ';'; + case '"': return '\''; + case '<': return ','; + case '>': return '.'; + case '?': return '/'; + case '~': return '`'; + default: return c; + } +} + +// 检查字符是否是有效的ASCII字符 +inline bool IsValidAscii(char c) { + return c >= 32 && c <= 126; // 可打印ASCII字符 +} + +} // namespace KeyUtils + +} // namespace Robot diff --git a/3rdparty/autogui-cpp/src/types.h b/3rdparty/autogui-cpp/src/types.h new file mode 100644 index 0000000..e9498d7 --- /dev/null +++ b/3rdparty/autogui-cpp/src/types.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace Robot { + +struct Point { + int x; + int y; + + [[nodiscard]] double Distance(Point target) const { + return sqrt(pow(target.x - x, 2) + pow(target.y - y, 2)); + } +}; + +} // namespace Robot diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e0a237..ec6098c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,9 +8,6 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_PREFIX_PATH "/home/misaki/Qt6.3/6.6.3/gcc_64") # 设置Qt6安装路径(此处请根据你的Qt6安装位置填写) -# 添加ElaWidgetTools UI库项目 -add_subdirectory(3rdparty/ElaWidgetTools) - # 设置是否为debug模式 默认为Debug if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) @@ -40,6 +37,9 @@ file(GLOB_RECURSE YosugaSrc "src/Render/TextRender/Inc/*.h" "src/Core/Src/*.cpp" "src/Core/Inc/*.h" + "src/Utils/Inc/*.hpp" + "src/Utils/Inc/*.h" + "src/Utils/Src/*.cpp" ) # 查找Qt6模块以及其他必须模块 @@ -47,16 +47,18 @@ find_package(Qt6 COMPONENTS Core Gui Widgets - Network - Svg - SerialPort - WebSockets - Multimedia + Network # 网络 + Svg # svg + SerialPort # 串口 + WebSockets # websocket + Multimedia # 音频 OpenGLWidgets Concurrent REQUIRED) find_package(OpenGL REQUIRED) +add_subdirectory(3rdparty/ElaWidgetTools) # 添加ElaWidgetTools UI库 +add_subdirectory(3rdparty/autogui-cpp) # 添加autogui-cpp GUI自动化库 add_executable(${PROJECT_NAME} main.cpp ${LAppLive2D} ${YosugaSrc}) @@ -166,6 +168,7 @@ target_link_libraries(${PROJECT_NAME} target_link_libraries(${PROJECT_NAME} PRIVATE ElaWidgetTools + autogui-cpp Qt::Core Qt::Gui Qt::Widgets @@ -196,6 +199,7 @@ target_include_directories(${PROJECT_NAME} src/Render/TextRender/Inc src/Core/Inc src/DAO/Inc + src/Utils/Inc ) @@ -262,7 +266,7 @@ if(PLAT STREQUAL "windows") set(WINDEPLOYQT_CMD "${WINDEPLOYQT_CMD} \"$\"") # 添加自定义命令运行windeployqt - # 最简单的版本 - 只传递目录 + # 只传递目录 add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND "${WINDEPLOYQT_EXE}" "$" COMMENT "Running windeployqt to deploy Qt dependencies..." diff --git a/README.md b/README.md index f1b29a1..f55ddf8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ _**本项目为Yosuga.**_ 本项目使用CMake构建,基于C++Qt6.6.3以及Live2D官方SDK(CubismSdkForNative-5-r.4.1)实现Live2D桌面宠物 -(本项目由Yosuga[Qt5] 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台) +(本项目由[Yosuga-qt5](https://github.com/Misakityan/Yosuga-qt5) 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台) 环境为: diff --git a/src/Core/Inc/AppCore.h b/src/Core/Inc/AppCore.h new file mode 100644 index 0000000..d86295c --- /dev/null +++ b/src/Core/Inc/AppCore.h @@ -0,0 +1,11 @@ +// +// Created by misaki on 2026/1/24. +// + +#pragma once + +/** + * 客户端业务核心 + * 1. 处理来自服务端的数据,分发并执行 + * 2. 完成非阻塞的事件循环处理,构建业务状态机 + */ \ No newline at end of file diff --git a/src/Core/Src/AppCore.cpp b/src/Core/Src/AppCore.cpp new file mode 100644 index 0000000..e0e52e7 --- /dev/null +++ b/src/Core/Src/AppCore.cpp @@ -0,0 +1,5 @@ +// +// Created by misaki on 2026/1/24. +// + +#include "AppCore.h" \ No newline at end of file diff --git a/src/Core/Src/GLCore.cpp b/src/Core/Src/GLCore.cpp index 46b4c4b..34c47e2 100644 --- a/src/Core/Src/GLCore.cpp +++ b/src/Core/Src/GLCore.cpp @@ -81,7 +81,7 @@ GLCore::GLCore(const int width, const int height, QWidget *parent) this->setMouseTracking(true); // 连接一些必要的信号与槽 - connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL); + connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL); // 关闭窗口信号 // 注册当前实例到中介类 AppContext::RegisterGLCore(this); diff --git a/src/DAO/Src/NetWorkDO.cpp b/src/DAO/Src/NetWorkDO.cpp index 0136469..1df373a 100644 --- a/src/DAO/Src/NetWorkDO.cpp +++ b/src/DAO/Src/NetWorkDO.cpp @@ -44,7 +44,7 @@ NetworkDO::~NetworkDO() // 业务逻辑实现 void NetworkDO::registerSender(SenderFunc sender) { - QMutexLocker locker(&m_mutex); // 简单保护一下赋值 + QMutexLocker locker(&m_mutex); // 加个小锁,简单保护一下赋值 m_sender = std::move(sender); } diff --git a/src/NetWorkHandle/Inc/serialportmanager.h b/src/NetWorkHandle/Inc/serialportmanager.h new file mode 100644 index 0000000..e8522cf --- /dev/null +++ b/src/NetWorkHandle/Inc/serialportmanager.h @@ -0,0 +1,189 @@ +// +// Created by misaki on 2026/1/26. +// + +/** + * 串口通信模块 —— 支持多设备并行 + JSON 协议 + * 每个 SerialPortClient 实例对应一个物理串口 + */ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * @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; /// 配置缓存 +}; \ No newline at end of file diff --git a/src/NetWorkHandle/Src/serialportmanager.cpp b/src/NetWorkHandle/Src/serialportmanager.cpp new file mode 100644 index 0000000..0344013 --- /dev/null +++ b/src/NetWorkHandle/Src/serialportmanager.cpp @@ -0,0 +1,583 @@ +// +// Created by misaki on 2026/1/26. +// + +#include "serialportmanager.h" +#include +#include +#include +#include +#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::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 encoded; + auto result = cobs::encode(encoded, std::span( + reinterpret_cast(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(result.status))); + return; + } + + // 发送编码数据 + 0x00 + const qint64 written = m_serial->write(reinterpret_cast(encoded.data()), static_cast(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 decoded; + auto result = cobs::decode(decoded, std::span( + reinterpret_cast(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(result.status))); + continue; + } + // 解析 JSON + QByteArray jsonData(reinterpret_cast(decoded.data()), static_cast(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(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; +} \ No newline at end of file diff --git a/src/Setting/Inc/Setting.h b/src/Setting/Inc/Setting.h index bf53d7d..03f927a 100644 --- a/src/Setting/Inc/Setting.h +++ b/src/Setting/Inc/Setting.h @@ -2,8 +2,7 @@ // Created by Administrator on 2025/1/21. // -#ifndef AIRI_DESKTOPGRIL_SETTING_H -#define AIRI_DESKTOPGRIL_SETTING_H +#pragma once #include #include @@ -64,5 +63,3 @@ private: }; - -#endif //AIRI_DESKTOPGRIL_SETTING_H diff --git a/src/Setting/Inc/UISetting.h b/src/Setting/Inc/UISetting.h index a5691bb..808bf73 100644 --- a/src/Setting/Inc/UISetting.h +++ b/src/Setting/Inc/UISetting.h @@ -2,8 +2,7 @@ // Created by Administrator on 2025/3/2. // -#ifndef AIRI_DESKTOPGRIL_UISETTING_H -#define AIRI_DESKTOPGRIL_UISETTING_H +#pragma once #include "BasePage.h" class ElaRadioButton; @@ -25,5 +24,3 @@ private: ElaRadioButton* _maximumButton = nullptr; ElaRadioButton* _autoButton = nullptr; }; - -#endif //AIRI_DESKTOPGRIL_UISETTING_H diff --git a/src/Utils/Inc/cobs.hpp b/src/Utils/Inc/cobs.hpp new file mode 100644 index 0000000..5a7cbf1 --- /dev/null +++ b/src/Utils/Inc/cobs.hpp @@ -0,0 +1,227 @@ +// +// Created by misaki on 2026/1/26. +// + +/** + * cobs.hpp + * 所谓COBS,即Consistent Overhead Byte Stuffing(持续开销字节填充) + * 是一种将字节包编码成不包含值为零的字节(0x00)形式的方法。 + * 输入的字节包可以包含从 0x00 到 0xFF 的全部范围内的字节。 + * COBS 编码的数据包保证生成字节范围 0x01 到 0xFF 的数据包。 + * 因此,在通信协议中,数据包边界可以用 0x00 字节可靠地界定。 + * + * 在Yosuga项目当中,COBS编码被用于解决Yosuga与嵌入式设备使用串口收发数据时出现的粘包问题。 + * 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。 + * + * 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。 + */ +#pragma once +#include +#include +#include +#include +#include + +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 dst, const std::span 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(dst_write_ptr - dst_start_ptr); + return result; +} + +inline DecodeResult decode_core(std::span dst, const std::span 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(src_end_ptr - src_read_ptr); + if (len_code > remaining) { + result.status = Status::INPUT_TOO_SHORT; + len_code = static_cast(remaining); + } + + remaining = static_cast(dst_end_ptr - dst_write_ptr); + if (len_code > remaining) { + result.status = Status::OUT_BUFFER_OVERFLOW; + len_code = static_cast(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(dst_write_ptr - dst_start_ptr); + return result; +} + +// 便捷接口(std::vector) +inline EncodeResult encode(std::vector& dst, const std::span 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& dst, const std::span 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(reinterpret_cast(dst.data()), dst.size()), + std::span(reinterpret_cast(src.data()), src.size()) + ); + dst.resize(result.out_len); + return result; +} + +inline DecodeResult decode(std::string& dst, const std::span src) noexcept { + std::vector temp; + const auto result = decode(temp, src); + if (result.status == Status::OK) { + dst.assign(reinterpret_cast(temp.data()), temp.size()); + } + return {result.out_len, result.status}; +} + +// 类型安全辅助函数 +template +requires std::is_trivially_copyable_v +inline EncodeResult encode(std::vector& dst, const T& obj) noexcept { + return encode(dst, std::span( + reinterpret_cast(&obj), sizeof(T) + )); +} + +template +requires std::is_trivially_copyable_v +inline DecodeResult decode(T& obj, const std::span src) noexcept { + return decode_core(std::span( + reinterpret_cast(&obj), sizeof(T) + ), src); +} + +} // namespace cobs \ No newline at end of file