From 89b23b229114f32ded231146a41e96f07e0a8b6a Mon Sep 17 00:00:00 2001 From: stli Date: Fri, 17 Apr 2026 16:25:19 +0800 Subject: [PATCH] Push layered Qt host source files --- 20260320_new_keyboard.sln | 14 +- 20260320_new_keyboard.slnx | 3 + 20260320_new_keyboard.vcxproj | 106 +- 20260320_new_keyboard.vcxproj.filters | 112 +- APP/APP_GlassCard.cpp | 10 - APP/APP_GlassCard.h | 11 - APP/APP_KeyButton.cpp | 37 +- APP/APP_KeyButton.h | 22 +- APP/APP_KeypadModel.cpp | 105 +- APP/APP_KeypadModel.h | 11 +- APP/APP_Theme.cpp | 154 ++- APP/APP_Theme.h | 17 +- APP/APP_UIWindow.cpp | 1202 ++++++++++++--------- APP/APP_UIWindow.h | 149 ++- APP/APP_UIWindow_Feature.cpp | 291 +++++ APP/APP_UIWindow_Private.h | 12 + APP/APP_UIWindow_Record.cpp | 427 ++++++++ COM/Com_Def.h | 35 + COM/Com_Protocol.cpp | 975 +++++++++++++++++ COM/Com_Protocol.h | 117 +++ DRI/Dri_Ble.cpp | 817 +++++++++++++++ DRI/Dri_Ble.h | 32 + DRI/Dri_Cdc.cpp | 623 +++++++++++ DRI/Dri_Cdc.h | 40 + DRI/Dri_Consumer.cpp | 164 +-- DRI/Dri_Consumer.h | 35 +- DRI/Dri_Hid.cpp | 189 ++++ DRI/Dri_Hid.h | 41 + DRI/Dri_NkroRaw.cpp | 33 +- DRI/Dri_NkroRaw.h | 23 +- DRI/Dri_Nus.cpp | 920 ++++++++++++++++ DRI/Dri_Nus.h | 42 + DRI/Dri_Vendor.cpp | 408 +++----- DRI/Dri_Vendor.h | 43 +- LOGIC/Lgc_Consumer.cpp | 41 - LOGIC/Lgc_Consumer.h | 31 - LOGIC/Lgc_Core.cpp | 1398 ++++++++++++++++--------- LOGIC/Lgc_Core.h | 248 ++++- LOGIC/Lgc_Core_Command.cpp | 813 ++++++++++++++ LOGIC/Lgc_Core_Config.cpp | 366 +++++++ LOGIC/Lgc_Core_Control.cpp | 320 ++++++ LOGIC/Lgc_Core_Input.cpp | 702 +++++++++++++ LOGIC/Lgc_Core_Private.h | 54 + LOGIC/Lgc_Func_Button.cpp | 374 ++++--- LOGIC/Lgc_Func_Button.h | 68 +- LOGIC/Lgc_Func_Button_Parse.cpp | 278 +++++ LOGIC/Lgc_Func_Button_Private.h | 61 ++ LOGIC/Lgc_Func_Button_Run.cpp | 258 +++++ LOGIC/Lgc_Nkro.cpp | 70 -- LOGIC/Lgc_Nkro.h | 40 - LOGIC/Lgc_Vendor.cpp | 71 -- LOGIC/Lgc_Vendor.h | 31 - MID/Mid_Ble.cpp | 49 + MID/Mid_Ble.h | 9 + MID/Mid_Def.cpp | 168 +-- MID/Mid_Def.h | 67 +- function_config.json | 27 + main.cpp | 46 +- 58 files changed, 10349 insertions(+), 2461 deletions(-) create mode 100644 APP/APP_UIWindow_Feature.cpp create mode 100644 APP/APP_UIWindow_Private.h create mode 100644 APP/APP_UIWindow_Record.cpp create mode 100644 COM/Com_Def.h create mode 100644 COM/Com_Protocol.cpp create mode 100644 COM/Com_Protocol.h create mode 100644 DRI/Dri_Ble.cpp create mode 100644 DRI/Dri_Ble.h create mode 100644 DRI/Dri_Cdc.cpp create mode 100644 DRI/Dri_Cdc.h create mode 100644 DRI/Dri_Hid.cpp create mode 100644 DRI/Dri_Hid.h create mode 100644 DRI/Dri_Nus.cpp create mode 100644 DRI/Dri_Nus.h delete mode 100644 LOGIC/Lgc_Consumer.cpp delete mode 100644 LOGIC/Lgc_Consumer.h create mode 100644 LOGIC/Lgc_Core_Command.cpp create mode 100644 LOGIC/Lgc_Core_Config.cpp create mode 100644 LOGIC/Lgc_Core_Control.cpp create mode 100644 LOGIC/Lgc_Core_Input.cpp create mode 100644 LOGIC/Lgc_Core_Private.h create mode 100644 LOGIC/Lgc_Func_Button_Parse.cpp create mode 100644 LOGIC/Lgc_Func_Button_Private.h create mode 100644 LOGIC/Lgc_Func_Button_Run.cpp delete mode 100644 LOGIC/Lgc_Nkro.cpp delete mode 100644 LOGIC/Lgc_Nkro.h delete mode 100644 LOGIC/Lgc_Vendor.cpp delete mode 100644 LOGIC/Lgc_Vendor.h create mode 100644 MID/Mid_Ble.cpp create mode 100644 MID/Mid_Ble.h create mode 100644 function_config.json diff --git a/20260320_new_keyboard.sln b/20260320_new_keyboard.sln index d11eed0..f1bfd50 100644 --- a/20260320_new_keyboard.sln +++ b/20260320_new_keyboard.sln @@ -1,8 +1,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31912.275 +# Visual Studio Version 18 +VisualStudioVersion = 18.4.11626.88 stable MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "20260320_new_keyboard", "20260320_new_keyboard.vcxproj", "{33F77093-3FF4-4E32-B971-301ABF0133C5}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "20260320_new_keyboard", "20260320_new_keyboard.vcxproj", "{24E649F5-C374-ADB3-DE49-835B2FCDE3DE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -10,10 +10,10 @@ Global Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {33F77093-3FF4-4E32-B971-301ABF0133C5}.Debug|x64.ActiveCfg = Debug|x64 - {33F77093-3FF4-4E32-B971-301ABF0133C5}.Debug|x64.Build.0 = Debug|x64 - {33F77093-3FF4-4E32-B971-301ABF0133C5}.Release|x64.ActiveCfg = Release|x64 - {33F77093-3FF4-4E32-B971-301ABF0133C5}.Release|x64.Build.0 = Release|x64 + {24E649F5-C374-ADB3-DE49-835B2FCDE3DE}.Debug|x64.ActiveCfg = Debug|x64 + {24E649F5-C374-ADB3-DE49-835B2FCDE3DE}.Debug|x64.Build.0 = Debug|x64 + {24E649F5-C374-ADB3-DE49-835B2FCDE3DE}.Release|x64.ActiveCfg = Release|x64 + {24E649F5-C374-ADB3-DE49-835B2FCDE3DE}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/20260320_new_keyboard.slnx b/20260320_new_keyboard.slnx index 38837f3..2db0c9c 100644 --- a/20260320_new_keyboard.slnx +++ b/20260320_new_keyboard.slnx @@ -1,5 +1,8 @@ + + + diff --git a/20260320_new_keyboard.vcxproj b/20260320_new_keyboard.vcxproj index 8b22d51..bb0621e 100644 --- a/20260320_new_keyboard.vcxproj +++ b/20260320_new_keyboard.vcxproj @@ -1,4 +1,4 @@ - + @@ -15,19 +15,21 @@ QtVS_v304 10.0 10.0 - $(LocalAppData)\QtMsBuild - $(MSBuildProjectDirectory)\QtMsBuild + $(LocalAppData)\QtMsBuild + $(MSBuildProjectDirectory)\QtMsBuild Application - v143 + v145 true Unicode Application - v143 + v145 false true Unicode @@ -36,18 +38,32 @@ + - D:\Qt\5.13.1\msvc2015_64 - core;gui;widgets + $([System.IO.Path]::GetDirectoryName('$(QTDIR)')) + $(QTDIR) + $([System.IO.Path]::GetDirectoryName('$(QT)')) + $(QT) + D:\App\Qt\5.13.1\msvc2015_64 + 5.13.1_msvc2015_64 + core;gui;widgets;serialport;bluetooth debug - D:\Qt\5.13.1\msvc2015_64 - core;gui;widgets + $([System.IO.Path]::GetDirectoryName('$(QTDIR)')) + $(QTDIR) + $([System.IO.Path]::GetDirectoryName('$(QT)')) + $(QT) + D:\App\Qt\5.13.1\msvc2015_64 + 5.13.1_msvc2015_64 + core;gui;widgets;serialport;bluetooth release - - + + @@ -97,42 +113,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/20260320_new_keyboard.vcxproj.filters b/20260320_new_keyboard.vcxproj.filters index d685d7e..3c56e70 100644 --- a/20260320_new_keyboard.vcxproj.filters +++ b/20260320_new_keyboard.vcxproj.filters @@ -6,46 +6,32 @@ qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - {93995380-89BD-4b04-88EB-625FBE52EBFB} + {93995380-89BD-4B04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;xsd - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - {99349809-55BA-4b9d-BF79-8FDBB0286EB3} - ui - - + {A64D7E2E-38B0-4FC7-A5E5-86BE065B5E14} - - {A8E15F2D-A7FA-4303-B845-0E6A75B5906B} + + {D7D98364-D54B-4AA0-8C30-2F059606DE39} - + {80F53A7E-0DA9-44B4-85ED-69D404BDE88A} - - {0D0A047D-7113-47DE-9A05-D7BA3B12F9E2} + + {A8E15F2D-A7FA-4303-B845-0E6A75B5906B} - - {3D7D0A5D-F0CB-49A6-A6D0-FA66F58B3111} - - + {2B601A06-899A-4D88-932D-9F6ED625B1E8} - - {72C4499C-5806-4BBC-869D-0E4B33D867B6} + + {D5F327AA-D1B8-4154-BE13-FA8C94BE8425} - + {E72E5301-BE63-408E-BD35-F817FD4A0D0E} - - {7CB18420-5F64-4E2A-81CA-6A5E2BA4CFC8} - - - {7814B0B6-EA6A-49D3-9B6B-BC144B1AF55F} + + {72C4499C-5806-4BBC-869D-0E4B33D867B6} @@ -67,36 +53,42 @@ Source Files\APP - + + Source Files\APP + + + Source Files\APP + + + Source Files\COM + + Source Files\DRI - + Source Files\DRI - - Source Files\DRI + + Source Files\LOGIC - - Source Files\DEBUG + + Source Files\LOGIC - + + Source Files\LOGIC + + Source Files\LOGIC Source Files\LOGIC - + Source Files\LOGIC - + Source Files\LOGIC - - Source Files\LOGIC - - - Source Files\MID - Header Files\APP @@ -112,38 +104,32 @@ Header Files\APP - + + Header Files\APP + + + Header Files\COM + + + Header Files\COM + + Header Files\DRI - + Header Files\DRI - - Header Files\DRI + + Header Files\LOGIC - - Header Files\DEBUG - - - Header Files\DEBUG - - + Header Files\LOGIC Header Files\LOGIC - + Header Files\LOGIC - - Header Files\LOGIC - - - Header Files\LOGIC - - - Header Files\MID - diff --git a/APP/APP_GlassCard.cpp b/APP/APP_GlassCard.cpp index 9bb7615..a29dbc5 100644 --- a/APP/APP_GlassCard.cpp +++ b/APP/APP_GlassCard.cpp @@ -7,9 +7,7 @@ namespace APP { APP_GlassCard::APP_GlassCard(QWidget* parent) : QFrame(parent) { - // 交给我们自己统一绘制卡片外观,不使用 QFrame 默认边框。 setFrameShape(QFrame::NoFrame); - // 背景由 paintEvent 自绘,这里不走样式表背景。 setAttribute(Qt::WA_StyledBackground, false); } @@ -17,11 +15,6 @@ void APP_GlassCard::paintEvent(QPaintEvent* event) { Q_UNUSED(event); - /* - * 卡片外观刻意保持简单,方便教学时理解自绘流程: - * 1. 先画一个带圆角的深色底板 - * 2. 再画一圈细边框 - */ const QRectF BodyRect = rect().adjusted(1.0, 1.0, -1.0, -1.0); const qreal Radius = 22.0; const QColor FillColor(30, 35, 43); @@ -29,12 +22,9 @@ void APP_GlassCard::paintEvent(QPaintEvent* event) QPainter Painter(this); Painter.setRenderHint(QPainter::Antialiasing, true); - - // 先画卡片主体。 Painter.setPen(QPen(BorderColor, 1.0)); Painter.setBrush(FillColor); Painter.drawRoundedRect(BodyRect, Radius, Radius); - } } // namespace APP diff --git a/APP/APP_GlassCard.h b/APP/APP_GlassCard.h index 0aa1540..6e90e79 100644 --- a/APP/APP_GlassCard.h +++ b/APP/APP_GlassCard.h @@ -4,23 +4,12 @@ namespace APP { -/* - * 这是项目里所有“卡片容器”的基础控件。 - * - * 它只负责统一外观,不负责任何业务逻辑: - * 1. 统一圆角卡片风格 - * 2. 统一边框和暗色底板 - * - * 上层像主页卡片、调试卡片都直接继承它。 - */ class APP_GlassCard : public QFrame { public: - // 构造一个带统一外观的卡片容器。 explicit APP_GlassCard(QWidget* parent = nullptr); protected: - // 卡片背景和圆角边框都在这里自绘。 void paintEvent(QPaintEvent* event) override; }; diff --git a/APP/APP_KeyButton.cpp b/APP/APP_KeyButton.cpp index 0d68be2..fa6790e 100644 --- a/APP/APP_KeyButton.cpp +++ b/APP/APP_KeyButton.cpp @@ -4,12 +4,9 @@ #include #include -namespace { +namespace +{ -/* - * 这个小工具函数用来把两种颜色按比例混合。 - * 当前项目里主要用它来根据按键状态生成不同深浅的背景色和边框色。 - */ QColor App_Func_MixColor(const QColor& Left, const QColor& Right, qreal Value) { const qreal Rate = qBound(0.0, Value, 1.0); @@ -29,7 +26,7 @@ APP_KeyButton::APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent) : QPushButton(parent), appKeyInfo(KeyInfo) { - // 这里把按钮本身的交互属性定下来,后面就主要交给 paintEvent 自绘。 + appHintText = appKeyInfo.hint; setCursor(Qt::PointingHandCursor); setFlat(true); setMinimumSize(78, 78); @@ -38,7 +35,6 @@ APP_KeyButton::APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent) void APP_KeyButton::App_Func_SetLatched(bool IsLatched) { - // 状态没变时不重复刷新,避免无意义重绘。 if (appIsLatched == IsLatched) { return; @@ -50,7 +46,6 @@ void APP_KeyButton::App_Func_SetLatched(bool IsLatched) void APP_KeyButton::App_Func_SetPressed(bool IsPressed) { - // 状态没变时同样直接返回。 if (appIsPressed == IsPressed) { return; @@ -60,18 +55,27 @@ void APP_KeyButton::App_Func_SetPressed(bool IsPressed) update(); } +void APP_KeyButton::App_Func_SetHintText(const QString& HintText) +{ + if (appHintText == HintText) + { + return; + } + + appHintText = HintText; + update(); +} + void APP_KeyButton::paintEvent(QPaintEvent* event) { Q_UNUSED(event); - // 留一点内边距,让卡片式按键看起来不那么“顶满”。 const QRectF ButtonRect = rect().adjusted(6.0, 6.0, -6.0, -6.0); const qreal Radius = 14.0; QColor FillColor = App_Func_GetBackgroundColor(); QColor OutlineColor = App_Func_GetBorderColor(); - // 鼠标按下时再做一层轻微压暗,形成“压下去”的感觉。 if (isDown()) { FillColor = FillColor.darker(108); @@ -81,24 +85,20 @@ void APP_KeyButton::paintEvent(QPaintEvent* event) QPainter Painter(this); Painter.setRenderHint(QPainter::Antialiasing, true); Painter.setRenderHint(QPainter::TextAntialiasing, true); - - // 先画背景和边框。 Painter.setPen(QPen(OutlineColor, 1.2)); Painter.setBrush(FillColor); Painter.drawRoundedRect(ButtonRect, Radius, Radius); - // hint 一般显示在左上角,比如 Num、Fn 这类短提示。 - if (!appKeyInfo.hint.isEmpty()) + if (!appHintText.isEmpty()) { Painter.setFont(APP_Theme::App_Func_GetKeyHintFont()); Painter.setPen(App_Func_GetTextColor().lighter(115)); Painter.drawText( ButtonRect.adjusted(10.0, 8.0, -10.0, -8.0), Qt::AlignLeft | Qt::AlignTop, - appKeyInfo.hint.toUpper()); + appHintText.toUpper()); } - // 主文字放中间。文字长度不同就稍微缩一下字号,避免挤出按钮。 QFont LabelFont = APP_Theme::App_Func_GetKeyLabelFont(); if (appKeyInfo.label.size() > 2) { @@ -116,7 +116,6 @@ void APP_KeyButton::paintEvent(QPaintEvent* event) QColor APP_KeyButton::App_Func_GetAccentColor() const { - // 每个 tone 对应一组强调色,用来让不同类别的键略有区分。 switch (appKeyInfo.tone) { case APP_KeyTone::Aqua: @@ -133,16 +132,13 @@ QColor APP_KeyButton::App_Func_GetAccentColor() const QColor APP_KeyButton::App_Func_GetBackgroundColor() const { - // 先以统一暗色为底,再按状态叠加强调色。 const QColor BaseColor(55, 61, 70); - // 按下态最强。 if (appIsPressed) { return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), 0.56); } - // 锁定态次之。 if (appKeyInfo.tone != APP_KeyTone::Normal || appIsLatched) { return App_Func_MixColor(BaseColor, App_Func_GetAccentColor(), appIsLatched ? 0.35 : 0.18); @@ -170,7 +166,6 @@ QColor APP_KeyButton::App_Func_GetBorderColor() const QColor APP_KeyButton::App_Func_GetTextColor() const { - // 当前项目固定用高亮浅色字,保证暗底上阅读清晰。 return QColor(238, 242, 247); } diff --git a/APP/APP_KeyButton.h b/APP/APP_KeyButton.h index 3ecfd17..a5303af 100644 --- a/APP/APP_KeyButton.h +++ b/APP/APP_KeyButton.h @@ -6,45 +6,27 @@ namespace APP { -/* - * 这是小键盘界面里的单个按键控件。 - * - * 它的职责很单纯: - * 1. 保存这颗键自己的显示信息 - * 2. 根据“锁定态 / 按下态”切换颜色 - * 3. 自己完成绘制 - * - * 它不负责协议解析,也不直接参与 DRI / LGC 逻辑。 - */ class APP_KeyButton : public QPushButton { public: explicit APP_KeyButton(const APP_KeyInfo& KeyInfo, QWidget* parent = nullptr); - // 锁定态通常表示这颗键处于某种功能模式。 void App_Func_SetLatched(bool IsLatched); - - // 按下态表示实体键或逻辑键当前真的处于按下中。 void App_Func_SetPressed(bool IsPressed); + void App_Func_SetHintText(const QString& HintText); protected: - // 每颗键都自己画背景、边框、提示文字和主文字。 void paintEvent(QPaintEvent* event) override; private: - // 下面这些函数只负责给 paintEvent 提供颜色。 QColor App_Func_GetAccentColor() const; QColor App_Func_GetBackgroundColor() const; QColor App_Func_GetBorderColor() const; QColor App_Func_GetTextColor() const; - // 这颗键对应的显示信息。 APP_KeyInfo appKeyInfo; - - // 是否处于锁定态。 + QString appHintText; bool appIsLatched = false; - - // 是否处于按下态。 bool appIsPressed = false; }; diff --git a/APP/APP_KeypadModel.cpp b/APP/APP_KeypadModel.cpp index 03ee4bf..2f8ea02 100644 --- a/APP/APP_KeypadModel.cpp +++ b/APP/APP_KeypadModel.cpp @@ -5,39 +5,24 @@ namespace APP { APP_KeypadModel::APP_KeypadModel() { appKeyList = { - {QStringLiteral("num"), QStringLiteral("Num"), QStringLiteral(""), 0x0053, 0, 0, 1, 1, APP_KeyTone::Aqua}, - {QStringLiteral("divide"), QStringLiteral("/"), QStringLiteral(""), 0x0054, 0, 1, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("multiply"), QStringLiteral("*"), QStringLiteral(""), 0x0055, 0, 2, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("minus"), QStringLiteral("-"), QStringLiteral(""), 0x0056, 0, 3, 1, 1, APP_KeyTone::Amber}, + {QStringLiteral("num"), QStringLiteral("Num"), QString(), 0x0053, 0, 0, 1, 1, APP_KeyTone::Aqua}, + {QStringLiteral("divide"), QStringLiteral("/"), QString(), 0x0054, 0, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("multiply"), QStringLiteral("*"), QString(), 0x0055, 0, 2, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("minus"), QStringLiteral("-"), QString(), 0x0056, 0, 3, 1, 1, APP_KeyTone::Amber}, {QStringLiteral("7"), QStringLiteral("7"), QStringLiteral("Home"), 0x005F, 1, 0, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("8"), QStringLiteral("8"), QStringLiteral("↑"), 0x0060, 1, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("8"), QStringLiteral("8"), QStringLiteral("Up"), 0x0060, 1, 1, 1, 1, APP_KeyTone::Normal}, {QStringLiteral("9"), QStringLiteral("9"), QStringLiteral("PgUp"), 0x0061, 1, 2, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("plus"), QStringLiteral("+"), QStringLiteral(""), 0x0057, 1, 3, 2, 1, APP_KeyTone::Aqua}, - {QStringLiteral("4"), QStringLiteral("4"), QStringLiteral("←"), 0x005C, 2, 0, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("5"), QStringLiteral("5"), QStringLiteral(""), 0x005D, 2, 1, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("6"), QStringLiteral("6"), QStringLiteral("→"), 0x005E, 2, 2, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("plus"), QStringLiteral("+"), QString(), 0x0057, 1, 3, 2, 1, APP_KeyTone::Aqua}, + {QStringLiteral("4"), QStringLiteral("4"), QStringLiteral("Left"), 0x005C, 2, 0, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("5"), QStringLiteral("5"), QString(), 0x005D, 2, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("6"), QStringLiteral("6"), QStringLiteral("Right"), 0x005E, 2, 2, 1, 1, APP_KeyTone::Normal}, {QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("End"), 0x0059, 3, 0, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("2"), QStringLiteral("2"), QStringLiteral("↓"), 0x005A, 3, 1, 1, 1, APP_KeyTone::Normal}, + {QStringLiteral("2"), QStringLiteral("2"), QStringLiteral("Down"), 0x005A, 3, 1, 1, 1, APP_KeyTone::Normal}, {QStringLiteral("3"), QStringLiteral("3"), QStringLiteral("PgDn"), 0x005B, 3, 2, 1, 1, APP_KeyTone::Normal}, - {QStringLiteral("enter"), QStringLiteral("Enter"), QStringLiteral(""), 0x0058, 3, 3, 2, 1, APP_KeyTone::Blue}, + {QStringLiteral("enter"), QStringLiteral("Enter"), QString(), 0x0058, 3, 3, 2, 1, APP_KeyTone::Blue}, {QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("Ins"), 0x0062, 4, 0, 1, 2, APP_KeyTone::Normal}, {QStringLiteral("dot"), QStringLiteral("."), QStringLiteral("Del"), 0x0063, 4, 2, 1, 1, APP_KeyTone::Normal} }; - - appFunctionKeyList = { - {QStringLiteral("7"), QStringLiteral("7"), QStringLiteral("功能"), 0x005F, 0, 0, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("8"), QStringLiteral("8"), QStringLiteral("功能"), 0x0060, 0, 1, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("9"), QStringLiteral("9"), QStringLiteral("功能"), 0x0061, 0, 2, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("minus"), QStringLiteral("-"), QStringLiteral("功能"), 0x0056, 0, 3, 1, 1, APP_KeyTone::Amber}, - {QStringLiteral("4"), QStringLiteral("4"), QStringLiteral("功能"), 0x005C, 1, 0, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("5"), QStringLiteral("5"), QStringLiteral("功能"), 0x005D, 1, 1, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("6"), QStringLiteral("6"), QStringLiteral("功能"), 0x005E, 1, 2, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("plus"), QStringLiteral("+"), QStringLiteral("功能"), 0x0057, 1, 3, 2, 1, APP_KeyTone::Aqua}, - {QStringLiteral("1"), QStringLiteral("1"), QStringLiteral("功能"), 0x0059, 2, 0, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("2"), QStringLiteral("2"), QStringLiteral("功能"), 0x005A, 2, 1, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("3"), QStringLiteral("3"), QStringLiteral("功能"), 0x005B, 2, 2, 1, 1, APP_KeyTone::Blue}, - {QStringLiteral("0"), QStringLiteral("0"), QStringLiteral("功能"), 0x0062, 3, 0, 1, 3, APP_KeyTone::Blue} - }; } const QVector& APP_KeypadModel::App_Func_GetKeyList() const @@ -45,11 +30,6 @@ const QVector& APP_KeypadModel::App_Func_GetKeyList() const return appKeyList; } -const QVector& APP_KeypadModel::App_Func_GetFunctionKeyList() const -{ - return appFunctionKeyList; -} - bool APP_KeypadModel::App_Func_IsLatched(const QString& KeyId) const { return (KeyId == QStringLiteral("num")) && appNumLockOn; @@ -73,6 +53,32 @@ quint16 APP_KeypadModel::App_Func_GetUsageFromKeyId(const QString& KeyId) const return 0; } +QString APP_KeypadModel::App_Func_GetKeyIdFromUsage(quint16 Usage) const +{ + for (const APP_KeyInfo& KeyInfo : appKeyList) + { + if (KeyInfo.usage == Usage) + { + return KeyInfo.id; + } + } + + return QString(); +} + +QString APP_KeypadModel::App_Func_GetDefaultHint(const QString& KeyId) const +{ + for (const APP_KeyInfo& KeyInfo : appKeyList) + { + if (KeyInfo.id == KeyId) + { + return KeyInfo.hint; + } + } + + return QString(); +} + void APP_KeypadModel::App_Func_SetNumLockOn(bool IsOn) { appNumLockOn = IsOn; @@ -89,7 +95,7 @@ void APP_KeypadModel::App_Func_SetPressedKeysFromUsageList(const QVector& App_Func_GetKeyList() const; - const QVector& App_Func_GetFunctionKeyList() const; bool App_Func_IsLatched(const QString& KeyId) const; bool App_Func_IsPressed(const QString& KeyId) const; quint16 App_Func_GetUsageFromKeyId(const QString& KeyId) const; + QString App_Func_GetKeyIdFromUsage(quint16 Usage) const; + QString App_Func_GetDefaultHint(const QString& KeyId) const; void App_Func_SetNumLockOn(bool IsOn); void App_Func_ClearPressedKeys(); void App_Func_SetPressedKeysFromUsageList(const QVector& UsageList); - void App_Func_SetSwapUsagePair(quint16 UsageLeft, quint16 UsageRight, bool IsEnabled); private: - quint16 App_Func_MapUsageForUi(quint16 Usage) const; - QString App_Func_GetKeyIdFromUsage(quint16 Usage) const; - QVector appKeyList; - QVector appFunctionKeyList; QStringList appPressedKeyIdList; bool appNumLockOn = false; - bool appIsSwapOn = false; - quint16 appSwapUsageLeft = 0; - quint16 appSwapUsageRight = 0; }; } // namespace APP diff --git a/APP/APP_Theme.cpp b/APP/APP_Theme.cpp index 7aec925..5e3fb89 100644 --- a/APP/APP_Theme.cpp +++ b/APP/APP_Theme.cpp @@ -7,11 +7,6 @@ namespace APP { QPalette APP_Theme::App_Func_GetPalette() { - /* - * 不用样式表时,Qt 最稳妥的统一美化方式就是调色板: - * 1. 先选 Fusion 风格 - * 2. 再给标准控件一组统一颜色 - */ QPalette Palette; const QColor WindowColor(20, 25, 33); @@ -45,13 +40,140 @@ QPalette APP_Theme::App_Func_GetPalette() Palette.setColor(QPalette::Disabled, QPalette::WindowText, DimTextColor); Palette.setColor(QPalette::Disabled, QPalette::Text, DimTextColor); Palette.setColor(QPalette::Disabled, QPalette::ButtonText, DimTextColor); + Palette.setColor(QPalette::Disabled, QPalette::Base, QColor(30, 35, 43)); + Palette.setColor(QPalette::Disabled, QPalette::Button, QColor(34, 40, 49)); + Palette.setColor(QPalette::Disabled, QPalette::Window, QColor(24, 29, 36)); + Palette.setColor(QPalette::Disabled, QPalette::Highlight, QColor(60, 68, 78)); + Palette.setColor(QPalette::Disabled, QPalette::HighlightedText, DimTextColor); return Palette; } +QString APP_Theme::App_Func_GetStyleSheet() +{ + return QStringLiteral(R"( +QWidget { + color: #eef2f7; + background-color: transparent; +} +QTabWidget::pane { + border: 0; + background: transparent; +} +QTabBar::tab { + background: rgba(56, 64, 76, 0.78); + color: #b9c3cf; + border: 1px solid #5d6a78; + border-bottom: none; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + padding: 8px 18px; + margin-right: 6px; + min-width: 92px; +} +QTabBar::tab:selected { + background: #2d3742; + color: #f7fbff; +} +QTabBar::tab:hover:!selected { + background: #394553; + color: #eff4f8; +} +QPushButton { + background: #303947; + color: #eff4f8; + border: 1px solid #667587; + border-radius: 8px; + padding: 8px 14px; + min-height: 18px; +} +QPushButton:hover { + background: #384354; + border-color: #7b8da1; +} +QPushButton:pressed { + background: #25303b; +} +QPushButton:disabled { + background: #252c35; + color: #7d8794; + border-color: #414b58; +} +QLineEdit, +QPlainTextEdit, +QTableWidget, +QComboBox, +QSpinBox { + background: #1d242d; + color: #edf3f8; + border: 1px solid #4f5d6d; + border-radius: 8px; + selection-background-color: #48b8a2; + selection-color: #102022; +} +QLineEdit, +QComboBox, +QSpinBox { + padding: 6px 8px; +} +QPlainTextEdit, +QTableWidget { + border-radius: 10px; +} +QHeaderView::section { + background: #313b49; + color: #d8e0e8; + border: none; + border-bottom: 1px solid #4f5d6d; + padding: 7px 8px; +} +QTableCornerButton::section { + background: #313b49; + border: none; + border-bottom: 1px solid #4f5d6d; +} +QTableWidget { + alternate-background-color: #232b35; + gridline-color: #2f3945; +} +QTableWidget::item { + padding: 4px 6px; +} +QScrollBar:vertical { + background: transparent; + width: 10px; + margin: 4px 0 4px 0; +} +QScrollBar::handle:vertical { + background: #53606f; + min-height: 28px; + border-radius: 5px; +} +QScrollBar::add-line:vertical, +QScrollBar::sub-line:vertical, +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: transparent; + height: 0; +} +QMenu { + background: #1f262f; + color: #eef2f7; + border: 1px solid #53606f; + padding: 6px; +} +QMenu::item { + padding: 7px 14px; + border-radius: 6px; +} +QMenu::item:selected { + background: #33404d; +} +)"); +} + QFont APP_Theme::App_Func_GetBodyFont() { - // 正文用相对稳妥、系统常见的字体候选。 QFont Font(App_Func_PickFontFamily(QStringList() << QStringLiteral("Segoe UI Variable Text") << QStringLiteral("Microsoft YaHei UI") @@ -62,21 +184,8 @@ QFont APP_Theme::App_Func_GetBodyFont() return Font; } -QFont APP_Theme::App_Func_GetTitleFont() -{ - // 页面标题字号更大、字重更高。 - QFont Font(App_Func_PickFontFamily(QStringList() - << QStringLiteral("Segoe UI Variable Display Semibold") - << QStringLiteral("Microsoft YaHei UI") - << QStringLiteral("Bahnschrift SemiBold"))); - Font.setPointSize(21); - Font.setWeight(QFont::DemiBold); - return Font; -} - QFont APP_Theme::App_Func_GetMetricFont() { - // 指标类标题用比正文更有力度的字重。 QFont Font(App_Func_PickFontFamily(QStringList() << QStringLiteral("Bahnschrift SemiBold") << QStringLiteral("Segoe UI Semibold") @@ -88,7 +197,6 @@ QFont APP_Theme::App_Func_GetMetricFont() QFont APP_Theme::App_Func_GetKeyLabelFont() { - // 按键主文字字号较大,保证小键盘一眼能看清。 QFont Font(App_Func_PickFontFamily(QStringList() << QStringLiteral("Bahnschrift SemiBold") << QStringLiteral("Segoe UI Semibold") @@ -99,7 +207,6 @@ QFont APP_Theme::App_Func_GetKeyLabelFont() QFont APP_Theme::App_Func_GetKeyHintFont() { - // 按键 hint 放左上角,所以字号更小。 QFont Font(App_Func_PickFontFamily(QStringList() << QStringLiteral("Segoe UI Semibold") << QStringLiteral("Bahnschrift SemiBold") @@ -111,20 +218,17 @@ QFont APP_Theme::App_Func_GetKeyHintFont() QString APP_Theme::App_Func_PickFontFamily(const QStringList& FamilyList) { - // 从候选字体里依次挑选系统真实存在的字体。 const QFontDatabase Database; const QStringList AvailableFamilyList = Database.families(); - for (int Index = 0; Index < FamilyList.size(); ++Index) + for (const QString& Family : FamilyList) { - const QString& Family = FamilyList.at(Index); if (AvailableFamilyList.contains(Family)) { return Family; } } - // 如果都不存在,就退回 Qt 当前默认字体。 return QApplication::font().family(); } diff --git a/APP/APP_Theme.h b/APP/APP_Theme.h index 61924d7..7173423 100644 --- a/APP/APP_Theme.h +++ b/APP/APP_Theme.h @@ -6,32 +6,17 @@ namespace APP { -/* - * 主题模块现在只保留一套固定暗色风格。 - * - * - 不参与 DRI 枚举 - * - 不参与协议解析 - * - 不参与业务判断 - */ class APP_Theme { public: - // 返回标准控件使用的统一调色板。 static QPalette App_Func_GetPalette(); - - // 正文说明文字的默认字体。 + static QString App_Func_GetStyleSheet(); static QFont App_Func_GetBodyFont(); - // 页面标题字体。 - static QFont App_Func_GetTitleFont(); - // 指标、卡片主标题使用的强调字体。 static QFont App_Func_GetMetricFont(); - // 键帽中央主文字字体。 static QFont App_Func_GetKeyLabelFont(); - // 键帽角落提示文字字体。 static QFont App_Func_GetKeyHintFont(); private: - // 从候选字体列表中挑出当前系统真实存在的一项。 static QString App_Func_PickFontFamily(const QStringList& FamilyList); }; diff --git a/APP/APP_UIWindow.cpp b/APP/APP_UIWindow.cpp index 71c257c..d33c0d1 100644 --- a/APP/APP_UIWindow.cpp +++ b/APP/APP_UIWindow.cpp @@ -3,161 +3,180 @@ #include "APP/APP_GlassCard.h" #include "APP/APP_KeyButton.h" #include "APP/APP_Theme.h" -#include "LOGIC/Lgc_Func_Button.h" -#include +#include "APP/APP_UIWindow_Private.h" +#include #include -#include #include #include #include +#include #include #include #include +#include #include -#include +#include +#include +#include +#include #include +#include +#include #include +#include namespace APP { -namespace { - -class App_TabBar : public QTabBar +namespace { -public: - explicit App_TabBar(QWidget* parent = nullptr) - : QTabBar(parent) - { - setDrawBase(false); - setExpanding(true); - setUsesScrollButtons(false); - setElideMode(Qt::ElideNone); - setMouseTracking(true); - setCursor(Qt::PointingHandCursor); - } -protected: - QSize tabSizeHint(int index) const override - { - QSize Size = QTabBar::tabSizeHint(index); - Size.rheight() = qMax(Size.height(), 38); - Size.rwidth() += 18; - return Size; - } - - void mouseMoveEvent(QMouseEvent* event) override - { - const int HoverIndex = tabAt(event->pos()); - if (appHoverIndex != HoverIndex) - { - appHoverIndex = HoverIndex; - update(); - } - - QTabBar::mouseMoveEvent(event); - } - - void leaveEvent(QEvent* event) override - { - appHoverIndex = -1; - update(); - QTabBar::leaveEvent(event); - } - - void paintEvent(QPaintEvent* event) override - { - Q_UNUSED(event); - - QPainter Painter(this); - Painter.setRenderHint(QPainter::Antialiasing, true); - Painter.setFont(APP_Theme::App_Func_GetBodyFont()); - - for (int Index = 0; Index < count(); ++Index) - { - QRect TabRect = tabRect(Index).adjusted(3, 5, -3, 0); - const bool IsSelected = (Index == currentIndex()); - const bool IsHovered = (Index == appHoverIndex); - - QColor FillColor(36, 40, 48); - QColor BorderColor(92, 102, 114); - QColor TextColor(214, 222, 232); - - if (IsHovered) - { - FillColor = QColor(48, 54, 64); - } - - if (IsSelected) - { - FillColor = QColor(58, 64, 74); - BorderColor = QColor(120, 132, 146); - TextColor = QColor(238, 242, 247); - } - - Painter.setPen(QPen(BorderColor, 1.0)); - Painter.setBrush(FillColor); - Painter.drawRoundedRect(TabRect, 8.0, 8.0); - - Painter.setPen(TextColor); - Painter.drawText(TabRect, Qt::AlignCenter, tabText(Index)); - } - } - -private: - int appHoverIndex = -1; +enum App_Enum_PacketTestRow +{ + App_Enum_PacketTestRow_TxHelloReq = 0, + App_Enum_PacketTestRow_RxHelloRsp, + App_Enum_PacketTestRow_TxBitmap, + App_Enum_PacketTestRow_RxFunctionKeyEvent, + App_Enum_PacketTestRow_RxLedState, + App_Enum_PacketTestRow_TxTimeSync, + App_Enum_PacketTestRow_TxThemeRgb, + App_Enum_PacketTestRow_RxAck, + App_Enum_PacketTestRow_RxError, + App_Enum_PacketTestRow_Count }; -/* - * Qt 把 setTabBar() 设成 protected, - * 所以这里保留一个最小子类,只负责注入我们自绘的页签栏。 - * 这层不是业务封装,而是 Qt API 访问限制带来的必要适配。 - */ -class App_PageTabWidget : public QTabWidget +QLabel* App_Func_CreateLabel( + QWidget* parent, + const QString& Text, + const QFont& Font, + bool WordWrap = false) { -public: - explicit App_PageTabWidget(QWidget* parent = nullptr) - : QTabWidget(parent) - { - setTabBar(new App_TabBar(this)); - setDocumentMode(true); - tabBar()->setExpanding(true); - } -}; - -QLabel* App_Func_CreateLabel(QWidget* parent, const QString& text, const QFont& font, bool wordWrap = false) -{ - QLabel* label = new QLabel(text, parent); - label->setFont(font); - label->setAttribute(Qt::WA_TranslucentBackground, true); - label->setWordWrap(wordWrap); - return label; + QLabel* p_Label = new QLabel(Text, parent); + p_Label->setFont(Font); + p_Label->setAttribute(Qt::WA_TranslucentBackground, true); + p_Label->setWordWrap(WordWrap); + return p_Label; } APP_GlassCard* App_Func_CreateCard(QWidget* parent, QVBoxLayout** pp_Layout) { - APP_GlassCard* card = new APP_GlassCard(parent); - - QVBoxLayout* layout = new QVBoxLayout(card); - layout->setContentsMargins(20, 20, 20, 20); - layout->setSpacing(14); - *pp_Layout = layout; - return card; + APP_GlassCard* p_Card = new APP_GlassCard(parent); + QVBoxLayout* p_Layout = new QVBoxLayout(p_Card); + p_Layout->setContentsMargins(20, 20, 20, 20); + p_Layout->setSpacing(14); + *pp_Layout = p_Layout; + return p_Card; } -void App_Func_SetGridStretch(QGridLayout* grid, int columnCount, int rowCount) +void App_Func_SetGridStretch(QGridLayout* p_Grid, int ColumnCount, int RowCount) { - for (int column = 0; column < columnCount; ++column) + for (int Column = 0; Column < ColumnCount; ++Column) { - grid->setColumnStretch(column, 1); + p_Grid->setColumnStretch(Column, 1); } - for (int row = 0; row < rowCount; ++row) + for (int Row = 0; Row < RowCount; ++Row) { - grid->setRowStretch(row, 1); + p_Grid->setRowStretch(Row, 1); } } +bool App_Func_RegisterRawInputWindow(WId WindowId) +{ + RAWINPUTDEVICE Device = {}; + Device.usUsagePage = 0x01; + Device.usUsage = 0x06; + Device.dwFlags = RIDEV_INPUTSINK; + Device.hwndTarget = reinterpret_cast(WindowId); + return RegisterRawInputDevices(&Device, 1, sizeof(Device)) == TRUE; +} + +QString App_Func_BoolText(bool Value) +{ + return Value ? QStringLiteral("Yes") : QStringLiteral("No"); +} + +Com_Enum_RawPacketSource App_Func_GetPacketTestTargetSource(const QComboBox* p_Combo) +{ + return p_Combo == nullptr + ? Com_Enum_RawPacketSource_None + : static_cast(p_Combo->currentData().toInt()); +} + +QString App_Func_GetPacketTestTargetText(Com_Enum_RawPacketSource Source) +{ + switch (Source) + { + case Com_Enum_RawPacketSource_UsbCdc: + return QStringLiteral("USB CDC only"); + case Com_Enum_RawPacketSource_BleNus: + return QStringLiteral("BLE NUS only"); + default: + return QStringLiteral("Auto (ready transport)"); + } +} + +QString App_Func_FormatPacketType(quint32 RawType) +{ + switch (static_cast(RawType & 0xFFU)) + { + case Com_Enum_ProtocolType_HelloReq: return QStringLiteral("HelloReq"); + case Com_Enum_ProtocolType_HelloRsp: return QStringLiteral("HelloRsp"); + case Com_Enum_ProtocolType_Bitmap: return QStringLiteral("Bitmap"); + case Com_Enum_ProtocolType_FunctionKeyEvent: return QStringLiteral("FunctionKeyEvent"); + case Com_Enum_ProtocolType_LedState: return QStringLiteral("LedState"); + case Com_Enum_ProtocolType_TimeSync: return QStringLiteral("TimeSync"); + case Com_Enum_ProtocolType_ThemeRgb: return QStringLiteral("ThemeRgb"); + case Com_Enum_ProtocolType_Ack: return QStringLiteral("Ack"); + case Com_Enum_ProtocolType_Error: return QStringLiteral("Error"); + default: + return RawType == 0 + ? QStringLiteral("None") + : QStringLiteral("0x%1").arg(RawType, 0, 16).toUpper(); + } +} + +QString App_Func_FormatVidPid(quint32 VendorId, quint32 ProductId) +{ + return QStringLiteral("%1:%2") + .arg(VendorId, 4, 16, QLatin1Char('0')) + .arg(ProductId, 4, 16, QLatin1Char('0')) + .toUpper(); +} + +QFont App_Func_GetMonoFont() +{ + QFont Font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + Font.setPointSize(9); + return Font; +} + } // namespace +int App_Func_GetFeatureStackIndex(Lgc_FunctionFeature_Type Type) +{ + switch (Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + case Lgc_FunctionFeature_Type::KeySequence: + return 0; + case Lgc_FunctionFeature_Type::Website: + return 1; + default: + return 0; + } +} + +bool App_Func_IsKeyRecordFeatureType(Lgc_FunctionFeature_Type Type) +{ + return (Type == Lgc_FunctionFeature_Type::KeyCombination) || + (Type == Lgc_FunctionFeature_Type::KeySequence); +} + +QString App_Func_GetWebsiteEditText(const QString& UrlText) +{ + const QString DisplayText = UrlText.trimmed(); + return DisplayText.isEmpty() ? QStringLiteral("https://") : DisplayText; +} + App_UIWindow::App_UIWindow(QWidget* parent) : QWidget(parent) { @@ -165,45 +184,44 @@ App_UIWindow::App_UIWindow(QWidget* parent) App_Func_InitUi(); App_Func_InitConnect(); App_Func_InitLogic(); - App_Func_RefreshUi(); + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + App_Func_RefreshPacketTestPanel(); + update(); } App_UIWindow::~App_UIWindow() { - Lgc_Core_Func_Close(&appLgcState); + Lgc_Core_Close(&appLgcState); } void App_UIWindow::paintEvent(QPaintEvent* event) { Q_UNUSED(event); - QPainter Painter(this); Painter.setRenderHint(QPainter::Antialiasing, true); Painter.fillRect(rect(), palette().color(QPalette::Window)); } +void App_UIWindow::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + App_Func_UpdateFeatureEditorHeight(); +} + bool App_UIWindow::nativeEvent(const QByteArray& EventType, void* p_Message, long* p_Result) { - /* - * Windows 原生消息不在 APP 层解析。 - * APP 只做一件事:把消息往下转给 LGC / DRI。 - */ - Lgc_Core_Func_HandleNativeMessage(&appLgcState, p_Message); + Q_UNUSED(EventType); + App_Func_HandleSequenceRecordMessage(p_Message); + Lgc_Core_HandleNativeMessage(&appLgcState, p_Message); return QWidget::nativeEvent(EventType, p_Message, p_Result); } void App_UIWindow::App_Func_InitWindow() { - setWindowTitle(QStringLiteral("数字键盘上位机")); - -#if APP_ENABLE_DEBUG_WINDOW - setMinimumSize(640, 960); - resize(700, 1020); -#else - setMinimumSize(620, 760); - resize(680, 820); -#endif - + setWindowTitle(QStringLiteral("Keyboard Control")); + setMinimumSize(760, 820); + resize(820, 900); setAttribute(Qt::WA_StyledBackground, true); } @@ -213,322 +231,237 @@ void App_UIWindow::App_Func_InitUi() p_RootLayout->setContentsMargins(26, 22, 26, 24); p_RootLayout->setSpacing(14); - /* - * 页面切换直接用 QTabWidget。 - * 这比自己维护一套切页按钮更短,也更适合教学。 - */ - App_PageTabWidget* p_PageTab = new App_PageTabWidget(this); - - p_PageTab->addTab(App_Func_CreatePadCard(), QStringLiteral("小键盘")); - p_PageTab->addTab(App_Func_CreateFunctionRegisterCard(), QStringLiteral("功能注册")); - p_PageTab->addTab(App_Func_CreateFunctionConfigCard(), QStringLiteral("功能配置")); - -#if APP_ENABLE_DEBUG_WINDOW - p_PageTab->addTab(App_Func_CreateDebugCard(), QStringLiteral("调试")); -#endif - - p_RootLayout->addWidget(p_PageTab, 1); + appPageTab = new QTabWidget(this); + appPageTab->setDocumentMode(true); + appPageTab->setMovable(false); + appPageTab->addTab(App_Func_CreatePadCard(), QStringLiteral("Keypad")); + appFeaturePageIndex = + appPageTab->addTab(App_Func_CreateFunctionConfigCard(), QStringLiteral("Functions")); + appPacketTestPageIndex = + appPageTab->addTab(App_Func_CreatePacketTestCard(), QStringLiteral("Device Test")); + p_RootLayout->addWidget(appPageTab, 1); } void App_UIWindow::App_Func_InitConnect() { - /* - * 这份教学版刻意把所有 connect 放在同一个函数里, - * 让学生顺着“谁发信号 -> 谁收信号”一眼就能看完。 - */ - connect(&appTimerPoll, &QTimer::timeout, this, &App_UIWindow::App_Func_OnPollTimer); + connect(&appTimerPoll, &QTimer::timeout, this, [this]() + { + if (Lgc_Core_Poll(&appLgcState)) + { + App_Func_RefreshAfterLogicChange(); + } + }); + connect(&appTimerAutoRefreshDevice, &QTimer::timeout, this, [this]() + { + const Lgc_Core_Struct_View View = Lgc_Core_GetView(&appLgcState); + if (View.IsConnected || View.HasOpenTransport) + { + return; + } - /* - * 这些控件都在 InitUi 阶段固定创建完成, - * 所以这里直接 connect,不再加“防御性空指针判断”干扰阅读。 - */ - const auto ConnectConfigSignal = [this](auto Sender, auto Signal) + Lgc_Core_RefreshDevice(&appLgcState); + App_Func_RefreshKeypadState(); + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + App_Func_RefreshPacketTestPanel(); + update(); + }); + + connect(appFeatureAddButton, &QPushButton::clicked, this, &App_UIWindow::App_Func_AddFeature); + connect(appFeatureDeleteButton, &QPushButton::clicked, this, &App_UIWindow::App_Func_DeleteFeature); + connect(appFeatureTimeSyncButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_SendTimeSync(&appLgcState); + App_Func_RefreshAfterLogicChange(); + }); + connect(appFeatureThemeSwitchButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_SendThemeSwitch(&appLgcState); + App_Func_RefreshAfterLogicChange(); + }); + + connect(appFeatureTable, &QTableWidget::itemSelectionChanged, this, [this]() + { + const QModelIndexList IndexList = appFeatureTable->selectionModel()->selectedRows(); + if (IndexList.isEmpty()) + { + App_Func_SelectFeature(0); + return; + } + + const int Row = IndexList.first().row(); + App_Func_SelectFeature(appFeatureTable->item(Row, 0)->data(Qt::UserRole).toInt()); + }); + + const auto ConnectSaveSignal = [this](auto Sender, auto Signal) { connect(Sender, Signal, this, [this]() { - App_Func_UpdateFunctionConfigFromUi(); + if (!appIsUpdatingFeatureUi) + { + App_Func_SaveFeatureFromUi(); + } }); }; - ConnectConfigSignal(appFunctionEditMacroText, &QLineEdit::textChanged); - ConnectConfigSignal(appFunctionEditWebsite, &QLineEdit::textChanged); - ConnectConfigSignal(appFunctionComboSwapLeft, qOverload(&QComboBox::currentIndexChanged)); - ConnectConfigSignal(appFunctionComboSwapRight, qOverload(&QComboBox::currentIndexChanged)); + ConnectSaveSignal(appFeatureNameEdit, &QLineEdit::textChanged); + ConnectSaveSignal(appFeatureDescriptionEdit, &QLineEdit::textChanged); + ConnectSaveSignal(appFeatureTypeCombo, qOverload(&QComboBox::currentIndexChanged)); + ConnectSaveSignal(appFeatureSequenceEdit, &QLineEdit::textChanged); + ConnectSaveSignal(appFeatureWebsiteEdit, &QLineEdit::textChanged); - /* - * 功能键按钮统一交给 QButtonGroup 管, - * 这样既能减少每颗按钮各写一段 lambda, - * 也正好把 Qt 里“按钮分组 + id 分发”的知识点展示出来。 - */ - connect(appFunctionButtonGroup, qOverload(&QButtonGroup::buttonClicked), this, [this](int ButtonId) + connect( + appFeatureSequenceRecordStartButton, + &QPushButton::clicked, + this, + &App_UIWindow::App_Func_StartSequenceRecording); + connect( + appFeatureSequenceRecordStopButton, + &QPushButton::clicked, + this, + &App_UIWindow::App_Func_StopSequenceRecording); + + connect(appPacketTestRefreshButton, &QPushButton::clicked, this, [this]() { - App_Func_OnFunctionKeyClicked(static_cast(ButtonId)); + Lgc_Core_RefreshDevice(&appLgcState); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestTargetCombo, qOverload(&QComboBox::currentIndexChanged), this, [this]() + { + App_Func_RefreshPacketTestPanel(); + }); + connect(appPacketTestSendHelloButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_TestSendHello( + &appLgcState, + App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestSendBitmapCurrentButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_TestSendBitmapCurrentConfig( + &appLgcState, + App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestSendBitmapAllOnButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_TestSendBitmapAllEnabled( + &appLgcState, + App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestSendBitmapAllOffButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_TestSendBitmapAllDisabled( + &appLgcState, + App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestSendTimeSyncButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_TestSendTimeSync( + &appLgcState, + App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestSendThemeButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_TestSendThemeRgb( + &appLgcState, + static_cast(appPacketTestThemeRedSpin->value()), + static_cast(appPacketTestThemeGreenSpin->value()), + static_cast(appPacketTestThemeBlueSpin->value()), + App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + App_Func_RefreshAfterLogicChange(); + }); + connect(appPacketTestClearLogButton, &QPushButton::clicked, this, [this]() + { + Lgc_Core_ClearTestLog(&appLgcState); + App_Func_RefreshAfterLogicChange(); }); - -#if APP_ENABLE_DEBUG_WINDOW - connect(appDebugPanel->Debug_Func_GetRefreshButton(), &QPushButton::clicked, - this, &App_UIWindow::App_Func_OnRefreshDeviceClicked); - - connect(appDebugPanel->Debug_Func_GetClearButton(), &QPushButton::clicked, - this, &App_UIWindow::App_Func_OnClearLogClicked); - - connect(appDebugPanel->Debug_Func_GetApplyConfigButton(), &QPushButton::clicked, - this, &App_UIWindow::App_Func_OnApplyDeviceConfigClicked); -#endif } void App_UIWindow::App_Func_InitLogic() { - Lgc_Core_Func_Init(&appLgcState); - Lgc_Core_Func_SetWindowHandle(&appLgcState, reinterpret_cast(winId())); + Lgc_Core_Init(&appLgcState); + const WId WindowId = winId(); + Lgc_Core_SetWindowHandle(&appLgcState, reinterpret_cast(WindowId)); + if (!App_Func_RegisterRawInputWindow(WindowId)) + { + Lgc_Core_SetStatusText(&appLgcState, QStringLiteral("Raw keyboard capture init failed.")); + } - App_Func_UpdateFunctionConfigFromUi(); + Lgc_Core_LoadFunctionConfig(&appLgcState); + App_Func_RefreshFeatureTable(); -#if APP_ENABLE_DEBUG_WINDOW - App_Func_RefreshDeviceConfigFromState(); -#endif - - Lgc_Core_Func_Start(&appLgcState); - - /* - * 轮询不仅服务调试窗口, - * 小键盘状态页和功能键页本身也要靠它持续拿最新状态。 - * 所以这里始终启动,不跟调试开关绑定。 - */ + Lgc_Core_Start(&appLgcState); appTimerPoll.setInterval(30); appTimerPoll.start(); - - App_Func_RefreshAfterLogicChange(); -} - -void App_UIWindow::App_Func_RefreshUi() -{ + appTimerAutoRefreshDevice.setInterval(1500); + appTimerAutoRefreshDevice.start(); + App_Func_RefreshKeypadState(); App_Func_RefreshKeypadButtons(); - App_Func_RefreshFunctionButtons(); App_Func_RefreshFunctionStatus(); + App_Func_RefreshPacketTestPanel(); update(); } -void App_UIWindow::App_Func_RefreshKeypadState() -{ - appKeypadModel.App_Func_SetNumLockOn(appLgcState.IsSystemNumLockOn); - appKeypadModel.App_Func_SetSwapUsagePair( - appLgcState.SwapUsageLeft, - appLgcState.SwapUsageRight, - appLgcState.IsSwapModeOn); - - const QVector* p_UsageList = nullptr; - if (appLgcState.IsPhysicalKeyStateValid) - { - p_UsageList = &appLgcState.PhysicalUsageList; - } - else if (appLgcState.IsVisibleKeyStateValid) - { - p_UsageList = &appLgcState.VisibleUsageList; - } - - if (p_UsageList != nullptr) - { - appKeypadModel.App_Func_SetPressedKeysFromUsageList(*p_UsageList); - } - else - { - appKeypadModel.App_Func_ClearPressedKeys(); - } -} - -void App_UIWindow::App_Func_RefreshKeypadButtons() -{ - for (auto It = appKeypadButtonMap.begin(); It != appKeypadButtonMap.end(); ++It) - { - APP_KeyButton* p_Button = It.value(); - p_Button->App_Func_SetLatched(appKeypadModel.App_Func_IsLatched(It.key())); - p_Button->App_Func_SetPressed(appKeypadModel.App_Func_IsPressed(It.key())); - } -} - -void App_UIWindow::App_Func_RefreshFunctionButtons() -{ - for (auto It = appFunctionButtonMap.begin(); It != appFunctionButtonMap.end(); ++It) - { - const QString KeyId = It.key(); - APP_KeyButton* p_Button = It.value(); - const quint16 Usage = appKeypadModel.App_Func_GetUsageFromKeyId(KeyId); - const bool IsFunctionMode = Lgc_Core_Func_IsUsageFunctionMode(&appLgcState, Usage); - - p_Button->App_Func_SetLatched(IsFunctionMode); - p_Button->App_Func_SetPressed(appKeypadModel.App_Func_IsPressed(KeyId)); - } -} - -void App_UIWindow::App_Func_RefreshFunctionStatus() -{ - appFunctionLabelStatus->setText(appLgcState.TextFunctionStatus.isEmpty() - ? QStringLiteral("等待功能键动作。") - : appLgcState.TextFunctionStatus); -} - -void App_UIWindow::App_Func_RefreshDebugView() -{ -#if APP_ENABLE_DEBUG_WINDOW - appDebugPanel->Debug_Func_SetConnectionText(appLgcState.TextConnection, appLgcState.IsConnected); - appDebugPanel->Debug_Func_SetLogText(appLgcState.TextLog); -#endif -} - -void App_UIWindow::App_Func_RefreshAfterLogicChange() -{ - /* - * 逻辑层状态一旦变化,界面层真正需要做的事情其实只有这三步: - * 1. 先把 LGC 状态同步到 APP 自己的显示模型 - * 2. 再把调试文本同步到调试页 - * 3. 最后统一刷新界面控件 - */ - App_Func_RefreshKeypadState(); - App_Func_RefreshDebugView(); - App_Func_RefreshUi(); -} - -void App_UIWindow::App_Func_UpdateFunctionConfigFromUi() -{ - const quint16 OldSwapUsageLeft = appLgcState.FunctionButtonConfig.SwapUsageLeft; - const quint16 OldSwapUsageRight = appLgcState.FunctionButtonConfig.SwapUsageRight; - - appLgcState.FunctionButtonConfig.MacroText = appFunctionEditMacroText->text(); - appLgcState.FunctionButtonConfig.WebsiteUrl = appFunctionEditWebsite->text(); - appLgcState.FunctionButtonConfig.SwapUsageLeft = - static_cast(appFunctionComboSwapLeft->currentData().toUInt()); - appLgcState.FunctionButtonConfig.SwapUsageRight = - static_cast(appFunctionComboSwapRight->currentData().toUInt()); - - if (appLgcState.IsSwapModeOn && - ((OldSwapUsageLeft != appLgcState.FunctionButtonConfig.SwapUsageLeft) || - (OldSwapUsageRight != appLgcState.FunctionButtonConfig.SwapUsageRight))) - { - Lgc_Core_Func_SetSwapMode( - &appLgcState, - appLgcState.FunctionButtonConfig.SwapUsageLeft, - appLgcState.FunctionButtonConfig.SwapUsageRight, - true); - App_Func_RefreshAfterLogicChange(); - } -} - -void App_UIWindow::App_Func_OnFunctionKeyClicked(quint16 Usage) -{ - const bool IsFunctionMode = Lgc_Core_Func_IsUsageFunctionMode(&appLgcState, Usage); - Lgc_Core_Func_SetUsageFunctionMode(&appLgcState, Usage, !IsFunctionMode); - App_Func_RefreshAfterLogicChange(); -} - -void App_UIWindow::App_Func_OnPollTimer() -{ - if (Lgc_Core_Func_Poll(&appLgcState)) - { - App_Func_RefreshAfterLogicChange(); - } -} - -#if APP_ENABLE_DEBUG_WINDOW -void App_UIWindow::App_Func_RefreshDeviceConfigFromState() -{ - appDebugPanel->Debug_Func_SetDeviceConfigText( - appLgcState.DeviceConfig.VendorId, - appLgcState.DeviceConfig.ProductId); - - appDebugPanel->Debug_Func_SetConfigStatusText( - QStringLiteral("当前目标 VID:PID = 0x%1:0x%2") - .arg(appLgcState.DeviceConfig.VendorId, 4, 16, QLatin1Char('0')) - .arg(appLgcState.DeviceConfig.ProductId, 4, 16, QLatin1Char('0')) - .toUpper(), - true); -} - -void App_UIWindow::App_Func_OnApplyDeviceConfigClicked() -{ - quint16 VendorId = 0; - quint16 ProductId = 0; - - if (!appDebugPanel->Debug_Func_TryGetDeviceConfig(&VendorId, &ProductId)) - { - appDebugPanel->Debug_Func_SetConfigStatusText( - QStringLiteral("VID / PID 输入无效,请输入十六进制,例如 1209 和 0001。"), - false); - return; - } - - appLgcState.DeviceConfig.VendorId = VendorId; - appLgcState.DeviceConfig.ProductId = ProductId; - App_Func_OnRefreshDeviceClicked(); -} - -void App_UIWindow::App_Func_OnRefreshDeviceClicked() -{ - Lgc_Core_Func_RefreshDevice(&appLgcState); - App_Func_RefreshDeviceConfigFromState(); - App_Func_RefreshAfterLogicChange(); -} - -void App_UIWindow::App_Func_OnClearLogClicked() -{ - Lgc_Core_Func_ClearLog(&appLgcState); - App_Func_RefreshDebugView(); -} -#endif - QWidget* App_UIWindow::App_Func_CreatePadCard() { QVBoxLayout* p_Layout = nullptr; APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout); - - p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("小键盘状态页"), - APP_Theme::App_Func_GetMetricFont())); - p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("这一页直接显示实体小键盘当前的按下状态,NumLock 也会同步点亮。"), - APP_Theme::App_Func_GetBodyFont(), true)); + p_Layout->addWidget( + App_Func_CreateLabel(p_Card, QStringLiteral("Keypad"), APP_Theme::App_Func_GetMetricFont())); + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral( + "Left click simulates a real press/release. Right click binds the current key to a " + "function. After binding, the button shows the function name and tooltip."), + APP_Theme::App_Func_GetBodyFont(), + true)); QGridLayout* p_Grid = new QGridLayout(); p_Grid->setSpacing(14); - const QVector& KeyList = appKeypadModel.App_Func_GetKeyList(); - - for (const APP_KeyInfo& Key : KeyList) + for (const APP_KeyInfo& Key : appKeypadModel.App_Func_GetKeyList()) { APP_KeyButton* p_Button = new APP_KeyButton(Key, p_Card); + p_Button->setContextMenuPolicy(Qt::CustomContextMenu); + connect( + p_Button, + &QPushButton::pressed, + this, + [this, Key]() + { + Lgc_Core_HandleUiKeyPress(&appLgcState, Key.usage); + App_Func_RefreshUi(); + }); + connect( + p_Button, + &QPushButton::released, + this, + [this, Key]() + { + Lgc_Core_HandleUiKeyRelease(&appLgcState, Key.usage); + App_Func_RefreshUi(); + }); + connect( + p_Button, + &QWidget::customContextMenuRequested, + this, + [this, p_Button, Key](const QPoint&) + { + App_Func_ShowKeyMenu( + Key.usage, + p_Button->mapToGlobal(QPoint(p_Button->width() / 2, p_Button->height() / 2))); + }); + appKeypadButtonMap.insert(Key.id, p_Button); p_Grid->addWidget(p_Button, Key.row, Key.column, Key.rowSpan, Key.columnSpan); } App_Func_SetGridStretch(p_Grid, 4, 5); - - p_Layout->addLayout(p_Grid, 1); - return p_Card; -} - -QWidget* App_UIWindow::App_Func_CreateFunctionRegisterCard() -{ - QVBoxLayout* p_Layout = nullptr; - APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout); - - p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("功能注册"), - APP_Theme::App_Func_GetMetricFont())); - p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("这一页只负责决定哪些小键盘键切到功能键模式。按钮点亮表示该键已经注册为功能键;再次点击即可取消。"), - APP_Theme::App_Func_GetBodyFont(), true)); - - QGridLayout* p_Grid = new QGridLayout(); - p_Grid->setSpacing(14); - - const QVector& KeyList = appKeypadModel.App_Func_GetFunctionKeyList(); - appFunctionButtonGroup = new QButtonGroup(this); - - for (const APP_KeyInfo& Key : KeyList) - { - APP_KeyButton* p_Button = new APP_KeyButton(Key, p_Card); - appFunctionButtonMap.insert(Key.id, p_Button); - appFunctionButtonGroup->addButton(p_Button, static_cast(Key.usage)); - p_Grid->addWidget(p_Button, Key.row, Key.column, Key.rowSpan, Key.columnSpan); - } - - App_Func_SetGridStretch(p_Grid, 4, 4); p_Layout->addLayout(p_Grid, 1); return p_Card; } @@ -537,86 +470,401 @@ QWidget* App_UIWindow::App_Func_CreateFunctionConfigCard() { QVBoxLayout* p_Layout = nullptr; APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout); - - p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("功能配置"), - APP_Theme::App_Func_GetMetricFont())); - p_Layout->addWidget(App_Func_CreateLabel(p_Card, QStringLiteral("这一页只放功能案例参数。当前教学版保留 0=文本输入、1=按键交换、2=打开网址,学生能更清楚看到“注册”和“配置”是两件事。"), - APP_Theme::App_Func_GetBodyFont(), true)); - - QFormLayout* p_ConfigLayout = new QFormLayout(); - p_ConfigLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - p_ConfigLayout->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); - p_ConfigLayout->setFormAlignment(Qt::AlignLeft | Qt::AlignTop); - p_ConfigLayout->setHorizontalSpacing(10); - p_ConfigLayout->setVerticalSpacing(10); - const auto AddConfigRow = [p_Card, p_ConfigLayout](const QString& text, QWidget* field) + const auto AddRow = [p_Card](QFormLayout* p_Form, const QString& LabelText, QWidget* p_Field) { - p_ConfigLayout->addRow( - App_Func_CreateLabel(p_Card, text, APP_Theme::App_Func_GetBodyFont()), - field); + p_Form->addRow( + App_Func_CreateLabel(p_Card, LabelText, APP_Theme::App_Func_GetBodyFont()), + p_Field); }; - appFunctionEditMacroText = new QLineEdit(p_Card); - appFunctionEditMacroText->setText(QStringLiteral("HELLO WORLD!")); - appFunctionEditMacroText->setPlaceholderText(QStringLiteral("例如:HELLO WORLD!")); - AddConfigRow(QStringLiteral("功能键 0 文本"), appFunctionEditMacroText); + p_Layout->addWidget( + App_Func_CreateLabel(p_Card, QStringLiteral("Functions"), APP_Theme::App_Func_GetMetricFont())); + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral( + "The function list starts empty. Add a function first, then bind keypad keys to it. " + "Current types: shortcut, shortcut sequence, and website."), + APP_Theme::App_Func_GetBodyFont(), + true)); - appFunctionComboSwapLeft = new QComboBox(p_Card); - appFunctionComboSwapRight = new QComboBox(p_Card); + QHBoxLayout* p_TopRow = new QHBoxLayout(); + p_TopRow->setContentsMargins(0, 0, 0, 0); + p_TopRow->setSpacing(10); + appFeatureAddButton = new QPushButton(QStringLiteral("Add Function"), p_Card); + appFeatureDeleteButton = new QPushButton(QStringLiteral("Delete Function"), p_Card); + appFeatureTimeSyncButton = new QPushButton(QStringLiteral("Time Sync"), p_Card); + appFeatureThemeSwitchButton = new QPushButton(QStringLiteral("Theme Switch"), p_Card); + appFeatureDeleteButton->setEnabled(false); + p_TopRow->addWidget(appFeatureAddButton); + p_TopRow->addWidget(appFeatureDeleteButton); + p_TopRow->addWidget(appFeatureTimeSyncButton); + p_TopRow->addWidget(appFeatureThemeSwitchButton); + p_TopRow->addStretch(1); + p_Layout->addLayout(p_TopRow); - const QVector& FunctionKeyList = appKeypadModel.App_Func_GetFunctionKeyList(); - for (const APP_KeyInfo& Key : FunctionKeyList) - { - appFunctionComboSwapLeft->addItem(Key.label, static_cast(Key.usage)); - appFunctionComboSwapRight->addItem(Key.label, static_cast(Key.usage)); - } + appFeatureTable = new QTableWidget(p_Card); + appFeatureTable->setColumnCount(3); + appFeatureTable->setHorizontalHeaderLabels( + { QStringLiteral("Name"), QStringLiteral("Description"), QStringLiteral("Type") }); + appFeatureTable->setSelectionBehavior(QAbstractItemView::SelectRows); + appFeatureTable->setSelectionMode(QAbstractItemView::SingleSelection); + appFeatureTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + appFeatureTable->setAlternatingRowColors(true); + appFeatureTable->verticalHeader()->setVisible(false); + appFeatureTable->horizontalHeader()->setStretchLastSection(false); + appFeatureTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + appFeatureTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + appFeatureTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + appFeatureTable->setMinimumHeight(220); + p_Layout->addWidget(appFeatureTable); - const auto SetComboIndex = [](QComboBox* Combo, quint16 Usage) - { - const int Index = Combo->findData(static_cast(Usage)); - if (Index >= 0) - { - Combo->setCurrentIndex(Index); - } - }; + QFormLayout* p_Form = new QFormLayout(); + p_Form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); + p_Form->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter); + p_Form->setHorizontalSpacing(10); + p_Form->setVerticalSpacing(10); - SetComboIndex(appFunctionComboSwapLeft, 0x005C); - SetComboIndex(appFunctionComboSwapRight, 0x005D); + appFeatureNameEdit = new QLineEdit(p_Card); + appFeatureNameEdit->setPlaceholderText( + QStringLiteral("Example: Function 1 / Open 4399 / Prefix Macro")); + AddRow(p_Form, QStringLiteral("Name"), appFeatureNameEdit); - QWidget* p_SwapEditor = new QWidget(p_Card); - QHBoxLayout* p_SwapLayout = new QHBoxLayout(p_SwapEditor); - p_SwapLayout->setContentsMargins(0, 0, 0, 0); - p_SwapLayout->setSpacing(8); - p_SwapLayout->addWidget(appFunctionComboSwapLeft); - p_SwapLayout->addWidget(App_Func_CreateLabel(p_SwapEditor, QStringLiteral("<->"), - APP_Theme::App_Func_GetBodyFont())); - p_SwapLayout->addWidget(appFunctionComboSwapRight); - p_SwapLayout->addStretch(1); - AddConfigRow(QStringLiteral("功能键 1 交换"), p_SwapEditor); + appFeatureDescriptionEdit = new QLineEdit(p_Card); + appFeatureDescriptionEdit->setPlaceholderText( + QStringLiteral("Example: Open the 4399 home page")); + AddRow(p_Form, QStringLiteral("Description"), appFeatureDescriptionEdit); - appFunctionEditWebsite = new QLineEdit(p_Card); - appFunctionEditWebsite->setText(QStringLiteral("https://www.deepseek.com/")); - appFunctionEditWebsite->setPlaceholderText(QStringLiteral("例如:https://www.deepseek.com/")); - AddConfigRow(QStringLiteral("功能键 2 网址"), appFunctionEditWebsite); + appFeatureTypeCombo = new QComboBox(p_Card); + appFeatureTypeCombo->addItem( + Lgc_Core_GetFeatureTypeText(Lgc_FunctionFeature_Type::KeyCombination), + static_cast(Lgc_FunctionFeature_Type::KeyCombination)); + appFeatureTypeCombo->addItem( + Lgc_Core_GetFeatureTypeText(Lgc_FunctionFeature_Type::KeySequence), + static_cast(Lgc_FunctionFeature_Type::KeySequence)); + appFeatureTypeCombo->addItem( + Lgc_Core_GetFeatureTypeText(Lgc_FunctionFeature_Type::Website), + static_cast(Lgc_FunctionFeature_Type::Website)); + AddRow(p_Form, QStringLiteral("Type"), appFeatureTypeCombo); + + appFeatureEditorStack = new QStackedWidget(p_Card); + appFeatureEditorStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + + QWidget* p_SequencePage = new QWidget(appFeatureEditorStack); + QFormLayout* p_SequenceForm = new QFormLayout(p_SequencePage); + p_SequenceForm->setContentsMargins(0, 0, 0, 0); + p_SequenceForm->setHorizontalSpacing(10); + p_SequenceForm->setVerticalSpacing(10); + + QWidget* p_SequenceEditor = new QWidget(p_SequencePage); + QHBoxLayout* p_SequenceEditorLayout = new QHBoxLayout(p_SequenceEditor); + p_SequenceEditorLayout->setContentsMargins(0, 0, 0, 0); + p_SequenceEditorLayout->setSpacing(8); + appFeatureSequenceEdit = new QLineEdit(p_SequenceEditor); + appFeatureSequenceEdit->setPlaceholderText(QStringLiteral("Example: Ctrl+C")); + appFeatureSequenceRecordStartButton = + new QPushButton(QStringLiteral("Start Record"), p_SequenceEditor); + appFeatureSequenceRecordStopButton = + new QPushButton(QStringLiteral("Stop Record"), p_SequenceEditor); + appFeatureSequenceRecordStopButton->setEnabled(false); + p_SequenceEditorLayout->addWidget(appFeatureSequenceEdit, 1); + p_SequenceEditorLayout->addWidget(appFeatureSequenceRecordStartButton); + p_SequenceEditorLayout->addWidget(appFeatureSequenceRecordStopButton); + p_SequenceForm->addRow(QStringLiteral("Shortcut"), p_SequenceEditor); + + QWidget* p_WebsitePage = new QWidget(appFeatureEditorStack); + QVBoxLayout* p_WebsiteLayout = new QVBoxLayout(p_WebsitePage); + p_WebsiteLayout->setContentsMargins(0, 0, 0, 0); + p_WebsiteLayout->setSpacing(0); + appFeatureWebsiteEdit = new QLineEdit(p_WebsitePage); + appFeatureWebsiteEdit->setPlaceholderText(QStringLiteral("Example: 4399.com or www.4399.com")); + p_WebsiteLayout->addWidget(appFeatureWebsiteEdit); + + appFeatureEditorStack->addWidget(p_SequencePage); + appFeatureEditorStack->addWidget(p_WebsitePage); + AddRow(p_Form, QStringLiteral("Parameter"), appFeatureEditorStack); + + appFeatureBindingSummaryLabel = App_Func_CreateLabel( + p_Card, + QStringLiteral("No function selected yet."), + APP_Theme::App_Func_GetBodyFont(), + true); + AddRow(p_Form, QStringLiteral("Bindings"), appFeatureBindingSummaryLabel); appFunctionLabelStatus = App_Func_CreateLabel( - p_Card, QStringLiteral("等待功能键动作。"), - APP_Theme::App_Func_GetBodyFont(), true); - AddConfigRow(QStringLiteral("最近一次动作"), appFunctionLabelStatus); + p_Card, + QStringLiteral("Waiting for function-key activity."), + APP_Theme::App_Func_GetBodyFont(), + true); + AddRow(p_Form, QStringLiteral("Latest Action"), appFunctionLabelStatus); - p_Layout->addLayout(p_ConfigLayout); + p_Layout->addLayout(p_Form); p_Layout->addStretch(1); return p_Card; } -#if APP_ENABLE_DEBUG_WINDOW -QWidget* App_UIWindow::App_Func_CreateDebugCard() +QWidget* App_UIWindow::App_Func_CreatePacketTestCard() { - appDebugPanel = new DEBUG::Debug_Panel(this); - appDebugPanel->Debug_Func_SetConnectionText(QStringLiteral("未连接,等待枚举设备。"), false); - appDebugPanel->Debug_Func_SetLogText(QStringLiteral("等待收到输入包。")); - return appDebugPanel; + QVBoxLayout* p_Layout = nullptr; + APP_GlassCard* p_Card = App_Func_CreateCard(this, &p_Layout); + + p_Layout->addWidget( + App_Func_CreateLabel(p_Card, QStringLiteral("Device Packet Test"), APP_Theme::App_Func_GetMetricFont())); + p_Layout->addWidget(App_Func_CreateLabel( + p_Card, + QStringLiteral( + "Use this page to drive the real device. Choose Auto / USB CDC / BLE NUS first, " + "then send packets and watch the rolling log for HelloRsp / FunctionKeyEvent / " + "LedState / Ack / Error."), + APP_Theme::App_Func_GetBodyFont(), + true)); + + QHBoxLayout* p_TargetRow = new QHBoxLayout(); + p_TargetRow->setContentsMargins(0, 0, 0, 0); + p_TargetRow->setSpacing(8); + p_TargetRow->addWidget( + App_Func_CreateLabel( + p_Card, + QStringLiteral("Target Transport"), + APP_Theme::App_Func_GetBodyFont())); + appPacketTestTargetCombo = new QComboBox(p_Card); + appPacketTestTargetCombo->addItem( + QStringLiteral("Auto (ready transport)"), + static_cast(Com_Enum_RawPacketSource_None)); + appPacketTestTargetCombo->addItem( + QStringLiteral("USB CDC only"), + static_cast(Com_Enum_RawPacketSource_UsbCdc)); + appPacketTestTargetCombo->addItem( + QStringLiteral("BLE NUS only"), + static_cast(Com_Enum_RawPacketSource_BleNus)); + p_TargetRow->addWidget(appPacketTestTargetCombo, 1); + p_Layout->addLayout(p_TargetRow); + + QGridLayout* p_ButtonGrid = new QGridLayout(); + p_ButtonGrid->setContentsMargins(0, 0, 0, 0); + p_ButtonGrid->setHorizontalSpacing(10); + p_ButtonGrid->setVerticalSpacing(10); + appPacketTestRefreshButton = new QPushButton(QStringLiteral("Refresh Device"), p_Card); + appPacketTestSendHelloButton = new QPushButton(QStringLiteral("Send HelloReq"), p_Card); + appPacketTestSendBitmapCurrentButton = new QPushButton(QStringLiteral("Send Bitmap(Config)"), p_Card); + appPacketTestSendBitmapAllOnButton = new QPushButton(QStringLiteral("Send Bitmap(All On)"), p_Card); + appPacketTestSendBitmapAllOffButton = new QPushButton(QStringLiteral("Send Bitmap(All Off)"), p_Card); + appPacketTestSendTimeSyncButton = new QPushButton(QStringLiteral("Send TimeSync"), p_Card); + p_ButtonGrid->addWidget(appPacketTestRefreshButton, 0, 0); + p_ButtonGrid->addWidget(appPacketTestSendHelloButton, 0, 1); + p_ButtonGrid->addWidget(appPacketTestSendTimeSyncButton, 0, 2); + p_ButtonGrid->addWidget(appPacketTestSendBitmapCurrentButton, 1, 0); + p_ButtonGrid->addWidget(appPacketTestSendBitmapAllOnButton, 1, 1); + p_ButtonGrid->addWidget(appPacketTestSendBitmapAllOffButton, 1, 2); + p_ButtonGrid->setColumnStretch(0, 1); + p_ButtonGrid->setColumnStretch(1, 1); + p_ButtonGrid->setColumnStretch(2, 1); + p_Layout->addLayout(p_ButtonGrid); + + QHBoxLayout* p_ThemeRow = new QHBoxLayout(); + p_ThemeRow->setContentsMargins(0, 0, 0, 0); + p_ThemeRow->setSpacing(8); + p_ThemeRow->addWidget( + App_Func_CreateLabel(p_Card, QStringLiteral("Theme RGB"), APP_Theme::App_Func_GetBodyFont())); + + const auto CreateSpin = [p_Card](int Value) + { + QSpinBox* p_Spin = new QSpinBox(p_Card); + p_Spin->setRange(0, 255); + p_Spin->setValue(Value); + p_Spin->setButtonSymbols(QAbstractSpinBox::NoButtons); + p_Spin->setAlignment(Qt::AlignCenter); + p_Spin->setFixedWidth(56); + return p_Spin; + }; + + appPacketTestThemeRedSpin = CreateSpin(247); + appPacketTestThemeGreenSpin = CreateSpin(37); + appPacketTestThemeBlueSpin = CreateSpin(133); + appPacketTestSendThemeButton = new QPushButton(QStringLiteral("Send ThemeRgb"), p_Card); + appPacketTestClearLogButton = new QPushButton(QStringLiteral("Clear Log"), p_Card); + p_ThemeRow->addWidget(appPacketTestThemeRedSpin); + p_ThemeRow->addWidget(appPacketTestThemeGreenSpin); + p_ThemeRow->addWidget(appPacketTestThemeBlueSpin); + p_ThemeRow->addWidget(appPacketTestSendThemeButton); + p_ThemeRow->addWidget(appPacketTestClearLogButton); + p_ThemeRow->addStretch(1); + p_Layout->addLayout(p_ThemeRow); + + appPacketTestTransportLabel = + App_Func_CreateLabel(p_Card, QStringLiteral("Transport: -"), APP_Theme::App_Func_GetBodyFont(), true); + appPacketTestProtocolLabel = + App_Func_CreateLabel(p_Card, QStringLiteral("Protocol: -"), APP_Theme::App_Func_GetBodyFont(), true); + appPacketTestHelloLabel = + App_Func_CreateLabel(p_Card, QStringLiteral("HelloRsp: -"), APP_Theme::App_Func_GetBodyFont(), true); + appPacketTestAckLabel = + App_Func_CreateLabel(p_Card, QStringLiteral("Ack: -"), APP_Theme::App_Func_GetBodyFont(), true); + appPacketTestErrorLabel = + App_Func_CreateLabel(p_Card, QStringLiteral("Error: -"), APP_Theme::App_Func_GetBodyFont(), true); + appPacketTestBitmapLabel = + App_Func_CreateLabel(p_Card, QStringLiteral("Bitmaps: -"), APP_Theme::App_Func_GetBodyFont(), true); + p_Layout->addWidget(appPacketTestTransportLabel); + p_Layout->addWidget(appPacketTestProtocolLabel); + p_Layout->addWidget(appPacketTestHelloLabel); + p_Layout->addWidget(appPacketTestAckLabel); + p_Layout->addWidget(appPacketTestErrorLabel); + p_Layout->addWidget(appPacketTestBitmapLabel); + + appPacketTestTable = new QTableWidget(App_Enum_PacketTestRow_Count, 3, p_Card); + appPacketTestTable->setFont(App_Func_GetMonoFont()); + appPacketTestTable->setHorizontalHeaderLabels( + { QStringLiteral("Packet"), QStringLiteral("Direction"), QStringLiteral("Count") }); + appPacketTestTable->verticalHeader()->setVisible(false); + appPacketTestTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + appPacketTestTable->setSelectionMode(QAbstractItemView::NoSelection); + appPacketTestTable->setFocusPolicy(Qt::NoFocus); + appPacketTestTable->horizontalHeader()->setStretchLastSection(false); + appPacketTestTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + appPacketTestTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + appPacketTestTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + appPacketTestTable->setMinimumHeight(220); + + const QStringList PacketNames = { + QStringLiteral("HelloReq"), + QStringLiteral("HelloRsp"), + QStringLiteral("Bitmap"), + QStringLiteral("FunctionKeyEvent"), + QStringLiteral("LedState"), + QStringLiteral("TimeSync"), + QStringLiteral("ThemeRgb"), + QStringLiteral("Ack"), + QStringLiteral("Error") + }; + const QStringList Directions = { + QStringLiteral("Host -> Device"), + QStringLiteral("Device -> Host"), + QStringLiteral("Host -> Device"), + QStringLiteral("Device -> Host"), + QStringLiteral("Device -> Host"), + QStringLiteral("Host -> Device"), + QStringLiteral("Host -> Device"), + QStringLiteral("Device -> Host"), + QStringLiteral("Device -> Host") + }; + + for (int Row = 0; Row < App_Enum_PacketTestRow_Count; ++Row) + { + appPacketTestTable->setItem(Row, 0, new QTableWidgetItem(PacketNames.at(Row))); + appPacketTestTable->setItem(Row, 1, new QTableWidgetItem(Directions.at(Row))); + appPacketTestTable->setItem(Row, 2, new QTableWidgetItem(QStringLiteral("0"))); + } + p_Layout->addWidget(appPacketTestTable); + + appPacketTestLogEdit = new QPlainTextEdit(p_Card); + appPacketTestLogEdit->setFont(App_Func_GetMonoFont()); + appPacketTestLogEdit->setReadOnly(true); + appPacketTestLogEdit->setLineWrapMode(QPlainTextEdit::NoWrap); + appPacketTestLogEdit->setMinimumHeight(190); + p_Layout->addWidget(appPacketTestLogEdit, 1); + return p_Card; +} + +void App_UIWindow::App_Func_RefreshPacketTestTable() +{ + if (appPacketTestTable == nullptr) + { + return; + } + + const Lgc_Core_Struct_TestView TestView = Lgc_Core_GetTestView(&appLgcState); + const int Counts[App_Enum_PacketTestRow_Count] = { + TestView.TxHelloReqCount, + TestView.RxHelloRspCount, + TestView.TxBitmapCount, + TestView.RxFunctionKeyEventCount, + TestView.RxLedStateCount, + TestView.TxTimeSyncCount, + TestView.TxThemeRgbCount, + TestView.RxAckCount, + TestView.RxErrorCount + }; + + for (int Row = 0; Row < App_Enum_PacketTestRow_Count; ++Row) + { + if (QTableWidgetItem* p_Item = appPacketTestTable->item(Row, 2)) + { + p_Item->setText(QString::number(Counts[Row])); + } + } +} + +void App_UIWindow::App_Func_RefreshPacketTestPanel() +{ + if (appPacketTestTransportLabel == nullptr) + { + return; + } + + const Lgc_Core_Struct_TestView TestView = Lgc_Core_GetTestView(&appLgcState); + const QString TargetText = + App_Func_GetPacketTestTargetText(App_Func_GetPacketTestTargetSource(appPacketTestTargetCombo)); + appPacketTestTransportLabel->setText( + QStringLiteral("Transport\nTarget: %1\nUSB: Open=%2 | Port=%3\nBLE: Open=%4 | Connected=%5 | Endpoint=%6") + .arg(TargetText) + .arg(App_Func_BoolText(TestView.IsUsbOpened)) + .arg(TestView.UsbPortName.isEmpty() ? QStringLiteral("-") : TestView.UsbPortName) + .arg(App_Func_BoolText(TestView.IsNusOpened)) + .arg(App_Func_BoolText(TestView.IsNusConnected)) + .arg(TestView.NusEndpointSummary.isEmpty() + ? QStringLiteral("-") + : TestView.NusEndpointSummary)); + appPacketTestProtocolLabel->setText( + QStringLiteral("Protocol\nReady: Device=%1 | USB=%2 | BLE=%3\nPending: USB=0x%4 | BLE=0x%5\nStatus: %6") + .arg(App_Func_BoolText(TestView.DeviceReady)) + .arg(App_Func_BoolText(TestView.IsUsbProtocolReady)) + .arg(App_Func_BoolText(TestView.IsNusProtocolReady)) + .arg(QString::number(TestView.PendingUsbCommandBits, 16).toUpper()) + .arg(QString::number(TestView.PendingNusCommandBits, 16).toUpper()) + .arg(TestView.StatusText.isEmpty() ? QStringLiteral("-") : TestView.StatusText)); + appPacketTestHelloLabel->setText( + QStringLiteral("HelloRsp\nVersion: v%1 | FW %2.%3\nVID/PID: %4 | Caps: 0x%5") + .arg(TestView.HelloProtocolVersion) + .arg(TestView.HelloFirmwareMajor) + .arg(TestView.HelloFirmwareMinor) + .arg(App_Func_FormatVidPid(TestView.HelloVendorId, TestView.HelloProductId)) + .arg(QString::number(TestView.HelloCapabilityFlags, 16).toUpper())); + appPacketTestAckLabel->setText( + QStringLiteral("Ack\nLastAck: %1\nLastTx: %2\nPayload: %3") + .arg(App_Func_FormatPacketType(TestView.LastAckedType)) + .arg(TestView.LastTxSummary.isEmpty() ? QStringLiteral("-") : TestView.LastTxSummary) + .arg(TestView.LastTxHex)); + appPacketTestErrorLabel->setText( + QStringLiteral("Error\nType: %1 | Code: %2\nLastRx: %3\nPayload: %4") + .arg(App_Func_FormatPacketType(TestView.LastErrorType)) + .arg(TestView.LastErrorCode) + .arg(TestView.LastRxSummary.isEmpty() ? QStringLiteral("-") : TestView.LastRxSummary) + .arg(TestView.LastRxHex)); + appPacketTestBitmapLabel->setText( + QStringLiteral("Bitmap\nCurrent: %1\nLastEvent: %2\nLED Mask: 0x%3") + .arg(TestView.FunctionMaskHex) + .arg(TestView.LastFunctionEventHex) + .arg(QString::number(TestView.DeviceLedMask, 16).toUpper())); + + App_Func_RefreshPacketTestTable(); + if (appPacketTestLogEdit->toPlainText() != TestView.LogText) + { + appPacketTestLogEdit->setPlainText(TestView.LogText); + appPacketTestLogEdit->verticalScrollBar()->setValue( + appPacketTestLogEdit->verticalScrollBar()->maximum()); + } +} + +void App_UIWindow::App_Func_RefreshUi() +{ + App_Func_RefreshKeypadState(); + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + App_Func_RefreshPacketTestPanel(); + update(); +} + +void App_UIWindow::App_Func_RefreshAfterLogicChange() +{ + App_Func_RefreshUi(); } -#endif } // namespace APP diff --git a/APP/APP_UIWindow.h b/APP/APP_UIWindow.h index 39ffe56..535c4ac 100644 --- a/APP/APP_UIWindow.h +++ b/APP/APP_UIWindow.h @@ -1,130 +1,117 @@ #pragma once #include "APP/APP_KeypadModel.h" -#include "DEBUG/Debug_Config.h" #include "LOGIC/Lgc_Core.h" #include +#include #include #include -#if APP_ENABLE_DEBUG_WINDOW -#include "DEBUG/Debug_Panel.h" -#endif - class QLabel; -class QButtonGroup; class QComboBox; class QLineEdit; +class QPlainTextEdit; +class QPushButton; +class QResizeEvent; +class QSpinBox; +class QStackedWidget; +class QTabWidget; +class QTableWidget; namespace APP { class APP_KeyButton; -/* - * APP 主窗口负责把界面搭起来,并把 UI 事件转交给逻辑层。 - * 它只做三件事: - * 1. 创建界面 - * 2. 周期性刷新 UI - * 3. 把 Windows 原生消息转给 LGC / DRI - */ class App_UIWindow : public QWidget { public: - // 构造主窗口并完成 UI、信号和逻辑初始化。 explicit App_UIWindow(QWidget* parent = nullptr); - // 析构时负责收尾逻辑层资源。 ~App_UIWindow() override; protected: - // 自绘主窗口背景和简单装饰。 void paintEvent(QPaintEvent* event) override; - // 接收 Windows 原生消息,再转给 LGC / DRI 处理。 + void resizeEvent(QResizeEvent* event) override; bool nativeEvent(const QByteArray& EventType, void* p_Message, long* p_Result) override; private: - // 设置窗口标题、大小和基础属性。 void App_Func_InitWindow(); - // 创建主界面控件和布局。 void App_Func_InitUi(); - // 连接按钮、定时器等信号。 void App_Func_InitConnect(); - // 初始化逻辑层状态和设备配置。 void App_Func_InitLogic(); - // 执行一轮完整 UI 刷新。 - void App_Func_RefreshUi(); - // 刷新主键盘区状态摘要。 + void App_Func_RefreshKeypadState(); - // 刷新主键盘按钮显示。 void App_Func_RefreshKeypadButtons(); - // 刷新功能键按钮显示。 - void App_Func_RefreshFunctionButtons(); - // 刷新功能配置区状态文字。 + void App_Func_RefreshFeatureTable(); void App_Func_RefreshFunctionStatus(); - // 刷新调试页显示内容。 - void App_Func_RefreshDebugView(); - // 逻辑状态改变后集中刷新必要区域。 + void App_Func_RefreshPacketTestPanel(); + void App_Func_RefreshPacketTestTable(); + void App_Func_RefreshUi(); void App_Func_RefreshAfterLogicChange(); - // 把界面里的功能配置回写到逻辑层。 - void App_Func_UpdateFunctionConfigFromUi(); - // 处理功能键区按钮点击。 - void App_Func_OnFunctionKeyClicked(quint16 Usage); - // 定时轮询设备输入与状态。 - void App_Func_OnPollTimer(); -#if APP_ENABLE_DEBUG_WINDOW - /* - * 如果后续要整体删除调试功能, - * 可以优先从这些入口和 APP_ENABLE_DEBUG_WINDOW 宏开始删。 - */ - // 处理“应用 VID/PID”按钮点击。 - void App_Func_OnApplyDeviceConfigClicked(); - // 处理“刷新设备”按钮点击。 - void App_Func_OnRefreshDeviceClicked(); - // 处理“清空日志”按钮点击。 - void App_Func_OnClearLogClicked(); - // 用逻辑层当前状态刷新调试页输入框。 - void App_Func_RefreshDeviceConfigFromState(); -#endif + void App_Func_SelectFeature(int FeatureId, bool SwitchToFeaturePage = false); + void App_Func_SaveFeatureFromUi(); + void App_Func_UpdateFeatureEditorState(); + void App_Func_UpdateFeatureEditorHeight(); + void App_Func_AddFeature(); + void App_Func_DeleteFeature(); + void App_Func_ShowKeyMenu(quint16 Usage, const QPoint& GlobalPos); + void App_Func_StartSequenceRecording(); + void App_Func_StopSequenceRecording(); + void App_Func_UpdateSequenceRecordingUi(); + bool App_Func_HandleSequenceRecordMessage(void* p_Message); - // 创建左侧主键盘卡片。 QWidget* App_Func_CreatePadCard(); - // 创建右侧功能键配置卡片。 - QWidget* App_Func_CreateFunctionRegisterCard(); QWidget* App_Func_CreateFunctionConfigCard(); + QWidget* App_Func_CreatePacketTestCard(); -#if APP_ENABLE_DEBUG_WINDOW - // 创建调试卡片。 - QWidget* App_Func_CreateDebugCard(); -#endif - - // 键盘布局模型,负责提供按键元数据。 APP_KeypadModel appKeypadModel; - // 主键盘按钮映射表:键名 -> 按钮对象。 QHash appKeypadButtonMap; - // 功能键按钮映射表:键名 -> 按钮对象。 - QHash appFunctionButtonMap; - // 功能键按钮组,用来统一处理点击。 - QButtonGroup* appFunctionButtonGroup = nullptr; + QTabWidget* appPageTab = nullptr; + int appFeaturePageIndex = 0; + int appPacketTestPageIndex = 0; - // 功能区状态说明标签。 QLabel* appFunctionLabelStatus = nullptr; - // 功能键 0 的文本宏输入框。 - QLineEdit* appFunctionEditMacroText = nullptr; - // 功能键 1 左侧交换键选择框。 - QComboBox* appFunctionComboSwapLeft = nullptr; - // 功能键 1 右侧交换键选择框。 - QComboBox* appFunctionComboSwapRight = nullptr; - // 功能键 2 的网址输入框。 - QLineEdit* appFunctionEditWebsite = nullptr; + QTableWidget* appFeatureTable = nullptr; + QPushButton* appFeatureAddButton = nullptr; + QPushButton* appFeatureDeleteButton = nullptr; + QPushButton* appFeatureTimeSyncButton = nullptr; + QPushButton* appFeatureThemeSwitchButton = nullptr; + QLabel* appFeatureBindingSummaryLabel = nullptr; + QLineEdit* appFeatureNameEdit = nullptr; + QLineEdit* appFeatureDescriptionEdit = nullptr; + QComboBox* appFeatureTypeCombo = nullptr; + QStackedWidget* appFeatureEditorStack = nullptr; + QLineEdit* appFeatureSequenceEdit = nullptr; + QLineEdit* appFeatureWebsiteEdit = nullptr; + QPushButton* appFeatureSequenceRecordStartButton = nullptr; + QPushButton* appFeatureSequenceRecordStopButton = nullptr; + QLabel* appPacketTestTransportLabel = nullptr; + QLabel* appPacketTestProtocolLabel = nullptr; + QLabel* appPacketTestHelloLabel = nullptr; + QLabel* appPacketTestAckLabel = nullptr; + QLabel* appPacketTestErrorLabel = nullptr; + QLabel* appPacketTestBitmapLabel = nullptr; + QTableWidget* appPacketTestTable = nullptr; + QPlainTextEdit* appPacketTestLogEdit = nullptr; + QPushButton* appPacketTestRefreshButton = nullptr; + QComboBox* appPacketTestTargetCombo = nullptr; + QPushButton* appPacketTestSendHelloButton = nullptr; + QPushButton* appPacketTestSendBitmapCurrentButton = nullptr; + QPushButton* appPacketTestSendBitmapAllOnButton = nullptr; + QPushButton* appPacketTestSendBitmapAllOffButton = nullptr; + QPushButton* appPacketTestSendTimeSyncButton = nullptr; + QSpinBox* appPacketTestThemeRedSpin = nullptr; + QSpinBox* appPacketTestThemeGreenSpin = nullptr; + QSpinBox* appPacketTestThemeBlueSpin = nullptr; + QPushButton* appPacketTestSendThemeButton = nullptr; + QPushButton* appPacketTestClearLogButton = nullptr; + bool appIsUpdatingFeatureUi = false; + bool appIsSequenceRecording = false; + int appSelectedFeatureId = 0; + QSet appSequenceRecordingPressedKeySet; -#if APP_ENABLE_DEBUG_WINDOW - // 调试面板实例。 - DEBUG::Debug_Panel* appDebugPanel = nullptr; -#endif - - // 周期轮询逻辑层的定时器。 QTimer appTimerPoll; - // APP 层持有的逻辑总状态。 + QTimer appTimerAutoRefreshDevice; Lgc_Core_Struct_State appLgcState; }; diff --git a/APP/APP_UIWindow_Feature.cpp b/APP/APP_UIWindow_Feature.cpp new file mode 100644 index 0000000..94833c6 --- /dev/null +++ b/APP/APP_UIWindow_Feature.cpp @@ -0,0 +1,291 @@ +#include "APP/APP_UIWindow.h" + +#include "APP/APP_KeyButton.h" +#include "APP/APP_UIWindow_Private.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace APP { + +namespace +{ + +QString App_Func_GetDefaultKeyDescription(quint16 Usage) +{ + return QStringLiteral( + "Default numpad key: %1. Left click simulates a real press/release. " + "Right click binds a function.") + .arg(Lgc_Core_GetUsageShortText(Usage)); +} + +} // namespace + +void App_UIWindow::App_Func_RefreshKeypadState() +{ + const Lgc_Core_Struct_View View = Lgc_Core_GetView(&appLgcState); + appKeypadModel.App_Func_SetNumLockOn(View.IsSystemNumLockOn); + + if (View.IsPhysicalKeyStateValid) + { + appKeypadModel.App_Func_SetPressedKeysFromUsageList(View.PhysicalUsageList); + return; + } + + if (View.IsVisibleKeyStateValid) + { + appKeypadModel.App_Func_SetPressedKeysFromUsageList(View.VisibleUsageList); + return; + } + + appKeypadModel.App_Func_ClearPressedKeys(); +} + +void App_UIWindow::App_Func_RefreshKeypadButtons() +{ + for (auto It = appKeypadButtonMap.begin(); It != appKeypadButtonMap.end(); ++It) + { + const QString KeyId = It.key(); + const quint16 Usage = appKeypadModel.App_Func_GetUsageFromKeyId(KeyId); + const int FeatureId = Lgc_Core_GetUsageFeatureId(&appLgcState, Usage); + APP_KeyButton* p_Button = It.value(); + + p_Button->App_Func_SetLatched( + appKeypadModel.App_Func_IsLatched(KeyId) || (FeatureId > 0)); + p_Button->App_Func_SetPressed(appKeypadModel.App_Func_IsPressed(KeyId)); + p_Button->App_Func_SetHintText( + FeatureId > 0 + ? Lgc_Core_GetFeatureNameById(&appLgcState, FeatureId) + : appKeypadModel.App_Func_GetDefaultHint(KeyId)); + p_Button->setToolTip( + FeatureId > 0 + ? Lgc_Core_GetFeatureDescriptionById(&appLgcState, FeatureId) + : App_Func_GetDefaultKeyDescription(Usage)); + } +} + +void App_UIWindow::App_Func_RefreshFeatureTable() +{ + const QVector FeatureIdList = Lgc_Core_GetFeatureIdList(&appLgcState); + const bool HasFeatures = !FeatureIdList.isEmpty(); + + QSignalBlocker Blocker(appFeatureTable); + appFeatureTable->clearContents(); + appFeatureTable->setRowCount(FeatureIdList.size()); + + int TargetRow = -1; + for (int Row = 0; Row < FeatureIdList.size(); ++Row) + { + const int FeatureId = FeatureIdList.at(Row); + const Lgc_FunctionFeature_Definition Feature = + Lgc_Core_GetFeature(&appLgcState, FeatureId); + QTableWidgetItem* p_NameItem = + new QTableWidgetItem(Lgc_Core_GetFeatureNameById(&appLgcState, FeatureId)); + p_NameItem->setData(Qt::UserRole, FeatureId); + appFeatureTable->setItem(Row, 0, p_NameItem); + appFeatureTable->setItem( + Row, + 1, + new QTableWidgetItem(Lgc_Core_GetFeatureDescriptionById(&appLgcState, FeatureId))); + appFeatureTable->setItem( + Row, + 2, + new QTableWidgetItem(Lgc_Core_GetFeatureTypeText(Feature.Type))); + if (FeatureId == appSelectedFeatureId) + { + TargetRow = Row; + } + } + + appSelectedFeatureId = + TargetRow >= 0 ? appSelectedFeatureId : (HasFeatures ? FeatureIdList.first() : 0); + TargetRow = TargetRow >= 0 ? TargetRow : (HasFeatures ? 0 : -1); + if (TargetRow >= 0) + { + appFeatureTable->selectRow(TargetRow); + } + else + { + appFeatureTable->clearSelection(); + } + + App_Func_UpdateFeatureEditorState(); +} + +void App_UIWindow::App_Func_RefreshFunctionStatus() +{ + const Lgc_Core_Struct_View View = Lgc_Core_GetView(&appLgcState); + appFunctionLabelStatus->setText( + View.TextFunctionStatus.isEmpty() + ? QStringLiteral("Waiting for function-key activity.") + : View.TextFunctionStatus); +} + +void App_UIWindow::App_Func_SelectFeature(int FeatureId, bool SwitchToFeaturePage) +{ + if (appIsSequenceRecording && (FeatureId != appSelectedFeatureId)) + { + App_Func_StopSequenceRecording(); + } + + appSelectedFeatureId = + Lgc_Core_GetFeature(&appLgcState, FeatureId).Id > 0 ? FeatureId : 0; + if (appSelectedFeatureId > 0) + { + for (int Row = 0; Row < appFeatureTable->rowCount(); ++Row) + { + QTableWidgetItem* p_Item = appFeatureTable->item(Row, 0); + if ((p_Item != nullptr) && (p_Item->data(Qt::UserRole).toInt() == appSelectedFeatureId)) + { + QSignalBlocker Blocker(appFeatureTable); + appFeatureTable->selectRow(Row); + break; + } + } + } + + App_Func_UpdateFeatureEditorState(); + if (SwitchToFeaturePage) + { + appPageTab->setCurrentIndex(appFeaturePageIndex); + } +} + +void App_UIWindow::App_Func_SaveFeatureFromUi() +{ + if (appSelectedFeatureId <= 0) + { + return; + } + + Lgc_FunctionFeature_Definition Feature = + Lgc_Core_GetFeature(&appLgcState, appSelectedFeatureId); + if (Feature.Id <= 0) + { + return; + } + + Feature.Name = appFeatureNameEdit->text(); + Feature.Description = appFeatureDescriptionEdit->text(); + Feature.Type = + static_cast(appFeatureTypeCombo->currentData().toInt()); + Feature.SequenceText = appFeatureSequenceEdit->text(); + Feature.WebsiteUrl = appFeatureWebsiteEdit->text(); + if ((Feature.Type == Lgc_FunctionFeature_Type::Website) && + Feature.WebsiteUrl.trimmed().isEmpty()) + { + Feature.WebsiteUrl = QStringLiteral("https://"); + QSignalBlocker Blocker(appFeatureWebsiteEdit); + appFeatureWebsiteEdit->setText(Feature.WebsiteUrl); + } + + Lgc_Core_UpdateFeature(&appLgcState, Feature); + + appFeatureEditorStack->setCurrentIndex(App_Func_GetFeatureStackIndex(Feature.Type)); + App_Func_UpdateFeatureEditorHeight(); + Lgc_Core_SaveFunctionConfig(&appLgcState); + App_Func_RefreshAfterLogicChange(); // FIX: keep Device Test and status text aligned after feature edits. +} + +void App_UIWindow::App_Func_UpdateFeatureEditorHeight() +{ + if ((appFeatureEditorStack == nullptr) || (appFeatureEditorStack->currentWidget() == nullptr)) + { + return; + } + + const int Height = appFeatureEditorStack->currentWidget()->sizeHint().height(); + if (appFeatureEditorStack->height() != Height) + { + appFeatureEditorStack->setFixedHeight(Height); + } +} + +void App_UIWindow::App_Func_UpdateFeatureEditorState() +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_Core_GetFeature(&appLgcState, appSelectedFeatureId); + const bool HasFeature = Feature.Id > 0; + + if (appIsSequenceRecording && (!HasFeature || !App_Func_IsKeyRecordFeatureType(Feature.Type))) + { + App_Func_StopSequenceRecording(); + } + + appFeatureNameEdit->setEnabled(HasFeature); + appFeatureDescriptionEdit->setEnabled(HasFeature); + appFeatureTypeCombo->setEnabled(HasFeature); + appFeatureEditorStack->setEnabled(HasFeature); + appFeatureDeleteButton->setEnabled(HasFeature); + + appIsUpdatingFeatureUi = true; + if (!HasFeature) + { + appFeatureNameEdit->clear(); + appFeatureDescriptionEdit->clear(); + appFeatureSequenceEdit->clear(); + appFeatureWebsiteEdit->clear(); + appFeatureTypeCombo->setCurrentIndex(0); + appFeatureEditorStack->setCurrentIndex(0); + App_Func_UpdateFeatureEditorHeight(); + appFeatureBindingSummaryLabel->setText(QStringLiteral("No function selected yet.")); + appIsUpdatingFeatureUi = false; + App_Func_UpdateSequenceRecordingUi(); + return; + } + + appFeatureNameEdit->setText(Feature.Name); + appFeatureDescriptionEdit->setText(Feature.Description); + const int TypeIndex = appFeatureTypeCombo->findData(static_cast(Feature.Type)); + if (TypeIndex >= 0) + { + appFeatureTypeCombo->setCurrentIndex(TypeIndex); + } + appFeatureSequenceEdit->setText(Feature.SequenceText); + appFeatureWebsiteEdit->setText(App_Func_GetWebsiteEditText(Feature.WebsiteUrl)); + appFeatureEditorStack->setCurrentIndex(App_Func_GetFeatureStackIndex(Feature.Type)); + appFeatureSequenceEdit->setPlaceholderText( + Feature.Type == Lgc_FunctionFeature_Type::KeySequence + ? QStringLiteral("Example: Ctrl+C -> Ctrl+A") + : QStringLiteral("Example: Ctrl+C")); + App_Func_UpdateFeatureEditorHeight(); + appFeatureBindingSummaryLabel->setText( + Lgc_Core_GetFeatureBindingSummary(&appLgcState, Feature.Id)); + appIsUpdatingFeatureUi = false; + App_Func_UpdateSequenceRecordingUi(); +} + +void App_UIWindow::App_Func_AddFeature() +{ + const int FeatureId = Lgc_Core_AddFeature(&appLgcState); + Lgc_Core_SaveFunctionConfig(&appLgcState); + App_Func_RefreshFeatureTable(); // FIX: the feature table must update before reselection. + App_Func_RefreshAfterLogicChange(); // FIX: refresh Device Test and current config state after add. + App_Func_SelectFeature(FeatureId, true); +} + +void App_UIWindow::App_Func_DeleteFeature() +{ + if (appSelectedFeatureId <= 0) + { + return; + } + + if (appIsSequenceRecording) + { + App_Func_StopSequenceRecording(); + } + + Lgc_Core_DeleteFeature(&appLgcState, appSelectedFeatureId); + appSelectedFeatureId = 0; + Lgc_Core_SaveFunctionConfig(&appLgcState); + App_Func_RefreshFeatureTable(); // FIX: rebuild the feature table after delete. + App_Func_RefreshAfterLogicChange(); // FIX: refresh Device Test and current config state after delete. +} + +} // namespace APP diff --git a/APP/APP_UIWindow_Private.h b/APP/APP_UIWindow_Private.h new file mode 100644 index 0000000..6f63568 --- /dev/null +++ b/APP/APP_UIWindow_Private.h @@ -0,0 +1,12 @@ +#pragma once + +#include "LOGIC/Lgc_Func_Button.h" +#include + +namespace APP { + +int App_Func_GetFeatureStackIndex(Lgc_FunctionFeature_Type Type); +bool App_Func_IsKeyRecordFeatureType(Lgc_FunctionFeature_Type Type); +QString App_Func_GetWebsiteEditText(const QString& UrlText); + +} // namespace APP diff --git a/APP/APP_UIWindow_Record.cpp b/APP/APP_UIWindow_Record.cpp new file mode 100644 index 0000000..c3e6a78 --- /dev/null +++ b/APP/APP_UIWindow_Record.cpp @@ -0,0 +1,427 @@ +#include "APP/APP_UIWindow.h" + +#include "APP/APP_UIWindow_Private.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace APP { + +namespace +{ + +bool App_Func_IsModifierRecordToken(const QString& Token) +{ + return (Token == QStringLiteral("Ctrl")) || + (Token == QStringLiteral("Shift")) || + (Token == QStringLiteral("Alt")) || + (Token == QStringLiteral("Win")); +} + +QStringList App_Func_GetOrderedRecordedModifierTokens(const QSet& PressedKeySet) +{ + const QStringList ModifierOrder = { + QStringLiteral("Ctrl"), + QStringLiteral("Shift"), + QStringLiteral("Alt"), + QStringLiteral("Win") + }; + + QStringList Result; + for (const QString& ModifierToken : ModifierOrder) + { + if (PressedKeySet.contains(ModifierToken)) + { + Result.append(ModifierToken); + } + } + return Result; +} + +QString App_Func_GetRecordedCombinationText( + const QSet& PressedKeySet, + const QString& TriggerToken) +{ + QStringList TokenList = App_Func_GetOrderedRecordedModifierTokens(PressedKeySet); + if (!TriggerToken.isEmpty() && !App_Func_IsModifierRecordToken(TriggerToken)) + { + TokenList.append(TriggerToken); + } + return TokenList.join(QStringLiteral("+")); +} + +bool App_Func_TryGetRawKeyboard(void* p_Message, RAWKEYBOARD& Keyboard) +{ + if (p_Message == nullptr) + { + return false; + } + + MSG* p_Msg = reinterpret_cast(p_Message); + if (p_Msg->message != WM_INPUT) + { + return false; + } + + UINT NeedSize = 0; + GetRawInputData( + reinterpret_cast(p_Msg->lParam), + RID_INPUT, + nullptr, + &NeedSize, + sizeof(RAWINPUTHEADER)); + if (NeedSize == 0) + { + return false; + } + + QByteArray Buffer(static_cast(NeedSize), 0); + if (GetRawInputData( + reinterpret_cast(p_Msg->lParam), + RID_INPUT, + Buffer.data(), + &NeedSize, + sizeof(RAWINPUTHEADER)) == static_cast(-1)) + { + return false; + } + + const RAWINPUT* p_Input = reinterpret_cast(Buffer.constData()); + if (p_Input->header.dwType != RIM_TYPEKEYBOARD) + { + return false; + } + + Keyboard = p_Input->data.keyboard; + return true; +} + +QString App_Func_GetRecordedKeyToken(const RAWKEYBOARD& Keyboard) +{ + const bool IsE0 = (Keyboard.Flags & RI_KEY_E0) != 0; + const USHORT VirtualKey = Keyboard.VKey; + + if ((VirtualKey >= 'A') && (VirtualKey <= 'Z')) + { + return QString(QChar(static_cast(VirtualKey))); + } + if ((VirtualKey >= '0') && (VirtualKey <= '9')) + { + return QString(QChar(static_cast(VirtualKey))); + } + if ((VirtualKey >= VK_F1) && (VirtualKey <= VK_F24)) + { + return QStringLiteral("F%1").arg(VirtualKey - VK_F1 + 1); + } + + switch (VirtualKey) + { + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + return QStringLiteral("Ctrl"); + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + return QStringLiteral("Shift"); + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + return QStringLiteral("Alt"); + case VK_LWIN: + case VK_RWIN: + return QStringLiteral("Win"); + case VK_RETURN: + return IsE0 ? QStringLiteral("NumEnter") : QStringLiteral("Enter"); + case VK_SPACE: + return QStringLiteral("Space"); + case VK_TAB: + return QStringLiteral("Tab"); + case VK_ESCAPE: + return QStringLiteral("Esc"); + case VK_BACK: + return QStringLiteral("Backspace"); + case VK_DELETE: + return QStringLiteral("Delete"); + case VK_INSERT: + return QStringLiteral("Insert"); + case VK_HOME: + return QStringLiteral("Home"); + case VK_END: + return QStringLiteral("End"); + case VK_PRIOR: + return QStringLiteral("PageUp"); + case VK_NEXT: + return QStringLiteral("PageDown"); + case VK_LEFT: + return QStringLiteral("Left"); + case VK_RIGHT: + return QStringLiteral("Right"); + case VK_UP: + return QStringLiteral("Up"); + case VK_DOWN: + return QStringLiteral("Down"); + case VK_CAPITAL: + return QStringLiteral("CapsLock"); + case VK_SNAPSHOT: + return QStringLiteral("PrintScreen"); + case VK_SCROLL: + return QStringLiteral("ScrollLock"); + case VK_PAUSE: + return QStringLiteral("Pause"); + case VK_NUMPAD0: + return QStringLiteral("Num0"); + case VK_NUMPAD1: + return QStringLiteral("Num1"); + case VK_NUMPAD2: + return QStringLiteral("Num2"); + case VK_NUMPAD3: + return QStringLiteral("Num3"); + case VK_NUMPAD4: + return QStringLiteral("Num4"); + case VK_NUMPAD5: + return QStringLiteral("Num5"); + case VK_NUMPAD6: + return QStringLiteral("Num6"); + case VK_NUMPAD7: + return QStringLiteral("Num7"); + case VK_NUMPAD8: + return QStringLiteral("Num8"); + case VK_NUMPAD9: + return QStringLiteral("Num9"); + case VK_DIVIDE: + return QStringLiteral("Num/"); + case VK_MULTIPLY: + return QStringLiteral("Num*"); + case VK_SUBTRACT: + return QStringLiteral("Num-"); + case VK_ADD: + return QStringLiteral("Num+"); + case VK_DECIMAL: + return QStringLiteral("Num."); + case VK_OEM_COMMA: + return QStringLiteral("Comma"); + case VK_OEM_PERIOD: + return QStringLiteral("Period"); + case VK_OEM_1: + return QStringLiteral("Semicolon"); + case VK_OEM_2: + return QStringLiteral("Slash"); + case VK_OEM_3: + return QStringLiteral("Grave"); + case VK_OEM_4: + return QStringLiteral("LeftBracket"); + case VK_OEM_5: + return QStringLiteral("Backslash"); + case VK_OEM_6: + return QStringLiteral("RightBracket"); + case VK_OEM_7: + return QStringLiteral("Quote"); + case VK_OEM_MINUS: + return QStringLiteral("Minus"); + case VK_OEM_PLUS: + return QStringLiteral("Equal"); + default: + return QString(); + } +} + +} // namespace + +void App_UIWindow::App_Func_StartSequenceRecording() +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_Core_GetFeature(&appLgcState, appSelectedFeatureId); + if ((Feature.Id <= 0) || !App_Func_IsKeyRecordFeatureType(Feature.Type)) + { + return; + } + + { + QSignalBlocker Blocker(appFeatureSequenceEdit); + appFeatureSequenceEdit->setText(QString()); + } + + App_Func_SaveFeatureFromUi(); + if (!Lgc_Core_BeginSequenceRecording(&appLgcState, appSelectedFeatureId)) + { + return; + } + + appIsSequenceRecording = true; + appSequenceRecordingPressedKeySet.clear(); + App_Func_UpdateSequenceRecordingUi(); + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + update(); + appFeatureSequenceEdit->setFocus(Qt::OtherFocusReason); +} + +void App_UIWindow::App_Func_StopSequenceRecording() +{ + Lgc_Core_EndSequenceRecording(&appLgcState); + appIsSequenceRecording = false; + appSequenceRecordingPressedKeySet.clear(); + App_Func_UpdateSequenceRecordingUi(); + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + update(); +} + +void App_UIWindow::App_Func_UpdateSequenceRecordingUi() +{ + const Lgc_FunctionFeature_Definition Feature = + Lgc_Core_GetFeature(&appLgcState, appSelectedFeatureId); + const bool HasFeature = Feature.Id > 0; + const bool CanRecord = HasFeature && App_Func_IsKeyRecordFeatureType(Feature.Type); + const bool IsLocked = appIsSequenceRecording; + + appFeatureTable->setEnabled(!IsLocked); + appFeatureAddButton->setEnabled(!IsLocked); + appFeatureDeleteButton->setEnabled(HasFeature && !IsLocked); + appFeatureNameEdit->setEnabled(HasFeature && !IsLocked); + appFeatureDescriptionEdit->setEnabled(HasFeature && !IsLocked); + appFeatureTypeCombo->setEnabled(HasFeature && !IsLocked); + appFeatureWebsiteEdit->setEnabled(HasFeature && !IsLocked); + appFeatureSequenceEdit->setReadOnly(IsLocked); + appFeatureSequenceRecordStartButton->setEnabled(CanRecord && !IsLocked); + appFeatureSequenceRecordStopButton->setEnabled(CanRecord && IsLocked); +} + +bool App_UIWindow::App_Func_HandleSequenceRecordMessage(void* p_Message) +{ + if (!appIsSequenceRecording) + { + return false; + } + + RAWKEYBOARD Keyboard = {}; + if (!App_Func_TryGetRawKeyboard(p_Message, Keyboard)) + { + return false; + } + + const QString Token = App_Func_GetRecordedKeyToken(Keyboard); + if (Token.isEmpty()) + { + return false; + } + + const bool IsPressed = (Keyboard.Flags & RI_KEY_BREAK) == 0; + if (IsPressed) + { + if (appSequenceRecordingPressedKeySet.contains(Token)) + { + return false; + } + + appSequenceRecordingPressedKeySet.insert(Token); + if (!App_Func_IsModifierRecordToken(Token)) + { + const Lgc_FunctionFeature_Definition Feature = + Lgc_Core_GetFeature(&appLgcState, appSelectedFeatureId); + const QString CombinationText = + App_Func_GetRecordedCombinationText(appSequenceRecordingPressedKeySet, Token); + if ((Feature.Id <= 0) || + !App_Func_IsKeyRecordFeatureType(Feature.Type) || + CombinationText.isEmpty()) + { + return false; + } + + const QString OldText = appFeatureSequenceEdit->text().trimmed(); + const QString NewText = + Feature.Type == Lgc_FunctionFeature_Type::KeySequence + ? (OldText.isEmpty() + ? CombinationText + : QStringLiteral("%1 -> %2").arg(OldText, CombinationText)) + : CombinationText; + { + QSignalBlocker Blocker(appFeatureSequenceEdit); + appFeatureSequenceEdit->setText(NewText); + appFeatureSequenceEdit->setCursorPosition(NewText.size()); + } + + App_Func_SaveFeatureFromUi(); + Lgc_Core_UpdateSequenceRecordingStatus(&appLgcState, NewText); + App_Func_RefreshKeypadButtons(); + App_Func_RefreshFunctionStatus(); + update(); + return true; + } + return false; + } + + appSequenceRecordingPressedKeySet.remove(Token); + return false; +} +void App_UIWindow::App_Func_ShowKeyMenu(quint16 Usage, const QPoint& GlobalPos) +{ + const int CurrentFeatureId = Lgc_Core_GetUsageFeatureId(&appLgcState, Usage); + QMenu Menu(this); + + QAction* p_NoneAction = Menu.addAction(QStringLiteral("No Function")); + p_NoneAction->setCheckable(true); + p_NoneAction->setChecked(CurrentFeatureId <= 0); + p_NoneAction->setData(0); + + const QVector FeatureIdList = Lgc_Core_GetFeatureIdList(&appLgcState); + if (!FeatureIdList.isEmpty()) + { + Menu.addSeparator(); + for (int FeatureId : FeatureIdList) + { + QAction* p_Action = + Menu.addAction(QStringLiteral("Bind to %1") + .arg(Lgc_Core_GetFeatureNameById(&appLgcState, FeatureId))); + p_Action->setCheckable(true); + p_Action->setChecked(FeatureId == CurrentFeatureId); + p_Action->setData(FeatureId); + p_Action->setToolTip(Lgc_Core_GetFeatureDescriptionById(&appLgcState, FeatureId)); + } + } + else + { + Menu.addSeparator(); + QAction* p_EmptyAction = Menu.addAction(QStringLiteral("No function yet. Please add one first.")); + p_EmptyAction->setEnabled(false); + } + + Menu.addSeparator(); + QAction* p_OpenFeaturePageAction = Menu.addAction(QStringLiteral("Open Function List")); + p_OpenFeaturePageAction->setData(-1); + + QAction* p_SelectedAction = Menu.exec(GlobalPos); + if (p_SelectedAction == nullptr) + { + return; + } + + const int ActionData = p_SelectedAction->data().toInt(); + if (ActionData == -1) + { + App_Func_SelectFeature(CurrentFeatureId, true); + return; + } + + if (!Lgc_Core_BindUsageToFeature(&appLgcState, Usage, ActionData)) + { + return; + } + + Lgc_Core_SaveFunctionConfig(&appLgcState); + App_Func_RefreshFeatureTable(); // FIX: keep the feature summary in sync after right-click binding. + App_Func_RefreshAfterLogicChange(); // FIX: refresh Device Test logs and auto-sync state immediately. + const int FeatureToSelect = ActionData > 0 ? ActionData : CurrentFeatureId; // FIX + if (FeatureToSelect > 0) + { + App_Func_SelectFeature(FeatureToSelect); + } +} + +} // namespace APP diff --git a/COM/Com_Def.h b/COM/Com_Def.h new file mode 100644 index 0000000..e2dfc87 --- /dev/null +++ b/COM/Com_Def.h @@ -0,0 +1,35 @@ +#pragma once + +#include "COM/Com_Protocol.h" +#include +#include + +enum Com_Enum_RawPacketSource : quint8 +{ + Com_Enum_RawPacketSource_None = 0, + Com_Enum_RawPacketSource_UsbCdc, + Com_Enum_RawPacketSource_BleNus +}; + +const quint16 COM_CONST_VENDOR_ID_DEFAULT = 0x1209; +const quint16 COM_CONST_PRODUCT_ID_DEFAULT = 0x0001; +const int COM_CONST_KEYBOARD_USAGE_MAX = 0x00E7; +const int COM_CONST_USAGE_BITMAP_SIZE = 29; + +struct Com_Struct_DeviceConfig +{ + quint16 VendorId = COM_CONST_VENDOR_ID_DEFAULT; + quint16 ProductId = COM_CONST_PRODUCT_ID_DEFAULT; +}; + +struct Com_Struct_RawPacket +{ + bool IsValid = false; + Com_Enum_RawPacketSource Source = Com_Enum_RawPacketSource_None; + Com_Enum_ProtocolType ProtocolType = Com_Enum_ProtocolType_None; + QByteArray ByteArray; + QString PortName; + + // Stable candidate id used by LOGIC to confirm or discard a transport candidate. + QString EndpointId; +}; diff --git a/COM/Com_Protocol.cpp b/COM/Com_Protocol.cpp new file mode 100644 index 0000000..856bdba --- /dev/null +++ b/COM/Com_Protocol.cpp @@ -0,0 +1,975 @@ +#include "COM/Com_Protocol.h" + +namespace +{ + +constexpr quint8 kProtoWireVarint = 0; +constexpr quint8 kProtoWireFixed64 = 1; +constexpr quint8 kProtoWireBytes = 2; +constexpr quint8 kProtoWireFixed32 = 5; + +constexpr quint32 kFieldHelloReq = 1; +constexpr quint32 kFieldHelloRsp = 2; +constexpr quint32 kFieldBitmap = 3; +constexpr quint32 kFieldFunctionKeyEvent = 4; +constexpr quint32 kFieldLedState = 5; +constexpr quint32 kFieldTimeSync = 6; +constexpr quint32 kFieldThemeRgb = 7; +constexpr quint32 kFieldAck = 8; +constexpr quint32 kFieldError = 9; + +constexpr quint16 kUsageModifierFirst = 0x00E0; +constexpr quint16 kUsageModifierLast = 0x00E7; +constexpr quint16 kUsageNormalLast = 0x00DF; +constexpr int kUsageBitmapSize = 29; + +bool Com_Protocol_ReadVarint(const QByteArray& Bytes, int* p_Offset, quint64* p_Value) +{ + quint64 Value = 0; + int Shift = 0; + int Offset = *p_Offset; + + while (Offset < Bytes.size()) + { + const quint8 Byte = static_cast(Bytes.at(Offset++)); + Value |= static_cast(Byte & 0x7FU) << Shift; + if ((Byte & 0x80U) == 0U) + { + *p_Offset = Offset; + *p_Value = Value; + return true; + } + + Shift += 7; + if (Shift >= 64) + { + return false; + } + } + + return false; +} + +bool Com_Protocol_ReadKey( + const QByteArray& Bytes, + int* p_Offset, + quint32* p_FieldNumber, + quint8* p_WireType) +{ + quint64 Key = 0; + if (!Com_Protocol_ReadVarint(Bytes, p_Offset, &Key)) + { + return false; + } + + *p_FieldNumber = static_cast(Key >> 3); + *p_WireType = static_cast(Key & 0x07U); + return *p_FieldNumber > 0; +} + +bool Com_Protocol_ReadLengthDelimited( + const QByteArray& Bytes, + int* p_Offset, + QByteArray* p_Value) +{ + quint64 Length = 0; + if (!Com_Protocol_ReadVarint(Bytes, p_Offset, &Length)) + { + return false; + } + + if ((Length > static_cast(Bytes.size())) || + (*p_Offset > Bytes.size() - static_cast(Length))) + { + return false; + } + + *p_Value = Bytes.mid(*p_Offset, static_cast(Length)); + *p_Offset += static_cast(Length); + return true; +} + +bool Com_Protocol_ReadFixed32(const QByteArray& Bytes, int* p_Offset, quint32* p_Value) +{ + if (*p_Offset > Bytes.size() - 4) + { + return false; + } + + quint32 Value = 0; + for (int Index = 0; Index < 4; ++Index) + { + Value |= static_cast( + static_cast(Bytes.at(*p_Offset + Index))) << (Index * 8); + } + + *p_Offset += 4; + *p_Value = Value; + return true; +} + +bool Com_Protocol_ReadFixed64(const QByteArray& Bytes, int* p_Offset, quint64* p_Value) +{ + if (*p_Offset > Bytes.size() - 8) + { + return false; + } + + quint64 Value = 0; + for (int Index = 0; Index < 8; ++Index) + { + Value |= static_cast( + static_cast(Bytes.at(*p_Offset + Index))) << (Index * 8); + } + + *p_Offset += 8; + *p_Value = Value; + return true; +} + +bool Com_Protocol_SkipField(const QByteArray& Bytes, int* p_Offset, quint8 WireType) +{ + switch (WireType) + { + case kProtoWireVarint: + { + quint64 Dummy = 0; + return Com_Protocol_ReadVarint(Bytes, p_Offset, &Dummy); + } + + case kProtoWireFixed64: + if (*p_Offset > Bytes.size() - 8) + { + return false; + } + *p_Offset += 8; + return true; + + case kProtoWireBytes: + { + QByteArray Dummy; + return Com_Protocol_ReadLengthDelimited(Bytes, p_Offset, &Dummy); + } + + case kProtoWireFixed32: + if (*p_Offset > Bytes.size() - 4) + { + return false; + } + *p_Offset += 4; + return true; + + default: + return false; + } +} + +void Com_Protocol_AppendVarint(QByteArray* p_Bytes, quint64 Value) +{ + do + { + quint8 Byte = static_cast(Value & 0x7FU); + Value >>= 7; + if (Value != 0) + { + Byte = static_cast(Byte | 0x80U); + } + p_Bytes->append(static_cast(Byte)); + } while (Value != 0); +} + +void Com_Protocol_AppendKey(QByteArray* p_Bytes, quint32 FieldNumber, quint8 WireType) +{ + Com_Protocol_AppendVarint( + p_Bytes, + (static_cast(FieldNumber) << 3) | static_cast(WireType)); +} + +void Com_Protocol_AppendUInt32Field(QByteArray* p_Bytes, quint32 FieldNumber, quint32 Value) +{ + Com_Protocol_AppendKey(p_Bytes, FieldNumber, kProtoWireVarint); + Com_Protocol_AppendVarint(p_Bytes, Value); +} + +void Com_Protocol_AppendSInt32Field(QByteArray* p_Bytes, quint32 FieldNumber, qint32 Value) +{ + const quint32 ZigZag = static_cast( + (static_cast(Value) << 1) ^ static_cast(Value >> 31)); + Com_Protocol_AppendUInt32Field(p_Bytes, FieldNumber, ZigZag); +} + +void Com_Protocol_AppendFixed32Field(QByteArray* p_Bytes, quint32 FieldNumber, quint32 Value) +{ + Com_Protocol_AppendKey(p_Bytes, FieldNumber, kProtoWireFixed32); + for (int Index = 0; Index < 4; ++Index) + { + p_Bytes->append(static_cast((Value >> (Index * 8)) & 0xFFU)); + } +} + +void Com_Protocol_AppendFixed64Field(QByteArray* p_Bytes, quint32 FieldNumber, quint64 Value) +{ + Com_Protocol_AppendKey(p_Bytes, FieldNumber, kProtoWireFixed64); + for (int Index = 0; Index < 8; ++Index) + { + p_Bytes->append(static_cast((Value >> (Index * 8)) & 0xFFU)); + } +} + +void Com_Protocol_AppendBytesField( + QByteArray* p_Bytes, + quint32 FieldNumber, + const QByteArray& Value) +{ + Com_Protocol_AppendKey(p_Bytes, FieldNumber, kProtoWireBytes); + Com_Protocol_AppendVarint(p_Bytes, static_cast(Value.size())); + p_Bytes->append(Value); +} + +quint32 Com_Protocol_FieldNumberForType(Com_Enum_ProtocolType Type) +{ + switch (Type) + { + case Com_Enum_ProtocolType_HelloReq: return kFieldHelloReq; + case Com_Enum_ProtocolType_HelloRsp: return kFieldHelloRsp; + case Com_Enum_ProtocolType_Bitmap: return kFieldBitmap; + case Com_Enum_ProtocolType_FunctionKeyEvent: return kFieldFunctionKeyEvent; + case Com_Enum_ProtocolType_LedState: return kFieldLedState; + case Com_Enum_ProtocolType_TimeSync: return kFieldTimeSync; + case Com_Enum_ProtocolType_ThemeRgb: return kFieldThemeRgb; + case Com_Enum_ProtocolType_Ack: return kFieldAck; + case Com_Enum_ProtocolType_Error: return kFieldError; + default: + return 0; + } +} + +Com_Enum_ProtocolType Com_Protocol_TypeForFieldNumber(quint32 FieldNumber) +{ + switch (FieldNumber) + { + case kFieldHelloReq: return Com_Enum_ProtocolType_HelloReq; + case kFieldHelloRsp: return Com_Enum_ProtocolType_HelloRsp; + case kFieldBitmap: return Com_Enum_ProtocolType_Bitmap; + case kFieldFunctionKeyEvent: return Com_Enum_ProtocolType_FunctionKeyEvent; + case kFieldLedState: return Com_Enum_ProtocolType_LedState; + case kFieldTimeSync: return Com_Enum_ProtocolType_TimeSync; + case kFieldThemeRgb: return Com_Enum_ProtocolType_ThemeRgb; + case kFieldAck: return Com_Enum_ProtocolType_Ack; + case kFieldError: return Com_Enum_ProtocolType_Error; + default: + return Com_Enum_ProtocolType_None; + } +} + +QByteArray Com_Protocol_EncodeEnvelope(Com_Enum_ProtocolType Type, const QByteArray& MessageBytes) +{ + QByteArray PacketBody; + const quint32 FieldNumber = Com_Protocol_FieldNumberForType(Type); + if ((FieldNumber == 0) || MessageBytes.isEmpty()) + { + return PacketBody; + } + + Com_Protocol_AppendBytesField(&PacketBody, FieldNumber, MessageBytes); + return PacketBody; +} + +bool Com_Protocol_DecodeEnvelope( + const QByteArray& PacketBody, + Com_Enum_ProtocolType* p_Type, + QByteArray* p_MessageBytes) +{ + int Offset = 0; + while (Offset < PacketBody.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + if (!Com_Protocol_ReadKey(PacketBody, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + const Com_Enum_ProtocolType Type = Com_Protocol_TypeForFieldNumber(FieldNumber); + if (Type == Com_Enum_ProtocolType_None) + { + if (!Com_Protocol_SkipField(PacketBody, &Offset, WireType)) + { + return false; + } + continue; + } + + if (WireType != kProtoWireBytes) + { + return false; + } + + QByteArray MessageBytes; + if (!Com_Protocol_ReadLengthDelimited(PacketBody, &Offset, &MessageBytes)) + { + return false; + } + + *p_Type = Type; + *p_MessageBytes = MessageBytes; + return true; + } + + return false; +} + +bool Com_Protocol_DecodeEnvelopeForExpectedType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType ExpectedType, + QByteArray* p_MessageBytes) +{ + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + if (!Com_Protocol_DecodeEnvelope(PacketBody, &Type, p_MessageBytes)) + { + return false; + } + + return Type == ExpectedType; +} + +bool Com_Protocol_DecodeHelloRspMessage( + const QByteArray& MessageBytes, + Com_Struct_ProtocolHelloRsp* p_Message) +{ + *p_Message = Com_Struct_ProtocolHelloRsp(); + int Offset = 0; + while (Offset < MessageBytes.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + quint64 Value = 0; + if (!Com_Protocol_ReadKey(MessageBytes, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + switch (FieldNumber) + { + case 1: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->ProtocolVersion = static_cast(Value); + break; + + case 2: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->VendorId = static_cast(Value); + break; + + case 3: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->ProductId = static_cast(Value); + break; + + case 4: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->FirmwareMajor = static_cast(Value); + break; + + case 5: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->FirmwareMinor = static_cast(Value); + break; + + case 6: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->CapabilityFlags = static_cast(Value); + break; + + default: + if (!Com_Protocol_SkipField(MessageBytes, &Offset, WireType)) + { + return false; + } + break; + } + } + + return true; +} + +bool Com_Protocol_DecodeUsageBitmapMessage( + const QByteArray& MessageBytes, + QByteArray* p_UsageBitmap) +{ + p_UsageBitmap->clear(); + + int Offset = 0; + while (Offset < MessageBytes.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + if (!Com_Protocol_ReadKey(MessageBytes, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + if (FieldNumber == 1) + { + if ((WireType != kProtoWireBytes) || + !Com_Protocol_ReadLengthDelimited(MessageBytes, &Offset, p_UsageBitmap)) + { + return false; + } + + return Com_Protocol_IsUsageBitmapValid(*p_UsageBitmap); + } + + if (!Com_Protocol_SkipField(MessageBytes, &Offset, WireType)) + { + return false; + } + } + + return false; +} + +bool Com_Protocol_DecodeFunctionKeyEventMessage( + const QByteArray& MessageBytes, + Com_Struct_ProtocolFunctionKeyEvent* p_Message) +{ + *p_Message = Com_Struct_ProtocolFunctionKeyEvent(); + + bool HasBitmap = false; + bool HasLegacyUsage = false; + bool HasLegacyAction = false; + + int Offset = 0; + while (Offset < MessageBytes.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + quint64 Value = 0; + if (!Com_Protocol_ReadKey(MessageBytes, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + switch (FieldNumber) + { + case 1: + if (WireType == kProtoWireBytes) + { + if (!Com_Protocol_ReadLengthDelimited( + MessageBytes, + &Offset, + &p_Message->UsageBitmap) || + !Com_Protocol_IsUsageBitmapValid(p_Message->UsageBitmap)) + { + return false; + } + + HasBitmap = true; + break; + } + + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + + if (Value > 0xFFFFU) + { + return false; + } + + p_Message->Usage = static_cast(Value); + HasLegacyUsage = true; + break; + + case 2: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + + p_Message->Action = static_cast(Value); + HasLegacyAction = true; + break; + + default: + if (!Com_Protocol_SkipField(MessageBytes, &Offset, WireType)) + { + return false; + } + break; + } + } + + p_Message->HasUsageAction = HasLegacyUsage && HasLegacyAction; + return HasBitmap || p_Message->HasUsageAction; +} + +bool Com_Protocol_DecodeLedStateMessage( + const QByteArray& MessageBytes, + Com_Struct_ProtocolLedState* p_Message) +{ + *p_Message = Com_Struct_ProtocolLedState(); + int Offset = 0; + while (Offset < MessageBytes.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + quint64 Value = 0; + if (!Com_Protocol_ReadKey(MessageBytes, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + if (FieldNumber == 1) + { + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + + p_Message->LedMask = static_cast(Value); + continue; + } + + if (!Com_Protocol_SkipField(MessageBytes, &Offset, WireType)) + { + return false; + } + } + + return true; +} + +bool Com_Protocol_DecodeAckMessage( + const QByteArray& MessageBytes, + Com_Struct_ProtocolAck* p_Message) +{ + *p_Message = Com_Struct_ProtocolAck(); + int Offset = 0; + while (Offset < MessageBytes.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + quint64 Value = 0; + if (!Com_Protocol_ReadKey(MessageBytes, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + if (FieldNumber == 1) + { + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + + p_Message->AckedType = static_cast(Value); + continue; + } + + if (!Com_Protocol_SkipField(MessageBytes, &Offset, WireType)) + { + return false; + } + } + + return true; +} + +bool Com_Protocol_DecodeErrorMessage( + const QByteArray& MessageBytes, + Com_Struct_ProtocolError* p_Message) +{ + *p_Message = Com_Struct_ProtocolError(); + int Offset = 0; + while (Offset < MessageBytes.size()) + { + quint32 FieldNumber = 0; + quint8 WireType = 0; + quint64 Value = 0; + if (!Com_Protocol_ReadKey(MessageBytes, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + switch (FieldNumber) + { + case 1: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->ErrorType = static_cast(Value); + break; + + case 2: + if ((WireType != kProtoWireVarint) || + !Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value)) + { + return false; + } + p_Message->ErrorCode = static_cast(Value); + break; + + default: + if (!Com_Protocol_SkipField(MessageBytes, &Offset, WireType)) + { + return false; + } + break; + } + } + + return true; +} + +bool Com_Protocol_GetBitmapPosition(quint16 Usage, int* p_ByteIndex, quint8* p_BitMask) +{ + if ((p_ByteIndex == nullptr) || (p_BitMask == nullptr)) + { + return false; + } + + if ((Usage >= kUsageModifierFirst) && (Usage <= kUsageModifierLast)) + { + *p_ByteIndex = 0; + *p_BitMask = static_cast(1U << (Usage - kUsageModifierFirst)); + return true; + } + + if (Usage <= kUsageNormalLast) + { + *p_ByteIndex = 1 + static_cast(Usage / 8U); + *p_BitMask = static_cast(1U << (Usage % 8U)); + return true; + } + + return false; +} + +} // namespace + +bool Com_Protocol_DecodeMessageType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType* p_Type) +{ + if (p_Type == nullptr) + { + return false; + } + + QByteArray MessageBytes; + return Com_Protocol_DecodeEnvelope(PacketBody, p_Type, &MessageBytes); +} + +bool Com_Protocol_TryTakePacket( + QByteArray* p_StreamBuffer, + QByteArray* p_PacketBody, + Com_Enum_ProtocolType* p_Type) +{ + if ((p_StreamBuffer == nullptr) || + (p_PacketBody == nullptr) || + (p_Type == nullptr)) + { + return false; + } + + p_PacketBody->clear(); + *p_Type = Com_Enum_ProtocolType_None; + if (p_StreamBuffer->isEmpty()) + { + return false; + } + + int Offset = 0; + quint32 FieldNumber = 0; + quint8 WireType = 0; + if (!Com_Protocol_ReadKey(*p_StreamBuffer, &Offset, &FieldNumber, &WireType)) + { + return false; + } + + const Com_Enum_ProtocolType Type = Com_Protocol_TypeForFieldNumber(FieldNumber); + if ((Type == Com_Enum_ProtocolType_None) || (WireType != kProtoWireBytes)) + { + return false; + } + + QByteArray MessageBytes; + if (!Com_Protocol_ReadLengthDelimited(*p_StreamBuffer, &Offset, &MessageBytes)) + { + return false; + } + + *p_PacketBody = p_StreamBuffer->left(Offset); + *p_Type = Type; + p_StreamBuffer->remove(0, Offset); + return true; +} + +QByteArray Com_Protocol_EncodeHelloReq() +{ + QByteArray MessageBytes; + Com_Protocol_AppendUInt32Field(&MessageBytes, 1, 1); + return Com_Protocol_EncodeEnvelope(Com_Enum_ProtocolType_HelloReq, MessageBytes); +} + +QByteArray Com_Protocol_EncodeBitmap(const QByteArray& UsageBitmap) +{ + if (!Com_Protocol_IsUsageBitmapValid(UsageBitmap)) + { + return QByteArray(); + } + + QByteArray MessageBytes; + Com_Protocol_AppendBytesField(&MessageBytes, 1, UsageBitmap); + return Com_Protocol_EncodeEnvelope(Com_Enum_ProtocolType_Bitmap, MessageBytes); +} + +QByteArray Com_Protocol_EncodeTimeSync( + quint32 Version, + quint32 Flags, + qint32 TimezoneMinutes, + quint64 UtcMilliseconds, + quint32 AccuracyMilliseconds) +{ + QByteArray MessageBytes; + Com_Protocol_AppendUInt32Field(&MessageBytes, 1, Version); + Com_Protocol_AppendUInt32Field(&MessageBytes, 2, Flags); + Com_Protocol_AppendSInt32Field(&MessageBytes, 3, TimezoneMinutes); + Com_Protocol_AppendFixed64Field(&MessageBytes, 4, UtcMilliseconds); + Com_Protocol_AppendFixed32Field(&MessageBytes, 5, AccuracyMilliseconds); + return Com_Protocol_EncodeEnvelope(Com_Enum_ProtocolType_TimeSync, MessageBytes); +} + +QByteArray Com_Protocol_EncodeThemeRgb(quint8 Red, quint8 Green, quint8 Blue) +{ + QByteArray MessageBytes; + Com_Protocol_AppendUInt32Field(&MessageBytes, 1, Red); + Com_Protocol_AppendUInt32Field(&MessageBytes, 2, Green); + Com_Protocol_AppendUInt32Field(&MessageBytes, 3, Blue); + return Com_Protocol_EncodeEnvelope(Com_Enum_ProtocolType_ThemeRgb, MessageBytes); +} + +bool Com_Protocol_DecodeHelloRsp( + const QByteArray& PacketBody, + Com_Struct_ProtocolHelloRsp* p_Message) +{ + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + QByteArray MessageBytes; + if ((p_Message == nullptr) || + !Com_Protocol_DecodeEnvelope(PacketBody, &Type, &MessageBytes) || + (Type != Com_Enum_ProtocolType_HelloRsp)) + { + return false; + } + + return Com_Protocol_DecodeHelloRspMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_DecodeFunctionKeyEvent( + const QByteArray& PacketBody, + Com_Struct_ProtocolFunctionKeyEvent* p_Message) +{ + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + QByteArray MessageBytes; + if ((p_Message == nullptr) || + !Com_Protocol_DecodeEnvelope(PacketBody, &Type, &MessageBytes) || + (Type != Com_Enum_ProtocolType_FunctionKeyEvent)) + { + return false; + } + + return Com_Protocol_DecodeFunctionKeyEventMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_DecodeLedState( + const QByteArray& PacketBody, + Com_Struct_ProtocolLedState* p_Message) +{ + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + QByteArray MessageBytes; + if ((p_Message == nullptr) || + !Com_Protocol_DecodeEnvelope(PacketBody, &Type, &MessageBytes) || + (Type != Com_Enum_ProtocolType_LedState)) + { + return false; + } + + return Com_Protocol_DecodeLedStateMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_DecodeAck( + const QByteArray& PacketBody, + Com_Struct_ProtocolAck* p_Message) +{ + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + QByteArray MessageBytes; + if ((p_Message == nullptr) || + !Com_Protocol_DecodeEnvelope(PacketBody, &Type, &MessageBytes) || + (Type != Com_Enum_ProtocolType_Ack)) + { + return false; + } + + return Com_Protocol_DecodeAckMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_DecodeError( + const QByteArray& PacketBody, + Com_Struct_ProtocolError* p_Message) +{ + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + QByteArray MessageBytes; + if ((p_Message == nullptr) || + !Com_Protocol_DecodeEnvelope(PacketBody, &Type, &MessageBytes) || + (Type != Com_Enum_ProtocolType_Error)) + { + return false; + } + + return Com_Protocol_DecodeErrorMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_DecodeAckForType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType EnvelopeType, + Com_Struct_ProtocolAck* p_Message) +{ + QByteArray MessageBytes; + if ((p_Message == nullptr) || + (EnvelopeType != Com_Enum_ProtocolType_Ack) || + !Com_Protocol_DecodeEnvelopeForExpectedType( + PacketBody, + Com_Enum_ProtocolType_Ack, + &MessageBytes)) + { + return false; + } + + return Com_Protocol_DecodeAckMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_DecodeErrorForType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType EnvelopeType, + Com_Struct_ProtocolError* p_Message) +{ + QByteArray MessageBytes; + if ((p_Message == nullptr) || + (EnvelopeType != Com_Enum_ProtocolType_Error) || + !Com_Protocol_DecodeEnvelopeForExpectedType( + PacketBody, + Com_Enum_ProtocolType_Error, + &MessageBytes)) + { + return false; + } + + return Com_Protocol_DecodeErrorMessage(MessageBytes, p_Message); +} + +bool Com_Protocol_IsUsageBitmapValid(const QByteArray& UsageBitmap) +{ + return UsageBitmap.size() == kUsageBitmapSize; +} + +QByteArray Com_Protocol_CreateUsageBitmap() +{ + return QByteArray(kUsageBitmapSize, 0); +} + +bool Com_Protocol_TestUsageBitmapBit(const QByteArray& UsageBitmap, quint16 Usage) +{ + int ByteIndex = 0; + quint8 BitMask = 0; + if (!Com_Protocol_IsUsageBitmapValid(UsageBitmap) || + !Com_Protocol_GetBitmapPosition(Usage, &ByteIndex, &BitMask)) + { + return false; + } + + return (static_cast(UsageBitmap.at(ByteIndex)) & BitMask) != 0U; +} + +bool Com_Protocol_SetUsageBitmapBit(QByteArray* p_UsageBitmap, quint16 Usage, bool IsPressed) +{ + int ByteIndex = 0; + quint8 BitMask = 0; + if ((p_UsageBitmap == nullptr) || + !Com_Protocol_IsUsageBitmapValid(*p_UsageBitmap) || + !Com_Protocol_GetBitmapPosition(Usage, &ByteIndex, &BitMask)) + { + return false; + } + + quint8 ByteValue = static_cast(p_UsageBitmap->at(ByteIndex)); + if (IsPressed) + { + ByteValue = static_cast(ByteValue | BitMask); + } + else + { + ByteValue = static_cast(ByteValue & ~BitMask); + } + + (*p_UsageBitmap)[ByteIndex] = static_cast(ByteValue); + return true; +} + +QVector Com_Protocol_BuildPressedUsageList(const QByteArray& UsageBitmap) +{ + QVector UsageList; + if (!Com_Protocol_IsUsageBitmapValid(UsageBitmap)) + { + return UsageList; + } + + for (quint16 Usage = 0; Usage <= kUsageNormalLast; ++Usage) + { + if (Com_Protocol_TestUsageBitmapBit(UsageBitmap, Usage)) + { + UsageList.append(Usage); + } + } + + for (quint16 Usage = kUsageModifierFirst; Usage <= kUsageModifierLast; ++Usage) + { + if (Com_Protocol_TestUsageBitmapBit(UsageBitmap, Usage)) + { + UsageList.append(Usage); + } + } + + return UsageList; +} diff --git a/COM/Com_Protocol.h b/COM/Com_Protocol.h new file mode 100644 index 0000000..98b9351 --- /dev/null +++ b/COM/Com_Protocol.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include + +enum Com_Enum_ProtocolType : quint8 +{ + Com_Enum_ProtocolType_None = 0x00, + Com_Enum_ProtocolType_HelloReq = 0x01, + Com_Enum_ProtocolType_HelloRsp = 0x02, + Com_Enum_ProtocolType_Bitmap = 0x10, + Com_Enum_ProtocolType_FunctionKeyEvent = 0x20, + Com_Enum_ProtocolType_LedState = 0x21, + Com_Enum_ProtocolType_TimeSync = 0x30, + Com_Enum_ProtocolType_ThemeRgb = 0x31, + Com_Enum_ProtocolType_Ack = 0x7E, + Com_Enum_ProtocolType_Error = 0x7F +}; + +enum Com_Enum_ProtocolErrorCode : quint32 +{ + Com_Enum_ProtocolErrorCode_None = 0, + Com_Enum_ProtocolErrorCode_UnknownType = 1, + Com_Enum_ProtocolErrorCode_InvalidLength = 2, + Com_Enum_ProtocolErrorCode_InvalidParam = 3, + Com_Enum_ProtocolErrorCode_NotReady = 4 +}; + +enum Com_Enum_ProtocolKeyAction : quint32 +{ + Com_Enum_ProtocolKeyAction_None = 0, + Com_Enum_ProtocolKeyAction_Press = 1, + Com_Enum_ProtocolKeyAction_Release = 2 +}; + +struct Com_Struct_ProtocolHelloRsp +{ + quint32 ProtocolVersion = 0; + quint32 VendorId = 0; + quint32 ProductId = 0; + quint32 FirmwareMajor = 0; + quint32 FirmwareMinor = 0; + quint32 CapabilityFlags = 0; +}; + +struct Com_Struct_ProtocolFunctionKeyEvent +{ + QByteArray UsageBitmap; + bool HasUsageAction = false; + quint16 Usage = 0; + quint32 Action = Com_Enum_ProtocolKeyAction_None; +}; + +struct Com_Struct_ProtocolLedState +{ + quint32 LedMask = 0; +}; + +struct Com_Struct_ProtocolAck +{ + quint32 AckedType = 0; +}; + +struct Com_Struct_ProtocolError +{ + quint32 ErrorType = 0; + quint32 ErrorCode = 0; +}; + +bool Com_Protocol_DecodeMessageType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType* p_Type); +bool Com_Protocol_TryTakePacket( + QByteArray* p_StreamBuffer, + QByteArray* p_PacketBody, + Com_Enum_ProtocolType* p_Type); + +QByteArray Com_Protocol_EncodeHelloReq(); +QByteArray Com_Protocol_EncodeBitmap(const QByteArray& UsageBitmap); +QByteArray Com_Protocol_EncodeTimeSync( + quint32 Version, + quint32 Flags, + qint32 TimezoneMinutes, + quint64 UtcMilliseconds, + quint32 AccuracyMilliseconds); +QByteArray Com_Protocol_EncodeThemeRgb(quint8 Red, quint8 Green, quint8 Blue); + +bool Com_Protocol_DecodeHelloRsp( + const QByteArray& PacketBody, + Com_Struct_ProtocolHelloRsp* p_Message); +bool Com_Protocol_DecodeFunctionKeyEvent( + const QByteArray& PacketBody, + Com_Struct_ProtocolFunctionKeyEvent* p_Message); +bool Com_Protocol_DecodeLedState( + const QByteArray& PacketBody, + Com_Struct_ProtocolLedState* p_Message); +bool Com_Protocol_DecodeAck( + const QByteArray& PacketBody, + Com_Struct_ProtocolAck* p_Message); +bool Com_Protocol_DecodeError( + const QByteArray& PacketBody, + Com_Struct_ProtocolError* p_Message); +bool Com_Protocol_DecodeAckForType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType EnvelopeType, + Com_Struct_ProtocolAck* p_Message); +bool Com_Protocol_DecodeErrorForType( + const QByteArray& PacketBody, + Com_Enum_ProtocolType EnvelopeType, + Com_Struct_ProtocolError* p_Message); + +bool Com_Protocol_IsUsageBitmapValid(const QByteArray& UsageBitmap); +QByteArray Com_Protocol_CreateUsageBitmap(); +bool Com_Protocol_TestUsageBitmapBit(const QByteArray& UsageBitmap, quint16 Usage); +bool Com_Protocol_SetUsageBitmapBit(QByteArray* p_UsageBitmap, quint16 Usage, bool IsPressed); +QVector Com_Protocol_BuildPressedUsageList(const QByteArray& UsageBitmap); diff --git a/DRI/Dri_Ble.cpp b/DRI/Dri_Ble.cpp new file mode 100644 index 0000000..9690c00 --- /dev/null +++ b/DRI/Dri_Ble.cpp @@ -0,0 +1,817 @@ +#include "DRI/Dri_Ble.h" +#include "DRI/Dri_Hid.h" +#include "MID/Mid_Ble.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "BluetoothAPIs.lib") +#pragma comment(lib, "hid.lib") +#pragma comment(lib, "setupapi.lib") + +struct Dri_Ble_Struct_ServiceContext; +struct Dri_Ble_Struct_HidPort; +struct Dri_Ble_Struct_Subscription +{ + Dri_Ble_Struct_Port* Port = nullptr; + Dri_Ble_Struct_ServiceContext* Service = nullptr; + BLUETOOTH_GATT_EVENT_HANDLE Event = nullptr; + QString DeviceLabel; + QString ServiceUuid; + QString CharUuid; +}; +struct Dri_Ble_Struct_Context +{ + QMutex Mutex; + QList Queue; + QVector Services; + QVector Hids; + std::atomic CallbackCount { 0 }; +}; +struct Dri_Ble_Struct_ServiceContext +{ + Dri_Ble_Struct_Context* Ctx = nullptr; + HANDLE Handle = INVALID_HANDLE_VALUE; + QString Uuid; + QVector Subs; +}; +struct Dri_Ble_Struct_HidPort +{ + Dri_Hid_Struct_ReadPort ReadPort; + HANDLE WriteHandle = INVALID_HANDLE_VALUE; + quint16 UsagePage = 0; + quint16 Usage = 0; + quint16 OutputLength = 0; +}; + +namespace +{ + +struct ServiceIf { QString Path; QString InstanceId; QString Uuid; }; +struct DeviceIf { QString Path; QString InstanceId; QString Address; }; +struct HidIf +{ + QString Path; + QString InstanceId; + quint16 VendorId = 0; + quint16 ProductId = 0; + quint16 UsagePage = 0; + quint16 Usage = 0; + quint16 InputLength = 0; + quint16 OutputLength = 0; +}; +struct PnpIf +{ + QString Address; + quint16 VendorId = 0; + quint16 ProductId = 0; +}; +const QString kSvcHidBrace = QStringLiteral("{00001812-0000-1000-8000-00805F9B34FB}"); +const QString kSvcCustom = QStringLiteral("0B7F5000-38D2-4F62-8F6F-36C4FD73A110"); +const QString kSvcDis = QStringLiteral("0000180A-0000-1000-8000-00805F9B34FB"); +const QString kChrPnpId = QStringLiteral("00002A50-0000-1000-8000-00805F9B34FB"); +const quint16 kUsagePageKeyboard = 0x0001; +const quint16 kUsageKeyboard = 0x0006; + +QString HResultText(HRESULT hr) { return QStringLiteral("0x%1").arg(static_cast(hr), 8, 16, QLatin1Char('0')).toUpper(); } +bool IsCommandHid(quint16 page, quint16 usage) { return (page == MID_CONST_USAGE_PAGE_VENDOR_COMMAND) && (usage == MID_CONST_USAGE_VENDOR_COMMAND); } +bool IsUsefulHid(quint16 page, quint16 usage) { return ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) || ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) || ((page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR)) || IsCommandHid(page, usage); } +bool IsVendorHid(quint16 page, quint16 usage) { return (page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR); } +Mid_Enum_RawPacketSource HidPacketSource(quint16 page, quint16 usage) +{ + if ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) return Mid_Enum_RawPacketSource_BleHidKeyboard; + if ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) return Mid_Enum_RawPacketSource_BleHidConsumer; + if (IsVendorHid(page, usage)) return Mid_Enum_RawPacketSource_BleHidVendor; + if (IsCommandHid(page, usage)) return Mid_Enum_RawPacketSource_BleHidVendorCommand; + return Mid_Enum_RawPacketSource_None; +} +QString ExtractAddress(const QString& text) { auto m = QRegularExpression(QStringLiteral("([0-9A-F]{12})(?=[\\\\&_]|$)")).match(text.toUpper()); return m.hasMatch() ? m.captured(1) : QString(); } +QString ServiceUuidFromId(const QString& id) { auto m = QRegularExpression(QStringLiteral("\\{([0-9A-Fa-f-]{36})\\}")).match(id); return m.hasMatch() ? m.captured(1).toUpper() : QString(); } +QString DeviceLabel(const QString& addr) { return addr.isEmpty() ? QStringLiteral("BLE") : QStringLiteral("BLE-%1").arg(addr); } +QString FormatVidPidLabel(quint16 vendorId, quint16 productId) +{ + return QStringLiteral("0x%1:0x%2") + .arg(vendorId, 4, 16, QLatin1Char('0')) + .arg(productId, 4, 16, QLatin1Char('0')) + .toUpper(); +} +QString PortName(const QString& dev, const QString& src) { const QString base = dev.isEmpty() ? QStringLiteral("Bluetooth") : QStringLiteral("Bluetooth(%1)").arg(dev); return src.isEmpty() ? base : QStringLiteral("%1/%2").arg(base, src); } +QString HidSource(quint16 page, quint16 usage) +{ + if ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) return QStringLiteral("HID Keyboard"); + if ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) return QStringLiteral("HID Consumer"); + if ((page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR)) return QStringLiteral("HID Vendor"); + if (IsCommandHid(page, usage)) return QStringLiteral("HID Vendor Command"); + return QStringLiteral("HID UsagePage 0x%1 / Usage 0x%2").arg(page, 4, 16, QLatin1Char('0')).arg(usage, 4, 16, QLatin1Char('0')).toUpper(); +} +QString DeviceInstanceId(HDEVINFO set, SP_DEVINFO_DATA* info) +{ + DWORD need = 0; + SetupDiGetDeviceInstanceIdW(set, info, nullptr, 0, &need); + if (need == 0) return QString(); + QVector buf(static_cast(need) + 1, 0); + return SetupDiGetDeviceInstanceIdW(set, info, buf.data(), static_cast(buf.size()), nullptr) ? QString::fromWCharArray(buf.constData()).trimmed() : QString(); +} +bool InterfacePath(HDEVINFO set, SP_DEVICE_INTERFACE_DATA* itf, SP_DEVINFO_DATA* info, QString* path) +{ + DWORD need = 0; + SetupDiGetDeviceInterfaceDetailW(set, itf, nullptr, 0, &need, info); + if (need == 0) return false; + QByteArray buf(static_cast(need), 0); + auto* detail = reinterpret_cast(buf.data()); + detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); + if (!SetupDiGetDeviceInterfaceDetailW(set, itf, detail, need, nullptr, info)) return false; + *path = QString::fromWCharArray(detail->DevicePath); + return true; +} +QUuid QtUuid(const BTH_LE_UUID& uuid) +{ + if (uuid.IsShortUuid) return QUuid(QStringLiteral("{%1-0000-1000-8000-00805F9B34FB}").arg(uuid.Value.ShortUuid, 4, 16, QLatin1Char('0'))); + const GUID& g = uuid.Value.LongUuid; + return QUuid(g.Data1, g.Data2, g.Data3, g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3], g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]); +} +QString CharText(const BTH_LE_GATT_CHARACTERISTIC& c) +{ + const QString uuid = QtUuid(c.CharacteristicUuid).toString(QUuid::WithoutBraces).toUpper(); + return uuid == QStringLiteral("00000000-0000-0000-0000-000000000000") ? QStringLiteral("HANDLE 0x%1").arg(c.CharacteristicValueHandle, 4, 16, QLatin1Char('0')).toUpper() : uuid; +} +HANDLE OpenService(const QString& path) +{ + HANDLE handle = CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + return handle != INVALID_HANDLE_VALUE ? handle : CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); +} +void QueuePacket( + Dri_Ble_Struct_Context* ctx, + Mid_Enum_RawPacketSource source, + const QString& portName, + const QByteArray& bytes) +{ + if ((ctx == nullptr) || bytes.isEmpty()) return; + Mid_Struct_RawPacket packet; + packet.IsValid = true; + packet.Source = source; + packet.PortName = portName; + packet.ByteArray = bytes; + QMutexLocker lock(&ctx->Mutex); ctx->Queue.append(packet); +} + +QVector EnumServices() +{ + QVector out; + HDEVINFO set = SetupDiGetClassDevsW(&GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return out; + SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE, i, &itf); ++i) + { + SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; + QString path; + if (!InterfacePath(set, &itf, &info, &path)) continue; + ServiceIf item; item.Path = path; item.InstanceId = DeviceInstanceId(set, &info); item.Uuid = ServiceUuidFromId(item.InstanceId); out.append(item); + } + SetupDiDestroyDeviceInfoList(set); + return out; +} + +QVector EnumBleDevices() +{ + QVector out; + HDEVINFO set = SetupDiGetClassDevsW( + &GUID_BLUETOOTHLE_DEVICE_INTERFACE, + nullptr, + nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return out; + + SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_BLUETOOTHLE_DEVICE_INTERFACE, i, &itf); ++i) + { + SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; + QString path; + if (!InterfacePath(set, &itf, &info, &path)) continue; + + DeviceIf item; + item.Path = path; + item.InstanceId = DeviceInstanceId(set, &info); + item.Address = ExtractAddress(item.InstanceId); + if (item.Address.isEmpty()) continue; + out.append(item); + } + + SetupDiDestroyDeviceInfoList(set); + return out; +} + +QVector EnumBleHids() +{ + QVector out; + GUID hidGuid; HidD_GetHidGuid(&hidGuid); + HDEVINFO set = SetupDiGetClassDevsW(&hidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); + if (set == INVALID_HANDLE_VALUE) return out; + SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; + for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &hidGuid, i, &itf); ++i) + { + SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; + QString path; + if (!InterfacePath(set, &itf, &info, &path)) continue; + const QString id = DeviceInstanceId(set, &info).toUpper(); + if (!id.contains(kSvcHidBrace)) continue; + HANDLE handle = CreateFileW(reinterpret_cast(path.utf16()), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if (handle == INVALID_HANDLE_VALUE) continue; + HIDD_ATTRIBUTES attrs = {}; + attrs.Size = sizeof(attrs); + PHIDP_PREPARSED_DATA prep = nullptr; + HIDP_CAPS caps = {}; + const bool ok = + HidD_GetAttributes(handle, &attrs) && + HidD_GetPreparsedData(handle, &prep) && + (HidP_GetCaps(prep, &caps) == HIDP_STATUS_SUCCESS) && + IsUsefulHid(caps.UsagePage, caps.Usage) && + (caps.InputReportByteLength > 0); + if (prep != nullptr) HidD_FreePreparsedData(prep); + CloseHandle(handle); + if (!ok) continue; + HidIf item; + item.Path = path; + item.InstanceId = id; + item.VendorId = attrs.VendorID; + item.ProductId = attrs.ProductID; + item.UsagePage = caps.UsagePage; + item.Usage = caps.Usage; + item.InputLength = caps.InputReportByteLength; + item.OutputLength = caps.OutputReportByteLength; + out.append(item); + } + SetupDiDestroyDeviceInfoList(set); + return out; +} + +QVector Characteristics(HANDLE handle) +{ + USHORT count = 0; + HRESULT hr = BluetoothGATTGetCharacteristics(handle, nullptr, 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {}; + QVector out(count); + hr = BluetoothGATTGetCharacteristics(handle, nullptr, count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) return {}; + out.resize(count); return out; +} + +QVector Descriptors(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c) +{ + USHORT count = 0; + HRESULT hr = BluetoothGATTGetDescriptors(handle, const_cast(&c), 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {}; + QVector out(count); + hr = BluetoothGATTGetDescriptors(handle, const_cast(&c), count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) return {}; + out.resize(count); return out; +} + +bool ReadValue(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c, QByteArray* out) +{ + USHORT need = 0; + HRESULT hr = BluetoothGATTGetCharacteristicValue(handle, const_cast(&c), 0, nullptr, &need, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return false; + if (need == 0) return false; + QByteArray buffer(static_cast(need), 0); + auto* value = reinterpret_cast(buffer.data()); + hr = BluetoothGATTGetCharacteristicValue(handle, const_cast(&c), need, value, nullptr, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE); + if (FAILED(hr)) return false; + *out = QByteArray(reinterpret_cast(value->Data), static_cast(value->DataSize)); + return true; +} + +VOID CALLBACK OnGattEvent(BTH_LE_GATT_EVENT_TYPE type, PVOID out, PVOID context) +{ + auto* sub = static_cast(context); + if ((sub == nullptr) || (sub->Service == nullptr) || (sub->Service->Ctx == nullptr)) return; + Dri_Ble_Struct_Context* ctx = sub->Service->Ctx; + ctx->CallbackCount.fetch_add(1, std::memory_order_acq_rel); + if ((type == CharacteristicValueChangedEvent) && (out != nullptr) && (sub->Port != nullptr)) + { + const auto* evt = static_cast(out); + if ((evt->CharacteristicValue != nullptr) && (evt->CharacteristicValue->DataSize > 0)) QueuePacket(ctx, Mid_Enum_RawPacketSource_BleGatt, PortName(sub->DeviceLabel, QStringLiteral("SVC %1 / CHR %2").arg(sub->ServiceUuid, sub->CharUuid)), QByteArray(reinterpret_cast(evt->CharacteristicValue->Data), static_cast(evt->CharacteristicValue->DataSize))); + } + ctx->CallbackCount.fetch_sub(1, std::memory_order_acq_rel); +} + +bool Subscribe(Dri_Ble_Struct_Port* port, Dri_Ble_Struct_ServiceContext* svc, const QString& deviceLabel, const BTH_LE_GATT_CHARACTERISTIC& c, QString* error) +{ + auto* sub = new Dri_Ble_Struct_Subscription(); + sub->Port = port; sub->Service = svc; sub->DeviceLabel = deviceLabel; sub->ServiceUuid = svc->Uuid; sub->CharUuid = CharText(c); + BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION reg = {}; reg.NumCharacteristics = 1; reg.Characteristics[0] = c; + HRESULT hr = BluetoothGATTRegisterEvent(svc->Handle, CharacteristicValueChangedEvent, ®, OnGattEvent, sub, &sub->Event, BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) { if (error != nullptr) *error = QStringLiteral("BLE notify register failed: %1 / %2 / %3").arg(svc->Uuid, sub->CharUuid, HResultText(hr)); delete sub; return false; } + bool configured = false; + for (const BTH_LE_GATT_DESCRIPTOR& d : Descriptors(svc->Handle, c)) + { + if (d.DescriptorType != ClientCharacteristicConfiguration) continue; + BTH_LE_GATT_DESCRIPTOR_VALUE value = {}; value.DescriptorType = ClientCharacteristicConfiguration; + value.ClientCharacteristicConfiguration.IsSubscribeToNotification = c.IsNotifiable; + value.ClientCharacteristicConfiguration.IsSubscribeToIndication = c.IsIndicatable; + if (SUCCEEDED(BluetoothGATTSetDescriptorValue(svc->Handle, const_cast(&d), &value, BLUETOOTH_GATT_FLAG_NONE))) { configured = true; break; } + } + if (!configured) + { + if (sub->Event != nullptr) BluetoothGATTUnregisterEvent(sub->Event, BLUETOOTH_GATT_FLAG_NONE); + if (error != nullptr) *error = QStringLiteral("BLE notify enable failed: %1 / %2").arg(svc->Uuid, sub->CharUuid); + delete sub; + return false; + } + svc->Subs.append(sub); + return true; +} + +bool AttachService(Dri_Ble_Struct_Port* port, Dri_Ble_Struct_Context* ctx, const ServiceIf& item, const QString& deviceLabel, int* charCount, int* subCount) +{ + auto* svc = new Dri_Ble_Struct_ServiceContext(); + svc->Ctx = ctx; svc->Uuid = item.Uuid; svc->Handle = OpenService(item.Path); + if (svc->Handle == INVALID_HANDLE_VALUE) { delete svc; return false; } + const auto list = Characteristics(svc->Handle); + if (list.isEmpty()) { CloseHandle(svc->Handle); delete svc; return false; } + bool keep = false; + for (const BTH_LE_GATT_CHARACTERISTIC& c : list) + { + if (!c.IsReadable && !c.IsNotifiable && !c.IsIndicatable) continue; + const QString charUuid = CharText(c); + ++(*charCount); + if (c.IsNotifiable || c.IsIndicatable) { if (Subscribe(port, svc, deviceLabel, c, nullptr)) { keep = true; ++(*subCount); } } + if (c.IsReadable) { QByteArray bytes; if (ReadValue(svc->Handle, c, &bytes) && !bytes.isEmpty()) QueuePacket(ctx, Mid_Enum_RawPacketSource_BleGatt, PortName(deviceLabel, QStringLiteral("SVC %1 / CHR %2").arg(item.Uuid, charUuid)), bytes); } + } + if (keep) { ctx->Services.append(svc); return true; } + CloseHandle(svc->Handle); + delete svc; + return true; +} + +void CloseHidPort(Dri_Ble_Struct_HidPort* hid) +{ + Dri_Hid_CloseReadPort(&hid->ReadPort); + if ((hid->WriteHandle != nullptr) && (hid->WriteHandle != INVALID_HANDLE_VALUE)) CloseHandle(hid->WriteHandle); + delete hid; +} + +bool OpenHidPort(Dri_Ble_Struct_Context* ctx, const HidIf& item, const QString& deviceLabel) +{ + auto* hid = new Dri_Ble_Struct_HidPort(); + const QString source = HidSource(item.UsagePage, item.Usage); + hid->UsagePage = item.UsagePage; + hid->Usage = item.Usage; + hid->OutputLength = item.OutputLength; + QString textError; + if (!Dri_Hid_InitReadPort( + &hid->ReadPort, + item.Path, + item.InputLength, + HidPacketSource(item.UsagePage, item.Usage), + PortName(deviceLabel, source), + &textError)) + { + delete hid; + return false; + } + if (item.OutputLength > 0) + { + hid->WriteHandle = Dri_Hid_OpenWriteHandle(item.Path, nullptr); + } + ctx->Hids.append(hid); + return true; +} + +QString FormatAddressLabel(const QString& Address) +{ + const QString Formatted = Mid_FormatBleAddress(Address); + return Formatted.isEmpty() ? QStringLiteral("(unknown)") : Formatted; +} + +bool ParsePnpIdValue(const QByteArray& bytes, quint16* vendorId, quint16* productId) +{ + if (bytes.size() < 7) return false; + const quint8 vendorSource = static_cast(bytes.at(0)); + // Only accept USB-IF sourced PnP IDs so the fields map directly to USB VID/PID. + if (vendorSource != 0x02U) return false; + *vendorId = + static_cast(bytes.at(1)) | + (static_cast(static_cast(bytes.at(2))) << 8); + *productId = + static_cast(bytes.at(3)) | + (static_cast(static_cast(bytes.at(4))) << 8); + return true; +} + +QVector EnumPnps(const QVector& deviceList, QStringList* p_DebugList) +{ + QVector out; + for (const DeviceIf& device : deviceList) + { + HANDLE handle = OpenService(device.Path); + if (handle == INVALID_HANDLE_VALUE) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 打开 BLE 设备失败 %1") + .arg(FormatAddressLabel(device.Address))); + } + continue; + } + + USHORT serviceCount = 0; + HRESULT hr = BluetoothGATTGetServices( + handle, + 0, + nullptr, + &serviceCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 枚举服务失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + CloseHandle(handle); + continue; + } + + QVector services(serviceCount); + hr = BluetoothGATTGetServices( + handle, + serviceCount, + services.data(), + &serviceCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 读取服务列表失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + CloseHandle(handle); + continue; + } + services.resize(serviceCount); + + QByteArray valueBytes; + bool found = false; + quint16 vendorId = 0; + quint16 productId = 0; + QStringList serviceUuidList; + QStringList charUuidList; + bool hasPnpChar = false; + + for (const BTH_LE_GATT_SERVICE& service : services) + { + const QUuid serviceUuid = QtUuid(service.ServiceUuid); + const QString serviceUuidText = + serviceUuid.toString(QUuid::WithoutBraces).toUpper(); + serviceUuidList.append(serviceUuidText); + + USHORT charCount = 0; + hr = BluetoothGATTGetCharacteristics( + handle, + const_cast(&service), + 0, + nullptr, + &charCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 枚举 180A 特征失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + break; + } + + QVector chars(charCount); + hr = BluetoothGATTGetCharacteristics( + handle, + const_cast(&service), + charCount, + chars.data(), + &charCount, + BLUETOOTH_GATT_FLAG_NONE); + if (FAILED(hr)) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 读取 180A 特征失败 %1 / hr=%2") + .arg(FormatAddressLabel(device.Address), HResultText(hr))); + } + break; + } + chars.resize(charCount); + + for (const BTH_LE_GATT_CHARACTERISTIC& characteristic : chars) + { + const QString charUuid = CharText(characteristic); + charUuidList.append(QStringLiteral("%1/%2").arg(serviceUuidText, charUuid)); + if (charUuid != kChrPnpId) continue; + hasPnpChar = true; + if (!ReadValue(handle, characteristic, &valueBytes)) + { + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: 发现 2A50 但读取失败 %1 / svc=%2") + .arg(FormatAddressLabel(device.Address), serviceUuidText)); + } + break; + } + found = ParsePnpIdValue(valueBytes, &vendorId, &productId); + if (p_DebugList != nullptr) + { + p_DebugList->append( + QStringLiteral("PnP: %1 / svc=%2 / raw=%3 / parsed=%4") + .arg(FormatAddressLabel(device.Address)) + .arg(serviceUuidText) + .arg(Mid_GetHexText(valueBytes)) + .arg(found + ? FormatVidPidLabel(vendorId, productId) + : QStringLiteral("invalid"))); + } + break; + } + break; + } + + CloseHandle(handle); + if (!hasPnpChar && (p_DebugList != nullptr)) + { + p_DebugList->append( + QStringLiteral("PnP: 未发现 2A50 %1 / services=%2 / chars=%3") + .arg(FormatAddressLabel(device.Address)) + .arg(serviceUuidList.join(QStringLiteral(", "))) + .arg(charUuidList.join(QStringLiteral(", ")))); + } + if (!found) continue; + + bool exists = false; + for (const PnpIf& item : out) + { + if (item.Address == device.Address) + { + exists = true; + break; + } + } + if (exists) continue; + + PnpIf item; + item.Address = device.Address; + item.VendorId = vendorId; + item.ProductId = productId; + out.append(item); + } + return out; +} + +QStringList CollectPnpAddresses( + const QVector& pnpList, + quint16 vendorId, + quint16 productId) +{ + QStringList addressList; + for (const PnpIf& pnp : pnpList) + { + if ((pnp.VendorId == vendorId) && (pnp.ProductId == productId) && + !addressList.contains(pnp.Address)) + { + addressList.append(pnp.Address); + } + } + return addressList; +} + +QStringList CollectHidAddresses(const QVector& HidList) +{ + QStringList AddressList; + for (const HidIf& Hid : HidList) + { + const QString Address = ExtractAddress(Hid.InstanceId); + if (!Address.isEmpty() && !AddressList.contains(Address)) AddressList.append(Address); + } + return AddressList; +} + +QString BuildCandidateSummary( + const QVector& pnpList, + const QVector& HidList, + const QVector& ServiceList, + const QStringList& AllowedAddressList) +{ + QHash HidCountMap; + QHash GattCountMap; + QHash PnpTextMap; + for (const PnpIf& pnp : pnpList) + { + PnpTextMap.insert( + pnp.Address, + FormatVidPidLabel(pnp.VendorId, pnp.ProductId)); + } + for (const HidIf& Hid : HidList) + { + const QString Address = ExtractAddress(Hid.InstanceId); + if (!Address.isEmpty()) ++HidCountMap[Address]; + } + for (const ServiceIf& Service : ServiceList) + { + if (Service.Uuid != kSvcCustom) continue; + const QString Address = ExtractAddress(Service.InstanceId); + if (!Address.isEmpty() && AllowedAddressList.contains(Address)) ++GattCountMap[Address]; + } + + QStringList AddressList = HidCountMap.keys(); + for (const QString& Address : GattCountMap.keys()) if (!AddressList.contains(Address)) AddressList.append(Address); + if (AddressList.isEmpty()) return QStringLiteral("无"); + + std::sort(AddressList.begin(), AddressList.end()); + QStringList SummaryList; + for (const QString& Address : AddressList) + { + SummaryList.append( + QStringLiteral("%1 [pnp=%2, hid=%3, gatt=%4]") + .arg(FormatAddressLabel(Address)) + .arg(PnpTextMap.value(Address, QStringLiteral("unknown"))) + .arg(HidCountMap.value(Address)) + .arg(GattCountMap.value(Address))); + } + return SummaryList.join(QLatin1Char('\n')); +} + +QString BuildPnpSummary( + const QVector& pnpList, + quint16 vendorId, + quint16 productId) +{ + QStringList lineList; + for (const PnpIf& pnp : pnpList) + { + lineList.append( + QStringLiteral("%1 -> %2") + .arg(FormatAddressLabel(pnp.Address)) + .arg(FormatVidPidLabel(pnp.VendorId, pnp.ProductId))); + } + + if (lineList.isEmpty()) + { + return QStringLiteral( + "PnP 扫描结果:未发现任何 DIS/PnP ID。"); + } + + QString summary = QStringLiteral( + "PnP 扫描结果(目标 %1):\n%2") + .arg(FormatVidPidLabel(vendorId, productId)) + .arg(lineList.join(QLatin1Char('\n'))); + return summary; +} + +QString BuildPnpProbeSummary(const QStringList& debugList) +{ + if (debugList.isEmpty()) + { + return QStringLiteral("PnP 探测日志:无。"); + } + + return QStringLiteral("PnP 探测日志:\n%1").arg(debugList.join(QLatin1Char('\n'))); +} + +} // namespace + +void Dri_Ble_Close(Dri_Ble_Struct_Port* port) +{ + Dri_Ble_Struct_Context* ctx = port->p_Context; + if (ctx != nullptr) + { + for (Dri_Ble_Struct_HidPort* hid : ctx->Hids) CloseHidPort(hid); + for (Dri_Ble_Struct_ServiceContext* svc : ctx->Services) for (Dri_Ble_Struct_Subscription* sub : svc->Subs) { sub->Port = nullptr; if (sub->Event != nullptr) BluetoothGATTUnregisterEvent(sub->Event, BLUETOOTH_GATT_FLAG_NONE); } + while (ctx->CallbackCount.load(std::memory_order_acquire) > 0) Sleep(1); + for (Dri_Ble_Struct_ServiceContext* svc : ctx->Services) + { + for (Dri_Ble_Struct_Subscription* sub : svc->Subs) delete sub; + if ((svc->Handle != nullptr) && (svc->Handle != INVALID_HANDLE_VALUE)) CloseHandle(svc->Handle); + delete svc; + } + delete ctx; + } + *port = Dri_Ble_Struct_Port(); +} +bool Dri_Ble_Init(Dri_Ble_Struct_Port* port, const Mid_Struct_DeviceConfig& deviceConfig, QString* textStatus) +{ + Dri_Ble_Close(port); + port->IsOpened = true; + port->p_Context = new Dri_Ble_Struct_Context(); + + const QVector devices = EnumBleDevices(); + const QVector hids = EnumBleHids(); + const QVector services = EnumServices(); + QStringList pnpDebugList; + const QVector pnps = EnumPnps(devices, &pnpDebugList); + const QStringList HidAddressList = CollectPnpAddresses( + pnps, + deviceConfig.VendorId, + deviceConfig.ProductId); + QString targetAddress; + QString SelectStatus; + if (HidAddressList.size() == 1) + { + targetAddress = HidAddressList.first(); + } + else + { + SelectStatus = HidAddressList.isEmpty() + ? QStringLiteral("未检测到匹配 PnP ID %1 的 BLE 设备。") + .arg(FormatVidPidLabel(deviceConfig.VendorId, deviceConfig.ProductId)) + : QStringLiteral("检测到多个匹配 PnP ID %1 的 BLE 设备,无法唯一绑定。") + .arg(FormatVidPidLabel(deviceConfig.VendorId, deviceConfig.ProductId)); + } + QString deviceLabel = targetAddress.isEmpty() ? QString() : DeviceLabel(targetAddress); + + int charCount = 0; + int subCount = 0; + if (!targetAddress.isEmpty()) + { + for (const ServiceIf& item : services) + { + if ((item.Uuid == kSvcCustom) && (ExtractAddress(item.InstanceId) == targetAddress)) AttachService(port, port->p_Context, item, deviceLabel, &charCount, &subCount); + } + } + + if (deviceLabel.isEmpty() && !targetAddress.isEmpty()) deviceLabel = DeviceLabel(targetAddress); + + int hidCount = 0; + if (!targetAddress.isEmpty()) + { + for (const HidIf& hid : hids) + { + if ((ExtractAddress(hid.InstanceId) == targetAddress) && OpenHidPort(port->p_Context, hid, deviceLabel)) ++hidCount; + } + } + + if (deviceLabel.isEmpty() && !hids.isEmpty()) deviceLabel = QStringLiteral("BLE"); + + port->IsConnected = (charCount > 0) || (hidCount > 0); + + + return port->IsConnected; +} +bool Dri_Ble_Read(Dri_Ble_Struct_Port* port, Mid_Struct_RawPacket* packet, QString*) +{ + *packet = Mid_Struct_RawPacket(); + packet->PortName = QStringLiteral("Bluetooth"); + if (!port->IsOpened || (port->p_Context == nullptr)) return false; + for (Dri_Ble_Struct_HidPort* hid : port->p_Context->Hids) + { + Mid_Struct_RawPacket hidPacket; + if (Dri_Hid_Read(&hid->ReadPort, &hidPacket, nullptr)) + { + QMutexLocker lock(&port->p_Context->Mutex); + port->p_Context->Queue.append(hidPacket); + } + } + QMutexLocker lock(&port->p_Context->Mutex); + if (port->p_Context->Queue.isEmpty()) return false; + *packet = port->p_Context->Queue.takeFirst(); + return packet->IsValid; +} +bool Dri_Ble_Write(Dri_Ble_Struct_Port* port, const QByteArray& bytes, QString* textStatus) +{ + if (!port->IsOpened || (port->p_Context == nullptr)) { if (textStatus != nullptr) *textStatus = QStringLiteral("Bluetooth port is not open, packet send was skipped."); return false; } + if (bytes.isEmpty()) { if (textStatus != nullptr) *textStatus = QStringLiteral("Bluetooth packet is empty."); return false; } + const auto TryWrite = [&](auto MatchFunc, const QString& OkText) + { + for (Dri_Ble_Struct_HidPort* hid : port->p_Context->Hids) + { + if (!MatchFunc(hid->UsagePage, hid->Usage) || (hid->WriteHandle == INVALID_HANDLE_VALUE)) continue; + if (Dri_Hid_WritePacket(hid->WriteHandle, hid->OutputLength, bytes, nullptr)) + { + if (textStatus != nullptr) *textStatus = OkText; + return true; + } + } + return false; + }; + if (TryWrite(IsCommandHid, QStringLiteral("Bluetooth Vendor command packet sent."))) return true; + if (TryWrite(IsVendorHid, QStringLiteral("Bluetooth Vendor packet sent."))) return true; + if (textStatus != nullptr) *textStatus = QStringLiteral("No writable Bluetooth Vendor endpoint was found."); + return false; +} + diff --git a/DRI/Dri_Ble.h b/DRI/Dri_Ble.h new file mode 100644 index 0000000..9da61da --- /dev/null +++ b/DRI/Dri_Ble.h @@ -0,0 +1,32 @@ +#pragma once + +#include "MID/Mid_Def.h" +#include + +struct Dri_Ble_Struct_Context; + +// BLE port combines two channels: +// 1. custom GATT notifications +// 2. BLE HID reports and writes +struct Dri_Ble_Struct_Port +{ + bool IsOpened = false; + bool IsConnected = false; + QString TextEndpointSummary; + Dri_Ble_Struct_Context* p_Context = nullptr; +}; + +void Dri_Ble_Close(Dri_Ble_Struct_Port* p_Port); +bool Dri_Ble_Init( + Dri_Ble_Struct_Port* p_Port, + const Mid_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus); +bool Dri_Ble_Read( + Dri_Ble_Struct_Port* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); +bool Dri_Ble_Write( + Dri_Ble_Struct_Port* p_Port, + const QByteArray& ByteArray, + QString* p_TextStatus); + diff --git a/DRI/Dri_Cdc.cpp b/DRI/Dri_Cdc.cpp new file mode 100644 index 0000000..50ca6f4 --- /dev/null +++ b/DRI/Dri_Cdc.cpp @@ -0,0 +1,623 @@ +#include "DRI/Dri_Cdc.h" + +#include "COM/Com_Protocol.h" +#include +#include +#include +#include +#include +#include + +struct Dri_Cdc_Struct_Candidate +{ + QSerialPort* p_SerialPort = nullptr; + QString PortName; + QString EndpointId; + QString SystemLocation; + QByteArray PendingBytes; + QQueue PacketQueue; +}; + +struct Dri_Cdc_Struct_Context +{ + QVector CandidateList; + int ActiveCandidateIndex = -1; +}; + +namespace +{ + +QString Dri_Cdc_FormatPortLabel(const QSerialPortInfo& PortInfo) +{ + const QString Description = PortInfo.description().trimmed(); + return Description.isEmpty() + ? QStringLiteral("USB CDC (%1)").arg(PortInfo.portName()) + : QStringLiteral("USB CDC (%1 - %2)").arg(PortInfo.portName(), Description); +} + +QString Dri_Cdc_BuildEndpointId(const QSerialPortInfo& PortInfo) +{ + const QString SystemLocation = QDir::cleanPath(PortInfo.systemLocation()); + if (!SystemLocation.isEmpty()) + { + return SystemLocation; + } + + return PortInfo.portName().trimmed(); +} + +bool Dri_Cdc_IsPortStillPresent(const Dri_Cdc_Struct_Candidate& Candidate) +{ + for (const QSerialPortInfo& PortInfo : QSerialPortInfo::availablePorts()) + { + if (!Candidate.SystemLocation.isEmpty() && + (QDir::cleanPath(PortInfo.systemLocation()) + .compare(Candidate.SystemLocation, Qt::CaseInsensitive) == 0)) + { + return true; + } + } + + return false; +} + +void Dri_Cdc_DeleteCandidate(Dri_Cdc_Struct_Candidate* p_Candidate) +{ + if (p_Candidate == nullptr) + { + return; + } + + if ((p_Candidate->p_SerialPort != nullptr) && p_Candidate->p_SerialPort->isOpen()) + { + p_Candidate->p_SerialPort->setDataTerminalReady(false); + p_Candidate->p_SerialPort->close(); + } + + delete p_Candidate->p_SerialPort; + p_Candidate->p_SerialPort = nullptr; + p_Candidate->PendingBytes.clear(); + p_Candidate->PacketQueue.clear(); +} + +void Dri_Cdc_DeleteContext(Dri_Cdc_Struct_Context* p_Context) +{ + if (p_Context == nullptr) + { + return; + } + + for (int Index = 0; Index < p_Context->CandidateList.size(); ++Index) + { + Dri_Cdc_DeleteCandidate(&p_Context->CandidateList[Index]); + } + + delete p_Context; +} + +int Dri_Cdc_FindCandidateIndex( + const Dri_Cdc_Struct_Context* p_Context, + const QString& EndpointId) +{ + if ((p_Context == nullptr) || EndpointId.trimmed().isEmpty()) + { + return -1; + } + + for (int Index = 0; Index < p_Context->CandidateList.size(); ++Index) + { + const Dri_Cdc_Struct_Candidate& Candidate = p_Context->CandidateList.at(Index); + if (Candidate.EndpointId.compare(EndpointId, Qt::CaseInsensitive) == 0) + { + return Index; + } + } + + return -1; +} + +void Dri_Cdc_RefreshPortSummary( + Dri_Cdc_Struct_Port* p_Port, + Dri_Cdc_Struct_Context* p_Context) +{ + if ((p_Port == nullptr) || (p_Context == nullptr)) + { + return; + } + + if ((p_Context->ActiveCandidateIndex >= 0) && + ((p_Context->ActiveCandidateIndex >= p_Context->CandidateList.size()) || + (p_Context->CandidateList.at(p_Context->ActiveCandidateIndex).p_SerialPort == nullptr) || + !p_Context->CandidateList.at(p_Context->ActiveCandidateIndex).p_SerialPort->isOpen())) + { + p_Context->ActiveCandidateIndex = -1; + } + + QStringList OpenedPortList; + for (const Dri_Cdc_Struct_Candidate& Candidate : p_Context->CandidateList) + { + if ((Candidate.p_SerialPort != nullptr) && Candidate.p_SerialPort->isOpen()) + { + OpenedPortList.append(Candidate.PortName); + } + } + + if (OpenedPortList.isEmpty()) + { + p_Port->IsOpened = false; + p_Port->PortName.clear(); + return; + } + + p_Port->IsOpened = true; + if (p_Context->ActiveCandidateIndex >= 0) + { + p_Port->PortName = + p_Context->CandidateList.at(p_Context->ActiveCandidateIndex).PortName; + return; + } + + p_Port->PortName = OpenedPortList.size() == 1 + ? OpenedPortList.first() + : QStringLiteral("USB CDC candidates (%1)").arg(OpenedPortList.join(QStringLiteral(", "))); +} + +bool Dri_Cdc_WritePacket( + Dri_Cdc_Struct_Candidate* p_Candidate, + const QByteArray& PacketBody) +{ + if ((p_Candidate == nullptr) || + (p_Candidate->p_SerialPort == nullptr) || + !p_Candidate->p_SerialPort->isOpen()) + { + return false; + } + + const qint64 BytesWritten = p_Candidate->p_SerialPort->write(PacketBody); + if (BytesWritten != PacketBody.size()) + { + return false; + } + + p_Candidate->p_SerialPort->flush(); + return p_Candidate->p_SerialPort->waitForBytesWritten(100); +} + +void Dri_Cdc_EnqueuePacket( + Dri_Cdc_Struct_Candidate* p_Candidate, + const QByteArray& PacketBody, + Com_Enum_ProtocolType Type) +{ + if ((p_Candidate == nullptr) || PacketBody.isEmpty() || + (Type == Com_Enum_ProtocolType_None)) + { + return; + } + + Com_Struct_RawPacket Packet; + Packet.IsValid = true; + Packet.Source = Com_Enum_RawPacketSource_UsbCdc; + Packet.ProtocolType = Type; + Packet.ByteArray = PacketBody; + Packet.PortName = p_Candidate->PortName; + Packet.EndpointId = p_Candidate->EndpointId; + p_Candidate->PacketQueue.enqueue(Packet); +} + +void Dri_Cdc_BufferIncomingBytes( + Dri_Cdc_Struct_Candidate* p_Candidate, + const QByteArray& IncomingBytes) +{ + if ((p_Candidate == nullptr) || IncomingBytes.isEmpty()) + { + return; + } + + p_Candidate->PendingBytes.append(IncomingBytes); + + while (true) + { + QByteArray PacketBody; + Com_Enum_ProtocolType Type = Com_Enum_ProtocolType_None; + if (!Com_Protocol_TryTakePacket(&p_Candidate->PendingBytes, &PacketBody, &Type)) + { + break; + } + + Dri_Cdc_EnqueuePacket(p_Candidate, PacketBody, Type); + } +} + +bool Dri_Cdc_DequeuePacket( + Dri_Cdc_Struct_Candidate* p_Candidate, + Com_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + if ((p_Candidate == nullptr) || (p_Packet == nullptr) || p_Candidate->PacketQueue.isEmpty()) + { + return false; + } + + *p_Packet = p_Candidate->PacketQueue.dequeue(); + if (p_TextStatus != nullptr) + { + *p_TextStatus = + QStringLiteral("%1 RX %2") + .arg( + p_Candidate->PortName, + QString::fromLatin1(p_Packet->ByteArray.toHex(' ').toUpper())); + } + return true; +} + +} // namespace + +void Dri_Cdc_Close(Dri_Cdc_Struct_Port* p_Port) +{ + Dri_Cdc_DeleteContext(p_Port->p_Context); + *p_Port = Dri_Cdc_Struct_Port(); +} + +bool Dri_Cdc_Init( + Dri_Cdc_Struct_Port* p_Port, + const Com_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus) +{ + Dri_Cdc_Close(p_Port); + + const QVector PortInfoList = QSerialPortInfo::availablePorts().toVector(); + if (PortInfoList.isEmpty()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("No USB CDC port is available."); + } + return false; + } + + auto* p_Context = new Dri_Cdc_Struct_Context(); + QStringList ErrorList; + for (const QSerialPortInfo& PortInfo : PortInfoList) + { + Dri_Cdc_Struct_Candidate Candidate; + Candidate.p_SerialPort = new QSerialPort(PortInfo); + Candidate.PortName = Dri_Cdc_FormatPortLabel(PortInfo); + Candidate.EndpointId = Dri_Cdc_BuildEndpointId(PortInfo); + Candidate.SystemLocation = QDir::cleanPath(PortInfo.systemLocation()); + + Candidate.p_SerialPort->setBaudRate(QSerialPort::Baud115200); + Candidate.p_SerialPort->setDataBits(QSerialPort::Data8); + Candidate.p_SerialPort->setParity(QSerialPort::NoParity); + Candidate.p_SerialPort->setStopBits(QSerialPort::OneStop); + Candidate.p_SerialPort->setFlowControl(QSerialPort::NoFlowControl); + + if (!Candidate.p_SerialPort->open(QIODevice::ReadWrite)) + { + ErrorList.append( + QStringLiteral("%1: %2") + .arg(PortInfo.portName(), Candidate.p_SerialPort->errorString())); + Dri_Cdc_DeleteCandidate(&Candidate); + continue; + } + + Candidate.p_SerialPort->setDataTerminalReady(true); + p_Context->CandidateList.append(Candidate); + } + + if (p_Context->CandidateList.isEmpty()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = ErrorList.isEmpty() + ? QStringLiteral("USB CDC ports were found, but all opens failed.") + : QStringLiteral("USB CDC open failed: %1").arg(ErrorList.join(QStringLiteral(" | "))); + } + Dri_Cdc_DeleteContext(p_Context); + return false; + } + + p_Port->p_Context = p_Context; + Dri_Cdc_RefreshPortSummary(p_Port, p_Context); + Q_UNUSED(DeviceConfig); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("%1 connected.").arg(p_Port->PortName); + } + return true; +} + +bool Dri_Cdc_Read( + Dri_Cdc_Struct_Port* p_Port, + Com_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + *p_Packet = Com_Struct_RawPacket(); + p_Packet->Source = Com_Enum_RawPacketSource_UsbCdc; + p_Packet->PortName = p_Port->PortName; + + Dri_Cdc_Struct_Context* p_Context = p_Port->p_Context; + if (!p_Port->IsOpened || (p_Context == nullptr) || p_Context->CandidateList.isEmpty()) + { + return false; + } + + const int StartIndex = + p_Context->ActiveCandidateIndex >= 0 ? p_Context->ActiveCandidateIndex : 0; + const int EndIndex = + p_Context->ActiveCandidateIndex >= 0 + ? (p_Context->ActiveCandidateIndex + 1) + : p_Context->CandidateList.size(); + + for (int Index = StartIndex; Index < EndIndex; ++Index) + { + Dri_Cdc_Struct_Candidate& Candidate = p_Context->CandidateList[Index]; + if ((Candidate.p_SerialPort == nullptr) || !Candidate.p_SerialPort->isOpen()) + { + continue; + } + + if (!Dri_Cdc_IsPortStillPresent(Candidate)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("%1 disconnected.").arg(Candidate.PortName); + } + + Dri_Cdc_DeleteCandidate(&Candidate); + if (p_Context->ActiveCandidateIndex == Index) + { + Dri_Cdc_Close(p_Port); + return false; + } + + Dri_Cdc_RefreshPortSummary(p_Port, p_Context); + continue; + } + + if (Dri_Cdc_DequeuePacket(&Candidate, p_Packet, p_TextStatus)) + { + return true; + } + + const QByteArray IncomingBytes = Candidate.p_SerialPort->readAll(); + if (IncomingBytes.isEmpty()) + { + continue; + } + + Dri_Cdc_BufferIncomingBytes(&Candidate, IncomingBytes); + if (Dri_Cdc_DequeuePacket(&Candidate, p_Packet, p_TextStatus)) + { + return true; + } + + if ((p_TextStatus != nullptr) && !Candidate.PendingBytes.isEmpty()) + { + *p_TextStatus = + QStringLiteral("%1 has pending protobuf bytes and is waiting for the next CDC chunk.") + .arg(Candidate.PortName); + } + } + + Dri_Cdc_RefreshPortSummary(p_Port, p_Context); + if (!p_Port->IsOpened) + { + Dri_Cdc_Close(p_Port); + } + return false; +} + +bool Dri_Cdc_LockCandidate( + Dri_Cdc_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus) +{ + Dri_Cdc_Struct_Context* p_Context = p_Port->p_Context; + if (!p_Port->IsOpened || (p_Context == nullptr)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("USB CDC is not open, so the candidate cannot be locked."); + } + return false; + } + + const int ActiveIndex = Dri_Cdc_FindCandidateIndex(p_Context, EndpointId); + if (ActiveIndex < 0) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("The target USB CDC candidate was not found."); + } + return false; + } + + if ((p_Context->CandidateList.at(ActiveIndex).p_SerialPort == nullptr) || + !p_Context->CandidateList.at(ActiveIndex).p_SerialPort->isOpen()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("The target USB CDC candidate is already closed."); + } + return false; + } + + if (p_Context->ActiveCandidateIndex == ActiveIndex) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = + QStringLiteral("USB CDC already locked to %1.") + .arg(p_Context->CandidateList.at(ActiveIndex).PortName); + } + return true; + } + + for (int Index = 0; Index < p_Context->CandidateList.size(); ++Index) + { + if (Index == ActiveIndex) + { + continue; + } + + Dri_Cdc_DeleteCandidate(&p_Context->CandidateList[Index]); + } + + p_Context->ActiveCandidateIndex = ActiveIndex; + Dri_Cdc_RefreshPortSummary(p_Port, p_Context); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("USB CDC locked to %1.").arg(p_Port->PortName); + } + return true; +} + +bool Dri_Cdc_DiscardCandidate( + Dri_Cdc_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus) +{ + Dri_Cdc_Struct_Context* p_Context = p_Port->p_Context; + if (!p_Port->IsOpened || (p_Context == nullptr)) + { + return false; + } + + const int CandidateIndex = Dri_Cdc_FindCandidateIndex(p_Context, EndpointId); + if (CandidateIndex < 0) + { + return false; + } + + const QString PortLabel = p_Context->CandidateList.at(CandidateIndex).PortName; + const bool IsActiveCandidate = p_Context->ActiveCandidateIndex == CandidateIndex; + Dri_Cdc_DeleteCandidate(&p_Context->CandidateList[CandidateIndex]); + + if (IsActiveCandidate) + { + Dri_Cdc_Close(p_Port); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Discarded the locked USB CDC candidate %1.").arg(PortLabel); + } + return true; + } + + Dri_Cdc_RefreshPortSummary(p_Port, p_Context); + if (!p_Port->IsOpened) + { + Dri_Cdc_Close(p_Port); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("All USB CDC candidates were discarded."); + } + return true; + } + + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Discarded USB CDC candidate %1.").arg(PortLabel); + } + return true; +} + +bool Dri_Cdc_Write( + Dri_Cdc_Struct_Port* p_Port, + const QByteArray& PacketBody, + QString* p_TextStatus) +{ + Dri_Cdc_Struct_Context* p_Context = p_Port->p_Context; + if (!p_Port->IsOpened || (p_Context == nullptr) || p_Context->CandidateList.isEmpty()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("USB CDC is not open. Skip send."); + } + return false; + } + + if (PacketBody.isEmpty()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("USB CDC packet body is empty."); + } + return false; + } + + if (p_Context->ActiveCandidateIndex >= 0) + { + Dri_Cdc_Struct_Candidate& ActiveCandidate = + p_Context->CandidateList[p_Context->ActiveCandidateIndex]; + if (!Dri_Cdc_WritePacket(&ActiveCandidate, PacketBody)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = + QStringLiteral("%1 鍐欏叆澶辫触锛?2") + .arg( + ActiveCandidate.PortName, + ActiveCandidate.p_SerialPort == nullptr + ? QStringLiteral("port already closed") + : ActiveCandidate.p_SerialPort->errorString()); + } + + Dri_Cdc_Close(p_Port); + return false; + } + + if (p_TextStatus != nullptr) + { + *p_TextStatus = + QStringLiteral("%1 TX %2") + .arg( + ActiveCandidate.PortName, + QString::fromLatin1(PacketBody.toHex(' ').toUpper())); + } + return true; + } + + int SuccessCount = 0; + QStringList SuccessPortList; + QStringList ErrorList; + for (Dri_Cdc_Struct_Candidate& Candidate : p_Context->CandidateList) + { + if ((Candidate.p_SerialPort == nullptr) || !Candidate.p_SerialPort->isOpen()) + { + continue; + } + + if (Dri_Cdc_WritePacket(&Candidate, PacketBody)) + { + ++SuccessCount; + SuccessPortList.append(Candidate.PortName); + continue; + } + + ErrorList.append( + QStringLiteral("%1: %2") + .arg(Candidate.PortName, Candidate.p_SerialPort->errorString())); + } + + if (SuccessCount > 0) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = + QStringLiteral("USB CDC 骞挎挱 TX %1锛岀洰鏍囷細%2") + .arg( + QString::fromLatin1(PacketBody.toHex(' ').toUpper()), + SuccessPortList.join(QStringLiteral(", "))); + } + return true; + } + + if (p_TextStatus != nullptr) + { + *p_TextStatus = ErrorList.isEmpty() + ? QStringLiteral("USB CDC send failed.") + : QStringLiteral("USB CDC 鍙戦€佸け璐ワ細%1").arg(ErrorList.join(QStringLiteral(" | "))); + } + + return false; +} diff --git a/DRI/Dri_Cdc.h b/DRI/Dri_Cdc.h new file mode 100644 index 0000000..662f19d --- /dev/null +++ b/DRI/Dri_Cdc.h @@ -0,0 +1,40 @@ +#pragma once + +#include "COM/Com_Def.h" +#include + +struct Dri_Cdc_Struct_Context; + +struct Dri_Cdc_Struct_Port +{ + Dri_Cdc_Struct_Context* p_Context = nullptr; + bool IsOpened = false; + QString PortName; +}; + +void Dri_Cdc_Close(Dri_Cdc_Struct_Port* p_Port); +bool Dri_Cdc_Init( + Dri_Cdc_Struct_Port* p_Port, + const Com_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus); +bool Dri_Cdc_Read( + Dri_Cdc_Struct_Port* p_Port, + Com_Struct_RawPacket* p_Packet, + QString* p_TextStatus); + +// Lock the confirmed CDC candidate after LOGIC accepts a HelloRsp. +bool Dri_Cdc_LockCandidate( + Dri_Cdc_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus); + +// Drop one mismatched CDC candidate without tearing down the whole CDC scan set. +bool Dri_Cdc_DiscardCandidate( + Dri_Cdc_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus); + +bool Dri_Cdc_Write( + Dri_Cdc_Struct_Port* p_Port, + const QByteArray& PacketBody, + QString* p_TextStatus); diff --git a/DRI/Dri_Consumer.cpp b/DRI/Dri_Consumer.cpp index 56bb229..4066fb5 100644 --- a/DRI/Dri_Consumer.cpp +++ b/DRI/Dri_Consumer.cpp @@ -1,168 +1,52 @@ -#include "DRI/Dri_Consumer.h" +#include "DRI/Dri_Consumer.h" -namespace +void Dri_Consumer_Close(Dri_Consumer_Struct_Port* p_Port) { - -bool Dri_Consumer_Func_BeginRead(Dri_Consumer_Struct_Port* p_Port, QString* p_TextStatus) -{ - if (!p_Port->IsOpened || (p_Port->InputLength == 0)) - { - return false; - } - - if (p_Port->IsReadPending) - { - return true; - } - - ResetEvent(p_Port->HandleEvent); - p_Port->ReadBuffer.fill(0, p_Port->InputLength); - p_Port->OverlappedRead = {}; - p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; - - DWORD BytesRead = 0; - if (ReadFile( - p_Port->HandleRead, - p_Port->ReadBuffer.data(), - p_Port->InputLength, - &BytesRead, - &p_Port->OverlappedRead) || - (GetLastError() == ERROR_IO_PENDING)) - { - p_Port->IsReadPending = true; - return true; - } - - if (p_TextStatus != nullptr) - { - *p_TextStatus = QStringLiteral("Consumer 接口启动异步读取失败:%1").arg(GetLastError()); - } - return false; + Dri_Hid_CloseReadPort(&p_Port->ReadPort); } -} // namespace - -void Dri_Consumer_Func_Close(Dri_Consumer_Struct_Port* p_Port) -{ - if ((p_Port->HandleRead != INVALID_HANDLE_VALUE) && p_Port->IsReadPending) - { - CancelIoEx(p_Port->HandleRead, &p_Port->OverlappedRead); - } - if (p_Port->HandleRead != INVALID_HANDLE_VALUE) - { - CloseHandle(p_Port->HandleRead); - } - if (p_Port->HandleEvent != nullptr) - { - CloseHandle(p_Port->HandleEvent); - } - - *p_Port = Dri_Consumer_Struct_Port(); -} - -bool Dri_Consumer_Func_Open( +bool Dri_Consumer_Init( Dri_Consumer_Struct_Port* p_Port, const Mid_Struct_DeviceConfig& DeviceConfig, QString* p_TextStatus) { - Dri_Consumer_Func_Close(p_Port); + Dri_Consumer_Close(p_Port); + + Mid_Struct_DeviceMatch Match; + Match.VendorId = DeviceConfig.VendorId; + Match.ProductId = DeviceConfig.ProductId; + Match.UsagePage = MID_CONST_USAGE_PAGE_CONSUMER; + Match.Usage = MID_CONST_USAGE_CONSUMER; QString DevicePath; quint16 InputLength = 0; - if (!Mid_Func_FindHidInterface( - Mid_Func_GetConsumerMatch(DeviceConfig), + if (!Mid_FindHidInterface( + Match, &DevicePath, &InputLength, nullptr)) { if (p_TextStatus != nullptr) { - *p_TextStatus = QStringLiteral("未找到 Consumer 接口:000C / 0001。"); + *p_TextStatus = QStringLiteral("Consumer interface was not found: 000C / 0001."); } return false; } - p_Port->HandleRead = CreateFileW( - reinterpret_cast(DevicePath.utf16()), - GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, - OPEN_EXISTING, - FILE_FLAG_OVERLAPPED, - nullptr); - if (p_Port->HandleRead == INVALID_HANDLE_VALUE) - { - if (p_TextStatus != nullptr) - { - *p_TextStatus = QStringLiteral("Consumer 接口打开失败:%1").arg(GetLastError()); - } - return false; - } - - p_Port->HandleEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (p_Port->HandleEvent == nullptr) - { - if (p_TextStatus != nullptr) - { - *p_TextStatus = QStringLiteral("Consumer 接口创建事件失败:%1").arg(GetLastError()); - } - Dri_Consumer_Func_Close(p_Port); - return false; - } - - p_Port->InputLength = InputLength; - p_Port->ReadBuffer = QByteArray(InputLength, 0); - p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; - p_Port->IsOpened = true; - - Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus); - return true; + return Dri_Hid_InitReadPort( + &p_Port->ReadPort, + DevicePath, + InputLength, + Mid_Enum_RawPacketSource_UsbConsumerHid, + QStringLiteral("Consumer"), + p_TextStatus); } -bool Dri_Consumer_Func_Read( +bool Dri_Consumer_Read( Dri_Consumer_Struct_Port* p_Port, Mid_Struct_RawPacket* p_Packet, QString* p_TextStatus) { - p_Packet->IsValid = false; - p_Packet->ByteArray.clear(); - p_Packet->PortName = QStringLiteral("Consumer"); - - if (!p_Port->IsOpened) - { - return false; - } - - if (!p_Port->IsReadPending) - { - Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus); - return false; - } - - DWORD BytesRead = 0; - if (!GetOverlappedResult(p_Port->HandleRead, &p_Port->OverlappedRead, &BytesRead, FALSE)) - { - const DWORD ErrorCode = GetLastError(); - if (ErrorCode == ERROR_IO_INCOMPLETE) - { - return false; - } - - if (p_TextStatus != nullptr) - { - *p_TextStatus = QStringLiteral("Consumer 接口读包失败:%1").arg(ErrorCode); - } - Dri_Consumer_Func_Close(p_Port); - return false; - } - - p_Port->IsReadPending = false; - if (BytesRead > 0) - { - p_Packet->IsValid = true; - p_Packet->ByteArray = p_Port->ReadBuffer.left(static_cast(BytesRead)); - } - - Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus); - return p_Packet->IsValid; + return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus); } + diff --git a/DRI/Dri_Consumer.h b/DRI/Dri_Consumer.h index 1c45cbc..2cc5226 100644 --- a/DRI/Dri_Consumer.h +++ b/DRI/Dri_Consumer.h @@ -1,35 +1,20 @@ -#pragma once +#pragma once -#include "MID/Mid_Def.h" -#include -#include -#include +#include "DRI/Dri_Hid.h" -/* - * DRI Consumer 层:抽象 HID Consumer 端点,封装 Win32 句柄、缓存与状态。 - * - */ +// USB consumer report reader. struct Dri_Consumer_Struct_Port { - /* 设备读写所需的句柄与异步上下文 */ - HANDLE HandleRead = INVALID_HANDLE_VALUE; - HANDLE HandleEvent = nullptr; - OVERLAPPED OverlappedRead = {}; - /* 快速判断是否已经建立连接以及当前是否正挂起读 */ - bool IsOpened = false; - bool IsReadPending = false; - /* USB 输入报文长度 & 环形缓存 */ - quint16 InputLength = 0; - QByteArray ReadBuffer; + Dri_Hid_Struct_ReadPort ReadPort; }; -/* 主动释放 Consumer 端口,确保句柄/Overlapped 都被 reset。 */ -void Dri_Consumer_Func_Close(Dri_Consumer_Struct_Port* p_Port); -/* 打开设备:按照 VID/PID 查询接口并创建 Overlapped 句柄。 */ -bool Dri_Consumer_Func_Open(Dri_Consumer_Struct_Port* p_Port, +void Dri_Consumer_Close(Dri_Consumer_Struct_Port* p_Port); +bool Dri_Consumer_Init( + Dri_Consumer_Struct_Port* p_Port, const Mid_Struct_DeviceConfig& DeviceConfig, QString* p_TextStatus); -/* 读取最新的 Consumer 原始包,失败时返回状态描述。 */ -bool Dri_Consumer_Func_Read(Dri_Consumer_Struct_Port* p_Port, +bool Dri_Consumer_Read( + Dri_Consumer_Struct_Port* p_Port, Mid_Struct_RawPacket* p_Packet, QString* p_TextStatus); + diff --git a/DRI/Dri_Hid.cpp b/DRI/Dri_Hid.cpp new file mode 100644 index 0000000..ec87410 --- /dev/null +++ b/DRI/Dri_Hid.cpp @@ -0,0 +1,189 @@ +#include "DRI/Dri_Hid.h" + +#include + +#pragma comment(lib, "hid.lib") + +namespace +{ +bool Dri_Hid_BeginRead(Dri_Hid_Struct_ReadPort* p_Port, QString* p_TextStatus) +{ + if (!p_Port->IsOpened || (p_Port->InputLength == 0)) return false; + if (p_Port->IsReadPending) return true; + + ResetEvent(p_Port->HandleEvent); + p_Port->ReadBuffer.fill(0, p_Port->InputLength); + p_Port->OverlappedRead = {}; + p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; + + DWORD BytesRead = 0; + if (ReadFile( + p_Port->HandleRead, + p_Port->ReadBuffer.data(), + p_Port->InputLength, + &BytesRead, + &p_Port->OverlappedRead) || + (GetLastError() == ERROR_IO_PENDING)) + { + p_Port->IsReadPending = true; + return true; + } + + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 failed to start async read: %2").arg(p_Port->PortName).arg(GetLastError()); + return false; +} + +} // namespace + +void Dri_Hid_CloseReadPort(Dri_Hid_Struct_ReadPort* p_Port) +{ + if ((p_Port->HandleRead != INVALID_HANDLE_VALUE) && p_Port->IsReadPending) CancelIoEx(p_Port->HandleRead, &p_Port->OverlappedRead); + if (p_Port->HandleRead != INVALID_HANDLE_VALUE) CloseHandle(p_Port->HandleRead); + if (p_Port->HandleEvent != nullptr) CloseHandle(p_Port->HandleEvent); + *p_Port = Dri_Hid_Struct_ReadPort(); +} + +bool Dri_Hid_InitReadPort( + Dri_Hid_Struct_ReadPort* p_Port, + const QString& DevicePath, + quint16 InputLength, + Mid_Enum_RawPacketSource PacketSource, + const QString& PortName, + QString* p_TextStatus) +{ + Dri_Hid_CloseReadPort(p_Port); + + p_Port->HandleRead = CreateFileW( + reinterpret_cast(DevicePath.utf16()), + GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + nullptr); + if (p_Port->HandleRead == INVALID_HANDLE_VALUE) + { + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 failed to open read handle: %2").arg(PortName).arg(GetLastError()); + return false; + } + + p_Port->HandleEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (p_Port->HandleEvent == nullptr) + { + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 failed to create event: %2").arg(PortName).arg(GetLastError()); + Dri_Hid_CloseReadPort(p_Port); + return false; + } + + p_Port->InputLength = InputLength; + p_Port->PacketSource = PacketSource; + p_Port->PortName = PortName; + p_Port->ReadBuffer = QByteArray(InputLength, 0); + p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; + p_Port->IsOpened = true; + + if (!Dri_Hid_BeginRead(p_Port, p_TextStatus)) { Dri_Hid_CloseReadPort(p_Port); return false; } + return true; +} + +bool Dri_Hid_Read( + Dri_Hid_Struct_ReadPort* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + *p_Packet = Mid_Struct_RawPacket(); + p_Packet->Source = p_Port->PacketSource; + p_Packet->PortName = p_Port->PortName; + + if (!p_Port->IsOpened) return false; + if (!p_Port->IsReadPending) + { + Dri_Hid_BeginRead(p_Port, p_TextStatus); + return false; + } + + DWORD BytesRead = 0; + if (!GetOverlappedResult(p_Port->HandleRead, &p_Port->OverlappedRead, &BytesRead, FALSE)) + { + const DWORD ErrorCode = GetLastError(); + if (ErrorCode == ERROR_IO_INCOMPLETE) return false; + if (p_TextStatus != nullptr) *p_TextStatus = QStringLiteral("%1 read packet failed: %2").arg(p_Port->PortName).arg(ErrorCode); + Dri_Hid_CloseReadPort(p_Port); + return false; + } + + p_Port->IsReadPending = false; + if (BytesRead > 0) { p_Packet->IsValid = true; p_Packet->ByteArray = p_Port->ReadBuffer.left(static_cast(BytesRead)); } + + Dri_Hid_BeginRead(p_Port, p_TextStatus); + return p_Packet->IsValid; +} + +HANDLE Dri_Hid_OpenWriteHandle(const QString& DevicePath, QString* p_TextError) +{ + HANDLE HandleWrite = CreateFileW( + reinterpret_cast(DevicePath.utf16()), + GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + if (HandleWrite != INVALID_HANDLE_VALUE) return HandleWrite; + + const DWORD WriteError = GetLastError(); + HandleWrite = CreateFileW( + reinterpret_cast(DevicePath.utf16()), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + 0, + nullptr); + if ((HandleWrite == INVALID_HANDLE_VALUE) && (p_TextError != nullptr)) *p_TextError = QStringLiteral("GENERIC_WRITE=%1, ZeroAccess=%2").arg(WriteError).arg(GetLastError()); + return HandleWrite; +} + +bool Dri_Hid_WritePacket( + HANDLE HandleWrite, + quint16 OutputLength, + const QByteArray& Packet, + QString* p_TextError) +{ + if ((HandleWrite == INVALID_HANDLE_VALUE) || Packet.isEmpty()) return false; + + QByteArray Buffer = Packet; + if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast(Buffer.size()))) return true; + + const DWORD SetReportError = GetLastError(); + DWORD BytesWritten = 0; + if (WriteFile(HandleWrite, Buffer.constData(), static_cast(Buffer.size()), &BytesWritten, nullptr) && (BytesWritten == static_cast(Buffer.size()))) return true; + + const DWORD WriteFileError = GetLastError(); + if ((OutputLength > 0) && (Buffer.size() < OutputLength)) + { + Buffer.append(OutputLength - Buffer.size(), 0); + + if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast(Buffer.size()))) return true; + + const DWORD PaddedSetReportError = GetLastError(); + BytesWritten = 0; + if (WriteFile(HandleWrite, Buffer.constData(), static_cast(Buffer.size()), &BytesWritten, nullptr) && (BytesWritten == static_cast(Buffer.size()))) return true; + + const DWORD PaddedWriteFileError = GetLastError(); + if (p_TextError != nullptr) + { + *p_TextError = QStringLiteral( + "SetOutputReport=%1, WriteFile=%2, PaddedSetOutputReport=%3, PaddedWriteFile=%4") + .arg(SetReportError) + .arg(WriteFileError) + .arg(PaddedSetReportError) + .arg(PaddedWriteFileError); + } + return false; + } + + if (p_TextError != nullptr) *p_TextError = QStringLiteral("SetOutputReport=%1, WriteFile=%2").arg(SetReportError).arg(WriteFileError); + return false; +} + diff --git a/DRI/Dri_Hid.h b/DRI/Dri_Hid.h new file mode 100644 index 0000000..38cc159 --- /dev/null +++ b/DRI/Dri_Hid.h @@ -0,0 +1,41 @@ +#pragma once + +#include "MID/Mid_Def.h" +#include +#include +#include + +// Shared HID async read/write helpers used by USB and BLE HID paths. +struct Dri_Hid_Struct_ReadPort +{ + HANDLE HandleRead = INVALID_HANDLE_VALUE; + HANDLE HandleEvent = nullptr; + OVERLAPPED OverlappedRead = {}; + bool IsOpened = false; + bool IsReadPending = false; + quint16 InputLength = 0; + Mid_Enum_RawPacketSource PacketSource = Mid_Enum_RawPacketSource_None; + QString PortName; + QByteArray ReadBuffer; +}; + +void Dri_Hid_CloseReadPort(Dri_Hid_Struct_ReadPort* p_Port); +bool Dri_Hid_InitReadPort( + Dri_Hid_Struct_ReadPort* p_Port, + const QString& DevicePath, + quint16 InputLength, + Mid_Enum_RawPacketSource PacketSource, + const QString& PortName, + QString* p_TextStatus); +bool Dri_Hid_Read( + Dri_Hid_Struct_ReadPort* p_Port, + Mid_Struct_RawPacket* p_Packet, + QString* p_TextStatus); + +HANDLE Dri_Hid_OpenWriteHandle(const QString& DevicePath, QString* p_TextError); +bool Dri_Hid_WritePacket( + HANDLE HandleWrite, + quint16 OutputLength, + const QByteArray& Packet, + QString* p_TextError); + diff --git a/DRI/Dri_NkroRaw.cpp b/DRI/Dri_NkroRaw.cpp index f77363b..4b16c20 100644 --- a/DRI/Dri_NkroRaw.cpp +++ b/DRI/Dri_NkroRaw.cpp @@ -1,4 +1,4 @@ -#include "DRI/Dri_NkroRaw.h" +#include "DRI/Dri_NkroRaw.h" #include #include @@ -6,9 +6,9 @@ namespace { -/* ---------- 设备过滤与 Usage 映射 ---------- */ +// Device filter and scancode-to-usage mapping. -QString Dri_NkroRaw_Func_GetDevicePath(HANDLE DeviceHandle) +QString Dri_NkroRaw_GetDevicePath(HANDLE DeviceHandle) { if (DeviceHandle == nullptr) { @@ -31,7 +31,7 @@ QString Dri_NkroRaw_Func_GetDevicePath(HANDLE DeviceHandle) return QString::fromWCharArray(Buffer.constData()).trimmed(); } -bool Dri_NkroRaw_Func_IsTargetDevice(const QString& DevicePath, const Mid_Struct_DeviceConfig& DeviceConfig) +bool Dri_NkroRaw_IsTargetDevice(const QString& DevicePath, const Mid_Struct_DeviceConfig& DeviceConfig) { if (DevicePath.isEmpty()) { @@ -43,7 +43,7 @@ bool Dri_NkroRaw_Func_IsTargetDevice(const QString& DevicePath, const Mid_Struct UpperPath.contains(QStringLiteral("PID_%1").arg(DeviceConfig.ProductId, 4, 16, QLatin1Char('0')).toUpper()); } -quint16 Dri_NkroRaw_Func_GetUsage(const RAWKEYBOARD& Keyboard) +quint16 Dri_NkroRaw_GetUsage(const RAWKEYBOARD& Keyboard) { const bool IsE0 = (Keyboard.Flags & RI_KEY_E0) != 0; const bool IsE1 = (Keyboard.Flags & RI_KEY_E1) != 0; @@ -97,20 +97,18 @@ quint16 Dri_NkroRaw_Func_GetUsage(const RAWKEYBOARD& Keyboard) } // namespace -/* ---------- 生命周期 ---------- */ - -void Dri_NkroRaw_Func_Close(Dri_NkroRaw_Struct_Port* p_Port) +void Dri_NkroRaw_Close(Dri_NkroRaw_Struct_Port* p_Port) { *p_Port = Dri_NkroRaw_Struct_Port(); } -bool Dri_NkroRaw_Func_Open( +bool Dri_NkroRaw_Init( Dri_NkroRaw_Struct_Port* p_Port, const Mid_Struct_DeviceConfig& DeviceConfig, void* WindowHandle, QString* p_TextStatus) { - Dri_NkroRaw_Func_Close(p_Port); + Dri_NkroRaw_Close(p_Port); if (WindowHandle == nullptr) { @@ -147,9 +145,7 @@ bool Dri_NkroRaw_Func_Open( return true; } -/* ---------- RawInput 主流程 ---------- */ - -bool Dri_NkroRaw_Func_HandleNativeMessage( +bool Dri_NkroRaw_HandleMessage( Dri_NkroRaw_Struct_Port* p_Port, void* p_Message, QString* p_TextStatus) @@ -189,13 +185,13 @@ bool Dri_NkroRaw_Func_HandleNativeMessage( return false; } - const QString DevicePath = Dri_NkroRaw_Func_GetDevicePath(p_Input->header.hDevice); - if (!Dri_NkroRaw_Func_IsTargetDevice(DevicePath, p_Port->DeviceConfig)) + const QString DevicePath = Dri_NkroRaw_GetDevicePath(p_Input->header.hDevice); + if (!Dri_NkroRaw_IsTargetDevice(DevicePath, p_Port->DeviceConfig)) { return false; } - const quint16 Usage = Dri_NkroRaw_Func_GetUsage(p_Input->data.keyboard); + const quint16 Usage = Dri_NkroRaw_GetUsage(p_Input->data.keyboard); if (Usage == 0) { return false; @@ -249,6 +245,7 @@ bool Dri_NkroRaw_Func_HandleNativeMessage( Mid_Struct_RawPacket Packet; Packet.IsValid = true; + Packet.Source = Mid_Enum_RawPacketSource_UsbNkroRaw; Packet.PortName = QStringLiteral("NKRO(原生输入)"); Packet.ByteArray = QByteArray(MID_CONST_PACKET_SIZE_NKRO, 0); Packet.ByteArray[0] = static_cast(Mid_Enum_ReportId_Nkro); @@ -263,12 +260,13 @@ bool Dri_NkroRaw_Func_HandleNativeMessage( return true; } -bool Dri_NkroRaw_Func_Read( +bool Dri_NkroRaw_Read( Dri_NkroRaw_Struct_Port* p_Port, Mid_Struct_RawPacket* p_Packet, QString*) { p_Packet->IsValid = false; + p_Packet->Source = Mid_Enum_RawPacketSource_UsbNkroRaw; p_Packet->ByteArray.clear(); p_Packet->PortName = QStringLiteral("NKRO(原生输入)"); @@ -280,3 +278,4 @@ bool Dri_NkroRaw_Func_Read( *p_Packet = p_Port->PacketQueue.takeFirst(); return p_Packet->IsValid; } + diff --git a/DRI/Dri_NkroRaw.h b/DRI/Dri_NkroRaw.h index 092b1fe..29c0c1b 100644 --- a/DRI/Dri_NkroRaw.h +++ b/DRI/Dri_NkroRaw.h @@ -1,40 +1,31 @@ -#pragma once +#pragma once #include "MID/Mid_Def.h" #include #include #include -/* - * DRI NKRO RAW 层:负责注册 Windows RAWINPUT,接收键盘 104+ 通道。 - * 这里集中保存窗口句柄、设备配置、按键队列与 Modifier/Bitmap 缓冲。 - */ +// Windows RAWINPUT reader for the NKRO path. struct Dri_NkroRaw_Struct_Port { - /* 运行状态:是否已注册 RAWINPUT 以及宿主窗口 HWND */ bool IsOpened = false; void* WindowHandle = nullptr; Mid_Struct_DeviceConfig DeviceConfig; - /* 最新的 Modifier 字节和 NKRO Usage 位图 */ quint8 Modifier = 0; QByteArray UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, 0); - /* RAWINPUT 读取结果放入 PacketQueue,供上层逻辑顺序消费 */ QList PacketQueue; QString DevicePath; }; -/* 注销 RAWINPUT 并清空所有缓存。 */ -void Dri_NkroRaw_Func_Close(Dri_NkroRaw_Struct_Port* p_Port); -/* 注册 RAWINPUT:绑定目标窗口并监听指定 VID/PID。 */ -bool Dri_NkroRaw_Func_Open(Dri_NkroRaw_Struct_Port* p_Port, +void Dri_NkroRaw_Close(Dri_NkroRaw_Struct_Port* p_Port); +bool Dri_NkroRaw_Init(Dri_NkroRaw_Struct_Port* p_Port, const Mid_Struct_DeviceConfig& DeviceConfig, void* WindowHandle, QString* p_TextStatus); -/* 把 Windows 消息转为 NKRO 数据:核心教学入口。 */ -bool Dri_NkroRaw_Func_HandleNativeMessage(Dri_NkroRaw_Struct_Port* p_Port, +bool Dri_NkroRaw_HandleMessage(Dri_NkroRaw_Struct_Port* p_Port, void* p_Message, QString* p_TextStatus); -/* 上层每次取一个封装好的 RawPacket。 */ -bool Dri_NkroRaw_Func_Read(Dri_NkroRaw_Struct_Port* p_Port, +bool Dri_NkroRaw_Read(Dri_NkroRaw_Struct_Port* p_Port, Mid_Struct_RawPacket* p_Packet, QString* p_TextStatus); + diff --git a/DRI/Dri_Nus.cpp b/DRI/Dri_Nus.cpp new file mode 100644 index 0000000..f58fb0a --- /dev/null +++ b/DRI/Dri_Nus.cpp @@ -0,0 +1,920 @@ +#include "DRI/Dri_Nus.h" + +#include "COM/Com_Protocol.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct Dri_Nus_Struct_Candidate +{ + QBluetoothDeviceInfo DeviceInfo; + QString EndpointId; + QString DeviceLabel; + QString DeviceName; + QString AddressText; +}; + +struct Dri_Nus_Struct_Context +{ + QBluetoothDeviceDiscoveryAgent* p_DiscoveryAgent = nullptr; + QLowEnergyController* p_Controller = nullptr; + QLowEnergyService* p_Service = nullptr; + + QVector CandidateList; + int CurrentCandidateIndex = -1; + int LockedCandidateIndex = -1; + bool IsDiscoveryFinished = false; + bool HasTargetService = false; + + QLowEnergyCharacteristic WriteCharacteristic; + QLowEnergyCharacteristic NotifyCharacteristic; + QLowEnergyDescriptor NotifyDescriptor; + + QQueue PacketQueue; +}; + +namespace +{ + +const QString kPreferredBleDeviceName = QStringLiteral("WH Mini Keyboard"); +const QBluetoothUuid kServiceUuid( + QUuid(QStringLiteral("6E400001-B5A3-F393-E0A9-E50E24DCCA9E"))); +const QBluetoothUuid kRxCharacteristicUuid( + QUuid(QStringLiteral("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"))); +const QBluetoothUuid kTxCharacteristicUuid( + QUuid(QStringLiteral("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"))); + +QString Dri_Nus_NormalizeAddressText(const QBluetoothAddress& Address) +{ + const QString AddressText = Address.toString().trimmed().toUpper(); + return (AddressText == QStringLiteral("00:00:00:00:00:00")) + ? QString() + : AddressText; +} + +bool Dri_Nus_IsPreferredDeviceName(const QString& DeviceName) +{ + return DeviceName.trimmed().compare(kPreferredBleDeviceName, Qt::CaseInsensitive) == 0; +} + +QString Dri_Nus_FormatCandidateSummary(const Dri_Nus_Struct_Candidate& Candidate) +{ + QString Summary = Candidate.DeviceLabel; + if (!Candidate.AddressText.isEmpty()) + { + Summary += QStringLiteral(" [%1]").arg(Candidate.AddressText); + } + + return Summary; +} + +QString Dri_Nus_GetPacketTypeText(Com_Enum_ProtocolType Type) +{ + switch (Type) + { + case Com_Enum_ProtocolType_HelloReq: return QStringLiteral("HelloReq"); + case Com_Enum_ProtocolType_HelloRsp: return QStringLiteral("HelloRsp"); + case Com_Enum_ProtocolType_Bitmap: return QStringLiteral("Bitmap"); + case Com_Enum_ProtocolType_FunctionKeyEvent: return QStringLiteral("FunctionKeyEvent"); + case Com_Enum_ProtocolType_LedState: return QStringLiteral("LedState"); + case Com_Enum_ProtocolType_TimeSync: return QStringLiteral("TimeSync"); + case Com_Enum_ProtocolType_ThemeRgb: return QStringLiteral("ThemeRgb"); + case Com_Enum_ProtocolType_Ack: return QStringLiteral("Ack"); + case Com_Enum_ProtocolType_Error: return QStringLiteral("Error"); + default: return QStringLiteral("Unknown"); + } +} + +const Dri_Nus_Struct_Candidate* Dri_Nus_GetCurrentCandidate( + const Dri_Nus_Struct_Context* p_Context) +{ + if ((p_Context == nullptr) || + (p_Context->CurrentCandidateIndex < 0) || + (p_Context->CurrentCandidateIndex >= p_Context->CandidateList.size())) + { + return nullptr; + } + + return &p_Context->CandidateList.at(p_Context->CurrentCandidateIndex); +} + +QString Dri_Nus_BuildEndpointId( + const QBluetoothDeviceInfo& DeviceInfo, + int CandidateOrdinal) +{ + QString DeviceUuidText = DeviceInfo.deviceUuid().toString().trimmed(); + DeviceUuidText.remove(QLatin1Char('{')); + DeviceUuidText.remove(QLatin1Char('}')); + if (!DeviceUuidText.isEmpty()) + { + return QStringLiteral("uuid:%1").arg(DeviceUuidText.toUpper()); + } + + const QString AddressText = DeviceInfo.address().toString().trimmed().toUpper(); + if (!AddressText.isEmpty() && + (AddressText != QStringLiteral("00:00:00:00:00:00"))) + { + return QStringLiteral("addr:%1").arg(AddressText); + } + + const QString DeviceName = DeviceInfo.name().trimmed(); + if (!DeviceName.isEmpty()) + { + return QStringLiteral("name:%1#%2").arg(DeviceName).arg(CandidateOrdinal); + } + + return QStringLiteral("candidate:%1").arg(CandidateOrdinal); +} + +bool Dri_Nus_HasStableConnectIdentity(const QBluetoothDeviceInfo& DeviceInfo) +{ + QString DeviceUuidText = DeviceInfo.deviceUuid().toString().trimmed(); + DeviceUuidText.remove(QLatin1Char('{')); + DeviceUuidText.remove(QLatin1Char('}')); + if (!DeviceUuidText.isEmpty()) + { + return true; + } + + const QString AddressText = DeviceInfo.address().toString().trimmed().toUpper(); + return !AddressText.isEmpty() && + (AddressText != QStringLiteral("00:00:00:00:00:00")); +} + +bool Dri_Nus_IsCurrentCandidate( + const Dri_Nus_Struct_Context* p_Context, + const QString& EndpointId) +{ + const Dri_Nus_Struct_Candidate* p_CurrentCandidate = Dri_Nus_GetCurrentCandidate(p_Context); + return (p_CurrentCandidate != nullptr) && + (p_CurrentCandidate->EndpointId.compare(EndpointId, Qt::CaseInsensitive) == 0); +} + +void Dri_Nus_ResetServiceState(Dri_Nus_Struct_Port* p_Port) +{ + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + if (p_Context == nullptr) + { + return; + } + + p_Context->p_Service = nullptr; + p_Context->WriteCharacteristic = QLowEnergyCharacteristic(); + p_Context->NotifyCharacteristic = QLowEnergyCharacteristic(); + p_Context->NotifyDescriptor = QLowEnergyDescriptor(); + p_Context->HasTargetService = false; + p_Port->IsConnected = false; + p_Port->HasWriteAck = false; +} + +void Dri_Nus_ReleaseConnectionObjects(Dri_Nus_Struct_Context* p_Context) +{ + if (p_Context == nullptr) + { + return; + } + + p_Context->p_Service = nullptr; + p_Context->WriteCharacteristic = QLowEnergyCharacteristic(); + p_Context->NotifyCharacteristic = QLowEnergyCharacteristic(); + p_Context->NotifyDescriptor = QLowEnergyDescriptor(); + p_Context->HasTargetService = false; + + if (p_Context->p_Controller != nullptr) + { + p_Context->p_Controller->disconnect(); + p_Context->p_Controller->deleteLater(); + p_Context->p_Controller = nullptr; + } +} + +void Dri_Nus_QueuePacket( + Dri_Nus_Struct_Context* p_Context, + const QByteArray& PacketBody, + const QString& PortName, + const QString& EndpointId) +{ + if ((p_Context == nullptr) || PacketBody.isEmpty()) + { + return; + } + + Com_Struct_RawPacket Packet; + Packet.IsValid = Com_Protocol_DecodeMessageType(PacketBody, &Packet.ProtocolType); + Packet.Source = Com_Enum_RawPacketSource_BleNus; + Packet.PortName = PortName; + Packet.EndpointId = EndpointId; + Packet.ByteArray = PacketBody; + + if (Packet.IsValid) + { + p_Context->PacketQueue.enqueue(Packet); + } +} + +void Dri_Nus_DeleteContext(Dri_Nus_Struct_Context* p_Context) +{ + if (p_Context == nullptr) + { + return; + } + + if (p_Context->p_DiscoveryAgent != nullptr) + { + p_Context->p_DiscoveryAgent->stop(); + } + + if (p_Context->p_Controller != nullptr) + { + p_Context->p_Controller->disconnect(); + p_Context->p_Controller->disconnectFromDevice(); + delete p_Context->p_Controller; + p_Context->p_Controller = nullptr; + } + + delete p_Context->p_DiscoveryAgent; + delete p_Context; +} + +bool Dri_Nus_StartNextCandidate(Dri_Nus_Struct_Port* p_Port); + +void Dri_Nus_AdvanceFromCurrentCandidate( + Dri_Nus_Struct_Port* p_Port, + const QString& TextStatus) +{ + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + if (p_Context == nullptr) + { + return; + } + + const bool IsLockedCandidate = + (p_Context->LockedCandidateIndex >= 0) && + (p_Context->LockedCandidateIndex == p_Context->CurrentCandidateIndex); + + Dri_Nus_ResetServiceState(p_Port); + Dri_Nus_ReleaseConnectionObjects(p_Context); + p_Port->TextEndpointSummary = TextStatus; + + if (IsLockedCandidate) + { + // 宸茬‘璁ょ洰鏍囪澶囨柇寮€鍚庯紝鏈疆浼氳瘽蹇呴』缁撴潫锛涙槸鍚﹂噸鍚灇涓剧敱涓婂眰浼氳瘽閫昏緫鍐冲畾銆? p_Context->LockedCandidateIndex = -1; + p_Port->IsOpened = false; + return; + } + + QTimer::singleShot( + 0, + QCoreApplication::instance(), + [p_Port]() + { + Dri_Nus_StartNextCandidate(p_Port); + }); +} + +void Dri_Nus_AttachServiceSignals( + Dri_Nus_Struct_Port* p_Port, + Dri_Nus_Struct_Context* p_Context, + const QString& EndpointId, + const QString& DeviceLabel) +{ + QObject::connect( + p_Context->p_Service, + &QLowEnergyService::stateChanged, + [p_Port, p_Context, EndpointId, DeviceLabel](QLowEnergyService::ServiceState State) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + if (State != QLowEnergyService::ServiceDiscovered) + { + return; + } + + p_Context->WriteCharacteristic = + p_Context->p_Service->characteristic(kRxCharacteristicUuid); + p_Context->NotifyCharacteristic = + p_Context->p_Service->characteristic(kTxCharacteristicUuid); + + if (!p_Context->WriteCharacteristic.isValid() || + !p_Context->NotifyCharacteristic.isValid()) + { + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE candidate %1 is missing NUS characteristics. Try the next one.") + .arg(DeviceLabel)); + return; + } + + p_Context->NotifyDescriptor = p_Context->NotifyCharacteristic.descriptor( + QBluetoothUuid(QBluetoothUuid::ClientCharacteristicConfiguration)); + if (p_Context->NotifyDescriptor.isValid()) + { + p_Context->p_Service->writeDescriptor( + p_Context->NotifyDescriptor, + QByteArray::fromHex("0100")); + return; + } + + p_Port->IsConnected = true; + p_Port->TextEndpointSummary = + QStringLiteral("NUS service is ready on %1").arg(DeviceLabel); + }); + + QObject::connect( + p_Context->p_Service, + &QLowEnergyService::descriptorWritten, + [p_Port, p_Context, EndpointId, DeviceLabel]( + const QLowEnergyDescriptor& Descriptor, + const QByteArray&) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + if (Descriptor != p_Context->NotifyDescriptor) + { + return; + } + + p_Port->IsConnected = true; + p_Port->TextEndpointSummary = + QStringLiteral("NUS notify is ready on %1").arg(DeviceLabel); + }); + + QObject::connect( + p_Context->p_Service, + &QLowEnergyService::characteristicChanged, + [p_Context, EndpointId, DeviceLabel]( + const QLowEnergyCharacteristic& Characteristic, + const QByteArray& Value) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + if (Characteristic.uuid() != kTxCharacteristicUuid) + { + return; + } + + Dri_Nus_QueuePacket( + p_Context, + Value, + QStringLiteral("BLE NUS (%1)").arg(DeviceLabel), + EndpointId); + }); + + QObject::connect( + p_Context->p_Service, + &QLowEnergyService::characteristicWritten, + [p_Port, p_Context, EndpointId, DeviceLabel]( + const QLowEnergyCharacteristic& Characteristic, + const QByteArray& Value) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + if (Characteristic.uuid() != kRxCharacteristicUuid) + { + return; + } + + Com_Enum_ProtocolType PacketType = Com_Enum_ProtocolType_None; + const QString PacketTypeText = + Com_Protocol_DecodeMessageType(Value, &PacketType) + ? Dri_Nus_GetPacketTypeText(PacketType) + : QStringLiteral("unknown"); + p_Port->TextEndpointSummary = + QStringLiteral("BLE write acknowledged by RX characteristic: %1 (%2)") + .arg(DeviceLabel, PacketTypeText); + p_Port->HasWriteAck = true; + }); + + QObject::connect( + p_Context->p_Service, + static_cast( + &QLowEnergyService::error), + [p_Port, p_Context, EndpointId, DeviceLabel](QLowEnergyService::ServiceError) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE candidate %1 has a NUS service error.").arg(DeviceLabel)); + }); + + p_Context->p_Service->discoverDetails(); +} + +bool Dri_Nus_StartController( + Dri_Nus_Struct_Port* p_Port, + Dri_Nus_Struct_Context* p_Context, + int CandidateIndex) +{ + if ((p_Context == nullptr) || (p_Context->p_Controller != nullptr)) + { + return false; + } + + if ((CandidateIndex < 0) || (CandidateIndex >= p_Context->CandidateList.size())) + { + return false; + } + + const Dri_Nus_Struct_Candidate& Candidate = p_Context->CandidateList.at(CandidateIndex); + p_Context->CurrentCandidateIndex = CandidateIndex; + Dri_Nus_ResetServiceState(p_Port); + + if (!Candidate.DeviceInfo.isValid()) + { + p_Port->TextEndpointSummary = + QStringLiteral("BLE candidate is invalid: %1").arg(Candidate.DeviceLabel); + return false; + } + + if (!Dri_Nus_HasStableConnectIdentity(Candidate.DeviceInfo)) + { + p_Port->TextEndpointSummary = + QStringLiteral("BLE candidate %1 has no stable address/uuid. Skip connect.") + .arg(Candidate.DeviceLabel); + return false; + } + + p_Context->p_Controller = QLowEnergyController::createCentral( + Candidate.DeviceInfo, + QCoreApplication::instance()); + if (p_Context->p_Controller == nullptr) + { + p_Port->TextEndpointSummary = + QStringLiteral("Failed to create BLE controller for %1").arg(Candidate.DeviceLabel); + return false; + } + + const QString EndpointId = Candidate.EndpointId; + const QString DeviceLabel = Candidate.DeviceLabel; + p_Port->TextEndpointSummary = + QStringLiteral("BLE connecting to target device: %1") + .arg(Dri_Nus_FormatCandidateSummary(Candidate)); + + QObject::connect( + p_Context->p_Controller, + &QLowEnergyController::stateChanged, + [p_Port, p_Context, EndpointId, DeviceLabel](QLowEnergyController::ControllerState State) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + p_Port->TextEndpointSummary = + QStringLiteral("BLE controller state %1 for %2") + .arg(static_cast(State)) + .arg(DeviceLabel); + }); + + QObject::connect( + p_Context->p_Controller, + &QLowEnergyController::connected, + [p_Port, p_Context, EndpointId, DeviceLabel]() + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId) || + (p_Context->p_Controller == nullptr)) + { + return; + } + + p_Port->TextEndpointSummary = + QStringLiteral("BLE link connected. Discovering NUS service on %1") + .arg(DeviceLabel); + p_Context->p_Controller->discoverServices(); + }); + + QObject::connect( + p_Context->p_Controller, + &QLowEnergyController::disconnected, + [p_Port, p_Context, EndpointId, DeviceLabel]() + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE candidate disconnected: %1").arg(DeviceLabel)); + }); + + QObject::connect( + p_Context->p_Controller, + static_cast( + &QLowEnergyController::error), + [p_Port, p_Context, EndpointId, DeviceLabel](QLowEnergyController::Error) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE candidate connect error: %1").arg(DeviceLabel)); + }); + + QObject::connect( + p_Context->p_Controller, + &QLowEnergyController::serviceDiscovered, + [p_Port, p_Context, EndpointId, DeviceLabel](const QBluetoothUuid& ServiceUuid) + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + if (ServiceUuid == kServiceUuid) + { + p_Context->HasTargetService = true; + p_Port->TextEndpointSummary = + QStringLiteral("BLE found NUS service on %1").arg(DeviceLabel); + } + }); + + QObject::connect( + p_Context->p_Controller, + &QLowEnergyController::discoveryFinished, + [p_Port, p_Context, EndpointId, DeviceLabel]() + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId)) + { + return; + } + + if (!p_Context->HasTargetService) + { + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE candidate %1 does not expose the NUS service.").arg(DeviceLabel)); + return; + } + + p_Context->p_Service = + p_Context->p_Controller->createServiceObject(kServiceUuid, p_Context->p_Controller); + if (p_Context->p_Service == nullptr) + { + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE candidate %1 failed to create the NUS service object.") + .arg(DeviceLabel)); + return; + } + + Dri_Nus_AttachServiceSignals( + p_Port, + p_Context, + EndpointId, + DeviceLabel); + }); + + QTimer::singleShot( + 0, + p_Context->p_Controller, + [p_Port, p_Context, EndpointId, DeviceLabel]() + { + if (!Dri_Nus_IsCurrentCandidate(p_Context, EndpointId) || + (p_Context->p_Controller == nullptr)) + { + return; + } + + p_Port->TextEndpointSummary = + QStringLiteral("BLE starting GATT connect on %1").arg(DeviceLabel); + p_Context->p_Controller->connectToDevice(); + }); + return true; +} + +bool Dri_Nus_StartNextCandidate(Dri_Nus_Struct_Port* p_Port) +{ + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + if ((p_Context == nullptr) || + !p_Port->IsOpened || + !p_Context->IsDiscoveryFinished || + (p_Context->LockedCandidateIndex >= 0) || + (p_Context->p_Controller != nullptr)) + { + return false; + } + + for (int Index = p_Context->CurrentCandidateIndex + 1; + Index < p_Context->CandidateList.size(); + ++Index) + { + if (Dri_Nus_StartController(p_Port, p_Context, Index)) + { + return true; + } + } + + p_Port->IsOpened = false; + p_Port->TextEndpointSummary = p_Context->CandidateList.isEmpty() + ? QStringLiteral("No target BLE keyboard was discovered for NUS handshake.") + : QStringLiteral("All target BLE candidates failed NUS handshake confirmation."); + return false; +} + +} // namespace + +void Dri_Nus_Close(Dri_Nus_Struct_Port* p_Port) +{ + Dri_Nus_DeleteContext(p_Port->p_Context); + *p_Port = Dri_Nus_Struct_Port(); +} + +bool Dri_Nus_Init( + Dri_Nus_Struct_Port* p_Port, + const Com_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus) +{ + Dri_Nus_Close(p_Port); + + if ((QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() & + QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) == 0) + { + p_Port->TextEndpointSummary = QStringLiteral("The current Qt platform does not support BLE."); + if (p_TextStatus != nullptr) + { + *p_TextStatus = p_Port->TextEndpointSummary; + } + return false; + } + + auto* p_Context = new Dri_Nus_Struct_Context(); + p_Context->p_DiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(); + p_Context->p_DiscoveryAgent->setLowEnergyDiscoveryTimeout(3000); + + QObject::connect( + p_Context->p_DiscoveryAgent, + &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + [p_Port, p_Context](const QBluetoothDeviceInfo& DeviceInfo) + { + if ((DeviceInfo.coreConfigurations() & + QBluetoothDeviceInfo::LowEnergyCoreConfiguration) == 0) + { + return; + } + + Dri_Nus_Struct_Candidate Candidate; + Candidate.DeviceInfo = DeviceInfo; + Candidate.DeviceName = DeviceInfo.name().trimmed(); + if (!Dri_Nus_IsPreferredDeviceName(Candidate.DeviceName)) + { + return; + } + + Candidate.AddressText = Dri_Nus_NormalizeAddressText(DeviceInfo.address()); + Candidate.DeviceLabel = !Candidate.DeviceName.isEmpty() + ? Candidate.DeviceName + : Candidate.AddressText; + Candidate.EndpointId = + Dri_Nus_BuildEndpointId(DeviceInfo, p_Context->CandidateList.size() + 1); + + for (const Dri_Nus_Struct_Candidate& ExistingCandidate : p_Context->CandidateList) + { + if (ExistingCandidate.EndpointId.compare( + Candidate.EndpointId, + Qt::CaseInsensitive) == 0) + { + return; + } + } + + p_Context->CandidateList.append(Candidate); + p_Port->TextEndpointSummary = + QStringLiteral("BLE target candidate discovered: %1") + .arg(Dri_Nus_FormatCandidateSummary(Candidate)); + }); + + QObject::connect( + p_Context->p_DiscoveryAgent, + &QBluetoothDeviceDiscoveryAgent::finished, + [p_Port, p_Context]() + { + p_Context->IsDiscoveryFinished = true; + p_Port->TextEndpointSummary = p_Context->CandidateList.isEmpty() + ? QStringLiteral("BLE scan finished, but the target keyboard name was not found.") + : QStringLiteral("BLE scan finished. %1 target candidate(s) found. Starting handshake.") + .arg(p_Context->CandidateList.size()); + QTimer::singleShot( + 0, + p_Context->p_DiscoveryAgent, + [p_Port]() + { + Dri_Nus_StartNextCandidate(p_Port); + }); + }); + + QObject::connect( + p_Context->p_DiscoveryAgent, + static_cast( + &QBluetoothDeviceDiscoveryAgent::error), + [p_Port, p_Context](QBluetoothDeviceDiscoveryAgent::Error) + { + p_Context->IsDiscoveryFinished = true; + QTimer::singleShot( + 0, + p_Context->p_DiscoveryAgent, + [p_Port]() + { + Dri_Nus_StartNextCandidate(p_Port); + }); + }); + + p_Port->p_Context = p_Context; + p_Port->IsOpened = true; + p_Port->TextEndpointSummary = + QStringLiteral("BLE scan started. Looking for the target keyboard name only."); + p_Context->p_DiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); + + if (p_TextStatus != nullptr) + { + *p_TextStatus = p_Port->TextEndpointSummary; + } + + Q_UNUSED(DeviceConfig); + return true; +} + +bool Dri_Nus_Read( + Dri_Nus_Struct_Port* p_Port, + Com_Struct_RawPacket* p_Packet, + QString* p_TextStatus) +{ + *p_Packet = Com_Struct_RawPacket(); + p_Packet->Source = Com_Enum_RawPacketSource_BleNus; + p_Packet->PortName = QStringLiteral("BLE NUS"); + + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + if (!p_Port->IsOpened || (p_Context == nullptr) || p_Context->PacketQueue.isEmpty()) + { + return false; + } + + *p_Packet = p_Context->PacketQueue.dequeue(); + if (p_TextStatus != nullptr) + { + *p_TextStatus = p_Port->TextEndpointSummary; + } + + return p_Packet->IsValid; +} + +bool Dri_Nus_LockCandidate( + Dri_Nus_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus) +{ + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + const Dri_Nus_Struct_Candidate* p_CurrentCandidate = Dri_Nus_GetCurrentCandidate(p_Context); + if (!p_Port->IsOpened || !p_Port->IsConnected || (p_CurrentCandidate == nullptr)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("BLE NUS does not have a candidate ready to lock yet."); + } + return false; + } + + if (p_CurrentCandidate->EndpointId.compare(EndpointId, Qt::CaseInsensitive) != 0) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("The current BLE candidate does not match the lock target."); + } + return false; + } + + p_Context->LockedCandidateIndex = p_Context->CurrentCandidateIndex; + if (p_Context->p_DiscoveryAgent != nullptr) + { + p_Context->p_DiscoveryAgent->stop(); + } + + p_Port->TextEndpointSummary = + QStringLiteral("BLE NUS 宸查攣瀹氱洰鏍囪澶囷細%1").arg(p_CurrentCandidate->DeviceLabel); + if (p_TextStatus != nullptr) + { + *p_TextStatus = p_Port->TextEndpointSummary; + } + return true; +} + +bool Dri_Nus_DiscardCandidate( + Dri_Nus_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus) +{ + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + const Dri_Nus_Struct_Candidate* p_CurrentCandidate = Dri_Nus_GetCurrentCandidate(p_Context); + if (!p_Port->IsOpened || (p_CurrentCandidate == nullptr)) + { + return false; + } + + if (p_CurrentCandidate->EndpointId.compare(EndpointId, Qt::CaseInsensitive) != 0) + { + return false; + } + + const QString DeviceLabel = p_CurrentCandidate->DeviceLabel; + Dri_Nus_AdvanceFromCurrentCandidate( + p_Port, + QStringLiteral("BLE 宸蹭涪寮冩彙鎵嬩笉鍖归厤鍊欓€夛細%1").arg(DeviceLabel)); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("BLE 宸蹭涪寮冩彙鎵嬩笉鍖归厤鍊欓€夛細%1").arg(DeviceLabel); + } + return true; +} + +bool Dri_Nus_Write( + Dri_Nus_Struct_Port* p_Port, + const QByteArray& PacketBody, + QString* p_TextStatus) +{ + Dri_Nus_Struct_Context* p_Context = p_Port->p_Context; + const Dri_Nus_Struct_Candidate* p_CurrentCandidate = Dri_Nus_GetCurrentCandidate(p_Context); + if (!p_Port->IsOpened || + (p_Context == nullptr) || + (p_Context->p_Service == nullptr) || + (p_CurrentCandidate == nullptr)) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("BLE NUS is not ready yet. Skip send."); + } + return false; + } + + if (PacketBody.isEmpty() || !p_Context->WriteCharacteristic.isValid()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("BLE NUS write characteristic is not ready."); + } + return false; + } + + const auto CharacteristicProperties = p_Context->WriteCharacteristic.properties(); + const bool SupportsWriteWithResponse = + (CharacteristicProperties & QLowEnergyCharacteristic::Write) != 0; + const bool SupportsWriteWithoutResponse = + (CharacteristicProperties & QLowEnergyCharacteristic::WriteNoResponse) != 0; + if (!SupportsWriteWithoutResponse && !SupportsWriteWithResponse) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("BLE NUS RX characteristic has no writable property."); + } + return false; + } + + const QLowEnergyService::WriteMode PrimaryWriteMode = + SupportsWriteWithResponse + ? QLowEnergyService::WriteWithResponse + : QLowEnergyService::WriteWithoutResponse; + + p_Context->p_Service->writeCharacteristic( + p_Context->WriteCharacteristic, + PacketBody, + PrimaryWriteMode); + + if (p_TextStatus != nullptr) + { + const QString WriteModeText = + (PrimaryWriteMode == QLowEnergyService::WriteWithResponse) + ? QStringLiteral("write-with-response") + : QStringLiteral("write-without-response"); + *p_TextStatus = + QStringLiteral("BLE NUS sent protocol packet via %1: %2") + .arg(WriteModeText, p_CurrentCandidate->DeviceLabel); + } + + return true; +} diff --git a/DRI/Dri_Nus.h b/DRI/Dri_Nus.h new file mode 100644 index 0000000..5182bb9 --- /dev/null +++ b/DRI/Dri_Nus.h @@ -0,0 +1,42 @@ +#pragma once + +#include "COM/Com_Def.h" +#include + +struct Dri_Nus_Struct_Context; + +struct Dri_Nus_Struct_Port +{ + bool IsOpened = false; + bool IsConnected = false; + bool HasWriteAck = false; + QString TextEndpointSummary; + Dri_Nus_Struct_Context* p_Context = nullptr; +}; + +void Dri_Nus_Close(Dri_Nus_Struct_Port* p_Port); +bool Dri_Nus_Init( + Dri_Nus_Struct_Port* p_Port, + const Com_Struct_DeviceConfig& DeviceConfig, + QString* p_TextStatus); +bool Dri_Nus_Read( + Dri_Nus_Struct_Port* p_Port, + Com_Struct_RawPacket* p_Packet, + QString* p_TextStatus); + +// Lock the confirmed BLE NUS candidate after LOGIC accepts a HelloRsp. +bool Dri_Nus_LockCandidate( + Dri_Nus_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus); + +// Drop one mismatched BLE candidate and let the driver continue scanning. +bool Dri_Nus_DiscardCandidate( + Dri_Nus_Struct_Port* p_Port, + const QString& EndpointId, + QString* p_TextStatus); + +bool Dri_Nus_Write( + Dri_Nus_Struct_Port* p_Port, + const QByteArray& PacketBody, + QString* p_TextStatus); diff --git a/DRI/Dri_Vendor.cpp b/DRI/Dri_Vendor.cpp index 804140d..89c900d 100644 --- a/DRI/Dri_Vendor.cpp +++ b/DRI/Dri_Vendor.cpp @@ -1,338 +1,248 @@ -#include "DRI/Dri_Vendor.h" +#include "DRI/Dri_Vendor.h" -#include - -#pragma comment(lib, "hid.lib") +#include namespace { -/* ---------- 句柄与状态小工具 ---------- */ - -void Dri_Vendor_Func_SetStatus(QString* p_TextStatus, const QString& Text) +void Dri_Vendor_CloseHandle(HANDLE* p_Handle) { - if (p_TextStatus != nullptr) + if ((*p_Handle != nullptr) && (*p_Handle != INVALID_HANDLE_VALUE)) { - *p_TextStatus = Text; + CloseHandle(*p_Handle); } + *p_Handle = INVALID_HANDLE_VALUE; } -HANDLE Dri_Vendor_Func_OpenHandle(const QString& DevicePath, DWORD DesiredAccess, DWORD Flags) -{ - return CreateFileW( - reinterpret_cast(DevicePath.utf16()), - DesiredAccess, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, - OPEN_EXISTING, - Flags, - nullptr); -} - -bool Dri_Vendor_Func_BeginRead(Dri_Vendor_Struct_Port* p_Port, QString* p_TextStatus) -{ - if (!p_Port->IsOpened || (p_Port->InputLength == 0)) - { - return false; - } - - if (p_Port->IsReadPending) - { - return true; - } - - ResetEvent(p_Port->HandleEvent); - p_Port->ReadBuffer.fill(0, p_Port->InputLength); - p_Port->OverlappedRead = {}; - p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; - - DWORD BytesRead = 0; - if (ReadFile( - p_Port->HandleRead, - p_Port->ReadBuffer.data(), - p_Port->InputLength, - &BytesRead, - &p_Port->OverlappedRead) || - (GetLastError() == ERROR_IO_PENDING)) - { - p_Port->IsReadPending = true; - return true; - } - - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 接口启动异步读取失败:%1").arg(GetLastError())); - return false; -} - -HANDLE Dri_Vendor_Func_OpenWriteHandle(const QString& DevicePath, QString* p_TextError) -{ - HANDLE HandleWrite = Dri_Vendor_Func_OpenHandle(DevicePath, GENERIC_WRITE, 0); - if (HandleWrite != INVALID_HANDLE_VALUE) - { - return HandleWrite; - } - - const DWORD WriteError = GetLastError(); - HandleWrite = Dri_Vendor_Func_OpenHandle(DevicePath, 0, 0); - - if ((HandleWrite == INVALID_HANDLE_VALUE) && (p_TextError != nullptr)) - { - *p_TextError = QStringLiteral("GENERIC_WRITE=%1,ZeroAccess=%2") - .arg(WriteError) - .arg(GetLastError()); - } - return HandleWrite; -} - -bool Dri_Vendor_Func_TryWritePacket( +bool Dri_Vendor_TryWrite( HANDLE HandleWrite, quint16 OutputLength, const QByteArray& Packet, - QString* p_TextError) + const QString& SuccessText, + const QString& ErrorPrefix, + QString* p_TextStatus, + QStringList* p_ErrorList) { - if ((HandleWrite == INVALID_HANDLE_VALUE) || Packet.isEmpty()) - { - return false; - } - - QByteArray Buffer = Packet; - if ((OutputLength > 0) && (Buffer.size() < OutputLength)) - { - Buffer.append(OutputLength - Buffer.size(), 0); - } - - if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast(Buffer.size()))) + QString TextError; + if (Dri_Hid_WritePacket(HandleWrite, OutputLength, Packet, &TextError)) { + if (p_TextStatus != nullptr) + { + *p_TextStatus = SuccessText; + } return true; } - const DWORD SetReportError = GetLastError(); - DWORD BytesWritten = 0; - if (WriteFile( - HandleWrite, - Buffer.constData(), - static_cast(Buffer.size()), - &BytesWritten, - nullptr) && - (BytesWritten == static_cast(Buffer.size()))) + if (!TextError.isEmpty()) { - return true; - } - - if (p_TextError != nullptr) - { - *p_TextError = QStringLiteral("SetOutputReport=%1,WriteFile=%2") - .arg(SetReportError) - .arg(GetLastError()); + p_ErrorList->append(ErrorPrefix.arg(TextError)); } return false; } } // namespace -/* ---------- 生命周期 ---------- */ - -void Dri_Vendor_Func_Close(Dri_Vendor_Struct_Port* p_Port) +void Dri_Vendor_Close(Dri_Vendor_Struct_Port* p_Port) { - if ((p_Port->HandleRead != INVALID_HANDLE_VALUE) && p_Port->IsReadPending) - { - CancelIoEx(p_Port->HandleRead, &p_Port->OverlappedRead); - } - - for (HANDLE* p_Handle : { &p_Port->HandleRead, &p_Port->HandleWriteVendor, &p_Port->HandleWriteNkro, &p_Port->HandleEvent }) - { - if ((*p_Handle != nullptr) && (*p_Handle != INVALID_HANDLE_VALUE)) - { - CloseHandle(*p_Handle); - } - } - - *p_Port = Dri_Vendor_Struct_Port(); + Dri_Hid_CloseReadPort(&p_Port->ReadPort); + Dri_Vendor_CloseHandle(&p_Port->HandleWriteVendor); + Dri_Vendor_CloseHandle(&p_Port->HandleWriteCommand); + Dri_Vendor_CloseHandle(&p_Port->HandleWriteNkro); + p_Port->VendorWriteOutputLength = 0; + p_Port->CommandWriteOutputLength = 0; + p_Port->NkroWriteOutputLength = 0; + p_Port->IsBluetoothTransport = false; } -bool Dri_Vendor_Func_Open( +bool Dri_Vendor_Init( Dri_Vendor_Struct_Port* p_Port, const Mid_Struct_DeviceConfig& DeviceConfig, QString* p_TextStatus) { - Dri_Vendor_Func_Close(p_Port); + Dri_Vendor_Close(p_Port); + + Mid_Struct_DeviceMatch VendorMatch; + VendorMatch.VendorId = DeviceConfig.VendorId; + VendorMatch.ProductId = DeviceConfig.ProductId; + VendorMatch.UsagePage = MID_CONST_USAGE_PAGE_VENDOR; + VendorMatch.Usage = MID_CONST_USAGE_VENDOR; QString VendorPath; + QString VendorInstanceId; quint16 InputLength = 0; - quint16 OutputLength = 0; - if (!Mid_Func_FindHidInterface( - Mid_Func_GetVendorMatch(DeviceConfig), + quint16 VendorOutputLength = 0; + if (!Mid_FindHidInterface( + VendorMatch, &VendorPath, &InputLength, - &OutputLength)) + &VendorOutputLength, + &VendorInstanceId)) { - Dri_Vendor_Func_SetStatus(p_TextStatus, QStringLiteral("未找到 Vendor 接口:FF00 / 0002。")); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Vendor interface was not found: FF00 / 0002."); + } return false; } - p_Port->HandleRead = Dri_Vendor_Func_OpenHandle(VendorPath, GENERIC_READ, FILE_FLAG_OVERLAPPED); - if (p_Port->HandleRead == INVALID_HANDLE_VALUE) + p_Port->IsBluetoothTransport = Mid_IsBluetoothHidInstanceId(VendorInstanceId); + const QString VendorPortName = p_Port->IsBluetoothTransport + ? QStringLiteral("Bluetooth/HID Vendor") + : QStringLiteral("Vendor"); + + if (!Dri_Hid_InitReadPort( + &p_Port->ReadPort, + VendorPath, + InputLength, + Mid_Enum_RawPacketSource_UsbVendorHid, + VendorPortName, + p_TextStatus)) { - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 接口打开读句柄失败:%1").arg(GetLastError())); return false; } - p_Port->HandleEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (p_Port->HandleEvent == nullptr) - { - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 接口创建事件失败:%1").arg(GetLastError())); - Dri_Vendor_Func_Close(p_Port); - return false; - } + p_Port->HandleWriteVendor = Dri_Hid_OpenWriteHandle(VendorPath, nullptr); + p_Port->VendorWriteOutputLength = VendorOutputLength; - p_Port->HandleWriteVendor = Dri_Vendor_Func_OpenWriteHandle(VendorPath, nullptr); + QString CommandPath; + quint16 CommandOutputLength = 0; + Mid_Struct_DeviceMatch CommandMatch; + CommandMatch.VendorId = DeviceConfig.VendorId; + CommandMatch.ProductId = DeviceConfig.ProductId; + CommandMatch.UsagePage = MID_CONST_USAGE_PAGE_VENDOR_COMMAND; + CommandMatch.Usage = MID_CONST_USAGE_VENDOR_COMMAND; + if (Mid_FindHidInterface( + CommandMatch, + &CommandPath, + nullptr, + &CommandOutputLength)) + { + QString TextError; + p_Port->HandleWriteCommand = Dri_Hid_OpenWriteHandle(CommandPath, &TextError); + p_Port->CommandWriteOutputLength = CommandOutputLength; + if ((p_Port->HandleWriteCommand == INVALID_HANDLE_VALUE) && (p_TextStatus != nullptr)) + { + *p_TextStatus = QStringLiteral("Vendor command write handle failed: %1").arg(TextError); + } + } + else if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("Vendor command interface was not found: FF01 / 0005."); + } QString NkroPath; quint16 NkroOutputLength = 0; - if (Mid_Func_FindHidInterface( - Mid_Func_GetNkroMatch(DeviceConfig), + Mid_Struct_DeviceMatch NkroMatch; + NkroMatch.VendorId = DeviceConfig.VendorId; + NkroMatch.ProductId = DeviceConfig.ProductId; + NkroMatch.UsagePage = MID_CONST_USAGE_PAGE_NKRO; + NkroMatch.Usage = MID_CONST_USAGE_NKRO; + if (Mid_FindHidInterface( + NkroMatch, &NkroPath, nullptr, &NkroOutputLength)) { QString TextError; - p_Port->HandleWriteNkro = Dri_Vendor_Func_OpenWriteHandle(NkroPath, &TextError); + p_Port->HandleWriteNkro = Dri_Hid_OpenWriteHandle(NkroPath, &TextError); p_Port->NkroWriteOutputLength = NkroOutputLength; - - if (p_Port->HandleWriteNkro == INVALID_HANDLE_VALUE) + if ((p_Port->HandleWriteNkro == INVALID_HANDLE_VALUE) && (p_TextStatus != nullptr)) { - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 读链路已打开,但 NKRO 写句柄打开失败:%1").arg(TextError)); + *p_TextStatus = QStringLiteral("NKRO write handle failed: %1").arg(TextError); } } - else + else if (p_TextStatus != nullptr) { - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 读链路已打开,但没有找到 NKRO 写接口。")); + *p_TextStatus = QStringLiteral("NKRO write interface was not found."); } - p_Port->InputLength = InputLength; - p_Port->OutputLength = OutputLength; - p_Port->ReadBuffer = QByteArray(InputLength, 0); - p_Port->OverlappedRead.hEvent = p_Port->HandleEvent; - p_Port->IsOpened = true; - - if (!Dri_Vendor_Func_BeginRead(p_Port, p_TextStatus)) - { - Dri_Vendor_Func_Close(p_Port); - return false; - } return true; } -/* ---------- 读写流程 ---------- */ - -bool Dri_Vendor_Func_Read( +bool Dri_Vendor_Read( Dri_Vendor_Struct_Port* p_Port, Mid_Struct_RawPacket* p_Packet, QString* p_TextStatus) { - *p_Packet = Mid_Struct_RawPacket(); - p_Packet->PortName = QStringLiteral("Vendor"); - - if (!p_Port->IsOpened) - { - return false; - } - - if (!p_Port->IsReadPending) - { - Dri_Vendor_Func_BeginRead(p_Port, p_TextStatus); - return false; - } - - DWORD BytesRead = 0; - if (!GetOverlappedResult(p_Port->HandleRead, &p_Port->OverlappedRead, &BytesRead, FALSE)) - { - const DWORD ErrorCode = GetLastError(); - if (ErrorCode == ERROR_IO_INCOMPLETE) - { - return false; - } - - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 接口读包失败:%1").arg(ErrorCode)); - Dri_Vendor_Func_Close(p_Port); - return false; - } - - p_Port->IsReadPending = false; - if (BytesRead > 0) - { - p_Packet->IsValid = true; - p_Packet->ByteArray = p_Port->ReadBuffer.left(static_cast(BytesRead)); - } - - Dri_Vendor_Func_BeginRead(p_Port, p_TextStatus); - return p_Packet->IsValid; + return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus); } -bool Dri_Vendor_Func_Write( +bool Dri_Vendor_Write( Dri_Vendor_Struct_Port* p_Port, const QByteArray& ByteArray, QString* p_TextStatus) { - if (!p_Port->IsOpened) + const QString RouteLabel = p_Port->IsBluetoothTransport + ? QStringLiteral("Bluetooth Vendor") + : QStringLiteral("Vendor"); + + if (!p_Port->ReadPort.IsOpened) { - Dri_Vendor_Func_SetStatus( - p_TextStatus, - QStringLiteral("Vendor 读链路未打开,不能发送掩码。")); + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("%1 read path is not open, packet was not sent.").arg(RouteLabel); + } + return false; + } + if (ByteArray.isEmpty()) + { + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("%1 packet is empty.").arg(RouteLabel); + } return false; } - QStringList Errors; - const auto TryWriteTo = [&](HANDLE HandleWrite, - quint16 OutputLength, - const QString& SuccessText, - const QString& ErrorPrefix) + QStringList ErrorList; + const quint8 ReportId = static_cast(ByteArray.at(0)); + + if (ReportId == Mid_Enum_ReportId_VendorCommand) { - QString TextError; - if (Dri_Vendor_Func_TryWritePacket(HandleWrite, OutputLength, ByteArray, &TextError)) + if (Dri_Vendor_TryWrite( + p_Port->HandleWriteCommand, + p_Port->CommandWriteOutputLength, + ByteArray, + QStringLiteral("Packet sent to %1 command interface.").arg(RouteLabel), + RouteLabel + QStringLiteral(" command write failed: %1"), + p_TextStatus, + &ErrorList)) { - Dri_Vendor_Func_SetStatus(p_TextStatus, SuccessText); return true; } - if (!TextError.isEmpty()) + } + else + { + if (Dri_Vendor_TryWrite( + p_Port->HandleWriteNkro, + p_Port->NkroWriteOutputLength, + ByteArray, + QStringLiteral("Packet sent to NKRO write interface."), + QStringLiteral("NKRO write failed: %1"), + p_TextStatus, + &ErrorList)) { - Errors.append(ErrorPrefix.arg(TextError)); + return true; } - return false; - }; - if (TryWriteTo( - p_Port->HandleWriteNkro, - p_Port->NkroWriteOutputLength, - QStringLiteral("掩码已发送到 NKRO 写接口。"), - QStringLiteral("NKRO 写接口发送失败:%1"))) - { - return true; - } - if (TryWriteTo( - p_Port->HandleWriteVendor, - p_Port->OutputLength, - QStringLiteral("掩码已发送到 Vendor 写接口。"), - QStringLiteral("Vendor 写接口发送失败:%1"))) - { - return true; + if (Dri_Vendor_TryWrite( + p_Port->HandleWriteVendor, + p_Port->VendorWriteOutputLength, + ByteArray, + QStringLiteral("Packet sent to %1 write interface.").arg(RouteLabel), + RouteLabel + QStringLiteral(" write failed: %1"), + p_TextStatus, + &ErrorList)) + { + return true; + } } - Dri_Vendor_Func_SetStatus( - p_TextStatus, - Errors.isEmpty() - ? QStringLiteral("没有可用的写句柄,掩码发送未执行。") - : Errors.join(QStringLiteral("\n"))); + if (p_TextStatus != nullptr) + { + *p_TextStatus = ErrorList.isEmpty() + ? QStringLiteral("No writable output handle is available, packet send was skipped.") + : ErrorList.join(QStringLiteral("\n")); + } return false; } + diff --git a/DRI/Dri_Vendor.h b/DRI/Dri_Vendor.h index 97233eb..276688b 100644 --- a/DRI/Dri_Vendor.h +++ b/DRI/Dri_Vendor.h @@ -1,42 +1,31 @@ -#pragma once +#pragma once -#include "MID/Mid_Def.h" -#include -#include -#include +#include "DRI/Dri_Hid.h" -/* - * DRI Vendor 层:读写固件的 Vendor/NKRO HID 接口,支撑逻辑层功能诊断。 - * - */ +// USB vendor reader plus three write routes: vendor, command, and NKRO. struct Dri_Vendor_Struct_Port { - /* 读写句柄:Vendor 写、NKRO 写与通用读各占一个 */ - HANDLE HandleRead = INVALID_HANDLE_VALUE; + Dri_Hid_Struct_ReadPort ReadPort; HANDLE HandleWriteVendor = INVALID_HANDLE_VALUE; + HANDLE HandleWriteCommand = INVALID_HANDLE_VALUE; HANDLE HandleWriteNkro = INVALID_HANDLE_VALUE; - HANDLE HandleEvent = nullptr; - OVERLAPPED OverlappedRead = {}; - /* 运行状态 + 报文长度缓存,便于 UI/LOGIC 层快速判断 */ - bool IsOpened = false; - bool IsReadPending = false; - quint16 InputLength = 0; - quint16 OutputLength = 0; + quint16 VendorWriteOutputLength = 0; + quint16 CommandWriteOutputLength = 0; quint16 NkroWriteOutputLength = 0; - QByteArray ReadBuffer; + bool IsBluetoothTransport = false; }; -/* 关闭全部句柄,安全退出时务必调用以便复用设备。 */ -void Dri_Vendor_Func_Close(Dri_Vendor_Struct_Port* p_Port); -/* 根据 VID/PID 打开对应 HID 接口,并创建必需句柄。 */ -bool Dri_Vendor_Func_Open(Dri_Vendor_Struct_Port* p_Port, +void Dri_Vendor_Close(Dri_Vendor_Struct_Port* p_Port); +bool Dri_Vendor_Init( + Dri_Vendor_Struct_Port* p_Port, const Mid_Struct_DeviceConfig& DeviceConfig, QString* p_TextStatus); -/* 读取 Vendor 报文:一次返回一个 Mid_Struct_RawPacket。 */ -bool Dri_Vendor_Func_Read(Dri_Vendor_Struct_Port* p_Port, +bool Dri_Vendor_Read( + Dri_Vendor_Struct_Port* p_Port, Mid_Struct_RawPacket* p_Packet, QString* p_TextStatus); -/* 写入 Vendor/NKRO 报文,ByteArray 按固件定义组包。 */ -bool Dri_Vendor_Func_Write(Dri_Vendor_Struct_Port* p_Port, +bool Dri_Vendor_Write( + Dri_Vendor_Struct_Port* p_Port, const QByteArray& ByteArray, QString* p_TextStatus); + diff --git a/LOGIC/Lgc_Consumer.cpp b/LOGIC/Lgc_Consumer.cpp deleted file mode 100644 index f2c91fa..0000000 --- a/LOGIC/Lgc_Consumer.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "LOGIC/Lgc_Consumer.h" - -void Lgc_Consumer_Func_Parse(const QByteArray& ByteArray, Lgc_Consumer_Struct_Result* p_Result) -{ - // 当前调用链内部固定传有效结果对象,这里直接清成默认状态。 - *p_Result = Lgc_Consumer_Struct_Result(); - - // DRI 这轮没有交上来数据时,直接给出提示。 - if (ByteArray.isEmpty()) - { - p_Result->TextExplain = QStringLiteral("Consumer 端口没有收到数据。"); - return; - } - - // 第 0 字节不是 0x03,就说明这不是 Consumer 包。 - if (static_cast(ByteArray.at(0)) != Mid_Enum_ReportId_Consumer) - { - p_Result->TextExplain = QStringLiteral("这不是 report id 0x03。"); - return; - } - - p_Result->IsMatch = true; - - // 当前固件里的 0x03 包固定就是 3 字节。 - if (ByteArray.size() != MID_CONST_PACKET_SIZE_CONSUMER) - { - p_Result->TextExplain = QStringLiteral("0x03 包长度不对。"); - return; - } - - p_Result->IsLengthOk = true; - - // 第 1、2 字节按 little-endian 规则还原成 16 位 usage。 - p_Result->Usage = - static_cast(ByteArray.at(1)) | - (static_cast(static_cast(ByteArray.at(2))) << 8); - - // 当前项目里只保留简短解释,避免调试日志过于冗长。 - p_Result->TextExplain = QStringLiteral("0x03 Consumer:%1") - .arg(Mid_Func_GetConsumerUsageText(p_Result->Usage)); -} diff --git a/LOGIC/Lgc_Consumer.h b/LOGIC/Lgc_Consumer.h deleted file mode 100644 index ee4c4f6..0000000 --- a/LOGIC/Lgc_Consumer.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "MID/Mid_Def.h" - -/* - * 这份文件负责解析 report id 0x03 的 Consumer 包。 - * - * 当前下位机真实结构很简单: - * 1. 第 0 字节是 report id,固定为 0x03 - * 2. 第 1、2 字节拼成一个 16 位 consumer usage - * 3. 整包固定长度 3 字节 - * - * 所以这里不需要像 0x01 / 0x04 那样去拆 modifier 和 usage 位图。 - */ - -struct Lgc_Consumer_Struct_Result -{ - // 这一包是否匹配 report id 0x03。 - bool IsMatch = false; - - // 如果匹配 0x03,长度是否也正确。 - bool IsLengthOk = false; - - // 解析得到的 consumer usage。 - quint16 Usage = 0; - - // 给调试窗口显示的简短中文说明。 - QString TextExplain; -}; - -void Lgc_Consumer_Func_Parse(const QByteArray& ByteArray, Lgc_Consumer_Struct_Result* p_Result); diff --git a/LOGIC/Lgc_Core.cpp b/LOGIC/Lgc_Core.cpp index f22bebf..4a0727a 100644 --- a/LOGIC/Lgc_Core.cpp +++ b/LOGIC/Lgc_Core.cpp @@ -1,606 +1,1010 @@ -#include "LOGIC/Lgc_Core.h" +#include "LOGIC/Lgc_Core_Private.h" -#include "MID/Mid_Def.h" #include -#include #include namespace { -/* ---------- 日志与状态文本 ---------- */ +constexpr qint64 kCdcRefreshIntervalMs = 1000; +constexpr qint64 kPendingCommandTimeoutMs = 1800; +constexpr int kTestLogMaxLineCount = 240; -QString Lgc_Core_Func_GetTimeText() +quint64 Lgc_Core_ProtocolTypeBit(Com_Enum_ProtocolType Type) { - return QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz")); -} - -void Lgc_Core_Func_AppendLog(Lgc_Core_Struct_State* p_State, const QString& Text) -{ - if (Text.isEmpty()) + switch (Type) { - return; + case Com_Enum_ProtocolType_Bitmap: + case Com_Enum_ProtocolType_TimeSync: + case Com_Enum_ProtocolType_ThemeRgb: + break; + default: + return 0; } - if (p_State->TextLog.isEmpty()) + const quint32 RawType = static_cast(Type); + return RawType < 64U ? (1ULL << RawType) : 0ULL; +} + +bool Lgc_Core_TryRefreshCdcPort(Lgc_Core_Struct_State* p_State, QString* p_TextStatus) +{ + if ((p_State == nullptr) || + !p_State->IsStarted || + p_State->DeviceReady || + p_State->DriCdcPort.IsOpened) { - p_State->TextLog = Text; + return false; + } + + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); + if ((p_State->LastCdcRefreshAttemptMs != 0) && + ((NowMs - p_State->LastCdcRefreshAttemptMs) < kCdcRefreshIntervalMs)) + { + return false; + } + + p_State->LastCdcRefreshAttemptMs = NowMs; + const bool IsOpened = + Dri_Cdc_Init(&p_State->DriCdcPort, p_State->DeviceConfig, p_TextStatus); + if (IsOpened) + { + p_State->IsUsbHelloSent = false; + p_State->LastUsbHelloAttemptMs = 0; + } + + return IsOpened; +} + +bool Lgc_Core_LogNusSummaryIfChanged(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + const QString CurrentSummary = p_State->DriNusPort.TextEndpointSummary.trimmed(); + if (CurrentSummary.isEmpty() || + (CurrentSummary == p_State->LastLoggedNusEndpointSummary)) + { + return false; + } + + p_State->LastLoggedNusEndpointSummary = CurrentSummary; + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("BLE status: %1").arg(CurrentSummary)); + return true; +} + +bool Lgc_Core_UpdateNusConnectionWindow(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + const bool IsConnectedNow = p_State->DriNusPort.IsConnected; + if (IsConnectedNow == p_State->WasNusConnectedLastPoll) + { + return false; + } + + p_State->WasNusConnectedLastPoll = IsConnectedNow; + if (IsConnectedNow) + { + p_State->NusReadySinceMs = QDateTime::currentMSecsSinceEpoch(); + p_State->NusHelloRetryCount = 0; + p_State->LastNusHelloAttemptMs = 0; + p_State->HasLoggedNusWriteAck = false; + p_State->HasLoggedNusHelloTimeout = false; + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("BLE handshake window opened. Wait briefly before the first HelloReq.")); } else { - p_State->TextLog.append(QStringLiteral("\n\n")); - p_State->TextLog.append(Text); + p_State->NusReadySinceMs = 0; + p_State->NusHelloRetryCount = 0; + p_State->LastNusHelloAttemptMs = 0; + p_State->IsNusHelloSent = false; + p_State->HasLoggedNusWriteAck = false; + p_State->HasLoggedNusHelloTimeout = false; } - if (p_State->TextLog.size() > 24000) - { - p_State->TextLog = p_State->TextLog.right(20000); - } -} - -void Lgc_Core_Func_AppendStatusLog(Lgc_Core_Struct_State* p_State, const QString& Text) -{ - if (!Text.isEmpty()) - { - Lgc_Core_Func_AppendLog( - p_State, - QStringLiteral("[%1] 状态\n%2").arg(Lgc_Core_Func_GetTimeText(), Text)); - } -} - -void Lgc_Core_Func_AppendPacketLog( - Lgc_Core_Struct_State* p_State, - const QString& ActionText, - const QString& PortName, - const QByteArray& Packet, - const QString& ExplainText) -{ - QString Text = QStringLiteral("[%1] %2\n端口: %3\nHEX: %4") - .arg(Lgc_Core_Func_GetTimeText(), ActionText, PortName, Mid_Func_GetHexText(Packet)); - if (!ExplainText.isEmpty()) - { - Text.append(QLatin1Char('\n')); - Text.append(ExplainText); - } - Lgc_Core_Func_AppendLog(p_State, Text); -} - -struct Lgc_Core_Struct_MaskStateBackup -{ - QByteArray FunctionMaskBitmap; - QByteArray SwapMaskBitmap; - QByteArray KeyboardMaskBitmap; - bool IsSwapModeOn = false; - quint16 SwapUsageLeft = 0; - quint16 SwapUsageRight = 0; - bool IsSwapLeftPhysicalPressed = false; - bool IsSwapRightPhysicalPressed = false; -}; - -/* ---------- 按键状态与掩码 ---------- */ - -void Lgc_Core_Func_ClearAllKeyStates(Lgc_Core_Struct_State* p_State) -{ - p_State->IsVisibleKeyStateValid = false; - p_State->VisibleModifier = 0; - p_State->VisibleUsageList.clear(); - p_State->IsPhysicalKeyStateValid = false; - p_State->PhysicalModifier = 0; - p_State->PhysicalUsageList.clear(); - p_State->LastPhysicalUsageList.clear(); - p_State->IsSwapLeftPhysicalPressed = false; - p_State->IsSwapRightPhysicalPressed = false; -} - -void Lgc_Core_Func_CloseAllPorts(Lgc_Core_Struct_State* p_State) -{ - Dri_NkroRaw_Func_Close(&p_State->DriNkroPort); - Dri_Consumer_Func_Close(&p_State->DriConsumerPort); - Dri_Vendor_Func_Close(&p_State->DriVendorPort); -} - -Lgc_Core_Struct_MaskStateBackup Lgc_Core_Func_BackupMaskState(const Lgc_Core_Struct_State* p_State) -{ - return { - p_State->FunctionMaskBitmap, - p_State->SwapMaskBitmap, - p_State->KeyboardMaskBitmap, - p_State->IsSwapModeOn, - p_State->SwapUsageLeft, - p_State->SwapUsageRight, - p_State->IsSwapLeftPhysicalPressed, - p_State->IsSwapRightPhysicalPressed - }; -} - -void Lgc_Core_Func_RestoreMaskState( - Lgc_Core_Struct_State* p_State, - const Lgc_Core_Struct_MaskStateBackup& Backup) -{ - p_State->FunctionMaskBitmap = Backup.FunctionMaskBitmap; - p_State->SwapMaskBitmap = Backup.SwapMaskBitmap; - p_State->KeyboardMaskBitmap = Backup.KeyboardMaskBitmap; - p_State->IsSwapModeOn = Backup.IsSwapModeOn; - p_State->SwapUsageLeft = Backup.SwapUsageLeft; - p_State->SwapUsageRight = Backup.SwapUsageRight; - p_State->IsSwapLeftPhysicalPressed = Backup.IsSwapLeftPhysicalPressed; - p_State->IsSwapRightPhysicalPressed = Backup.IsSwapRightPhysicalPressed; -} - -bool Lgc_Core_Func_IsUsageEnabledInMask(const QByteArray& UsageBitmap, quint16 Usage) -{ - const int ByteIndex = Usage / 8; - if (ByteIndex >= UsageBitmap.size()) - { - return false; - } - - return (static_cast(UsageBitmap.at(ByteIndex)) & - static_cast(1U << (Usage % 8))) != 0; -} - -void Lgc_Core_Func_SetUsageEnabledInMask(QByteArray* p_UsageBitmap, quint16 Usage, bool IsEnabled) -{ - const int ByteIndex = Usage / 8; - if (ByteIndex >= p_UsageBitmap->size()) - { - return; - } - - const quint8 BitMask = static_cast(1U << (Usage % 8)); - quint8 Value = static_cast(p_UsageBitmap->at(ByteIndex)); - Value = IsEnabled ? static_cast(Value | BitMask) : static_cast(Value & static_cast(~BitMask)); - (*p_UsageBitmap)[ByteIndex] = static_cast(Value); -} - -void Lgc_Core_Func_FillMaskAllEnabled(QByteArray* p_UsageBitmap) -{ - *p_UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF)); -} - -void Lgc_Core_Func_RebuildKeyboardMask(Lgc_Core_Struct_State* p_State) -{ - if (p_State->FunctionMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE) - { - Lgc_Core_Func_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); - } - if (p_State->SwapMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE) - { - Lgc_Core_Func_FillMaskAllEnabled(&p_State->SwapMaskBitmap); - } - - p_State->KeyboardMaskBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF)); - for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index) - { - p_State->KeyboardMaskBitmap[Index] = static_cast( - static_cast(p_State->FunctionMaskBitmap.at(Index)) & - static_cast(p_State->SwapMaskBitmap.at(Index))); - } -} - -QByteArray Lgc_Core_Func_BuildVendorMaskPacket(const Lgc_Core_Struct_State* p_State) -{ - QByteArray Packet(MID_CONST_PACKET_SIZE_VENDOR, 0); - Packet[0] = static_cast(Mid_Enum_ReportId_Vendor); - Packet[1] = static_cast(0xFF); - for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index) - { - Packet[2 + Index] = p_State->KeyboardMaskBitmap.at(Index); - } - return Packet; -} - -bool Lgc_Core_Func_WriteCurrentMask( - Lgc_Core_Struct_State* p_State, - QString* p_TextStatus, - QByteArray* p_Packet) -{ - Lgc_Core_Func_RebuildKeyboardMask(p_State); - if (p_Packet != nullptr) - { - *p_Packet = Lgc_Core_Func_BuildVendorMaskPacket(p_State); - } - if (!p_State->DriVendorPort.IsOpened) - { - if (p_TextStatus != nullptr) - { - *p_TextStatus = QStringLiteral("Vendor 接口未打开,无法同步当前掩码。"); - } - return false; - } - - const QByteArray Packet = (p_Packet != nullptr) ? *p_Packet : Lgc_Core_Func_BuildVendorMaskPacket(p_State); - return Dri_Vendor_Func_Write(&p_State->DriVendorPort, Packet, p_TextStatus); -} - -bool Lgc_Core_Func_CommitMaskChange( - Lgc_Core_Struct_State* p_State, - const Lgc_Core_Struct_MaskStateBackup& Backup, - const QString& ExplainText) -{ - QString TextStatus; - QByteArray Packet; - if (!Lgc_Core_Func_WriteCurrentMask(p_State, &TextStatus, &Packet)) - { - Lgc_Core_Func_RestoreMaskState(p_State, Backup); - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); - return false; - } - - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); - Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("鍙戦€?"), QStringLiteral("Vendor"), Packet, ExplainText); return true; } -/* ---------- 数据包解析 ---------- */ - -bool Lgc_Core_Func_SyncSystemState(Lgc_Core_Struct_State* p_State) +QString Lgc_Core_FormatPendingCommandBits(quint64 PendingBits) { - const bool OldNumLock = p_State->IsSystemNumLockOn; - const bool OldConnected = p_State->IsConnected; - const QString OldConnection = p_State->TextConnection; - - p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0; - p_State->IsConnected = p_State->DriVendorPort.IsOpened; - p_State->TextConnection = p_State->IsConnected - ? QStringLiteral("已连接到 0xFF00 / 0x0002 Vendor 接口。") - : QStringLiteral("未连接到目标 Vendor 接口。"); - - return (OldNumLock != p_State->IsSystemNumLockOn) || - (OldConnected != p_State->IsConnected) || - (OldConnection != p_State->TextConnection); -} - -void Lgc_Core_Func_HandleNkroPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) -{ - Lgc_Nkro_Struct_Result Result; - Lgc_Nkro_Func_Parse(Packet.ByteArray, &Result); - - if (Result.IsMatch && Result.IsLengthOk) + QStringList TypeList; + if ((PendingBits & Lgc_Core_ProtocolTypeBit(Com_Enum_ProtocolType_Bitmap)) != 0) { - p_State->IsVisibleKeyStateValid = true; - p_State->VisibleModifier = Result.Modifier; - p_State->VisibleUsageList = Result.UsageList; + TypeList.append(QStringLiteral("Bitmap")); + } + if ((PendingBits & Lgc_Core_ProtocolTypeBit(Com_Enum_ProtocolType_TimeSync)) != 0) + { + TypeList.append(QStringLiteral("TimeSync")); + } + if ((PendingBits & Lgc_Core_ProtocolTypeBit(Com_Enum_ProtocolType_ThemeRgb)) != 0) + { + TypeList.append(QStringLiteral("ThemeRgb")); } - Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, Result.TextExplain); + return TypeList.isEmpty() + ? QStringLiteral("Unknown") + : TypeList.join(QStringLiteral(", ")); } -void Lgc_Core_Func_HandleConsumerPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) +bool Lgc_Core_ExpirePendingCommands(Lgc_Core_Struct_State* p_State) { - Lgc_Consumer_Struct_Result Result; - Lgc_Consumer_Func_Parse(Packet.ByteArray, &Result); - Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, Result.TextExplain); -} - -void Lgc_Core_Func_HandleVendorPacket(Lgc_Core_Struct_State* p_State, const Mid_Struct_RawPacket& Packet) -{ - Lgc_Vendor_Struct_Result Result; - Lgc_Vendor_Func_Parse(Packet.ByteArray, &Result); - - if (Result.IsMatch && Result.IsLengthOk && Result.VendorState.IsValid) + if (p_State == nullptr) { - p_State->IsPhysicalKeyStateValid = true; - p_State->PhysicalModifier = Result.VendorState.Modifier; - p_State->PhysicalUsageList = Result.VendorState.UsageList; - } - - Lgc_Core_Func_AppendPacketLog(p_State, QStringLiteral("收到"), Packet.PortName, Packet.ByteArray, Result.TextExplain); -} - -/* ---------- 功能键与交换逻辑 ---------- */ - -bool Lgc_Core_Func_HandleFunctionButtons(Lgc_Core_Struct_State* p_State) -{ - if (!p_State->IsPhysicalKeyStateValid) - { - p_State->LastPhysicalUsageList.clear(); return false; } + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); bool IsChanged = false; - for (quint16 Usage : p_State->PhysicalUsageList) + + const auto TryExpire = [&](Com_Enum_RawPacketSource Source) { - if (p_State->LastPhysicalUsageList.contains(Usage) || !Lgc_Core_Func_IsUsageFunctionMode(p_State, Usage)) + quint64* p_PendingBits = nullptr; + qint64* p_PendingSinceMs = nullptr; + + switch (Source) { - continue; + case Com_Enum_RawPacketSource_UsbCdc: + p_PendingBits = &p_State->PendingUsbCommandBits; + p_PendingSinceMs = &p_State->PendingUsbCommandSinceMs; + break; + case Com_Enum_RawPacketSource_BleNus: + p_PendingBits = &p_State->PendingNusCommandBits; + p_PendingSinceMs = &p_State->PendingNusCommandSinceMs; + break; + default: + return; } - QString TextStatus; - if (!Lgc_Func_Button_Func_HandlePressedUsage(p_State, Usage, &TextStatus) || TextStatus.isEmpty()) + if ((*p_PendingBits == 0) || + (*p_PendingSinceMs == 0) || + ((NowMs - *p_PendingSinceMs) < kPendingCommandTimeoutMs)) { - continue; + return; } - p_State->TextFunctionStatus = TextStatus; - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); + const quint64 ExpiredBits = *p_PendingBits; + *p_PendingBits = 0; + *p_PendingSinceMs = 0; + + if ((ExpiredBits & Lgc_Core_ProtocolTypeBit(Com_Enum_ProtocolType_Bitmap)) != 0) + { + p_State->BitmapSent = false; + p_State->BitmapDirty = true; + p_State->BitmapNextSendMs = NowMs; + } + + p_State->TextFunctionStatus = + QStringLiteral("%1 ACK timeout for %2. Pending state was cleared.") + .arg(Lgc_Core_GetTransportText(Source)) + .arg(Lgc_Core_FormatPendingCommandBits(ExpiredBits)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); IsChanged = true; - } + }; - p_State->LastPhysicalUsageList = p_State->PhysicalUsageList; - return IsChanged; -} - -void Lgc_Core_Func_ReleaseSwapOutputs( - quint16 UsageLeft, - quint16 UsageRight, - bool IsSwapLeftPhysicalPressed, - bool IsSwapRightPhysicalPressed) -{ - if (IsSwapLeftPhysicalPressed) - { - Lgc_Func_Button_Func_SendUsageToWindows(UsageRight, false); - } - if (IsSwapRightPhysicalPressed) - { - Lgc_Func_Button_Func_SendUsageToWindows(UsageLeft, false); - } -} - -void Lgc_Core_Func_ReleaseSwapOutputs(Lgc_Core_Struct_State* p_State) -{ - Lgc_Core_Func_ReleaseSwapOutputs( - p_State->SwapUsageLeft, - p_State->SwapUsageRight, - p_State->IsSwapLeftPhysicalPressed, - p_State->IsSwapRightPhysicalPressed); - p_State->IsSwapLeftPhysicalPressed = false; - p_State->IsSwapRightPhysicalPressed = false; -} - -bool Lgc_Core_Func_HandleSwapSource( - Lgc_Core_Struct_State* p_State, - quint16 PhysicalUsage, - quint16 TargetUsage, - bool* p_IsPhysicalPressed) -{ - const bool IsPressedNow = p_State->PhysicalUsageList.contains(PhysicalUsage); - if (IsPressedNow == *p_IsPhysicalPressed) - { - return false; - } - - if (!Lgc_Func_Button_Func_SendUsageToWindows(TargetUsage, IsPressedNow)) - { - p_State->TextFunctionStatus = QStringLiteral("按键交换补发失败。"); - Lgc_Core_Func_AppendStatusLog(p_State, p_State->TextFunctionStatus); - return true; - } - - *p_IsPhysicalPressed = IsPressedNow; - return true; -} - -bool Lgc_Core_Func_HandleSwapMode(Lgc_Core_Struct_State* p_State) -{ - const bool HadInjectedKey = p_State->IsSwapLeftPhysicalPressed || p_State->IsSwapRightPhysicalPressed; - - if (!p_State->IsSwapModeOn || !p_State->IsPhysicalKeyStateValid) - { - Lgc_Core_Func_ReleaseSwapOutputs(p_State); - return HadInjectedKey; - } - - bool IsChanged = false; - IsChanged |= Lgc_Core_Func_HandleSwapSource( - p_State, - p_State->SwapUsageLeft, - p_State->SwapUsageRight, - &p_State->IsSwapLeftPhysicalPressed); - IsChanged |= Lgc_Core_Func_HandleSwapSource( - p_State, - p_State->SwapUsageRight, - p_State->SwapUsageLeft, - &p_State->IsSwapRightPhysicalPressed); + TryExpire(Com_Enum_RawPacketSource_UsbCdc); + TryExpire(Com_Enum_RawPacketSource_BleNus); return IsChanged; } } // namespace -/* ---------- 对外接口 ---------- */ - -void Lgc_Core_Func_Init(Lgc_Core_Struct_State* p_State) +QString Lgc_Core_GetTransportText(Com_Enum_RawPacketSource Source) { - p_State->DriNkroPort = Dri_NkroRaw_Struct_Port(); - p_State->DriConsumerPort = Dri_Consumer_Struct_Port(); - p_State->DriVendorPort = Dri_Vendor_Struct_Port(); - p_State->DeviceConfig = Mid_Struct_DeviceConfig(); - p_State->TextConnection = QStringLiteral("未连接,等待枚举设备。"); - p_State->TextLog.clear(); - p_State->TextFunctionStatus = QStringLiteral("等待功能键动作。"); + switch (Source) + { + case Com_Enum_RawPacketSource_UsbCdc: + return QStringLiteral("USB CDC"); + case Com_Enum_RawPacketSource_BleNus: + return QStringLiteral("BLE NUS"); + default: + return QStringLiteral("Unknown transport"); + } +} - Lgc_Core_Func_ClearAllKeyStates(p_State); +QString Lgc_Core_GetProtocolTypeText(Com_Enum_ProtocolType Type) +{ + switch (Type) + { + case Com_Enum_ProtocolType_HelloReq: return QStringLiteral("HelloReq"); + case Com_Enum_ProtocolType_HelloRsp: return QStringLiteral("HelloRsp"); + case Com_Enum_ProtocolType_Bitmap: return QStringLiteral("Bitmap"); + case Com_Enum_ProtocolType_FunctionKeyEvent: return QStringLiteral("FunctionKeyEvent"); + case Com_Enum_ProtocolType_LedState: return QStringLiteral("LedState"); + case Com_Enum_ProtocolType_TimeSync: return QStringLiteral("TimeSync"); + case Com_Enum_ProtocolType_ThemeRgb: return QStringLiteral("ThemeRgb"); + case Com_Enum_ProtocolType_Ack: return QStringLiteral("Ack"); + case Com_Enum_ProtocolType_Error: return QStringLiteral("Error"); + default: return QStringLiteral("Unknown"); + } +} + +QString Lgc_Core_GetProtocolErrorText(quint32 ErrorCode) +{ + switch (ErrorCode) + { + case Com_Enum_ProtocolErrorCode_None: return QStringLiteral("NONE"); + case Com_Enum_ProtocolErrorCode_UnknownType: return QStringLiteral("UNKNOWN_TYPE"); + case Com_Enum_ProtocolErrorCode_InvalidLength: return QStringLiteral("INVALID_LENGTH"); + case Com_Enum_ProtocolErrorCode_InvalidParam: return QStringLiteral("INVALID_PARAM"); + case Com_Enum_ProtocolErrorCode_NotReady: return QStringLiteral("NOT_READY"); + default: return QStringLiteral("UNKNOWN_ERROR"); + } +} + +QString Lgc_Core_FormatPacketHex(const QByteArray& PacketBody) +{ + return PacketBody.isEmpty() + ? QStringLiteral("(empty)") + : QString::fromLatin1(PacketBody.toHex(' ').toUpper()); +} + +void Lgc_Core_TestAppendLog(Lgc_Core_Struct_State* p_State, const QString& LineText) +{ + if ((p_State == nullptr) || LineText.trimmed().isEmpty()) + { + return; + } + + const QString TimePrefix = + QDateTime::currentDateTime().toString(QStringLiteral("HH:mm:ss.zzz")); + p_State->TestLogLines.append(QStringLiteral("[%1] %2").arg(TimePrefix, LineText.trimmed())); + while (p_State->TestLogLines.size() > kTestLogMaxLineCount) + { + p_State->TestLogLines.removeFirst(); + } +} + +void Lgc_Core_TestRecordTxPacket( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type, + const QByteArray& PacketBody, + const QString& NoteText) +{ + if (p_State == nullptr) + { + return; + } + + switch (Type) + { + case Com_Enum_ProtocolType_HelloReq: + ++p_State->TestTxHelloReqCount; + break; + case Com_Enum_ProtocolType_Bitmap: + ++p_State->TestTxBitmapCount; + break; + case Com_Enum_ProtocolType_TimeSync: + ++p_State->TestTxTimeSyncCount; + break; + case Com_Enum_ProtocolType_ThemeRgb: + ++p_State->TestTxThemeRgbCount; + break; + default: + break; + } + + p_State->TestLastTxBytes = PacketBody; + p_State->TestLastTxSummary = + QStringLiteral("%1 TX %2") + .arg(Lgc_Core_GetTransportText(Source), Lgc_Core_GetProtocolTypeText(Type)); + if (!NoteText.trimmed().isEmpty()) + { + p_State->TestLastTxSummary += QStringLiteral(" - %1").arg(NoteText.trimmed()); + } + + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("%1 | %2") + .arg(p_State->TestLastTxSummary, Lgc_Core_FormatPacketHex(PacketBody))); +} + +void Lgc_Core_TestRecordRxPacket( + Lgc_Core_Struct_State* p_State, + const Com_Struct_RawPacket& Packet) +{ + if (p_State == nullptr) + { + return; + } + + switch (Packet.ProtocolType) + { + case Com_Enum_ProtocolType_HelloRsp: + ++p_State->TestRxHelloRspCount; + break; + case Com_Enum_ProtocolType_FunctionKeyEvent: + ++p_State->TestRxFunctionKeyEventCount; + break; + case Com_Enum_ProtocolType_LedState: + ++p_State->TestRxLedStateCount; + break; + case Com_Enum_ProtocolType_Ack: + ++p_State->TestRxAckCount; + break; + case Com_Enum_ProtocolType_Error: + ++p_State->TestRxErrorCount; + break; + default: + break; + } + + p_State->TestLastRxBytes = Packet.ByteArray; + p_State->TestLastRxSummary = + QStringLiteral("%1 RX %2") + .arg(Lgc_Core_GetTransportText(Packet.Source)) + .arg(Lgc_Core_GetProtocolTypeText(Packet.ProtocolType)); + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("%1 | %2") + .arg(p_State->TestLastRxSummary, Lgc_Core_FormatPacketHex(Packet.ByteArray))); +} + +void Lgc_Core_ResetProtocolState(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return; + } + + p_State->IsUsbProtocolReady = false; + p_State->IsNusProtocolReady = false; + p_State->IsUsbHelloSent = false; + p_State->IsNusHelloSent = false; + p_State->LastUsbHelloAttemptMs = 0; + p_State->LastNusHelloAttemptMs = 0; + p_State->NusReadySinceMs = 0; + p_State->NusHelloRetryCount = 0; + p_State->WasNusConnectedLastPoll = false; + p_State->HasLoggedNusWriteAck = false; + p_State->HasLoggedNusHelloTimeout = false; + p_State->HelloResponse = Com_Struct_ProtocolHelloRsp(); + p_State->DeviceLedMask = 0; + p_State->LastAckedType = 0; + p_State->LastErrorType = 0; + p_State->LastErrorCode = 0; + p_State->PendingUsbCommandBits = 0; + p_State->PendingUsbCommandSinceMs = 0; + p_State->PendingNusCommandBits = 0; + p_State->PendingNusCommandSinceMs = 0; + p_State->DeviceReady = false; + p_State->BitmapSent = false; + p_State->BitmapDirty = true; + p_State->BitmapRetryCount = 0; + p_State->BitmapNextSendMs = 0; + Lgc_Core_ClearDeviceReportedKeyStates(p_State); +} + +void Lgc_Core_ResetProtocolStateForSource( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source) +{ + if (p_State == nullptr) + { + return; + } + + switch (Source) + { + case Com_Enum_RawPacketSource_UsbCdc: + p_State->IsUsbProtocolReady = false; + p_State->IsUsbHelloSent = false; + p_State->LastUsbHelloAttemptMs = 0; + p_State->PendingUsbCommandBits = 0; + p_State->PendingUsbCommandSinceMs = 0; + break; + case Com_Enum_RawPacketSource_BleNus: + p_State->IsNusProtocolReady = false; + p_State->IsNusHelloSent = false; + p_State->LastNusHelloAttemptMs = 0; + p_State->NusReadySinceMs = p_State->DriNusPort.IsConnected + ? QDateTime::currentMSecsSinceEpoch() + : 0; + p_State->NusHelloRetryCount = 0; + p_State->WasNusConnectedLastPoll = p_State->DriNusPort.IsConnected; + p_State->HasLoggedNusWriteAck = false; + p_State->HasLoggedNusHelloTimeout = false; + p_State->PendingNusCommandBits = 0; + p_State->PendingNusCommandSinceMs = 0; + break; + default: + break; + } + + p_State->DeviceReady = p_State->IsUsbProtocolReady || p_State->IsNusProtocolReady; + if (!p_State->DeviceReady) + { + p_State->HelloResponse = Com_Struct_ProtocolHelloRsp(); + p_State->DeviceLedMask = 0; + p_State->LastAckedType = 0; + p_State->LastErrorType = 0; + p_State->LastErrorCode = 0; + } + + p_State->BitmapSent = false; + p_State->BitmapDirty = true; + p_State->BitmapRetryCount = 0; + p_State->BitmapNextSendMs = 0; + Lgc_Core_ClearDeviceReportedKeyStates(p_State); +} + +bool Lgc_Core_DeviceSupportsPacketType( + const Lgc_Core_Struct_State* p_State, + Com_Enum_ProtocolType Type) +{ + if (p_State == nullptr) + { + return false; + } + + switch (Type) + { + case Com_Enum_ProtocolType_Bitmap: + return (p_State->HelloResponse.CapabilityFlags & (1U << 0)) != 0; + case Com_Enum_ProtocolType_TimeSync: + return (p_State->HelloResponse.CapabilityFlags & (1U << 1)) != 0; + case Com_Enum_ProtocolType_ThemeRgb: + return (p_State->HelloResponse.CapabilityFlags & (1U << 2)) != 0; + case Com_Enum_ProtocolType_LedState: + return (p_State->HelloResponse.CapabilityFlags & (1U << 3)) != 0; + case Com_Enum_ProtocolType_FunctionKeyEvent: + return (p_State->HelloResponse.CapabilityFlags & (1U << 4)) != 0; + default: + return true; + } +} + +void Lgc_Core_AddPendingCommand( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type) +{ + const quint64 TypeBit = Lgc_Core_ProtocolTypeBit(Type); + if ((p_State == nullptr) || (TypeBit == 0)) + { + return; + } + + switch (Source) + { + case Com_Enum_RawPacketSource_UsbCdc: + if (p_State->PendingUsbCommandBits == 0) + { + p_State->PendingUsbCommandSinceMs = QDateTime::currentMSecsSinceEpoch(); + } + p_State->PendingUsbCommandBits |= TypeBit; + break; + case Com_Enum_RawPacketSource_BleNus: + if (p_State->PendingNusCommandBits == 0) + { + p_State->PendingNusCommandSinceMs = QDateTime::currentMSecsSinceEpoch(); + } + p_State->PendingNusCommandBits |= TypeBit; + break; + default: + break; + } +} + +bool Lgc_Core_ClearPendingCommand( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type) +{ + const quint64 TypeBit = Lgc_Core_ProtocolTypeBit(Type); + if ((p_State == nullptr) || (TypeBit == 0)) + { + return false; + } + + switch (Source) + { + case Com_Enum_RawPacketSource_UsbCdc: + { + const bool WasPending = (p_State->PendingUsbCommandBits & TypeBit) != 0; + p_State->PendingUsbCommandBits &= ~TypeBit; + if (p_State->PendingUsbCommandBits == 0) + { + p_State->PendingUsbCommandSinceMs = 0; + } + return WasPending; + } + case Com_Enum_RawPacketSource_BleNus: + { + const bool WasPending = (p_State->PendingNusCommandBits & TypeBit) != 0; + p_State->PendingNusCommandBits &= ~TypeBit; + if (p_State->PendingNusCommandBits == 0) + { + p_State->PendingNusCommandSinceMs = 0; + } + return WasPending; + } + default: + return false; + } +} + +bool Lgc_Core_HasPendingCommand( + const Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type) +{ + const quint64 TypeBit = Lgc_Core_ProtocolTypeBit(Type); + if ((p_State == nullptr) || (TypeBit == 0)) + { + return false; + } + + switch (Source) + { + case Com_Enum_RawPacketSource_UsbCdc: + return (p_State->PendingUsbCommandBits & TypeBit) != 0; + case Com_Enum_RawPacketSource_BleNus: + return (p_State->PendingNusCommandBits & TypeBit) != 0; + default: + return false; + } +} + +void Lgc_Core_Init(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return; + } + + *p_State = Lgc_Core_Struct_State(); + p_State->TextFunctionStatus = QStringLiteral("Waiting for function-key activity."); + Lgc_Core_ClearAllKeyStates(p_State); p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0; - Lgc_Core_Func_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); - Lgc_Core_Func_FillMaskAllEnabled(&p_State->SwapMaskBitmap); - Lgc_Core_Func_FillMaskAllEnabled(&p_State->KeyboardMaskBitmap); - - p_State->FunctionButtonConfig = Lgc_Func_Button_Struct_Config(); - p_State->SwapUsageLeft = p_State->FunctionButtonConfig.SwapUsageLeft; - p_State->SwapUsageRight = p_State->FunctionButtonConfig.SwapUsageRight; - p_State->WindowHandle = nullptr; - p_State->IsConnected = false; - p_State->IsStarted = false; + Lgc_Core_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); + Lgc_Core_FillMaskAllEnabled(&p_State->KeyboardMaskBitmap); + p_State->FunctionButtonConfig = Lgc_FunctionButton_Config(); + Lgc_Core_ApplyFunctionConfig(p_State); } -void Lgc_Core_Func_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle) +void Lgc_Core_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle) { - p_State->WindowHandle = WindowHandle; + if (p_State != nullptr) + { + p_State->WindowHandle = WindowHandle; + } } -void Lgc_Core_Func_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message) +void Lgc_Core_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message) { - QString TextStatus; - Dri_NkroRaw_Func_HandleNativeMessage(&p_State->DriNkroPort, p_Message, &TextStatus); - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); + Q_UNUSED(p_State); + Q_UNUSED(p_Message); } -void Lgc_Core_Func_Start(Lgc_Core_Struct_State* p_State) +void Lgc_Core_Start(Lgc_Core_Struct_State* p_State) { - if (p_State->IsStarted) + if ((p_State == nullptr) || p_State->IsStarted) { return; } p_State->IsStarted = true; - Lgc_Core_Func_RefreshDevice(p_State); + Lgc_Core_RefreshDevice(p_State); } -void Lgc_Core_Func_Close(Lgc_Core_Struct_State* p_State) +void Lgc_Core_Close(Lgc_Core_Struct_State* p_State) { - Lgc_Core_Func_ReleaseSwapOutputs(p_State); - Lgc_Core_Func_CloseAllPorts(p_State); - - p_State->TextConnection = QStringLiteral("未连接,等待枚举设备。"); - p_State->TextFunctionStatus = QStringLiteral("等待功能键动作。"); - p_State->IsConnected = false; - Lgc_Core_Func_ClearAllKeyStates(p_State); -} - -void Lgc_Core_Func_RefreshDevice(Lgc_Core_Struct_State* p_State) -{ - Lgc_Core_Func_ReleaseSwapOutputs(p_State); - Lgc_Core_Func_CloseAllPorts(p_State); - Lgc_Core_Func_ClearAllKeyStates(p_State); - - const auto OpenPort = [p_State](auto OpenFunc, auto* p_Port, auto... Args) + if (p_State == nullptr) { - QString TextStatus; - OpenFunc(p_Port, Args..., &TextStatus); - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); - }; + return; + } - OpenPort(Dri_NkroRaw_Func_Open, &p_State->DriNkroPort, p_State->DeviceConfig, p_State->WindowHandle); - OpenPort(Dri_Consumer_Func_Open, &p_State->DriConsumerPort, p_State->DeviceConfig); - OpenPort(Dri_Vendor_Func_Open, &p_State->DriVendorPort, p_State->DeviceConfig); + Lgc_Core_CloseAllPorts(p_State); + Lgc_Core_ClearAllKeyStates(p_State); + Lgc_Core_ResetProtocolState(p_State); + p_State->LastCdcRefreshAttemptMs = 0; + p_State->LastNusRefreshAttemptMs = 0; + p_State->TextFunctionStatus = QStringLiteral("Waiting for function-key activity."); + p_State->IsConnected = false; + p_State->IsStarted = false; +} +void Lgc_Core_RefreshDevice(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return; + } + + Lgc_Core_CloseAllPorts(p_State); + Lgc_Core_ClearAllKeyStates(p_State); + + QString CdcTextStatus; + QString NusTextStatus; + Dri_Cdc_Init(&p_State->DriCdcPort, p_State->DeviceConfig, &CdcTextStatus); + Dri_Nus_Init(&p_State->DriNusPort, p_State->DeviceConfig, &NusTextStatus); + + Lgc_Core_ResetProtocolState(p_State); + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); + p_State->LastCdcRefreshAttemptMs = NowMs; + p_State->LastNusRefreshAttemptMs = NowMs; + p_State->IsConnected = false; + + if (!CdcTextStatus.isEmpty()) + { + p_State->TextFunctionStatus = CdcTextStatus; + } + else if (!NusTextStatus.isEmpty()) + { + p_State->TextFunctionStatus = NusTextStatus; + } + + Lgc_Core_SendHello(p_State); + Lgc_Core_SyncSystemState(p_State); +} + +bool Lgc_Core_Poll(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + bool IsChanged = false; + Com_Struct_RawPacket Packet; QString TextStatus; - TextStatus.clear(); - Lgc_Core_Func_WriteCurrentMask(p_State, &TextStatus, nullptr); - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); - - Lgc_Core_Func_SyncSystemState(p_State); -} - -void Lgc_Core_Func_ClearLog(Lgc_Core_Struct_State* p_State) -{ - p_State->TextLog.clear(); -} - -bool Lgc_Core_Func_Poll(Lgc_Core_Struct_State* p_State) -{ - bool IsChanged = false; - Mid_Struct_RawPacket Packet; - const auto PollPort = [p_State, &Packet, &IsChanged](auto ReadFunc, auto* p_Port, auto HandlePacket) + if (Lgc_Core_TryRefreshCdcPort(p_State, &TextStatus)) { - QString TextStatus; - if (ReadFunc(p_Port, &Packet, &TextStatus)) - { - HandlePacket(p_State, Packet); - IsChanged = true; - } - if (!TextStatus.isEmpty()) - { - Lgc_Core_Func_AppendStatusLog(p_State, TextStatus); - IsChanged = true; - } - }; + IsChanged = true; + } + if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + IsChanged = true; + } - PollPort(Dri_NkroRaw_Func_Read, &p_State->DriNkroPort, Lgc_Core_Func_HandleNkroPacket); - PollPort(Dri_Consumer_Func_Read, &p_State->DriConsumerPort, Lgc_Core_Func_HandleConsumerPacket); - PollPort(Dri_Vendor_Func_Read, &p_State->DriVendorPort, Lgc_Core_Func_HandleVendorPacket); + TextStatus.clear(); + if (p_State->IsStarted && + !p_State->DeviceReady && + !p_State->DriNusPort.IsOpened) + { + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); + if ((p_State->LastNusRefreshAttemptMs == 0) || + ((NowMs - p_State->LastNusRefreshAttemptMs) >= kCdcRefreshIntervalMs)) + { + p_State->LastNusRefreshAttemptMs = NowMs; + if (Dri_Nus_Init(&p_State->DriNusPort, p_State->DeviceConfig, &TextStatus)) + { + p_State->IsNusHelloSent = false; + p_State->LastNusHelloAttemptMs = 0; + p_State->NusReadySinceMs = 0; + p_State->NusHelloRetryCount = 0; + p_State->WasNusConnectedLastPoll = false; + p_State->HasLoggedNusHelloTimeout = false; + IsChanged = true; + } + } + } + if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + IsChanged = true; + } - IsChanged |= Lgc_Core_Func_HandleFunctionButtons(p_State); - IsChanged |= Lgc_Core_Func_HandleSwapMode(p_State); - IsChanged |= Lgc_Core_Func_SyncSystemState(p_State); + IsChanged |= Lgc_Core_UpdateNusConnectionWindow(p_State); + Lgc_Core_SendHello(p_State); + IsChanged |= Lgc_Core_LogNusSummaryIfChanged(p_State); + + TextStatus.clear(); + if (Dri_Cdc_Read(&p_State->DriCdcPort, &Packet, &TextStatus)) + { + Lgc_Core_HandleCdcPacket(p_State, Packet); + IsChanged = true; + } + else if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + IsChanged = true; + } + + TextStatus.clear(); + if (Dri_Nus_Read(&p_State->DriNusPort, &Packet, &TextStatus)) + { + Lgc_Core_HandleNusPacket(p_State, Packet); + IsChanged = true; + } + else if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + IsChanged = true; + } + + IsChanged |= Lgc_Core_ExpirePendingCommands(p_State); + IsChanged |= Lgc_Core_ProcessBitmapSend(p_State); + IsChanged |= Lgc_Core_HandleFunctionButtons(p_State); + IsChanged |= Lgc_Core_SyncSystemState(p_State); return IsChanged; } -bool Lgc_Core_Func_SetUsageFunctionMode(Lgc_Core_Struct_State* p_State, quint16 Usage, bool IsEnabled) +Lgc_Core_Struct_View Lgc_Core_GetView(const Lgc_Core_Struct_State* p_State) { - if (p_State->FunctionMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE) + Lgc_Core_Struct_View View; + if (p_State == nullptr) { - Lgc_Core_Func_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); - } - if (Lgc_Core_Func_IsUsageFunctionMode(p_State, Usage) == IsEnabled) - { - return true; + return View; } - const Lgc_Core_Struct_MaskStateBackup Backup = Lgc_Core_Func_BackupMaskState(p_State); - - Lgc_Core_Func_SetUsageEnabledInMask(&p_State->FunctionMaskBitmap, Usage, !IsEnabled); - - const QString ExplainText = IsEnabled - ? QStringLiteral("已把 %1 切到功能键模式。").arg(Mid_Func_GetKeyboardUsageText(Usage)) - : QStringLiteral("已恢复 %1 的普通键模式。").arg(Mid_Func_GetKeyboardUsageText(Usage)); - return Lgc_Core_Func_CommitMaskChange(p_State, Backup, ExplainText); + View.TextFunctionStatus = p_State->TextFunctionStatus; + View.IsConnected = p_State->IsConnected; + View.HasOpenTransport = + p_State->DriCdcPort.IsOpened || + p_State->DriNusPort.IsOpened || + p_State->DriNusPort.IsConnected; + View.IsSystemNumLockOn = p_State->IsSystemNumLockOn; + View.IsVisibleKeyStateValid = p_State->IsVisibleKeyStateValid; + View.VisibleUsageList = p_State->VisibleUsageList; + View.IsPhysicalKeyStateValid = p_State->IsPhysicalKeyStateValid; + View.PhysicalUsageList = p_State->PhysicalUsageList; + View.IsFunctionSequenceRecording = p_State->IsFunctionSequenceRecording; + return View; } -bool Lgc_Core_Func_SetSwapMode( - Lgc_Core_Struct_State* p_State, - quint16 UsageLeft, - quint16 UsageRight, - bool IsEnabled) +void Lgc_Core_SetStatusText(Lgc_Core_Struct_State* p_State, const QString& TextStatus) { - if ((UsageLeft == 0) || (UsageRight == 0) || (UsageLeft == UsageRight)) + if (p_State != nullptr) { - return false; + p_State->TextFunctionStatus = TextStatus; } - if ((p_State->IsSwapModeOn == IsEnabled) && - (p_State->SwapUsageLeft == UsageLeft) && - (p_State->SwapUsageRight == UsageRight)) +} + +QVector Lgc_Core_GetFeatureIdList(const Lgc_Core_Struct_State* p_State) +{ + return p_State == nullptr + ? QVector() + : Lgc_FunctionButton_GetFeatureIdList(p_State->FunctionButtonConfig); +} + +Lgc_FunctionFeature_Definition Lgc_Core_GetFeature( + const Lgc_Core_Struct_State* p_State, + int FeatureId) +{ + return p_State == nullptr + ? Lgc_FunctionFeature_Definition() + : Lgc_FunctionButton_GetFeature(p_State->FunctionButtonConfig, FeatureId); +} + +bool Lgc_Core_HasFeature(const Lgc_Core_Struct_State* p_State, int FeatureId) +{ + return Lgc_Core_GetFeature(p_State, FeatureId).Id > 0; +} + +QString Lgc_Core_GetUsageShortText(quint16 Usage) +{ + return Lgc_FunctionButton_GetUsageShortText(Usage); +} + +QString Lgc_Core_GetFeatureTypeText(Lgc_FunctionFeature_Type Type) +{ + return Lgc_FunctionButton_GetFeatureTypeText(Type); +} + +QString Lgc_Core_GetFeatureNameById(const Lgc_Core_Struct_State* p_State, int FeatureId) +{ + const Lgc_FunctionFeature_Definition Feature = Lgc_Core_GetFeature(p_State, FeatureId); + return Feature.Id > 0 + ? Lgc_FunctionButton_GetFeatureName(Feature) + : QStringLiteral("No Function"); +} + +QString Lgc_Core_GetFeatureDescriptionById( + const Lgc_Core_Struct_State* p_State, + int FeatureId) +{ + return p_State == nullptr + ? QString() + : Lgc_FunctionButton_GetFeatureDescriptionById( + p_State->FunctionButtonConfig, + FeatureId); +} + +QString Lgc_Core_GetFeatureBindingSummary( + const Lgc_Core_Struct_State* p_State, + int FeatureId) +{ + return p_State == nullptr + ? QStringLiteral("No function selected yet.") + : Lgc_FunctionButton_GetFeatureBindingSummary( + p_State->FunctionButtonConfig, + FeatureId); +} + +int Lgc_Core_GetUsageFeatureId(const Lgc_Core_Struct_State* p_State, quint16 Usage) +{ + return p_State == nullptr + ? 0 + : Lgc_FunctionButton_GetUsageFeatureId(p_State->FunctionButtonConfig, Usage); +} + +bool Lgc_Core_HasUsageFeature(const Lgc_Core_Struct_State* p_State, quint16 Usage) +{ + return p_State != nullptr && + Lgc_FunctionButton_HasUsageFeature(p_State->FunctionButtonConfig, Usage); +} + +void Lgc_Core_ClearTestLog(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) { - return true; + return; } - const Lgc_Core_Struct_MaskStateBackup Backup = Lgc_Core_Func_BackupMaskState(p_State); + p_State->TestTxHelloReqCount = 0; + p_State->TestTxBitmapCount = 0; + p_State->TestTxTimeSyncCount = 0; + p_State->TestTxThemeRgbCount = 0; + p_State->TestRxHelloRspCount = 0; + p_State->TestRxFunctionKeyEventCount = 0; + p_State->TestRxLedStateCount = 0; + p_State->TestRxAckCount = 0; + p_State->TestRxErrorCount = 0; + p_State->TestLastTxSummary.clear(); + p_State->TestLastRxSummary.clear(); + p_State->TestLastTxBytes.clear(); + p_State->TestLastRxBytes.clear(); + p_State->TestLastFunctionEventBitmap.clear(); + p_State->TestLogLines.clear(); + p_State->LastLoggedNusEndpointSummary.clear(); +} - p_State->IsSwapModeOn = IsEnabled; - p_State->SwapUsageLeft = UsageLeft; - p_State->SwapUsageRight = UsageRight; - Lgc_Core_Func_FillMaskAllEnabled(&p_State->SwapMaskBitmap); - - if (IsEnabled) +Lgc_Core_Struct_TestView Lgc_Core_GetTestView(const Lgc_Core_Struct_State* p_State) +{ + Lgc_Core_Struct_TestView View; + if (p_State == nullptr) { - Lgc_Core_Func_SetUsageEnabledInMask(&p_State->SwapMaskBitmap, UsageLeft, false); - Lgc_Core_Func_SetUsageEnabledInMask(&p_State->SwapMaskBitmap, UsageRight, false); + return View; } - p_State->IsSwapLeftPhysicalPressed = false; - p_State->IsSwapRightPhysicalPressed = false; + View.StatusText = p_State->TextFunctionStatus; + View.UsbPortName = p_State->DriCdcPort.PortName; + View.NusEndpointSummary = p_State->DriNusPort.TextEndpointSummary; + View.IsUsbOpened = p_State->DriCdcPort.IsOpened; + View.IsNusOpened = p_State->DriNusPort.IsOpened; + View.IsNusConnected = p_State->DriNusPort.IsConnected; + View.IsUsbProtocolReady = p_State->IsUsbProtocolReady; + View.IsNusProtocolReady = p_State->IsNusProtocolReady; + View.DeviceReady = p_State->DeviceReady; + View.HelloProtocolVersion = p_State->HelloResponse.ProtocolVersion; + View.HelloVendorId = p_State->HelloResponse.VendorId; + View.HelloProductId = p_State->HelloResponse.ProductId; + View.HelloFirmwareMajor = p_State->HelloResponse.FirmwareMajor; + View.HelloFirmwareMinor = p_State->HelloResponse.FirmwareMinor; + View.HelloCapabilityFlags = p_State->HelloResponse.CapabilityFlags; + View.DeviceLedMask = p_State->DeviceLedMask; + View.LastAckedType = p_State->LastAckedType; + View.LastErrorType = p_State->LastErrorType; + View.LastErrorCode = p_State->LastErrorCode; + View.PendingUsbCommandBits = p_State->PendingUsbCommandBits; + View.PendingNusCommandBits = p_State->PendingNusCommandBits; + View.LastTxSummary = p_State->TestLastTxSummary; + View.LastRxSummary = p_State->TestLastRxSummary; + View.LastTxHex = Lgc_Core_FormatPacketHex(p_State->TestLastTxBytes); + View.LastRxHex = Lgc_Core_FormatPacketHex(p_State->TestLastRxBytes); + View.FunctionMaskHex = Lgc_Core_FormatPacketHex(p_State->FunctionMaskBitmap); + View.LastFunctionEventHex = Lgc_Core_FormatPacketHex(p_State->TestLastFunctionEventBitmap); + View.TxHelloReqCount = p_State->TestTxHelloReqCount; + View.TxBitmapCount = p_State->TestTxBitmapCount; + View.TxTimeSyncCount = p_State->TestTxTimeSyncCount; + View.TxThemeRgbCount = p_State->TestTxThemeRgbCount; + View.RxHelloRspCount = p_State->TestRxHelloRspCount; + View.RxFunctionKeyEventCount = p_State->TestRxFunctionKeyEventCount; + View.RxLedStateCount = p_State->TestRxLedStateCount; + View.RxAckCount = p_State->TestRxAckCount; + View.RxErrorCount = p_State->TestRxErrorCount; + View.LogText = p_State->TestLogLines.join(QLatin1Char('\n')); + return View; +} - const QString ExplainText = IsEnabled - ? QStringLiteral("已开启按键交换:%1 <-> %2") - .arg(Mid_Func_GetKeyboardUsageText(UsageLeft)) - .arg(Mid_Func_GetKeyboardUsageText(UsageRight)) - : QStringLiteral("已关闭按键交换:%1 <-> %2") - .arg(Mid_Func_GetKeyboardUsageText(UsageLeft)) - .arg(Mid_Func_GetKeyboardUsageText(UsageRight)); - if (!Lgc_Core_Func_CommitMaskChange(p_State, Backup, ExplainText)) +bool Lgc_Core_BeginSequenceRecording(Lgc_Core_Struct_State* p_State, int FeatureId) +{ + if (p_State == nullptr) { return false; } - if (Backup.IsSwapModeOn && - (!IsEnabled || (Backup.SwapUsageLeft != UsageLeft) || (Backup.SwapUsageRight != UsageRight))) + const Lgc_FunctionFeature_Definition Feature = Lgc_Core_GetFeature(p_State, FeatureId); + if (Feature.Id <= 0) { - Lgc_Core_Func_ReleaseSwapOutputs( - Backup.SwapUsageLeft, - Backup.SwapUsageRight, - Backup.IsSwapLeftPhysicalPressed, - Backup.IsSwapRightPhysicalPressed); + return false; } + + if ((Feature.Type != Lgc_FunctionFeature_Type::KeyCombination) && + (Feature.Type != Lgc_FunctionFeature_Type::KeySequence)) + { + return false; + } + + p_State->IsFunctionSequenceRecording = true; + p_State->TextFunctionStatus = + Feature.Type == Lgc_FunctionFeature_Type::KeySequence + ? QStringLiteral("Sequence recording started. Press keys to append combinations.") + : QStringLiteral("Combination recording started. Press the target combo."); return true; } -bool Lgc_Core_Func_IsUsageFunctionMode(const Lgc_Core_Struct_State* p_State, quint16 Usage) +void Lgc_Core_EndSequenceRecording(Lgc_Core_Struct_State* p_State) { - if (p_State->FunctionMaskBitmap.size() != MID_CONST_USAGE_BITMAP_SIZE) + if (p_State == nullptr) { - return false; + return; } - return !Lgc_Core_Func_IsUsageEnabledInMask(p_State->FunctionMaskBitmap, Usage); + p_State->IsFunctionSequenceRecording = false; + p_State->TextFunctionStatus = QStringLiteral("Sequence recording stopped. Changes saved."); +} + +void Lgc_Core_UpdateSequenceRecordingStatus( + Lgc_Core_Struct_State* p_State, + const QString& SequenceText) +{ + if ((p_State == nullptr) || SequenceText.trimmed().isEmpty()) + { + return; + } + + p_State->TextFunctionStatus = + QStringLiteral("Recording: %1").arg(SequenceText.trimmed()); +} + +void Lgc_Core_HandleUiKeyPress(Lgc_Core_Struct_State* p_State, quint16 Usage) +{ + if (p_State == nullptr) + { + return; + } + + QString TextStatus; + if (Lgc_Core_HasUsageFeature(p_State, Usage)) + { + if (!Lgc_FunctionButton_RunBinding(*p_State, Usage, TextStatus)) + { + TextStatus = QStringLiteral("%1 has no runnable feature.") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + } + else if (Lgc_FunctionButton_SendUsageToWindows(Usage, true)) + { + p_State->UiPressedUsageSet.insert(Usage); + TextStatus = QStringLiteral("Pressed %1.") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + else + { + TextStatus = QStringLiteral("Failed to press %1.") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + + if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + } +} + +void Lgc_Core_HandleUiKeyRelease(Lgc_Core_Struct_State* p_State, quint16 Usage) +{ + if ((p_State == nullptr) || !p_State->UiPressedUsageSet.remove(Usage)) + { + return; + } + + p_State->TextFunctionStatus = + Lgc_FunctionButton_SendUsageToWindows(Usage, false) + ? QStringLiteral("Released %1.").arg(Lgc_FunctionButton_GetUsageShortText(Usage)) + : QStringLiteral("Failed to release %1.") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); } diff --git a/LOGIC/Lgc_Core.h b/LOGIC/Lgc_Core.h index d4cf225..895ad28 100644 --- a/LOGIC/Lgc_Core.h +++ b/LOGIC/Lgc_Core.h @@ -1,85 +1,219 @@ #pragma once -#include "DRI/Dri_Consumer.h" -#include "DRI/Dri_NkroRaw.h" -#include "DRI/Dri_Vendor.h" -#include "LOGIC/Lgc_Consumer.h" +#include "COM/Com_Def.h" +#include "DRI/Dri_Cdc.h" +#include "DRI/Dri_Nus.h" #include "LOGIC/Lgc_Func_Button.h" -#include "LOGIC/Lgc_Nkro.h" -#include "LOGIC/Lgc_Vendor.h" #include +#include +#include +#include +#include -/* - * Lgc_Core:贯穿 UI / LOGIC / DRI 的单一状态容器。 - * 高密注释使得学生在不翻源码的情况下即可看懂所有成员职责。 - */ struct Lgc_Core_Struct_State { - /* DRI 端口:分别承接 RAW NKRO / Consumer / Vendor 通道 */ - Dri_NkroRaw_Struct_Port DriNkroPort; - Dri_Consumer_Struct_Port DriConsumerPort; - Dri_Vendor_Struct_Port DriVendorPort; + Dri_Nus_Struct_Port DriNusPort; + Dri_Cdc_Struct_Port DriCdcPort; - /* 设备与 UI 文本状态 */ - Mid_Struct_DeviceConfig DeviceConfig; - QString TextConnection; - QString TextLog; + Com_Struct_DeviceConfig DeviceConfig; QString TextFunctionStatus; - /* 可视化按键:UI 模型(来自解析 NKRO/Consumer 结果) */ bool IsVisibleKeyStateValid = false; - quint8 VisibleModifier = 0; QVector VisibleUsageList; - /* 物理按键:直接来自硬件,供 Swap/功能逻辑判断 */ bool IsPhysicalKeyStateValid = false; - quint8 PhysicalModifier = 0; QVector PhysicalUsageList; QVector LastPhysicalUsageList; - /* 键盘模式控制:NumLock、功能掩码、Swap 掩码等 */ bool IsSystemNumLockOn = false; QByteArray FunctionMaskBitmap; - QByteArray SwapMaskBitmap; QByteArray KeyboardMaskBitmap; - /* 功能键配置及 Swap 对应的运行期状态 */ - Lgc_Func_Button_Struct_Config FunctionButtonConfig; - bool IsSwapModeOn = false; - quint16 SwapUsageLeft = 0; - quint16 SwapUsageRight = 0; - bool IsSwapLeftPhysicalPressed = false; - bool IsSwapRightPhysicalPressed = false; + bool IsUsbProtocolReady = false; + bool IsNusProtocolReady = false; + bool IsUsbHelloSent = false; + bool IsNusHelloSent = false; + qint64 LastUsbHelloAttemptMs = 0; + qint64 LastNusHelloAttemptMs = 0; + qint64 NusReadySinceMs = 0; + int NusHelloRetryCount = 0; + bool WasNusConnectedLastPoll = false; + bool HasLoggedNusWriteAck = false; + bool HasLoggedNusHelloTimeout = false; + qint64 LastCdcRefreshAttemptMs = 0; + qint64 LastNusRefreshAttemptMs = 0; + Com_Struct_ProtocolHelloRsp HelloResponse; + quint32 DeviceLedMask = 0; + quint32 LastAckedType = 0; + quint32 LastErrorType = 0; + quint32 LastErrorCode = 0; + quint64 PendingUsbCommandBits = 0; + qint64 PendingUsbCommandSinceMs = 0; + quint64 PendingNusCommandBits = 0; + qint64 PendingNusCommandSinceMs = 0; + bool DeviceReady = false; + bool BitmapSent = false; + bool BitmapDirty = false; + int BitmapRetryCount = 0; + qint64 BitmapNextSendMs = 0; + + Lgc_FunctionButton_Config FunctionButtonConfig; + bool IsAltThemeEnabled = false; - /* 互操作:窗口句柄与核心运行状态 */ void* WindowHandle = nullptr; bool IsConnected = false; bool IsStarted = false; + bool IsFunctionSequenceRecording = false; + QSet UiPressedUsageSet; + + int TestTxHelloReqCount = 0; + int TestTxBitmapCount = 0; + int TestTxTimeSyncCount = 0; + int TestTxThemeRgbCount = 0; + int TestRxHelloRspCount = 0; + int TestRxFunctionKeyEventCount = 0; + int TestRxLedStateCount = 0; + int TestRxAckCount = 0; + int TestRxErrorCount = 0; + QString TestLastTxSummary; + QString TestLastRxSummary; + QByteArray TestLastTxBytes; + QByteArray TestLastRxBytes; + QByteArray TestLastFunctionEventBitmap; + QStringList TestLogLines; + QString LastLoggedNusEndpointSummary; }; -/* 初始化核心:清空状态并准备 DRI 端口。 */ -void Lgc_Core_Func_Init(Lgc_Core_Struct_State* p_State); -/* 告诉 LGC 使用哪个 HWND 接收 RAWINPUT。 */ -void Lgc_Core_Func_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle); -/* 每个 Windows 消息都交给核心处理。 */ -void Lgc_Core_Func_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message); -/* 启动:打开设备、刷新按键状态并进入轮询。 */ -void Lgc_Core_Func_Start(Lgc_Core_Struct_State* p_State); -/* 关闭:释放 DRI 端口与所有资源。 */ -void Lgc_Core_Func_Close(Lgc_Core_Struct_State* p_State); -/* 当 VID/PID 变化或用户点击刷新时调用。 */ -void Lgc_Core_Func_RefreshDevice(Lgc_Core_Struct_State* p_State); -/* 清空 LOG 文本,UI 立即同步。 */ -void Lgc_Core_Func_ClearLog(Lgc_Core_Struct_State* p_State); -/* 轮询 DRI 队列,返回“是否发生变化”以供 UI 决定是否刷新。 */ -bool Lgc_Core_Func_Poll(Lgc_Core_Struct_State* p_State); -/* 设置单个 Usage 是否是“功能模式” */ -bool Lgc_Core_Func_SetUsageFunctionMode(Lgc_Core_Struct_State* p_State, quint16 Usage, bool IsEnabled); -/* 打开/关闭 Swap 模式,并指定左右 Usage */ -bool Lgc_Core_Func_SetSwapMode( +struct Lgc_Core_Struct_View +{ + QString TextFunctionStatus; + bool IsConnected = false; + bool HasOpenTransport = false; + bool IsSystemNumLockOn = false; + bool IsVisibleKeyStateValid = false; + QVector VisibleUsageList; + bool IsPhysicalKeyStateValid = false; + QVector PhysicalUsageList; + bool IsFunctionSequenceRecording = false; +}; + +struct Lgc_Core_Struct_TestView +{ + QString StatusText; + QString UsbPortName; + QString NusEndpointSummary; + bool IsUsbOpened = false; + bool IsNusOpened = false; + bool IsNusConnected = false; + bool IsUsbProtocolReady = false; + bool IsNusProtocolReady = false; + bool DeviceReady = false; + quint32 HelloProtocolVersion = 0; + quint32 HelloVendorId = 0; + quint32 HelloProductId = 0; + quint32 HelloFirmwareMajor = 0; + quint32 HelloFirmwareMinor = 0; + quint32 HelloCapabilityFlags = 0; + quint32 DeviceLedMask = 0; + quint32 LastAckedType = 0; + quint32 LastErrorType = 0; + quint32 LastErrorCode = 0; + quint64 PendingUsbCommandBits = 0; + quint64 PendingNusCommandBits = 0; + QString LastTxSummary; + QString LastRxSummary; + QString LastTxHex; + QString LastRxHex; + QString FunctionMaskHex; + QString LastFunctionEventHex; + int TxHelloReqCount = 0; + int TxBitmapCount = 0; + int TxTimeSyncCount = 0; + int TxThemeRgbCount = 0; + int RxHelloRspCount = 0; + int RxFunctionKeyEventCount = 0; + int RxLedStateCount = 0; + int RxAckCount = 0; + int RxErrorCount = 0; + QString LogText; +}; + +void Lgc_Core_Init(Lgc_Core_Struct_State* p_State); +void Lgc_Core_SetWindowHandle(Lgc_Core_Struct_State* p_State, void* WindowHandle); +void Lgc_Core_HandleNativeMessage(Lgc_Core_Struct_State* p_State, void* p_Message); +void Lgc_Core_Start(Lgc_Core_Struct_State* p_State); +void Lgc_Core_Close(Lgc_Core_Struct_State* p_State); +void Lgc_Core_RefreshDevice(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_Poll(Lgc_Core_Struct_State* p_State); + +Lgc_Core_Struct_View Lgc_Core_GetView(const Lgc_Core_Struct_State* p_State); +void Lgc_Core_SetStatusText(Lgc_Core_Struct_State* p_State, const QString& TextStatus); + +QVector Lgc_Core_GetFeatureIdList(const Lgc_Core_Struct_State* p_State); +Lgc_FunctionFeature_Definition Lgc_Core_GetFeature( + const Lgc_Core_Struct_State* p_State, + int FeatureId); +bool Lgc_Core_HasFeature(const Lgc_Core_Struct_State* p_State, int FeatureId); +QString Lgc_Core_GetUsageShortText(quint16 Usage); +QString Lgc_Core_GetFeatureTypeText(Lgc_FunctionFeature_Type Type); +QString Lgc_Core_GetFeatureNameById(const Lgc_Core_Struct_State* p_State, int FeatureId); +QString Lgc_Core_GetFeatureDescriptionById( + const Lgc_Core_Struct_State* p_State, + int FeatureId); +QString Lgc_Core_GetFeatureBindingSummary( + const Lgc_Core_Struct_State* p_State, + int FeatureId); +int Lgc_Core_GetUsageFeatureId(const Lgc_Core_Struct_State* p_State, quint16 Usage); +bool Lgc_Core_HasUsageFeature(const Lgc_Core_Struct_State* p_State, quint16 Usage); + +bool Lgc_Core_LoadFunctionConfig(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_SaveFunctionConfig(Lgc_Core_Struct_State* p_State); +int Lgc_Core_AddFeature(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_UpdateFeature( Lgc_Core_Struct_State* p_State, - quint16 UsageLeft, - quint16 UsageRight, - bool IsEnabled); -/* 查询给定 Usage 当前是否处于功能模式 */ -bool Lgc_Core_Func_IsUsageFunctionMode(const Lgc_Core_Struct_State* p_State, quint16 Usage); + const Lgc_FunctionFeature_Definition& Feature); +bool Lgc_Core_DeleteFeature(Lgc_Core_Struct_State* p_State, int FeatureId); +bool Lgc_Core_BindUsageToFeature( + Lgc_Core_Struct_State* p_State, + quint16 Usage, + int FeatureId); + +bool Lgc_Core_BeginSequenceRecording(Lgc_Core_Struct_State* p_State, int FeatureId); +void Lgc_Core_EndSequenceRecording(Lgc_Core_Struct_State* p_State); +void Lgc_Core_UpdateSequenceRecordingStatus( + Lgc_Core_Struct_State* p_State, + const QString& SequenceText); +void Lgc_Core_HandleUiKeyPress(Lgc_Core_Struct_State* p_State, quint16 Usage); +void Lgc_Core_HandleUiKeyRelease(Lgc_Core_Struct_State* p_State, quint16 Usage); + +bool Lgc_Core_ApplyFunctionConfig(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_TestSendHello( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None); +bool Lgc_Core_TestSendBitmapCurrentConfig( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None); +bool Lgc_Core_TestSendBitmapAllEnabled( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None); +bool Lgc_Core_TestSendBitmapAllDisabled( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None); +bool Lgc_Core_TestSendTimeSync( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None); +bool Lgc_Core_SendTimeSync(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_SendThemeRgb( + Lgc_Core_Struct_State* p_State, + quint8 Red, + quint8 Green, + quint8 Blue); +bool Lgc_Core_TestSendThemeRgb( + Lgc_Core_Struct_State* p_State, + quint8 Red, + quint8 Green, + quint8 Blue, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None); +bool Lgc_Core_SendThemeSwitch(Lgc_Core_Struct_State* p_State); +void Lgc_Core_ClearTestLog(Lgc_Core_Struct_State* p_State); +Lgc_Core_Struct_TestView Lgc_Core_GetTestView(const Lgc_Core_Struct_State* p_State); diff --git a/LOGIC/Lgc_Core_Command.cpp b/LOGIC/Lgc_Core_Command.cpp new file mode 100644 index 0000000..11bb496 --- /dev/null +++ b/LOGIC/Lgc_Core_Command.cpp @@ -0,0 +1,813 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include +#include + +namespace +{ + +constexpr qint64 kHelloRetryIntervalMs = 700; +constexpr qint64 kNusHelloInitialDelayMs = 600; +constexpr qint64 kNusHelloRetryAfterWriteAckMs = 1800; +constexpr int kNusHelloRetryMaxCount = 6; +constexpr qint64 kBitmapInitialSendDelayMs = 300; +constexpr qint64 kBitmapRetryDelayMs = 200; + +struct Lgc_Core_Struct_ThemeColor +{ + quint8 Red; + quint8 Green; + quint8 Blue; +}; + +QString Lgc_Core_FormatUsageListText(const QVector& UsageList) +{ + if (UsageList.isEmpty()) + { + return QStringLiteral("(none)"); + } + + QStringList KeyList; + for (quint16 Usage : UsageList) + { + KeyList.append(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + + return KeyList.join(QStringLiteral(", ")); +} + +QString Lgc_Core_FormatUsageBitmapSummary(const QByteArray& UsageBitmap) +{ + if (!Com_Protocol_IsUsageBitmapValid(UsageBitmap)) + { + return QStringLiteral("(invalid)"); + } + + return Lgc_Core_FormatUsageListText( + Com_Protocol_BuildPressedUsageList(UsageBitmap)); +} + +bool Lgc_Core_SendPacket( + Lgc_Core_Struct_State* p_State, + Com_Enum_ProtocolType Type, + const QByteArray& PacketBody); +bool Lgc_Core_SendTestPacketToTarget( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource, + Com_Enum_ProtocolType Type, + const QByteArray& PacketBody, + const QString& NoteText); + +Lgc_Core_Struct_ThemeColor Lgc_Core_GetNextThemeColor(Lgc_Core_Struct_State* p_State) +{ + if ((p_State == nullptr) || !p_State->IsAltThemeEnabled) + { + return { 0xF7, 0x25, 0x85 }; + } + + return { 0x4C, 0xC9, 0xF0 }; +} + +bool Lgc_Core_SendBitmapNow( + Lgc_Core_Struct_State* p_State, + const QByteArray& UsageBitmap, + Com_Enum_RawPacketSource TargetSource = Com_Enum_RawPacketSource_None) +{ + if (p_State == nullptr) + { + return false; + } + + if (!Com_Protocol_IsUsageBitmapValid(UsageBitmap)) + { + p_State->TextFunctionStatus = QStringLiteral("Bitmap bytes are invalid."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + p_State->KeyboardMaskBitmap = UsageBitmap; + p_State->BitmapSent = false; + p_State->BitmapDirty = false; + p_State->BitmapRetryCount = 0; + p_State->BitmapNextSendMs = 0; + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("Bitmap payload keys: %1") + .arg(Lgc_Core_FormatUsageBitmapSummary(UsageBitmap))); // FIX: show which keys are in the manual bitmap payload. + const QByteArray PacketBody = Com_Protocol_EncodeBitmap(UsageBitmap); + return (TargetSource == Com_Enum_RawPacketSource_None) + ? Lgc_Core_SendPacket( + p_State, + Com_Enum_ProtocolType_Bitmap, + PacketBody) + : Lgc_Core_SendTestPacketToTarget( + p_State, + TargetSource, + Com_Enum_ProtocolType_Bitmap, + PacketBody, + QStringLiteral("manual target")); +} + +bool Lgc_Core_SendTestPacketToTarget( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource, + Com_Enum_ProtocolType Type, + const QByteArray& PacketBody, + const QString& NoteText) +{ + if (p_State == nullptr) + { + return false; + } + + const bool RequiresAck = + (Type == Com_Enum_ProtocolType_Bitmap) || + (Type == Com_Enum_ProtocolType_TimeSync) || + (Type == Com_Enum_ProtocolType_ThemeRgb); + const bool IsHelloReq = Type == Com_Enum_ProtocolType_HelloReq; + + if (PacketBody.isEmpty()) + { + p_State->TextFunctionStatus = + QStringLiteral("%1 build failed.").arg(Lgc_Core_GetProtocolTypeText(Type)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + QString TextStatus; + auto FinalizeSuccess = [&](Com_Enum_RawPacketSource Source) + { + const QString SummaryNote = NoteText.isEmpty() + ? TextStatus + : (TextStatus.isEmpty() + ? NoteText + : QStringLiteral("%1 | %2").arg(NoteText, TextStatus)); + Lgc_Core_TestRecordTxPacket( + p_State, + Source, + Type, + PacketBody, + SummaryNote); + if (Type == Com_Enum_ProtocolType_Bitmap) + { + Lgc_Core_ClearDeviceReportedKeyStates(p_State); + } + if (RequiresAck) + { + Lgc_Core_AddPendingCommand(p_State, Source, Type); + p_State->TextFunctionStatus = + TextStatus.isEmpty() + ? QStringLiteral("%1 sent on %2. Waiting for ACK.") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + .arg(Lgc_Core_GetTransportText(Source)) + : QStringLiteral("%1 Waiting for %2 ACK.") + .arg(TextStatus) + .arg(Lgc_Core_GetProtocolTypeText(Type)); + } + else if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + } + }; + + switch (TargetSource) + { + case Com_Enum_RawPacketSource_UsbCdc: + if (IsHelloReq) + { + if (!p_State->DriCdcPort.IsOpened) + { + p_State->TextFunctionStatus = + QStringLiteral("USB CDC is not open yet. HelloReq was not sent."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + } + else if (!p_State->IsUsbProtocolReady) + { + p_State->TextFunctionStatus = + QStringLiteral("USB CDC handshake is not ready yet. Skip %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + else if (!Lgc_Core_DeviceSupportsPacketType(p_State, Type)) + { + p_State->TextFunctionStatus = + QStringLiteral("Device capability does not support %1 on USB CDC. caps=0x%2") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + .arg(p_State->HelloResponse.CapabilityFlags, 0, 16); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + if (!Dri_Cdc_Write(&p_State->DriCdcPort, PacketBody, &TextStatus)) + { + p_State->TextFunctionStatus = TextStatus.isEmpty() + ? QStringLiteral("USB CDC send failed for %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + : TextStatus; + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + FinalizeSuccess(Com_Enum_RawPacketSource_UsbCdc); + return true; + + case Com_Enum_RawPacketSource_BleNus: + if (IsHelloReq) + { + if (!p_State->DriNusPort.IsConnected) + { + p_State->TextFunctionStatus = + QStringLiteral("BLE NUS is not connected yet. HelloReq was not sent."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + } + else if (!p_State->IsNusProtocolReady) + { + p_State->TextFunctionStatus = p_State->DriNusPort.IsConnected + ? QStringLiteral("BLE NUS handshake is not ready yet. Skip %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + : QStringLiteral("BLE NUS is not connected yet. Skip %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + else if (!Lgc_Core_DeviceSupportsPacketType(p_State, Type)) + { + p_State->TextFunctionStatus = + QStringLiteral("Device capability does not support %1 on BLE NUS. caps=0x%2") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + .arg(p_State->HelloResponse.CapabilityFlags, 0, 16); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + if (!Dri_Nus_Write(&p_State->DriNusPort, PacketBody, &TextStatus)) + { + p_State->TextFunctionStatus = TextStatus.isEmpty() + ? QStringLiteral("BLE NUS send failed for %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + : TextStatus; + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + FinalizeSuccess(Com_Enum_RawPacketSource_BleNus); + return true; + + default: + p_State->TextFunctionStatus = + QStringLiteral("Test target transport is invalid for %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } +} + +bool Lgc_Core_SendPacket( + Lgc_Core_Struct_State* p_State, + Com_Enum_ProtocolType Type, + const QByteArray& PacketBody) +{ + if (p_State == nullptr) + { + return false; + } + + const bool RequiresAck = + (Type == Com_Enum_ProtocolType_Bitmap) || + (Type == Com_Enum_ProtocolType_TimeSync) || + (Type == Com_Enum_ProtocolType_ThemeRgb); + + if (PacketBody.isEmpty()) + { + p_State->TextFunctionStatus = + QStringLiteral("%1 build failed.").arg(Lgc_Core_GetProtocolTypeText(Type)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + if (!p_State->DeviceReady) + { + p_State->TextFunctionStatus = + QStringLiteral("Handshake is not finished. Skip %1.") + .arg(Lgc_Core_GetProtocolTypeText(Type)); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + if (!Lgc_Core_DeviceSupportsPacketType(p_State, Type)) + { + p_State->TextFunctionStatus = + QStringLiteral("Device capability does not support %1. caps=0x%2") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + .arg(p_State->HelloResponse.CapabilityFlags, 0, 16); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + QString TextStatus; + if (p_State->IsUsbProtocolReady && + Dri_Cdc_Write(&p_State->DriCdcPort, PacketBody, &TextStatus)) + { + Lgc_Core_TestRecordTxPacket( + p_State, + Com_Enum_RawPacketSource_UsbCdc, + Type, + PacketBody, + TextStatus); + if (Type == Com_Enum_ProtocolType_Bitmap) + { + Lgc_Core_ClearDeviceReportedKeyStates(p_State); + } + if (RequiresAck) + { + Lgc_Core_AddPendingCommand( + p_State, + Com_Enum_RawPacketSource_UsbCdc, + Type); + p_State->TextFunctionStatus = + TextStatus.isEmpty() + ? QStringLiteral("%1 sent on USB CDC. Waiting for ACK.") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + : QStringLiteral("%1 Waiting for %2 ACK.") + .arg(TextStatus) + .arg(Lgc_Core_GetProtocolTypeText(Type)); + } + else if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + } + return true; + } + + if (p_State->IsNusProtocolReady && + Dri_Nus_Write(&p_State->DriNusPort, PacketBody, &TextStatus)) + { + Lgc_Core_TestRecordTxPacket( + p_State, + Com_Enum_RawPacketSource_BleNus, + Type, + PacketBody, + TextStatus); + if (Type == Com_Enum_ProtocolType_Bitmap) + { + Lgc_Core_ClearDeviceReportedKeyStates(p_State); + } + if (RequiresAck) + { + Lgc_Core_AddPendingCommand( + p_State, + Com_Enum_RawPacketSource_BleNus, + Type); + p_State->TextFunctionStatus = + TextStatus.isEmpty() + ? QStringLiteral("%1 sent on BLE NUS. Waiting for ACK.") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + : QStringLiteral("%1 Waiting for %2 ACK.") + .arg(TextStatus) + .arg(Lgc_Core_GetProtocolTypeText(Type)); + } + else if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + } + return true; + } + + p_State->TextFunctionStatus = TextStatus.isEmpty() + ? QStringLiteral("%1 send failed.").arg(Lgc_Core_GetProtocolTypeText(Type)) + : TextStatus; + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("TX %1 failed: %2") + .arg(Lgc_Core_GetProtocolTypeText(Type)) + .arg(p_State->TextFunctionStatus)); + return false; +} + +} // namespace + +void Lgc_Core_FillMaskAllEnabled(QByteArray* p_UsageBitmap) +{ + *p_UsageBitmap = QByteArray(COM_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF)); +} + +void Lgc_Core_ScheduleBitmapSend(Lgc_Core_Struct_State* p_State, qint64 DelayMs) +{ + if (p_State == nullptr) + { + return; + } + + p_State->BitmapDirty = true; + p_State->BitmapSent = false; + p_State->BitmapNextSendMs = + QDateTime::currentMSecsSinceEpoch() + (DelayMs > 0 ? DelayMs : 0); +} + +bool Lgc_Core_ProcessBitmapSend(Lgc_Core_Struct_State* p_State) +{ + if ((p_State == nullptr) || + !p_State->BitmapDirty || + p_State->BitmapSent || + !p_State->DeviceReady || + !Lgc_Core_DeviceSupportsPacketType(p_State, Com_Enum_ProtocolType_Bitmap) || + Lgc_Core_HasPendingCommand( + p_State, + Com_Enum_RawPacketSource_UsbCdc, + Com_Enum_ProtocolType_Bitmap) || + Lgc_Core_HasPendingCommand( + p_State, + Com_Enum_RawPacketSource_BleNus, + Com_Enum_ProtocolType_Bitmap)) + { + return false; + } + + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); + if ((p_State->BitmapNextSendMs != 0) && (NowMs < p_State->BitmapNextSendMs)) + { + return false; + } + + p_State->KeyboardMaskBitmap = p_State->FunctionMaskBitmap; + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("Bitmap auto-sync keys: %1") + .arg(Lgc_Core_FormatUsageBitmapSummary(p_State->KeyboardMaskBitmap))); // FIX: log the exact auto-sync bitmap before send. + const bool IsSent = Lgc_Core_SendPacket( + p_State, + Com_Enum_ProtocolType_Bitmap, + Com_Protocol_EncodeBitmap(p_State->KeyboardMaskBitmap)); + if (!IsSent) + { + p_State->BitmapNextSendMs = NowMs + kBitmapRetryDelayMs; + } + else + { + p_State->TextFunctionStatus = QStringLiteral("Bitmap queued. Waiting for ACK."); + } + + return true; +} + +bool Lgc_Core_SendHello(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + const QByteArray PacketBody = Com_Protocol_EncodeHelloReq(); + if (PacketBody.isEmpty()) + { + p_State->TextFunctionStatus = QStringLiteral("HelloReq build failed."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); + bool IsSent = false; + bool IsDeferred = false; + QString TextStatus; + + if (p_State->DriNusPort.IsConnected && + !p_State->IsNusProtocolReady) + { + const qint64 NusHelloRetryIntervalMs = + p_State->DriNusPort.HasWriteAck + ? kNusHelloRetryAfterWriteAckMs + : kHelloRetryIntervalMs; + + if (p_State->DriNusPort.HasWriteAck && !p_State->HasLoggedNusWriteAck) + { + p_State->HasLoggedNusWriteAck = true; + p_State->TextFunctionStatus = + QStringLiteral("BLE RX characteristic acknowledged HelloReq. Waiting for HelloRsp."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + } + + if ((p_State->NusReadySinceMs != 0) && + ((NowMs - p_State->NusReadySinceMs) < kNusHelloInitialDelayMs)) + { + // The firmware enables NUS TX only after the notify subscription settles. + IsDeferred = true; + } + else if (p_State->NusHelloRetryCount >= kNusHelloRetryMaxCount) + { + if (!p_State->HasLoggedNusHelloTimeout) + { + p_State->HasLoggedNusHelloTimeout = true; + p_State->TextFunctionStatus = p_State->DriNusPort.HasWriteAck + ? QStringLiteral("BLE transport is writable, but HelloRsp is still missing. Keep the link open and inspect firmware logs.") + : QStringLiteral("BLE link is up, but HelloRsp is still missing. Keep the link open and inspect firmware logs."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + } + IsDeferred = true; + } + else if ((p_State->LastNusHelloAttemptMs == 0) || + ((NowMs - p_State->LastNusHelloAttemptMs) >= NusHelloRetryIntervalMs)) + { + if (Dri_Nus_Write(&p_State->DriNusPort, PacketBody, &TextStatus)) + { + p_State->IsNusHelloSent = true; + p_State->LastNusHelloAttemptMs = NowMs; + ++p_State->NusHelloRetryCount; + p_State->HasLoggedNusHelloTimeout = false; + Lgc_Core_TestRecordTxPacket( + p_State, + Com_Enum_RawPacketSource_BleNus, + Com_Enum_ProtocolType_HelloReq, + PacketBody, + QStringLiteral("hello attempt %1 | %2") + .arg(p_State->NusHelloRetryCount) + .arg(TextStatus)); + IsSent = true; + } + } + } + + if (p_State->DriCdcPort.IsOpened && + !p_State->IsUsbProtocolReady && + ((p_State->LastUsbHelloAttemptMs == 0) || + ((NowMs - p_State->LastUsbHelloAttemptMs) >= kHelloRetryIntervalMs)) && + Dri_Cdc_Write(&p_State->DriCdcPort, PacketBody, &TextStatus)) + { + p_State->IsUsbHelloSent = true; + p_State->LastUsbHelloAttemptMs = NowMs; + Lgc_Core_TestRecordTxPacket( + p_State, + Com_Enum_RawPacketSource_UsbCdc, + Com_Enum_ProtocolType_HelloReq, + PacketBody, + TextStatus); + IsSent = true; + } + + if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + } + else if (!IsSent && !IsDeferred) + { + p_State->TextFunctionStatus = QStringLiteral("No transport is ready for auto HelloReq."); + } + + return IsSent; +} + +bool Lgc_Core_ApplyFunctionConfig(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + // Convert local feature bindings into the 29-byte device bitmap. + p_State->FunctionMaskBitmap = Com_Protocol_CreateUsageBitmap(); + + for (quint16 Usage : Lgc_FunctionButton_GetConfigurableUsages()) + { + if (Lgc_FunctionButton_HasUsageFeature(p_State->FunctionButtonConfig, Usage)) + { + Com_Protocol_SetUsageBitmapBit(&p_State->FunctionMaskBitmap, Usage, true); + } + } + + p_State->BitmapRetryCount = 0; + p_State->BitmapSent = false; + Lgc_Core_ScheduleBitmapSend( + p_State, + p_State->DeviceReady ? 0 : kHelloRetryIntervalMs); // FIX: when already ready, sync the new bitmap immediately. + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("Function config updated. Bitmap auto-sync scheduled.")); // FIX + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("Function config keys: %1") + .arg(Lgc_Core_FormatUsageBitmapSummary(p_State->FunctionMaskBitmap))); // FIX + if (p_State->DeviceReady) + { + (void)Lgc_Core_ProcessBitmapSend(p_State); // FIX: keep right-click binding aligned with the old immediate-sync behavior. + } + return true; +} + +bool Lgc_Core_SendTimeSync(Lgc_Core_Struct_State* p_State) +{ + return Lgc_Core_SendPacket( + p_State, + Com_Enum_ProtocolType_TimeSync, + Com_Protocol_EncodeTimeSync( + 1, + 0, + QDateTime::currentDateTime().offsetFromUtc() / 60, + static_cast(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()), + 1000)); +} + +bool Lgc_Core_SendThemeRgb( + Lgc_Core_Struct_State* p_State, + quint8 Red, + quint8 Green, + quint8 Blue) +{ + return Lgc_Core_SendPacket( + p_State, + Com_Enum_ProtocolType_ThemeRgb, + Com_Protocol_EncodeThemeRgb(Red, Green, Blue)); +} + +bool Lgc_Core_SendThemeSwitch(Lgc_Core_Struct_State* p_State) +{ + const Lgc_Core_Struct_ThemeColor ThemeColor = Lgc_Core_GetNextThemeColor(p_State); + if (!Lgc_Core_SendThemeRgb( + p_State, + ThemeColor.Red, + ThemeColor.Green, + ThemeColor.Blue)) + { + return false; + } + + if (p_State != nullptr) + { + p_State->IsAltThemeEnabled = !p_State->IsAltThemeEnabled; + } + return true; +} + +bool Lgc_Core_TestSendHello( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource) +{ + if (p_State == nullptr) + { + return false; + } + + const QByteArray PacketBody = Com_Protocol_EncodeHelloReq(); + if (PacketBody.isEmpty()) + { + p_State->TextFunctionStatus = QStringLiteral("HelloReq build failed."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + return false; + } + + const qint64 NowMs = QDateTime::currentMSecsSinceEpoch(); + if (TargetSource == Com_Enum_RawPacketSource_UsbCdc) + { + const bool IsSent = Lgc_Core_SendTestPacketToTarget( + p_State, + TargetSource, + Com_Enum_ProtocolType_HelloReq, + PacketBody, + QStringLiteral("manual target")); + if (IsSent) + { + p_State->IsUsbHelloSent = true; + p_State->LastUsbHelloAttemptMs = NowMs; + } + return IsSent; + } + + if (TargetSource == Com_Enum_RawPacketSource_BleNus) + { + const bool IsSent = Lgc_Core_SendTestPacketToTarget( + p_State, + TargetSource, + Com_Enum_ProtocolType_HelloReq, + PacketBody, + QStringLiteral("manual target")); + if (IsSent) + { + p_State->IsNusHelloSent = true; + p_State->LastNusHelloAttemptMs = NowMs; + p_State->NusHelloRetryCount = 1; + p_State->HasLoggedNusHelloTimeout = false; + } + return IsSent; + } + + bool IsSent = false; + QString TextStatus; + if (p_State->DriCdcPort.IsOpened && + Dri_Cdc_Write(&p_State->DriCdcPort, PacketBody, &TextStatus)) + { + p_State->IsUsbHelloSent = true; + p_State->LastUsbHelloAttemptMs = NowMs; + Lgc_Core_TestRecordTxPacket( + p_State, + Com_Enum_RawPacketSource_UsbCdc, + Com_Enum_ProtocolType_HelloReq, + PacketBody, + QStringLiteral("manual")); + IsSent = true; + } + + if (p_State->DriNusPort.IsConnected && + Dri_Nus_Write(&p_State->DriNusPort, PacketBody, &TextStatus)) + { + p_State->IsNusHelloSent = true; + p_State->LastNusHelloAttemptMs = NowMs; + p_State->NusHelloRetryCount = 1; + p_State->HasLoggedNusHelloTimeout = false; + Lgc_Core_TestRecordTxPacket( + p_State, + Com_Enum_RawPacketSource_BleNus, + Com_Enum_ProtocolType_HelloReq, + PacketBody, + QStringLiteral("manual")); + IsSent = true; + } + + if (!TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + } + else if (!IsSent) + { + p_State->TextFunctionStatus = QStringLiteral("No active transport is ready for HelloReq."); + Lgc_Core_TestAppendLog(p_State, p_State->TextFunctionStatus); + } + + return IsSent; +} + +bool Lgc_Core_TestSendBitmapCurrentConfig( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource) +{ + return Lgc_Core_SendBitmapNow( + p_State, + p_State == nullptr ? QByteArray() : p_State->FunctionMaskBitmap, + TargetSource); +} + +bool Lgc_Core_TestSendBitmapAllEnabled( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource) +{ + return Lgc_Core_SendBitmapNow( + p_State, + QByteArray(COM_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF)), + TargetSource); +} + +bool Lgc_Core_TestSendBitmapAllDisabled( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource) +{ + return Lgc_Core_SendBitmapNow( + p_State, + QByteArray(COM_CONST_USAGE_BITMAP_SIZE, 0), + TargetSource); +} + +bool Lgc_Core_TestSendTimeSync( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource TargetSource) +{ + const QByteArray PacketBody = Com_Protocol_EncodeTimeSync( + 1, + 0, + QDateTime::currentDateTime().offsetFromUtc() / 60, + static_cast(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()), + 1000); + return (TargetSource == Com_Enum_RawPacketSource_None) + ? Lgc_Core_SendPacket( + p_State, + Com_Enum_ProtocolType_TimeSync, + PacketBody) + : Lgc_Core_SendTestPacketToTarget( + p_State, + TargetSource, + Com_Enum_ProtocolType_TimeSync, + PacketBody, + QStringLiteral("manual target")); +} + +bool Lgc_Core_TestSendThemeRgb( + Lgc_Core_Struct_State* p_State, + quint8 Red, + quint8 Green, + quint8 Blue, + Com_Enum_RawPacketSource TargetSource) +{ + const QByteArray PacketBody = Com_Protocol_EncodeThemeRgb(Red, Green, Blue); + return (TargetSource == Com_Enum_RawPacketSource_None) + ? Lgc_Core_SendPacket( + p_State, + Com_Enum_ProtocolType_ThemeRgb, + PacketBody) + : Lgc_Core_SendTestPacketToTarget( + p_State, + TargetSource, + Com_Enum_ProtocolType_ThemeRgb, + PacketBody, + QStringLiteral("manual target")); +} diff --git a/LOGIC/Lgc_Core_Config.cpp b/LOGIC/Lgc_Core_Config.cpp new file mode 100644 index 0000000..bc6742f --- /dev/null +++ b/LOGIC/Lgc_Core_Config.cpp @@ -0,0 +1,366 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +constexpr int kFunctionConfigVersion = 1; +constexpr const char* kFunctionConfigFileName = "function_config.json"; + +QString Lgc_Core_GetFeatureTypeJsonKey(Lgc_FunctionFeature_Type Type) +{ + switch (Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + return QStringLiteral("key_combination"); + case Lgc_FunctionFeature_Type::KeySequence: + return QStringLiteral("key_sequence"); + case Lgc_FunctionFeature_Type::Website: + return QStringLiteral("website"); + default: + return QStringLiteral("key_combination"); + } +} + +Lgc_FunctionFeature_Type Lgc_Core_ParseFeatureTypeJsonKey(const QString& JsonKey) +{ + const QString NormalizedKey = JsonKey.trimmed().toLower(); + if ((NormalizedKey == QStringLiteral("key_combination")) || + (NormalizedKey == QStringLiteral("combination")) || + (NormalizedKey == QStringLiteral("shortcut"))) + { + return Lgc_FunctionFeature_Type::KeyCombination; + } + + if ((NormalizedKey == QStringLiteral("key_sequence")) || + (NormalizedKey == QStringLiteral("sequence"))) + { + return Lgc_FunctionFeature_Type::KeySequence; + } + + if ((NormalizedKey == QStringLiteral("website")) || + (NormalizedKey == QStringLiteral("url")) || + (NormalizedKey == QStringLiteral("web"))) + { + return Lgc_FunctionFeature_Type::Website; + } + + return Lgc_FunctionFeature_Type::KeyCombination; +} + +bool Lgc_Core_IsConfigurableUsage(quint16 Usage) +{ + return Lgc_FunctionButton_GetConfigurableUsages().contains(Usage); +} + +QVector Lgc_Core_GetUsageListForFeature( + const Lgc_Core_Struct_State* p_State, + int FeatureId) +{ + QVector UsageList; + if (p_State == nullptr) + { + return UsageList; + } + + for (quint16 Usage : Lgc_FunctionButton_GetConfigurableUsages()) + { + if (Lgc_FunctionButton_GetUsageFeatureId(p_State->FunctionButtonConfig, Usage) == FeatureId) + { + UsageList.append(Usage); + } + } + return UsageList; +} + +QJsonObject Lgc_Core_BuildFeatureJson( + const Lgc_Core_Struct_State* p_State, + const Lgc_FunctionFeature_Definition& Feature) +{ + QJsonObject JsonObject; + JsonObject.insert(QStringLiteral("id"), Feature.Id); + JsonObject.insert(QStringLiteral("name"), Feature.Name); + JsonObject.insert(QStringLiteral("description"), Feature.Description); + JsonObject.insert(QStringLiteral("type"), Lgc_Core_GetFeatureTypeJsonKey(Feature.Type)); + JsonObject.insert(QStringLiteral("sequence_text"), Feature.SequenceText); + JsonObject.insert(QStringLiteral("website_url"), Feature.WebsiteUrl); + + QJsonArray UsageArray; + for (quint16 Usage : Lgc_Core_GetUsageListForFeature(p_State, Feature.Id)) + { + UsageArray.append(static_cast(Usage)); + } + JsonObject.insert(QStringLiteral("usage_list"), UsageArray); + return JsonObject; +} + +bool Lgc_Core_ParseFunctionConfig( + const QJsonObject& RootObject, + Lgc_FunctionButton_Config* p_Config, + QString* p_TextError) +{ + auto SetError = [p_TextError](const QString& Text) + { + if (p_TextError != nullptr) + { + *p_TextError = Text; + } + }; + + if (p_Config == nullptr) + { + SetError(QStringLiteral("Function config target is null.")); + return false; + } + + *p_Config = Lgc_FunctionButton_Config(); + if (p_TextError != nullptr) + { + p_TextError->clear(); + } + + const QJsonValue VersionValue = RootObject.value(QStringLiteral("version")); + if (VersionValue.isDouble() && + (VersionValue.toInt(kFunctionConfigVersion) > kFunctionConfigVersion)) + { + SetError(QStringLiteral("Unsupported config version: %1") + .arg(VersionValue.toInt(kFunctionConfigVersion))); + return false; + } + + const QJsonArray FunctionArray = RootObject.value(QStringLiteral("functions")).toArray(); + for (const QJsonValue& FunctionValue : FunctionArray) + { + if (!FunctionValue.isObject()) + { + continue; + } + + const QJsonObject FunctionObject = FunctionValue.toObject(); + const int FeatureId = FunctionObject.value(QStringLiteral("id")).toInt(); + if (FeatureId <= 0) + { + continue; + } + + Lgc_FunctionFeature_Definition Feature; + Feature.Id = FeatureId; + Feature.Name = FunctionObject.value(QStringLiteral("name")).toString(); + Feature.Description = FunctionObject.value(QStringLiteral("description")).toString(); + Feature.SequenceText = FunctionObject.value(QStringLiteral("sequence_text")).toString(); + Feature.WebsiteUrl = FunctionObject.value(QStringLiteral("website_url")).toString(); + Feature.Type = Lgc_Core_ParseFeatureTypeJsonKey( + FunctionObject.value(QStringLiteral("type")).toString()); + + p_Config->FeatureMap.insert(FeatureId, Feature); + + const QJsonArray UsageArray = FunctionObject.value(QStringLiteral("usage_list")).toArray(); + for (const QJsonValue& UsageValue : UsageArray) + { + const int Usage = UsageValue.toInt(-1); + if ((Usage >= 0) && Lgc_Core_IsConfigurableUsage(static_cast(Usage))) + { + p_Config->UsageFeatureIdMap.insert(static_cast(Usage), FeatureId); + } + } + } + + return true; +} + +QString Lgc_Core_GetFunctionConfigFilePathInternal() +{ + const QDir AppDir(QCoreApplication::applicationDirPath()); + const QStringList CandidatePathList = { + QDir::current().absoluteFilePath(QLatin1String(kFunctionConfigFileName)), + AppDir.absoluteFilePath(QLatin1String(kFunctionConfigFileName)), + AppDir.absoluteFilePath(QStringLiteral("../") + QLatin1String(kFunctionConfigFileName)), + AppDir.absoluteFilePath(QStringLiteral("../../") + QLatin1String(kFunctionConfigFileName)) + }; + + for (const QString& CandidatePath : CandidatePathList) + { + const QString CleanPath = QDir::cleanPath(CandidatePath); + if (QFileInfo::exists(CleanPath)) + { + return CleanPath; + } + } + + return QDir::cleanPath(CandidatePathList.first()); +} + +} // namespace + +bool Lgc_Core_LoadFunctionConfig(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + const QString FilePath = Lgc_Core_GetFunctionConfigFilePathInternal(); + if (!QFileInfo::exists(FilePath)) + { + return true; + } + + QFile ConfigFile(FilePath); + if (!ConfigFile.open(QIODevice::ReadOnly)) + { + p_State->TextFunctionStatus = QStringLiteral("Function config load failed: %1") + .arg(ConfigFile.errorString()); + return false; + } + + QJsonParseError ParseError; + const QJsonDocument JsonDocument = + QJsonDocument::fromJson(ConfigFile.readAll(), &ParseError); + if ((ParseError.error != QJsonParseError::NoError) || !JsonDocument.isObject()) + { + p_State->TextFunctionStatus = QStringLiteral("Function config parse failed: %1") + .arg(ParseError.errorString()); + return false; + } + + Lgc_FunctionButton_Config LoadedConfig; + QString TextError; + if (!Lgc_Core_ParseFunctionConfig(JsonDocument.object(), &LoadedConfig, &TextError)) + { + p_State->TextFunctionStatus = QStringLiteral("Function config parse failed: %1") + .arg(TextError); + return false; + } + + p_State->FunctionButtonConfig = LoadedConfig; + Lgc_Core_ApplyFunctionConfig(p_State); + return true; +} + +bool Lgc_Core_SaveFunctionConfig(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + const QString FilePath = Lgc_Core_GetFunctionConfigFilePathInternal(); + const QFileInfo FileInfo(FilePath); + QDir ParentDir(FileInfo.absolutePath()); + if (!ParentDir.exists() && !ParentDir.mkpath(QStringLiteral("."))) + { + p_State->TextFunctionStatus = QStringLiteral("Function config save failed: %1") + .arg(QStringLiteral("Could not create config directory: %1") + .arg(FileInfo.absolutePath())); + return false; + } + + QJsonObject RootObject; + RootObject.insert(QStringLiteral("version"), kFunctionConfigVersion); + + QJsonArray FunctionArray; + for (int FeatureId : Lgc_FunctionButton_GetFeatureIdList(p_State->FunctionButtonConfig)) + { + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(p_State->FunctionButtonConfig, FeatureId); + if (Feature.Id > 0) + { + FunctionArray.append(Lgc_Core_BuildFeatureJson(p_State, Feature)); + } + } + RootObject.insert(QStringLiteral("functions"), FunctionArray); + + QSaveFile ConfigFile(FilePath); + if (!ConfigFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + p_State->TextFunctionStatus = QStringLiteral("Function config save failed: %1") + .arg(ConfigFile.errorString()); + return false; + } + + const QByteArray JsonData = QJsonDocument(RootObject).toJson(QJsonDocument::Indented); + if (ConfigFile.write(JsonData) != JsonData.size()) + { + p_State->TextFunctionStatus = QStringLiteral("Function config save failed: %1") + .arg(ConfigFile.errorString()); + ConfigFile.cancelWriting(); + return false; + } + + if (!ConfigFile.commit()) + { + p_State->TextFunctionStatus = QStringLiteral("Function config save failed: %1") + .arg(ConfigFile.errorString()); + return false; + } + + return true; +} + +int Lgc_Core_AddFeature(Lgc_Core_Struct_State* p_State) +{ + return p_State == nullptr ? 0 : Lgc_FunctionButton_AddFeature(p_State->FunctionButtonConfig); +} + +bool Lgc_Core_UpdateFeature( + Lgc_Core_Struct_State* p_State, + const Lgc_FunctionFeature_Definition& Feature) +{ + if ((p_State == nullptr) || (Feature.Id <= 0)) + { + return false; + } + + Lgc_FunctionButton_SetFeature(p_State->FunctionButtonConfig, Feature); + Lgc_Core_ApplyFunctionConfig(p_State); + return true; +} + +bool Lgc_Core_DeleteFeature(Lgc_Core_Struct_State* p_State, int FeatureId) +{ + if ((p_State == nullptr) || (FeatureId <= 0)) + { + return false; + } + + const QString FeatureName = Lgc_Core_GetFeatureNameById(p_State, FeatureId); + Lgc_FunctionButton_RemoveFeature(p_State->FunctionButtonConfig, FeatureId); + Lgc_Core_ApplyFunctionConfig(p_State); + p_State->TextFunctionStatus = QStringLiteral("宸插垹闄ゅ姛鑳斤細%1").arg(FeatureName); + return true; +} + +bool Lgc_Core_BindUsageToFeature( + Lgc_Core_Struct_State* p_State, + quint16 Usage, + int FeatureId) +{ + if (p_State == nullptr) + { + return false; + } + + if ((FeatureId > 0) && !Lgc_Core_HasFeature(p_State, FeatureId)) + { + return false; + } + + Lgc_FunctionButton_SetUsageFeatureId(p_State->FunctionButtonConfig, Usage, FeatureId); + Lgc_Core_ApplyFunctionConfig(p_State); + p_State->TextFunctionStatus = FeatureId > 0 + ? QStringLiteral("Bound key %1 to %2.") + .arg( + Lgc_FunctionButton_GetUsageShortText(Usage), + Lgc_Core_GetFeatureNameById(p_State, FeatureId)) + : QStringLiteral("Cleared the function binding on key %1.") + .arg(Lgc_FunctionButton_GetUsageShortText(Usage)); + return true; +} diff --git a/LOGIC/Lgc_Core_Control.cpp b/LOGIC/Lgc_Core_Control.cpp new file mode 100644 index 0000000..e9449b8 --- /dev/null +++ b/LOGIC/Lgc_Core_Control.cpp @@ -0,0 +1,320 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include +#include +#include + +namespace +{ + +struct Lgc_Core_Struct_ThemeColor +{ + quint8 Red; + quint8 Green; + quint8 Blue; +}; + +QString Lgc_Core_FormatThemeColor(quint8 Red, quint8 Green, quint8 Blue) +{ + return QStringLiteral("%1 %2 %3") + .arg(Red, 2, 16, QLatin1Char('0')) + .arg(Green, 2, 16, QLatin1Char('0')) + .arg(Blue, 2, 16, QLatin1Char('0')) + .toUpper(); +} + +Lgc_Core_Struct_ThemeColor Lgc_Core_GetNextThemeColor(Lgc_Core_Struct_State* p_State) +{ + p_State->IsAltThemeEnabled = !p_State->IsAltThemeEnabled; + + if (p_State->IsAltThemeEnabled) + { + return { 0xF7, 0x25, 0x85 }; + } + + return { 0x4C, 0xC9, 0xF0 }; +} + +bool Lgc_Core_IsUsageInRange(quint16 Usage) +{ + return Usage <= MID_CONST_KEYBOARD_USAGE_MAX; +} + +bool Lgc_Core_IsUsageEnabled(const QByteArray& Bitmap, quint16 Usage) +{ + if (!Lgc_Core_IsUsageInRange(Usage) || (Bitmap.size() < MID_CONST_USAGE_BITMAP_SIZE)) + { + return true; + } + + const int ByteIndex = Usage / 8; + const quint8 BitMask = static_cast(1U << (Usage % 8)); + const quint8 ByteValue = static_cast(Bitmap.at(ByteIndex)); + return (ByteValue & BitMask) != 0; +} + +void Lgc_Core_SetUsageEnabled(QByteArray* p_Bitmap, quint16 Usage, bool IsEnabled) +{ + if (!Lgc_Core_IsUsageInRange(Usage)) + { + return; + } + + if (p_Bitmap->size() < MID_CONST_USAGE_BITMAP_SIZE) + { + Lgc_Core_FillMaskAllEnabled(p_Bitmap); + } + + const int ByteIndex = Usage / 8; + const quint8 BitMask = static_cast(1U << (Usage % 8)); + quint8 ByteValue = static_cast(p_Bitmap->at(ByteIndex)); + + if (IsEnabled) + { + ByteValue = static_cast(ByteValue | BitMask); + } + else + { + ByteValue = static_cast(ByteValue & ~BitMask); + } + + (*p_Bitmap)[ByteIndex] = static_cast(ByteValue); +} + +void Lgc_Core_RebuildKeyboardMask(Lgc_Core_Struct_State* p_State) +{ + if (p_State->FunctionMaskBitmap.size() < MID_CONST_USAGE_BITMAP_SIZE) + { + Lgc_Core_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); + } + + p_State->KeyboardMaskBitmap.resize(MID_CONST_USAGE_BITMAP_SIZE); + for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index) + { + p_State->KeyboardMaskBitmap[Index] = p_State->FunctionMaskBitmap.at(Index); + } +} + +QByteArray Lgc_Core_BuildMaskPacket(const Lgc_Core_Struct_State* p_State) +{ + QByteArray Packet(MID_CONST_PACKET_SIZE_VENDOR, 0); + Packet[0] = static_cast(Mid_Enum_ReportId_Vendor); + for (int Index = 0; (Index < MID_CONST_USAGE_BITMAP_SIZE) && (Index < p_State->KeyboardMaskBitmap.size()); ++Index) + { + Packet[Index + 2] = p_State->KeyboardMaskBitmap.at(Index); + } + return Packet; +} + +QByteArray Lgc_Core_BuildCommandPacket(quint8 CommandId, const QByteArray& Payload) +{ + QByteArray Packet(MID_CONST_PACKET_SIZE_VENDOR_COMMAND, 0); + Packet[0] = static_cast(Mid_Enum_ReportId_VendorCommand); + Packet[1] = static_cast(CommandId); + for (int Index = 0; (Index < MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA) && (Index < Payload.size()); ++Index) + { + Packet[Index + 2] = Payload.at(Index); + } + return Packet; +} + +bool Lgc_Core_SendPacket( + Lgc_Core_Struct_State* p_State, + const QByteArray& Packet, + const QString& ExplainText) +{ + QString RouteText; + QString TextStatus; + const auto AppendStatus = [&TextStatus](const QString& StatusText) + { + if (StatusText.isEmpty()) + { + return; + } + + if (!TextStatus.isEmpty()) + { + TextStatus.append(QLatin1Char('\n')); + } + TextStatus.append(StatusText); + }; + const auto AppendRoute = [&RouteText](const QString& RouteName) + { + if (RouteName.isEmpty()) + { + return; + } + + if (!RouteText.isEmpty()) + { + RouteText.append(QStringLiteral(" -> ")); + } + RouteText.append(RouteName); + }; + const auto TrySendUsb = [&]() -> bool + { + if (!p_State->DriVendorPort.ReadPort.IsOpened) + { + return false; + } + + QString UsbStatus; + const QString RouteName = p_State->DriVendorPort.ReadPort.PortName; + if (Dri_Vendor_Write(&p_State->DriVendorPort, Packet, &UsbStatus)) + { + Lgc_Core_AppendPacketLog( + p_State, + QStringLiteral("发送"), + RouteName, + Packet, + ExplainText); + Lgc_Core_AppendStatusLog(p_State, UsbStatus); + return true; + } + + AppendRoute(RouteName); + AppendStatus(UsbStatus); + return false; + }; + const auto TrySendBle = [&]() -> bool + { + if (!p_State->DriBlePort.IsConnected) + { + return false; + } + + QString BleStatus; + if (Dri_Ble_Write(&p_State->DriBlePort, Packet, &BleStatus)) + { + Lgc_Core_AppendPacketLog( + p_State, + QStringLiteral("发送"), + QStringLiteral("Bluetooth/HID Vendor"), + Packet, + ExplainText); + Lgc_Core_AppendStatusLog(p_State, BleStatus); + return true; + } + + AppendRoute(QStringLiteral("Bluetooth/HID Vendor")); + AppendStatus(BleStatus); + return false; + }; + + if (p_State->DriBlePort.IsConnected) + { + if (TrySendBle() || TrySendUsb()) + { + return true; + } + } + else if (TrySendUsb()) + { + return true; + } + + if (RouteText.isEmpty()) + { + RouteText = QStringLiteral("Vendor"); + } + if (TextStatus.isEmpty()) + { + TextStatus = QStringLiteral("No connected USB/Bluetooth device was found."); + } + + Lgc_Core_AppendPacketLog( + p_State, + QStringLiteral("发送失败"), + RouteText, + Packet, + ExplainText); + Lgc_Core_AppendStatusLog(p_State, TextStatus); + return false; +} + +qint64 Lgc_Core_GetBeijingTimestampMs() +{ + return QDateTime::currentDateTimeUtc() + .toTimeZone(QTimeZone("Asia/Shanghai")) + .toMSecsSinceEpoch() + (8LL * 60LL * 60LL * 1000LL); +} + +QString Lgc_Core_GetBeijingTimeText(qint64 TimestampMs) +{ + const QTimeZone BeijingTimeZone("Asia/Shanghai"); + return QDateTime::fromMSecsSinceEpoch(TimestampMs, Qt::UTC) + .toTimeZone(BeijingTimeZone) + .toString(QStringLiteral("yyyy-MM-dd HH:mm:ss.zzz")); +} + +} // namespace + +void Lgc_Core_FillMaskAllEnabled(QByteArray* p_UsageBitmap) +{ + *p_UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, static_cast(0xFF)); +} + +bool Lgc_Core_SendCurrentMask(Lgc_Core_Struct_State* p_State) +{ + Lgc_Core_RebuildKeyboardMask(p_State); + return Lgc_Core_SendPacket( + p_State, + Lgc_Core_BuildMaskPacket(p_State), + QStringLiteral("0x04 键盘掩码同步")); +} + +bool Lgc_Core_ApplyFunctionConfig(Lgc_Core_Struct_State* p_State) +{ + Lgc_Core_FillMaskAllEnabled(&p_State->FunctionMaskBitmap); + + const QVector UsageList = Lgc_FunctionButton_GetConfigurableUsages(); + for (quint16 Usage : UsageList) + { + if (Lgc_FunctionButton_HasUsageFeature(p_State->FunctionButtonConfig, Usage)) + { + Lgc_Core_SetUsageEnabled(&p_State->FunctionMaskBitmap, Usage, false); + } + } + + if (p_State->IsStarted && + (p_State->DriVendorPort.ReadPort.IsOpened || p_State->DriBlePort.IsConnected)) + { + Lgc_Core_SendCurrentMask(p_State); + } + return true; +} + +bool Lgc_Core_SendTimeSync(Lgc_Core_Struct_State* p_State) +{ + const qint64 TimestampMs = Lgc_Core_GetBeijingTimestampMs(); + QByteArray Payload(MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA, 0); + for (int Index = 0; Index < MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA; ++Index) + { + Payload[Index] = static_cast((static_cast(TimestampMs) >> (Index * 8)) & 0xFF); + } + + return Lgc_Core_SendPacket( + p_State, + Lgc_Core_BuildCommandPacket(0x02, Payload), + QStringLiteral("0x05 0x02 时间同步(北京时间毫秒值:%1,北京时间:%2)") + .arg(QString::number(TimestampMs), Lgc_Core_GetBeijingTimeText(TimestampMs))); +} + +bool Lgc_Core_SendThemeSwitch(Lgc_Core_Struct_State* p_State) +{ + const Lgc_Core_Struct_ThemeColor ThemeColor = Lgc_Core_GetNextThemeColor(p_State); + QByteArray Payload(MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA, 0); + Payload[0] = static_cast(ThemeColor.Red); + Payload[1] = static_cast(ThemeColor.Green); + Payload[2] = static_cast(ThemeColor.Blue); + + return Lgc_Core_SendPacket( + p_State, + Lgc_Core_BuildCommandPacket(0x01, Payload), + QStringLiteral("0x05 0x01 theme switch (RGB:%1)") + .arg(Lgc_Core_FormatThemeColor( + ThemeColor.Red, + ThemeColor.Green, + ThemeColor.Blue))); +} + diff --git a/LOGIC/Lgc_Core_Input.cpp b/LOGIC/Lgc_Core_Input.cpp new file mode 100644 index 0000000..15dbe68 --- /dev/null +++ b/LOGIC/Lgc_Core_Input.cpp @@ -0,0 +1,702 @@ +#include "LOGIC/Lgc_Core_Private.h" + +#include +#include +#include + +namespace +{ + +constexpr quint32 kProtocolVersionCurrent = 1U; +constexpr qint64 kBitmapInitialSendDelayMs = 300; +constexpr qint64 kBitmapRetryDelayMs = 200; +constexpr int kBitmapRetryMaxCount = 5; + +QString Lgc_Core_FormatUsageListText(const QVector& UsageList) +{ + if (UsageList.isEmpty()) + { + return QStringLiteral("(none)"); + } + + QStringList KeyList; + for (quint16 Usage : UsageList) + { + KeyList.append(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + + return KeyList.join(QStringLiteral(", ")); +} + +QByteArray Lgc_Core_BuildUsageBitmapFromList(const QVector& UsageList) +{ + QByteArray UsageBitmap = Com_Protocol_CreateUsageBitmap(); + for (quint16 Usage : UsageList) + { + (void)Com_Protocol_SetUsageBitmapBit(&UsageBitmap, Usage, true); + } + + return UsageBitmap; +} + +Com_Enum_ProtocolType Lgc_Core_NormalizePacketTypeValue(quint32 RawType) +{ + switch (RawType) + { + case 1: return Com_Enum_ProtocolType_HelloReq; + case 2: return Com_Enum_ProtocolType_HelloRsp; + case 3: return Com_Enum_ProtocolType_Bitmap; + case 4: return Com_Enum_ProtocolType_FunctionKeyEvent; + case 5: return Com_Enum_ProtocolType_LedState; + case 6: return Com_Enum_ProtocolType_TimeSync; + case 7: return Com_Enum_ProtocolType_ThemeRgb; + case 8: return Com_Enum_ProtocolType_Ack; + case 9: return Com_Enum_ProtocolType_Error; + default: + break; + } + + const auto Type = static_cast(RawType & 0xFFU); + switch (Type) + { + case Com_Enum_ProtocolType_HelloReq: + case Com_Enum_ProtocolType_HelloRsp: + case Com_Enum_ProtocolType_Bitmap: + case Com_Enum_ProtocolType_FunctionKeyEvent: + case Com_Enum_ProtocolType_LedState: + case Com_Enum_ProtocolType_TimeSync: + case Com_Enum_ProtocolType_ThemeRgb: + case Com_Enum_ProtocolType_Ack: + case Com_Enum_ProtocolType_Error: + return (static_cast(Type) == RawType) + ? Type + : Com_Enum_ProtocolType_None; + default: + return Com_Enum_ProtocolType_None; + } +} + +QString Lgc_Core_FormatVidPid(quint32 VendorId, quint32 ProductId) +{ + return QStringLiteral("%1:%2") + .arg(VendorId, 4, 16, QLatin1Char('0')) + .arg(ProductId, 4, 16, QLatin1Char('0')) + .toUpper(); +} + +QString Lgc_Core_GetPacketTypeText(quint32 RawType) +{ + const Com_Enum_ProtocolType Type = Lgc_Core_NormalizePacketTypeValue(RawType); + if (Type == Com_Enum_ProtocolType_None) + { + return QStringLiteral("0x%1").arg(RawType, 2, 16, QLatin1Char('0')).toUpper(); + } + + const QString TypeText = Lgc_Core_GetProtocolTypeText(Type); + return (static_cast(Type) == RawType) + ? TypeText + : QStringLiteral("%1(tag 0x%2)") + .arg(TypeText) + .arg(RawType, 2, 16, QLatin1Char('0')) + .toUpper(); +} + +QString Lgc_Core_GetPacketSourceText(const Com_Struct_RawPacket& Packet) +{ + return Packet.PortName.isEmpty() + ? Lgc_Core_GetTransportText(Packet.Source) + : QStringLiteral("%1 %2") + .arg(Lgc_Core_GetTransportText(Packet.Source)) + .arg(Packet.PortName); +} + +bool Lgc_Core_IsHelloRspValid( + const Lgc_Core_Struct_State* /*p_State*/, + const Com_Struct_ProtocolHelloRsp& HelloRsp, + QString* p_TextError) +{ + auto SetError = [p_TextError](const QString& Text) + { + if (p_TextError != nullptr) + { + *p_TextError = Text; + } + }; + + if (p_TextError != nullptr) + { + p_TextError->clear(); + } + + if (HelloRsp.ProtocolVersion != kProtocolVersionCurrent) + { + SetError( + QStringLiteral("protocol_version=%1, expected %2") + .arg(HelloRsp.ProtocolVersion) + .arg(kProtocolVersionCurrent)); + return false; + } + + // Day0 live testing accepts any device identity that speaks the current protocol. + // Capability flags stay as runtime gates, but are not a reason to reject HelloRsp itself. + return true; +} + +bool Lgc_Core_ConfirmHelloRspSource( + Lgc_Core_Struct_State* p_State, + const Com_Struct_RawPacket& Packet, + QString* p_TextStatus) +{ + if (Packet.Source == Com_Enum_RawPacketSource_UsbCdc) + { + if (!Dri_Cdc_LockCandidate(&p_State->DriCdcPort, Packet.EndpointId, p_TextStatus)) + { + return false; + } + + Dri_Nus_Close(&p_State->DriNusPort); + p_State->LastNusRefreshAttemptMs = 0; + p_State->IsUsbProtocolReady = true; + p_State->IsUsbHelloSent = true; + p_State->IsNusProtocolReady = false; + p_State->IsNusHelloSent = false; + p_State->LastNusHelloAttemptMs = 0; + p_State->NusReadySinceMs = 0; + p_State->NusHelloRetryCount = 0; + p_State->WasNusConnectedLastPoll = false; + p_State->HasLoggedNusWriteAck = false; + p_State->HasLoggedNusHelloTimeout = false; + p_State->PendingNusCommandBits = 0; + return true; + } + + if (Packet.Source == Com_Enum_RawPacketSource_BleNus) + { + if (!Dri_Nus_LockCandidate(&p_State->DriNusPort, Packet.EndpointId, p_TextStatus)) + { + return false; + } + + Dri_Cdc_Close(&p_State->DriCdcPort); + p_State->LastCdcRefreshAttemptMs = 0; + p_State->IsUsbProtocolReady = false; + p_State->IsUsbHelloSent = false; + p_State->LastUsbHelloAttemptMs = 0; + p_State->PendingUsbCommandBits = 0; + p_State->IsNusProtocolReady = true; + p_State->IsNusHelloSent = true; + p_State->NusHelloRetryCount = 0; + p_State->HasLoggedNusWriteAck = false; + p_State->HasLoggedNusHelloTimeout = false; + return true; + } + + if (p_TextStatus != nullptr) + { + *p_TextStatus = QStringLiteral("HelloRsp arrived from an unknown transport."); + } + return false; +} + +void Lgc_Core_RejectHelloRsp( + Lgc_Core_Struct_State* p_State, + const Com_Struct_RawPacket& Packet, + const QString& TextError) +{ + Lgc_Core_ResetProtocolStateForSource(p_State, Packet.Source); + + if (Packet.Source == Com_Enum_RawPacketSource_UsbCdc) + { + QString TextStatus; + if (!Packet.EndpointId.isEmpty()) + { + Dri_Cdc_DiscardCandidate(&p_State->DriCdcPort, Packet.EndpointId, &TextStatus); + } + else + { + Dri_Cdc_Close(&p_State->DriCdcPort); + } + + if (!p_State->DriCdcPort.IsOpened) + { + p_State->LastCdcRefreshAttemptMs = 0; + } + + p_State->TextFunctionStatus = TextStatus.isEmpty() + ? QStringLiteral("%1 handshake rejected: %2") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(TextError) + : QStringLiteral("%1 handshake rejected: %2, %3") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(TextError) + .arg(TextStatus); + return; + } + + if (Packet.Source == Com_Enum_RawPacketSource_BleNus) + { + QString TextStatus; + if (!Packet.EndpointId.isEmpty()) + { + Dri_Nus_DiscardCandidate(&p_State->DriNusPort, Packet.EndpointId, &TextStatus); + } + else + { + Dri_Nus_Close(&p_State->DriNusPort); + } + + if (!p_State->DriNusPort.IsOpened) + { + p_State->LastNusRefreshAttemptMs = 0; + } + + p_State->TextFunctionStatus = TextStatus.isEmpty() + ? QStringLiteral("%1 handshake rejected: %2") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(TextError) + : QStringLiteral("%1 handshake rejected: %2, %3") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(TextError) + .arg(TextStatus); + return; + } + + p_State->TextFunctionStatus = + QStringLiteral("%1 handshake rejected: %2") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(TextError); +} + +} // namespace + +void Lgc_Core_ClearAllKeyStates(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return; + } + + Lgc_Core_ClearDeviceReportedKeyStates(p_State); + p_State->UiPressedUsageSet.clear(); +} + +void Lgc_Core_ClearDeviceReportedKeyStates(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return; + } + + p_State->IsVisibleKeyStateValid = false; + p_State->VisibleUsageList.clear(); + p_State->IsPhysicalKeyStateValid = false; + p_State->PhysicalUsageList.clear(); + p_State->LastPhysicalUsageList.clear(); + p_State->TestLastFunctionEventBitmap.clear(); +} + +void Lgc_Core_CloseAllPorts(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return; + } + + Dri_Nus_Close(&p_State->DriNusPort); + Dri_Cdc_Close(&p_State->DriCdcPort); +} + +bool Lgc_Core_SyncSystemState(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + const bool OldNumLock = p_State->IsSystemNumLockOn; + const bool OldConnected = p_State->IsConnected; + + p_State->IsSystemNumLockOn = (GetKeyState(VK_NUMLOCK) & 0x0001) != 0; + p_State->DeviceReady = p_State->IsUsbProtocolReady || p_State->IsNusProtocolReady; + p_State->IsConnected = p_State->DeviceReady; + + if (p_State->IsUsbProtocolReady && !p_State->DriCdcPort.IsOpened) + { + Lgc_Core_ResetProtocolStateForSource( + p_State, + Com_Enum_RawPacketSource_UsbCdc); + p_State->LastCdcRefreshAttemptMs = 0; + } + + if (p_State->IsNusProtocolReady && !p_State->DriNusPort.IsConnected) + { + Lgc_Core_ResetProtocolStateForSource( + p_State, + Com_Enum_RawPacketSource_BleNus); + p_State->LastNusRefreshAttemptMs = 0; + if (!p_State->DriNusPort.IsOpened) + { + Dri_Nus_Close(&p_State->DriNusPort); + } + } + + if (OldConnected && !p_State->IsConnected) + { + p_State->TextFunctionStatus = + QStringLiteral("Device disconnected. Trying to reconnect."); + } + + return (OldNumLock != p_State->IsSystemNumLockOn) || + (OldConnected != p_State->IsConnected); +} + +void Lgc_Core_HandleCdcPacket(Lgc_Core_Struct_State* p_State, const Com_Struct_RawPacket& Packet) +{ + if ((p_State != nullptr) && (Packet.Source == Com_Enum_RawPacketSource_UsbCdc)) + { + Lgc_Core_HandleProtocolPacket(p_State, Packet); + } +} + +void Lgc_Core_HandleNusPacket(Lgc_Core_Struct_State* p_State, const Com_Struct_RawPacket& Packet) +{ + if ((p_State != nullptr) && (Packet.Source == Com_Enum_RawPacketSource_BleNus)) + { + Lgc_Core_HandleProtocolPacket(p_State, Packet); + } +} + +void Lgc_Core_HandleProtocolPacket(Lgc_Core_Struct_State* p_State, const Com_Struct_RawPacket& Packet) +{ + if (p_State == nullptr) + { + return; + } + + Com_Enum_ProtocolType Type = Packet.ProtocolType; + if ((Type == Com_Enum_ProtocolType_None) && + !Com_Protocol_DecodeMessageType(Packet.ByteArray, &Type)) + { + return; + } + + Com_Struct_RawPacket LoggedPacket = Packet; + LoggedPacket.ProtocolType = Type; + Lgc_Core_TestRecordRxPacket(p_State, LoggedPacket); + + switch (Type) + { + case Com_Enum_ProtocolType_HelloRsp: + { + Com_Struct_ProtocolHelloRsp HelloRsp; + if (!Com_Protocol_DecodeHelloRsp(Packet.ByteArray, &HelloRsp)) + { + return; + } + + QString TextError; + if (!Lgc_Core_IsHelloRspValid(p_State, HelloRsp, &TextError)) + { + Lgc_Core_RejectHelloRsp(p_State, Packet, TextError); + return; + } + + QString TextStatus; + if (!Lgc_Core_ConfirmHelloRspSource(p_State, Packet, &TextStatus)) + { + p_State->TextFunctionStatus = TextStatus.isEmpty() + ? QStringLiteral("%1 target lock failed.") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + : TextStatus; + return; + } + + p_State->HelloResponse = HelloRsp; + p_State->DeviceReady = true; + p_State->IsConnected = true; + + QStringList AutoSentList; + if (Lgc_Core_DeviceSupportsPacketType(p_State, Com_Enum_ProtocolType_Bitmap)) + { + p_State->BitmapRetryCount = 0; + p_State->BitmapSent = false; + Lgc_Core_ScheduleBitmapSend(p_State, kBitmapInitialSendDelayMs); + AutoSentList.append(QStringLiteral("Bitmap scheduled")); + } + if (Lgc_Core_DeviceSupportsPacketType(p_State, Com_Enum_ProtocolType_TimeSync) && + Lgc_Core_SendTimeSync(p_State)) + { + AutoSentList.append(Lgc_Core_GetProtocolTypeText(Com_Enum_ProtocolType_TimeSync)); + } + + p_State->TextFunctionStatus = + QStringLiteral("Handshake OK on %1: protocol v%2, FW %3.%4, VID/PID %5, caps 0x%6") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(HelloRsp.ProtocolVersion) + .arg(HelloRsp.FirmwareMajor) + .arg(HelloRsp.FirmwareMinor) + .arg(Lgc_Core_FormatVidPid(HelloRsp.VendorId, HelloRsp.ProductId)) + .arg(HelloRsp.CapabilityFlags, 0, 16); + if (!AutoSentList.isEmpty()) + { + p_State->TextFunctionStatus += + QStringLiteral(" Auto TX: %1.").arg(AutoSentList.join(QStringLiteral(", "))); + } + return; + } + + case Com_Enum_ProtocolType_FunctionKeyEvent: + { + Com_Struct_ProtocolFunctionKeyEvent Event; + if (!Com_Protocol_DecodeFunctionKeyEvent(Packet.ByteArray, &Event)) + { + return; + } + + QByteArray EventBitmap = Event.UsageBitmap; + if (!Com_Protocol_IsUsageBitmapValid(EventBitmap)) + { + // 兼容旧固件的 usage + action 单键事件格式,统一还原成位图状态。 + if (!Event.HasUsageAction) + { + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("%1 FunctionKeyEvent payload format is not supported.") + .arg(Lgc_Core_GetPacketSourceText(Packet))); + return; + } + + EventBitmap = Com_Protocol_IsUsageBitmapValid(p_State->TestLastFunctionEventBitmap) + ? p_State->TestLastFunctionEventBitmap + : Lgc_Core_BuildUsageBitmapFromList(p_State->PhysicalUsageList); + + if (Event.Action == Com_Enum_ProtocolKeyAction_Press) + { + (void)Com_Protocol_SetUsageBitmapBit(&EventBitmap, Event.Usage, true); + } + else if (Event.Action == Com_Enum_ProtocolKeyAction_Release) + { + (void)Com_Protocol_SetUsageBitmapBit(&EventBitmap, Event.Usage, false); + } + else + { + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("%1 legacy FunctionKeyEvent has unknown action %2 on key %3.") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(Event.Action) + .arg(Lgc_FunctionButton_GetUsageShortText(Event.Usage))); + return; + } + + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("%1 legacy FunctionKeyEvent: %2 %3") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(Lgc_FunctionButton_GetUsageShortText(Event.Usage)) + .arg( + Event.Action == Com_Enum_ProtocolKeyAction_Press + ? QStringLiteral("PRESS") + : QStringLiteral("RELEASE"))); + } + + const QVector UsageList = + Com_Protocol_BuildPressedUsageList(EventBitmap); + p_State->IsPhysicalKeyStateValid = true; + p_State->IsVisibleKeyStateValid = true; + p_State->PhysicalUsageList = UsageList; + p_State->VisibleUsageList = UsageList; + p_State->TestLastFunctionEventBitmap = EventBitmap; + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral("%1 FunctionKeyEvent decoded keys: %2") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(Lgc_Core_FormatUsageListText(UsageList))); + return; + } + + case Com_Enum_ProtocolType_LedState: + { + Com_Struct_ProtocolLedState LedState; + if (!Com_Protocol_DecodeLedState(Packet.ByteArray, &LedState)) + { + return; + } + + p_State->DeviceLedMask = LedState.LedMask; + p_State->TextFunctionStatus = + QStringLiteral("%1 LED mask 0x%2") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(LedState.LedMask, 8, 16, QLatin1Char('0')) + .toUpper(); + return; + } + + case Com_Enum_ProtocolType_Ack: + { + Com_Struct_ProtocolAck Ack; + if (!Com_Protocol_DecodeAckForType(Packet.ByteArray, Type, &Ack)) + { + return; + } + + const QString AckedTypeText = Lgc_Core_GetPacketTypeText(Ack.AckedType); + const auto AckedType = Lgc_Core_NormalizePacketTypeValue(Ack.AckedType); + const bool WasPending = + Lgc_Core_ClearPendingCommand(p_State, Packet.Source, AckedType); + p_State->LastAckedType = + (AckedType == Com_Enum_ProtocolType_None) + ? Ack.AckedType + : static_cast(AckedType); + if (AckedType == Com_Enum_ProtocolType_Bitmap) + { + p_State->BitmapRetryCount = 0; + if (p_State->KeyboardMaskBitmap != p_State->FunctionMaskBitmap) + { + p_State->BitmapSent = false; + p_State->BitmapDirty = true; + p_State->BitmapNextSendMs = QDateTime::currentMSecsSinceEpoch(); + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral( + "%1 ACK %2 confirmed, but function config changed while the previous Bitmap was in flight. Auto-resending the latest Bitmap.") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(AckedTypeText)); + (void)Lgc_Core_ProcessBitmapSend(p_State); + return; + } + + p_State->BitmapSent = true; + p_State->BitmapDirty = false; + p_State->BitmapNextSendMs = 0; + } + + p_State->TextFunctionStatus = + QStringLiteral("%1 ACK %2%3") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(AckedTypeText) + .arg(WasPending ? QStringLiteral(" confirmed.") + : QStringLiteral(" (unexpected).")); + return; + } + + case Com_Enum_ProtocolType_Error: + { + Com_Struct_ProtocolError Error; + if (!Com_Protocol_DecodeErrorForType(Packet.ByteArray, Type, &Error)) + { + return; + } + + const QString ErrorTypeText = Lgc_Core_GetPacketTypeText(Error.ErrorType); + const auto ErrorType = Lgc_Core_NormalizePacketTypeValue(Error.ErrorType); + const bool WasPending = + Lgc_Core_ClearPendingCommand(p_State, Packet.Source, ErrorType); + p_State->LastErrorType = + (ErrorType == Com_Enum_ProtocolType_None) + ? Error.ErrorType + : static_cast(ErrorType); + p_State->LastErrorCode = Error.ErrorCode; + + if ((ErrorType == Com_Enum_ProtocolType_Bitmap) && + (Error.ErrorCode == Com_Enum_ProtocolErrorCode_NotReady)) + { + p_State->BitmapSent = false; + if (p_State->BitmapRetryCount < kBitmapRetryMaxCount) + { + ++p_State->BitmapRetryCount; + p_State->BitmapDirty = true; + p_State->BitmapNextSendMs = + QDateTime::currentMSecsSinceEpoch() + kBitmapRetryDelayMs; + p_State->TextFunctionStatus = + QStringLiteral("%1 ERROR NOT_READY for Bitmap. Retry %2/%3 scheduled.") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(p_State->BitmapRetryCount) + .arg(kBitmapRetryMaxCount); + return; + } + + p_State->BitmapDirty = false; + p_State->BitmapNextSendMs = 0; + p_State->TextFunctionStatus = + QStringLiteral("%1 ERROR NOT_READY for Bitmap. Retry limit reached.") + .arg(Lgc_Core_GetPacketSourceText(Packet)); + return; + } + + if (ErrorType == Com_Enum_ProtocolType_Bitmap) + { + p_State->BitmapSent = false; + if (p_State->KeyboardMaskBitmap != p_State->FunctionMaskBitmap) + { + p_State->BitmapDirty = true; + p_State->BitmapNextSendMs = QDateTime::currentMSecsSinceEpoch(); + Lgc_Core_TestAppendLog( + p_State, + QStringLiteral( + "%1 ERROR %2 for %3. A newer Bitmap config is waiting, so auto-sync will retry with the latest Bitmap.") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(Lgc_Core_GetProtocolErrorText(Error.ErrorCode)) + .arg(ErrorTypeText)); + (void)Lgc_Core_ProcessBitmapSend(p_State); + return; + } + + p_State->BitmapDirty = false; + p_State->BitmapNextSendMs = 0; + } + + p_State->TextFunctionStatus = + QStringLiteral("%1 ERROR %2 for %3%4") + .arg(Lgc_Core_GetPacketSourceText(Packet)) + .arg(Lgc_Core_GetProtocolErrorText(Error.ErrorCode)) + .arg(ErrorTypeText) + .arg(WasPending ? QStringLiteral(".") + : QStringLiteral(" (unexpected).")); + return; + } + + default: + return; + } +} + +bool Lgc_Core_HandleFunctionButtons(Lgc_Core_Struct_State* p_State) +{ + if (p_State == nullptr) + { + return false; + } + + if (!p_State->IsPhysicalKeyStateValid) + { + p_State->LastPhysicalUsageList.clear(); + return false; + } + + if (p_State->IsFunctionSequenceRecording) + { + p_State->LastPhysicalUsageList = p_State->PhysicalUsageList; + return false; + } + + bool IsChanged = false; + for (quint16 Usage : p_State->PhysicalUsageList) + { + if (p_State->LastPhysicalUsageList.contains(Usage) || + !Lgc_FunctionButton_HasUsageFeature(p_State->FunctionButtonConfig, Usage)) + { + continue; + } + + QString TextStatus; + if (Lgc_FunctionButton_RunBinding(*p_State, Usage, TextStatus) && !TextStatus.isEmpty()) + { + p_State->TextFunctionStatus = TextStatus; + IsChanged = true; + } + } + + p_State->LastPhysicalUsageList = p_State->PhysicalUsageList; + return IsChanged; +} diff --git a/LOGIC/Lgc_Core_Private.h b/LOGIC/Lgc_Core_Private.h new file mode 100644 index 0000000..2f2206e --- /dev/null +++ b/LOGIC/Lgc_Core_Private.h @@ -0,0 +1,54 @@ +#pragma once + +#include "LOGIC/Lgc_Core.h" + +QString Lgc_Core_GetTransportText(Com_Enum_RawPacketSource Source); +QString Lgc_Core_GetProtocolTypeText(Com_Enum_ProtocolType Type); +QString Lgc_Core_GetProtocolErrorText(quint32 ErrorCode); +QString Lgc_Core_FormatPacketHex(const QByteArray& PacketBody); +void Lgc_Core_TestAppendLog(Lgc_Core_Struct_State* p_State, const QString& LineText); +void Lgc_Core_TestRecordTxPacket( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type, + const QByteArray& PacketBody, + const QString& NoteText = QString()); +void Lgc_Core_TestRecordRxPacket( + Lgc_Core_Struct_State* p_State, + const Com_Struct_RawPacket& Packet); + +void Lgc_Core_ResetProtocolState(Lgc_Core_Struct_State* p_State); +void Lgc_Core_ResetProtocolStateForSource( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source); +bool Lgc_Core_DeviceSupportsPacketType( + const Lgc_Core_Struct_State* p_State, + Com_Enum_ProtocolType Type); +void Lgc_Core_AddPendingCommand( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type); +bool Lgc_Core_ClearPendingCommand( + Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type); +bool Lgc_Core_HasPendingCommand( + const Lgc_Core_Struct_State* p_State, + Com_Enum_RawPacketSource Source, + Com_Enum_ProtocolType Type); + +void Lgc_Core_ClearAllKeyStates(Lgc_Core_Struct_State* p_State); +void Lgc_Core_ClearDeviceReportedKeyStates(Lgc_Core_Struct_State* p_State); +void Lgc_Core_CloseAllPorts(Lgc_Core_Struct_State* p_State); +void Lgc_Core_FillMaskAllEnabled(QByteArray* p_UsageBitmap); +void Lgc_Core_ScheduleBitmapSend(Lgc_Core_Struct_State* p_State, qint64 DelayMs); +bool Lgc_Core_ProcessBitmapSend(Lgc_Core_Struct_State* p_State); + +bool Lgc_Core_SendHello(Lgc_Core_Struct_State* p_State); +bool Lgc_Core_SyncSystemState(Lgc_Core_Struct_State* p_State); + +void Lgc_Core_HandleCdcPacket(Lgc_Core_Struct_State* p_State, const Com_Struct_RawPacket& Packet); +void Lgc_Core_HandleNusPacket(Lgc_Core_Struct_State* p_State, const Com_Struct_RawPacket& Packet); +void Lgc_Core_HandleProtocolPacket(Lgc_Core_Struct_State* p_State, const Com_Struct_RawPacket& Packet); + +bool Lgc_Core_HandleFunctionButtons(Lgc_Core_Struct_State* p_State); diff --git a/LOGIC/Lgc_Func_Button.cpp b/LOGIC/Lgc_Func_Button.cpp index 1fe2280..c8c8e17 100644 --- a/LOGIC/Lgc_Func_Button.cpp +++ b/LOGIC/Lgc_Func_Button.cpp @@ -1,20 +1,59 @@ #include "LOGIC/Lgc_Func_Button.h" #include "LOGIC/Lgc_Core.h" -#include -#include -#include -#include +#include "LOGIC/Lgc_Func_Button_Private.h" +#include +#include namespace { -QString Lgc_Func_Button_Func_GetUsageShortText(quint16 Usage) +QString Lgc_FunctionButton_GetKeyboardUsageText(quint16 Usage) { switch (Usage) { + case 0x0053: return QStringLiteral("Num Lock"); + case 0x0054: return QStringLiteral("小键盘 /"); + case 0x0055: return QStringLiteral("小键盘 *"); + case 0x0056: return QStringLiteral("小键盘 -"); + case 0x0057: return QStringLiteral("小键盘 +"); + case 0x0058: return QStringLiteral("小键盘 Enter"); + case 0x0059: return QStringLiteral("小键盘 1"); + case 0x005A: return QStringLiteral("小键盘 2"); + case 0x005B: return QStringLiteral("小键盘 3"); + case 0x005C: return QStringLiteral("小键盘 4"); + case 0x005D: return QStringLiteral("小键盘 5"); + case 0x005E: return QStringLiteral("小键盘 6"); + case 0x005F: return QStringLiteral("小键盘 7"); + case 0x0060: return QStringLiteral("小键盘 8"); + case 0x0061: return QStringLiteral("小键盘 9"); + case 0x0062: return QStringLiteral("小键盘 0"); + case 0x0063: return QStringLiteral("小键盘 ."); + case 0x00E0: return QStringLiteral("Left Ctrl"); + case 0x00E1: return QStringLiteral("Left Shift"); + case 0x00E2: return QStringLiteral("Left Alt"); + case 0x00E3: return QStringLiteral("Left GUI"); + case 0x00E4: return QStringLiteral("Right Ctrl"); + case 0x00E5: return QStringLiteral("Right Shift"); + case 0x00E6: return QStringLiteral("Right Alt"); + case 0x00E7: return QStringLiteral("Right GUI"); + default: + return QStringLiteral("未知按键"); + } +} + +} // namespace + +QString Lgc_FunctionButton_GetUsageShortText(quint16 Usage) +{ + switch (Usage) + { + case 0x0053: return QStringLiteral("Num"); + case 0x0054: return QStringLiteral("/"); + case 0x0055: return QStringLiteral("*"); case 0x0056: return QStringLiteral("-"); case 0x0057: return QStringLiteral("+"); + case 0x0058: return QStringLiteral("Enter"); case 0x0059: return QStringLiteral("1"); case 0x005A: return QStringLiteral("2"); case 0x005B: return QStringLiteral("3"); @@ -25,167 +64,244 @@ QString Lgc_Func_Button_Func_GetUsageShortText(quint16 Usage) case 0x0060: return QStringLiteral("8"); case 0x0061: return QStringLiteral("9"); case 0x0062: return QStringLiteral("0"); + case 0x0063: return QStringLiteral("."); default: - return Mid_Func_GetKeyboardUsageText(Usage); + return Lgc_FunctionButton_GetKeyboardUsageText(Usage); } } -WORD Lgc_Func_Button_Func_GetWindowsVirtualKey(quint16 Usage) +QString Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type Type) { - switch (Usage) + switch (Type) { - case 0x0056: return VK_SUBTRACT; - case 0x0057: return VK_ADD; - case 0x0059: return VK_NUMPAD1; - case 0x005A: return VK_NUMPAD2; - case 0x005B: return VK_NUMPAD3; - case 0x005C: return VK_NUMPAD4; - case 0x005D: return VK_NUMPAD5; - case 0x005E: return VK_NUMPAD6; - case 0x005F: return VK_NUMPAD7; - case 0x0060: return VK_NUMPAD8; - case 0x0061: return VK_NUMPAD9; - case 0x0062: return VK_NUMPAD0; + case Lgc_FunctionFeature_Type::KeyCombination: + return QStringLiteral("Shortcut"); + case Lgc_FunctionFeature_Type::KeySequence: + return QStringLiteral("Shortcut Sequence"); + case Lgc_FunctionFeature_Type::Website: + return QStringLiteral("Open Website"); default: - return 0; + return QStringLiteral("Unknown"); } } -bool Lgc_Func_Button_Func_SendUnicodeText(const QString& Text) +QVector Lgc_FunctionButton_GetConfigurableUsages() { - if (Text.isEmpty()) - { - return false; - } - - QVector InputList; - InputList.reserve(Text.size() * 2); - for (QChar Character : Text) - { - INPUT InputDown = {}; - InputDown.type = INPUT_KEYBOARD; - InputDown.ki.wScan = Character.unicode(); - InputDown.ki.dwFlags = KEYEVENTF_UNICODE; - InputList.append(InputDown); - - INPUT InputUp = InputDown; - InputUp.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP; - InputList.append(InputUp); - } - - return SendInput(static_cast(InputList.size()), InputList.data(), sizeof(INPUT)) == - static_cast(InputList.size()); + return { + 0x0053, 0x0054, 0x0055, 0x0056, + 0x005F, 0x0060, 0x0061, 0x0057, + 0x005C, 0x005D, 0x005E, + 0x0059, 0x005A, 0x005B, 0x0058, + 0x0062, 0x0063 + }; } -void Lgc_Func_Button_Func_RunMacroText(Lgc_Core_Struct_State* p_State, QString* p_TextStatus) +QVector Lgc_FunctionButton_GetFeatureIdList(const Lgc_FunctionButton_Config& Config) { - const QString Text = p_State->FunctionButtonConfig.MacroText.trimmed(); - if (Text.isEmpty()) + QVector FeatureIdList = Config.FeatureMap.keys().toVector(); + std::sort(FeatureIdList.begin(), FeatureIdList.end()); + return FeatureIdList; +} + +Lgc_FunctionFeature_Definition Lgc_FunctionButton_GetFeature( + const Lgc_FunctionButton_Config& Config, + int FeatureId) +{ + return Config.FeatureMap.value(FeatureId); +} + +QString Lgc_FunctionButton_GetFeatureName(const Lgc_FunctionFeature_Definition& Feature) +{ + if (!Feature.Name.trimmed().isEmpty()) + { + return Feature.Name.trimmed(); + } + + return Feature.Id > 0 + ? QStringLiteral("Function %1").arg(Feature.Id) + : QStringLiteral("Unnamed Function"); +} + +QString Lgc_FunctionButton_GetFeatureDescription(const Lgc_FunctionFeature_Definition& Feature) +{ + if (Feature.Id <= 0) + { + return QStringLiteral("No function selected."); + } + + if (!Feature.Description.trimmed().isEmpty()) + { + return Feature.Description.trimmed(); + } + + switch (Feature.Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + return Feature.SequenceText.trimmed().isEmpty() + ? QStringLiteral("Send one shortcut. No shortcut is configured yet.") + : QStringLiteral("Send shortcut: %1").arg(Feature.SequenceText.trimmed()); + case Lgc_FunctionFeature_Type::KeySequence: + return Feature.SequenceText.trimmed().isEmpty() + ? QStringLiteral("Send a sequence of shortcuts. No sequence is configured yet.") + : QStringLiteral("Send shortcut sequence: %1").arg(Feature.SequenceText.trimmed()); + case Lgc_FunctionFeature_Type::Website: + return Feature.WebsiteUrl.trimmed().isEmpty() + ? QStringLiteral("Open a website. No URL is configured yet.") + : QStringLiteral("Open website: %1").arg(Feature.WebsiteUrl.trimmed()); + default: + return QStringLiteral("Unknown function."); + } +} + +QString Lgc_FunctionButton_GetFeatureDescriptionById( + const Lgc_FunctionButton_Config& Config, + int FeatureId) +{ + return Lgc_FunctionButton_GetFeatureDescription( + Lgc_FunctionButton_GetFeature(Config, FeatureId)); +} + +QString Lgc_FunctionButton_GetFeatureBindingSummary( + const Lgc_FunctionButton_Config& Config, + int FeatureId) +{ + if (FeatureId <= 0) + { + return QStringLiteral("No keys are bound."); + } + + QStringList KeyList; + for (quint16 Usage : Lgc_FunctionButton_GetConfigurableUsages()) + { + if (Lgc_FunctionButton_GetUsageFeatureId(Config, Usage) == FeatureId) + { + KeyList.append(Lgc_FunctionButton_GetUsageShortText(Usage)); + } + } + + return KeyList.isEmpty() + ? QStringLiteral("No keys are bound.") + : QStringLiteral("Bound keys: %1").arg(KeyList.join(QStringLiteral(", "))); +} + +int Lgc_FunctionButton_AddFeature(Lgc_FunctionButton_Config& Config) +{ + int FeatureId = 1; + while (Config.FeatureMap.contains(FeatureId)) + { + ++FeatureId; + } + + Lgc_FunctionFeature_Definition Feature; + Feature.Id = FeatureId; + Feature.Name = QStringLiteral("Function %1").arg(FeatureId); + Feature.Type = Lgc_FunctionFeature_Type::KeyCombination; + Config.FeatureMap.insert(FeatureId, Feature); + return FeatureId; +} + +void Lgc_FunctionButton_RemoveFeature(Lgc_FunctionButton_Config& Config, int FeatureId) +{ + if (FeatureId <= 0) { - *p_TextStatus = QStringLiteral("功能键 0 未配置输出文本。"); return; } - *p_TextStatus = Lgc_Func_Button_Func_SendUnicodeText(Text) - ? QStringLiteral("功能键 0 已输出文本:%1").arg(Text) - : QStringLiteral("功能键 0 输出文本失败。"); + Config.FeatureMap.remove(FeatureId); + + auto It = Config.UsageFeatureIdMap.begin(); + while (It != Config.UsageFeatureIdMap.end()) + { + if (It.value() == FeatureId) + { + It = Config.UsageFeatureIdMap.erase(It); + } + else + { + ++It; + } + } } -void Lgc_Func_Button_Func_RunSwapKey(Lgc_Core_Struct_State* p_State, QString* p_TextStatus) +void Lgc_FunctionButton_SetFeature( + Lgc_FunctionButton_Config& Config, + const Lgc_FunctionFeature_Definition& Feature) { - const quint16 UsageLeft = p_State->FunctionButtonConfig.SwapUsageLeft; - const quint16 UsageRight = p_State->FunctionButtonConfig.SwapUsageRight; - if ((UsageLeft == 0) || (UsageRight == 0) || (UsageLeft == UsageRight)) + if (Feature.Id <= 0) { - *p_TextStatus = QStringLiteral("功能键 1 的交换配置无效。"); return; } - if (!Lgc_Core_Func_SetSwapMode(p_State, UsageLeft, UsageRight, !p_State->IsSwapModeOn)) - { - *p_TextStatus = QStringLiteral("功能键 1 切换按键交换失败。"); - return; - } - - *p_TextStatus = p_State->IsSwapModeOn - ? QStringLiteral("功能键 1 已开启按键交换:%1 <-> %2") - .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageLeft)) - .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageRight)) - : QStringLiteral("功能键 1 已关闭按键交换:%1 <-> %2") - .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageLeft)) - .arg(Lgc_Func_Button_Func_GetUsageShortText(UsageRight)); + Config.FeatureMap.insert(Feature.Id, Feature); } -void Lgc_Func_Button_Func_RunOpenWebsite(Lgc_Core_Struct_State* p_State, QString* p_TextStatus) +int Lgc_FunctionButton_GetUsageFeatureId( + const Lgc_FunctionButton_Config& Config, + quint16 Usage) { - const QString UrlText = p_State->FunctionButtonConfig.WebsiteUrl.trimmed(); - const QUrl Url = QUrl::fromUserInput(UrlText); - if (UrlText.isEmpty() || !Url.isValid() || Url.isEmpty()) - { - *p_TextStatus = QStringLiteral("功能键 2 的网址配置无效。"); - return; - } - - *p_TextStatus = QDesktopServices::openUrl(Url) - ? QStringLiteral("功能键 2 已打开网址:%1").arg(Url.toString()) - : QStringLiteral("功能键 2 打开网址失败。"); + const int FeatureId = Config.UsageFeatureIdMap.value(Usage, 0); + return Config.FeatureMap.contains(FeatureId) ? FeatureId : 0; } -} // namespace - -bool Lgc_Func_Button_Func_SendUsageToWindows(quint16 Usage, bool IsPressed) -{ - const WORD VirtualKey = Lgc_Func_Button_Func_GetWindowsVirtualKey(Usage); - if (VirtualKey == 0) - { - return false; - } - - INPUT InputData = {}; - InputData.type = INPUT_KEYBOARD; - InputData.ki.wVk = VirtualKey; - if (!IsPressed) - { - InputData.ki.dwFlags = KEYEVENTF_KEYUP; - } - - return SendInput(1, &InputData, sizeof(INPUT)) == 1; -} - -bool Lgc_Func_Button_Func_HandlePressedUsage( - Lgc_Core_Struct_State* p_State, +void Lgc_FunctionButton_SetUsageFeatureId( + Lgc_FunctionButton_Config& Config, quint16 Usage, - QString* p_TextStatus) + int FeatureId) { - p_TextStatus->clear(); - switch (Usage) + if ((FeatureId <= 0) || !Config.FeatureMap.contains(FeatureId)) { - case 0x0062: - Lgc_Func_Button_Func_RunMacroText(p_State, p_TextStatus); - return true; + Config.UsageFeatureIdMap.remove(Usage); + return; + } - case 0x0059: - Lgc_Func_Button_Func_RunSwapKey(p_State, p_TextStatus); - return true; + Config.UsageFeatureIdMap.insert(Usage, FeatureId); +} - case 0x005A: - Lgc_Func_Button_Func_RunOpenWebsite(p_State, p_TextStatus); - return true; +bool Lgc_FunctionButton_HasUsageFeature( + const Lgc_FunctionButton_Config& Config, + quint16 Usage) +{ + return Lgc_FunctionButton_GetUsageFeatureId(Config, Usage) > 0; +} - case 0x0056: - case 0x0057: - case 0x005B: - case 0x005C: - case 0x005D: - case 0x005E: - case 0x005F: - case 0x0060: - case 0x0061: - *p_TextStatus = QStringLiteral("功能键 %1 还没有绑定具体功能。") - .arg(Lgc_Func_Button_Func_GetUsageShortText(Usage)); - return true; +bool Lgc_FunctionButton_SendUsageToWindows(quint16 Usage, bool IsPressed) +{ + return Lgc_FunctionButton_SendWindowsKey( + Lgc_FunctionButton_GetWindowsKey(Usage), + IsPressed); +} - default: +bool Lgc_FunctionButton_RunBinding( + Lgc_Core_Struct_State& State, + quint16 Usage, + QString& TextStatus) +{ + TextStatus.clear(); + + const int FeatureId = + Lgc_FunctionButton_GetUsageFeatureId(State.FunctionButtonConfig, Usage); + if (FeatureId <= 0) + { return false; } + + const Lgc_FunctionFeature_Definition Feature = + Lgc_FunctionButton_GetFeature(State.FunctionButtonConfig, FeatureId); + + switch (Feature.Type) + { + case Lgc_FunctionFeature_Type::KeyCombination: + Lgc_FunctionButton_RunKeyCombination(Feature, Usage, TextStatus); + return true; + case Lgc_FunctionFeature_Type::KeySequence: + Lgc_FunctionButton_RunKeySequence(Feature, Usage, TextStatus); + return true; + case Lgc_FunctionFeature_Type::Website: + Lgc_FunctionButton_RunOpenWebsite(Feature, TextStatus); + return true; + default: + TextStatus = QStringLiteral("Unknown function type: %1") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return true; + } } diff --git a/LOGIC/Lgc_Func_Button.h b/LOGIC/Lgc_Func_Button.h index 7e2bb44..be09fcd 100644 --- a/LOGIC/Lgc_Func_Button.h +++ b/LOGIC/Lgc_Func_Button.h @@ -1,19 +1,67 @@ #pragma once -#include "MID/Mid_Def.h" +#include +#include +#include struct Lgc_Core_Struct_State; -struct Lgc_Func_Button_Struct_Config +enum class Lgc_FunctionFeature_Type : quint8 { - QString MacroText = QStringLiteral("HELLO WORLD!"); - quint16 SwapUsageLeft = 0x005C; - quint16 SwapUsageRight = 0x005D; - QString WebsiteUrl = QStringLiteral("https://www.deepseek.com/"); + KeyCombination = 0, + KeySequence = 1, + Website = 2 }; -bool Lgc_Func_Button_Func_SendUsageToWindows(quint16 Usage, bool IsPressed); -bool Lgc_Func_Button_Func_HandlePressedUsage( - Lgc_Core_Struct_State* p_State, +struct Lgc_FunctionFeature_Definition +{ + int Id = 0; + QString Name; + QString Description; + Lgc_FunctionFeature_Type Type = Lgc_FunctionFeature_Type::KeyCombination; + QString SequenceText; + QString WebsiteUrl; +}; + +struct Lgc_FunctionButton_Config +{ + QHash FeatureMap; + QHash UsageFeatureIdMap; +}; + +QString Lgc_FunctionButton_GetUsageShortText(quint16 Usage); +QString Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type Type); +QVector Lgc_FunctionButton_GetConfigurableUsages(); +QVector Lgc_FunctionButton_GetFeatureIdList(const Lgc_FunctionButton_Config& Config); +Lgc_FunctionFeature_Definition Lgc_FunctionButton_GetFeature( + const Lgc_FunctionButton_Config& Config, + int FeatureId); +QString Lgc_FunctionButton_GetFeatureName(const Lgc_FunctionFeature_Definition& Feature); +QString Lgc_FunctionButton_GetFeatureDescription(const Lgc_FunctionFeature_Definition& Feature); +QString Lgc_FunctionButton_GetFeatureDescriptionById( + const Lgc_FunctionButton_Config& Config, + int FeatureId); +QString Lgc_FunctionButton_GetFeatureBindingSummary( + const Lgc_FunctionButton_Config& Config, + int FeatureId); +int Lgc_FunctionButton_AddFeature(Lgc_FunctionButton_Config& Config); +void Lgc_FunctionButton_RemoveFeature(Lgc_FunctionButton_Config& Config, int FeatureId); +void Lgc_FunctionButton_SetFeature( + Lgc_FunctionButton_Config& Config, + const Lgc_FunctionFeature_Definition& Feature); +int Lgc_FunctionButton_GetUsageFeatureId( + const Lgc_FunctionButton_Config& Config, + quint16 Usage); +void Lgc_FunctionButton_SetUsageFeatureId( + Lgc_FunctionButton_Config& Config, quint16 Usage, - QString* p_TextStatus); + int FeatureId); +bool Lgc_FunctionButton_HasUsageFeature( + const Lgc_FunctionButton_Config& Config, + quint16 Usage); + +bool Lgc_FunctionButton_SendUsageToWindows(quint16 Usage, bool IsPressed); +bool Lgc_FunctionButton_RunBinding( + Lgc_Core_Struct_State& State, + quint16 Usage, + QString& TextStatus); diff --git a/LOGIC/Lgc_Func_Button_Parse.cpp b/LOGIC/Lgc_Func_Button_Parse.cpp new file mode 100644 index 0000000..4a7b72b --- /dev/null +++ b/LOGIC/Lgc_Func_Button_Parse.cpp @@ -0,0 +1,278 @@ +#include "LOGIC/Lgc_Func_Button_Private.h" + +#include +#include + +namespace +{ + +struct Lgc_FunctionButton_Struct_SequenceKeySpec +{ + const char* Token; + WORD VirtualKey; + DWORD ExtraFlags; + bool IsModifier; + const char* Text; +}; + +bool Lgc_FunctionButton_IsSourceToken( + const QString& TrimmedToken, + const QString& UpperToken) +{ + Q_UNUSED(TrimmedToken); + return (UpperToken == QStringLiteral("SOURCE")) || + (UpperToken == QStringLiteral("SELF")); +} + +QStringList Lgc_FunctionButton_SplitCombinationTokens(const QString& Text) +{ + static const QRegularExpression kPlusSeparator(QStringLiteral("\\s*\\+\\s*")); + static const QRegularExpression kLooseSeparator(QStringLiteral("[\\s,;|]+")); + return Text.contains(QLatin1Char('+')) + ? Text.split(kPlusSeparator, QString::SkipEmptyParts) + : Text.split(kLooseSeparator, QString::SkipEmptyParts); +} + +QStringList Lgc_FunctionButton_SplitShortcutSegments(const QString& Text) +{ + static const QRegularExpression kSequenceSeparator( + QStringLiteral("\\s*(?:->|=>|\\x{2192})\\s*")); + return Text.split(kSequenceSeparator, QString::SkipEmptyParts); +} + +bool Lgc_FunctionButton_ParseTokenList( + const QStringList& TokenList, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText, + const QString& ErrorTemplate) +{ + p_KeyList->clear(); + if (p_ErrorText != nullptr) + { + p_ErrorText->clear(); + } + + for (const QString& Token : TokenList) + { + Lgc_FunctionButton_Struct_SequenceKey KeyItem; + if (!Lgc_FunctionButton_TryParseSequenceToken(Token, SourceUsage, &KeyItem)) + { + if (p_ErrorText != nullptr) + { + *p_ErrorText = ErrorTemplate.arg(Token.trimmed()); + } + p_KeyList->clear(); + return false; + } + + p_KeyList->append(KeyItem); + } + + return !p_KeyList->isEmpty(); +} + +bool Lgc_FunctionButton_TryLookupSequenceKeySpec( + const QString& UpperToken, + Lgc_FunctionButton_Struct_SequenceKey* p_KeyItem) +{ + static const Lgc_FunctionButton_Struct_SequenceKeySpec kSpecs[] = { + { "CTRL", VK_CONTROL, 0, true, "Ctrl" }, + { "CONTROL", VK_CONTROL, 0, true, "Ctrl" }, + { "SHIFT", VK_SHIFT, 0, true, "Shift" }, + { "ALT", VK_MENU, 0, true, "Alt" }, + { "WIN", VK_LWIN, 0, true, "Win" }, + { "META", VK_LWIN, 0, true, "Win" }, + { "ENTER", VK_RETURN, 0, false, "Enter" }, + { "NUMENTER", VK_RETURN, KEYEVENTF_EXTENDEDKEY, false, "NumEnter" }, + { "SPACE", VK_SPACE, 0, false, "Space" }, + { "TAB", VK_TAB, 0, false, "Tab" }, + { "ESC", VK_ESCAPE, 0, false, "Esc" }, + { "ESCAPE", VK_ESCAPE, 0, false, "Esc" }, + { "BACKSPACE", VK_BACK, 0, false, "Backspace" }, + { "DELETE", VK_DELETE, KEYEVENTF_EXTENDEDKEY, false, "Delete" }, + { "INSERT", VK_INSERT, KEYEVENTF_EXTENDEDKEY, false, "Insert" }, + { "HOME", VK_HOME, KEYEVENTF_EXTENDEDKEY, false, "Home" }, + { "END", VK_END, KEYEVENTF_EXTENDEDKEY, false, "End" }, + { "PAGEUP", VK_PRIOR, KEYEVENTF_EXTENDEDKEY, false, "PageUp" }, + { "PAGEDOWN", VK_NEXT, KEYEVENTF_EXTENDEDKEY, false, "PageDown" }, + { "LEFT", VK_LEFT, KEYEVENTF_EXTENDEDKEY, false, "Left" }, + { "RIGHT", VK_RIGHT, KEYEVENTF_EXTENDEDKEY, false, "Right" }, + { "UP", VK_UP, KEYEVENTF_EXTENDEDKEY, false, "Up" }, + { "DOWN", VK_DOWN, KEYEVENTF_EXTENDEDKEY, false, "Down" }, + { "CAPSLOCK", VK_CAPITAL, 0, false, "CapsLock" }, + { "PRINTSCREEN", VK_SNAPSHOT, KEYEVENTF_EXTENDEDKEY, false, "PrintScreen" }, + { "SCROLLLOCK", VK_SCROLL, 0, false, "ScrollLock" }, + { "PAUSE", VK_PAUSE, 0, false, "Pause" }, + { "NUM0", VK_NUMPAD0, 0, false, "Num0" }, + { "NUM1", VK_NUMPAD1, 0, false, "Num1" }, + { "NUM2", VK_NUMPAD2, 0, false, "Num2" }, + { "NUM3", VK_NUMPAD3, 0, false, "Num3" }, + { "NUM4", VK_NUMPAD4, 0, false, "Num4" }, + { "NUM5", VK_NUMPAD5, 0, false, "Num5" }, + { "NUM6", VK_NUMPAD6, 0, false, "Num6" }, + { "NUM7", VK_NUMPAD7, 0, false, "Num7" }, + { "NUM8", VK_NUMPAD8, 0, false, "Num8" }, + { "NUM9", VK_NUMPAD9, 0, false, "Num9" }, + { "NUM/", VK_DIVIDE, KEYEVENTF_EXTENDEDKEY, false, "Num/" }, + { "NUM*", VK_MULTIPLY, 0, false, "Num*" }, + { "NUM-", VK_SUBTRACT, 0, false, "Num-" }, + { "NUM+", VK_ADD, 0, false, "Num+" }, + { "NUM.", VK_DECIMAL, 0, false, "Num." }, + { "COMMA", VK_OEM_COMMA, 0, false, "Comma" }, + { "PERIOD", VK_OEM_PERIOD, 0, false, "Period" }, + { "SEMICOLON", VK_OEM_1, 0, false, "Semicolon" }, + { "SLASH", VK_OEM_2, 0, false, "Slash" }, + { "GRAVE", VK_OEM_3, 0, false, "Grave" }, + { "LEFTBRACKET", VK_OEM_4, 0, false, "LeftBracket" }, + { "BACKSLASH", VK_OEM_5, 0, false, "Backslash" }, + { "RIGHTBRACKET", VK_OEM_6, 0, false, "RightBracket" }, + { "QUOTE", VK_OEM_7, 0, false, "Quote" }, + { "MINUS", VK_OEM_MINUS, 0, false, "Minus" }, + { "EQUAL", VK_OEM_PLUS, 0, false, "Equal" } + }; + + for (const auto& Spec : kSpecs) + { + if (UpperToken == QLatin1String(Spec.Token)) + { + p_KeyItem->Key = { Spec.VirtualKey, Spec.ExtraFlags, Spec.IsModifier }; + p_KeyItem->Text = QString::fromLatin1(Spec.Text); + return true; + } + } + + return false; +} + +} // namespace + +bool Lgc_FunctionButton_TryParseSequenceToken( + const QString& Token, + quint16 SourceUsage, + Lgc_FunctionButton_Struct_SequenceKey* p_KeyItem) +{ + const QString TrimmedToken = Token.trimmed(); + const QString UpperToken = TrimmedToken.toUpper(); + if (UpperToken.isEmpty()) + { + return false; + } + + if (Lgc_FunctionButton_IsSourceToken(TrimmedToken, UpperToken)) + { + const Lgc_FunctionButton_Struct_WindowsKey SourceKey = + Lgc_FunctionButton_GetWindowsKey(SourceUsage); + if (SourceKey.VirtualKey == 0) + { + return false; + } + + p_KeyItem->Key = SourceKey; + p_KeyItem->Text = Lgc_FunctionButton_GetUsageShortText(SourceUsage); + return true; + } + + if ((UpperToken.size() == 1) && UpperToken.at(0).isLetterOrNumber()) + { + p_KeyItem->Key = { static_cast(UpperToken.at(0).unicode()), 0, false }; + p_KeyItem->Text = UpperToken; + return true; + } + + if (Lgc_FunctionButton_TryLookupSequenceKeySpec(UpperToken, p_KeyItem)) + { + return true; + } + + static const QRegularExpression kFunctionKeyPattern(QStringLiteral("^F(\\d{1,2})$")); + const QRegularExpressionMatch Match = kFunctionKeyPattern.match(UpperToken); + if (!Match.hasMatch()) + { + return false; + } + + const int FunctionIndex = Match.captured(1).toInt(); + if ((FunctionIndex < 1) || (FunctionIndex > 24)) + { + return false; + } + + p_KeyItem->Key = { static_cast(VK_F1 + FunctionIndex - 1), 0, false }; + p_KeyItem->Text = QStringLiteral("F%1").arg(FunctionIndex); + return true; +} + +bool Lgc_FunctionButton_IsSameWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Left, + const Lgc_FunctionButton_Struct_WindowsKey& Right) +{ + return (Left.VirtualKey == Right.VirtualKey) && + (Left.ExtraFlags == Right.ExtraFlags); +} + +bool Lgc_FunctionButton_ParseKeyCombinationText( + const QString& Text, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText) +{ + return Lgc_FunctionButton_ParseTokenList( + Lgc_FunctionButton_SplitCombinationTokens(Text), + SourceUsage, + p_KeyList, + p_ErrorText, + QStringLiteral("Unknown key token: %1")); +} + +QString Lgc_FunctionButton_FormatKeyCombination( + const QVector& KeyList) +{ + QStringList TextList; + for (const auto& KeyItem : KeyList) + { + TextList.append(KeyItem.Text); + } + return TextList.join(QStringLiteral("+")); +} + +bool Lgc_FunctionButton_ParseShortcutSequenceText( + const QString& Text, + quint16 SourceUsage, + QVector>* p_CombinationList, + QString* p_ErrorText) +{ + p_CombinationList->clear(); + if (p_ErrorText != nullptr) + { + p_ErrorText->clear(); + } + + for (const QString& SegmentText : Lgc_FunctionButton_SplitShortcutSegments(Text)) + { + QVector Combination; + if (!Lgc_FunctionButton_ParseKeyCombinationText( + SegmentText, + SourceUsage, + &Combination, + p_ErrorText)) + { + p_CombinationList->clear(); + return false; + } + p_CombinationList->append(Combination); + } + + return !p_CombinationList->isEmpty(); +} + +QString Lgc_FunctionButton_FormatShortcutSequence( + const QVector>& CombinationList) +{ + QStringList TextList; + for (const auto& Combination : CombinationList) + { + TextList.append(Lgc_FunctionButton_FormatKeyCombination(Combination)); + } + return TextList.join(QStringLiteral(" -> ")); +} diff --git a/LOGIC/Lgc_Func_Button_Private.h b/LOGIC/Lgc_Func_Button_Private.h new file mode 100644 index 0000000..f89e283 --- /dev/null +++ b/LOGIC/Lgc_Func_Button_Private.h @@ -0,0 +1,61 @@ +#pragma once + +#include "LOGIC/Lgc_Func_Button.h" +#include +#include + +struct Lgc_FunctionButton_Struct_WindowsKey +{ + WORD VirtualKey = 0; + DWORD ExtraFlags = 0; + bool IsModifier = false; +}; + +struct Lgc_FunctionButton_Struct_SequenceKey +{ + Lgc_FunctionButton_Struct_WindowsKey Key; + QString Text; +}; + +Lgc_FunctionButton_Struct_WindowsKey Lgc_FunctionButton_GetWindowsKey(quint16 Usage); +bool Lgc_FunctionButton_SendWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Key, + bool IsPressed); +bool Lgc_FunctionButton_IsSameWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Left, + const Lgc_FunctionButton_Struct_WindowsKey& Right); + +bool Lgc_FunctionButton_TryParseSequenceToken( + const QString& Token, + quint16 SourceUsage, + Lgc_FunctionButton_Struct_SequenceKey* p_KeyItem); +bool Lgc_FunctionButton_ParseKeyCombinationText( + const QString& Text, + quint16 SourceUsage, + QVector* p_KeyList, + QString* p_ErrorText); +QString Lgc_FunctionButton_FormatKeyCombination( + const QVector& KeyList); +bool Lgc_FunctionButton_ParseShortcutSequenceText( + const QString& Text, + quint16 SourceUsage, + QVector>* p_CombinationList, + QString* p_ErrorText); +QString Lgc_FunctionButton_FormatShortcutSequence( + const QVector>& CombinationList); + +bool Lgc_FunctionButton_SendKeyCombination( + const QVector& KeyList); +bool Lgc_FunctionButton_SendShortcutSequence( + const QVector>& CombinationList); +void Lgc_FunctionButton_RunKeyCombination( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString& TextStatus); +void Lgc_FunctionButton_RunKeySequence( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString& TextStatus); +void Lgc_FunctionButton_RunOpenWebsite( + const Lgc_FunctionFeature_Definition& Feature, + QString& TextStatus); diff --git a/LOGIC/Lgc_Func_Button_Run.cpp b/LOGIC/Lgc_Func_Button_Run.cpp new file mode 100644 index 0000000..2ee14f5 --- /dev/null +++ b/LOGIC/Lgc_Func_Button_Run.cpp @@ -0,0 +1,258 @@ +#include "LOGIC/Lgc_Func_Button_Private.h" + +#include +#include +#include + +namespace +{ + +constexpr DWORD LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS = 12; +constexpr DWORD LGC_FUNCTIONBUTTON_SEQUENCE_STEP_DELAY_MS = 60; + +void Lgc_FunctionButton_ReleaseKeys( + const QVector& KeyList, + int LastIndex) +{ + for (int Index = LastIndex; Index >= 0; --Index) + { + Lgc_FunctionButton_SendWindowsKey(KeyList.at(Index), false); + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } +} + +} // namespace + +Lgc_FunctionButton_Struct_WindowsKey Lgc_FunctionButton_GetWindowsKey(quint16 Usage) +{ + switch (Usage) + { + case 0x0053: return { VK_NUMLOCK, 0 }; + case 0x0054: return { VK_DIVIDE, KEYEVENTF_EXTENDEDKEY }; + case 0x0055: return { VK_MULTIPLY, 0 }; + case 0x0056: return { VK_SUBTRACT, 0 }; + case 0x0057: return { VK_ADD, 0 }; + case 0x0058: return { VK_RETURN, KEYEVENTF_EXTENDEDKEY }; + case 0x0059: return { VK_NUMPAD1, 0 }; + case 0x005A: return { VK_NUMPAD2, 0 }; + case 0x005B: return { VK_NUMPAD3, 0 }; + case 0x005C: return { VK_NUMPAD4, 0 }; + case 0x005D: return { VK_NUMPAD5, 0 }; + case 0x005E: return { VK_NUMPAD6, 0 }; + case 0x005F: return { VK_NUMPAD7, 0 }; + case 0x0060: return { VK_NUMPAD8, 0 }; + case 0x0061: return { VK_NUMPAD9, 0 }; + case 0x0062: return { VK_NUMPAD0, 0 }; + case 0x0063: return { VK_DECIMAL, 0 }; + case 0x00E0: return { VK_CONTROL, 0, true }; + case 0x00E1: return { VK_SHIFT, 0, true }; + case 0x00E2: return { VK_MENU, 0, true }; + case 0x00E3: return { VK_LWIN, 0, true }; + case 0x00E4: return { VK_CONTROL, KEYEVENTF_EXTENDEDKEY, true }; + case 0x00E5: return { VK_SHIFT, 0, true }; + case 0x00E6: return { VK_MENU, KEYEVENTF_EXTENDEDKEY, true }; + case 0x00E7: return { VK_RWIN, 0, true }; + default: + return {}; + } +} + +bool Lgc_FunctionButton_SendWindowsKey( + const Lgc_FunctionButton_Struct_WindowsKey& Key, + bool IsPressed) +{ + if (Key.VirtualKey == 0) + { + return false; + } + + INPUT InputData = {}; + InputData.type = INPUT_KEYBOARD; + InputData.ki.wVk = Key.VirtualKey; + InputData.ki.dwFlags = Key.ExtraFlags; + if (!IsPressed) + { + InputData.ki.dwFlags |= KEYEVENTF_KEYUP; + } + + return SendInput(1, &InputData, sizeof(INPUT)) == 1; +} + +bool Lgc_FunctionButton_SendKeyCombination( + const QVector& KeyList) +{ + QVector ModifierList; + QVector NormalKeyList; + + for (const auto& KeyItem : KeyList) + { + if (KeyItem.Key.VirtualKey == 0) + { + return false; + } + + if (KeyItem.Key.IsModifier) + { + bool IsDuplicate = false; + for (const auto& OldModifier : ModifierList) + { + if (Lgc_FunctionButton_IsSameWindowsKey(OldModifier, KeyItem.Key)) + { + IsDuplicate = true; + break; + } + } + + if (!IsDuplicate) + { + ModifierList.append(KeyItem.Key); + } + continue; + } + + NormalKeyList.append(KeyItem.Key); + } + + for (int ModifierIndex = 0; ModifierIndex < ModifierList.size(); ++ModifierIndex) + { + if (!Lgc_FunctionButton_SendWindowsKey(ModifierList.at(ModifierIndex), true)) + { + Lgc_FunctionButton_ReleaseKeys(ModifierList, ModifierIndex - 1); + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } + + if (NormalKeyList.isEmpty()) + { + Lgc_FunctionButton_ReleaseKeys(ModifierList, ModifierList.size() - 1); + return !ModifierList.isEmpty(); + } + + for (const auto& NormalKey : NormalKeyList) + { + if (!Lgc_FunctionButton_SendWindowsKey(NormalKey, true)) + { + Lgc_FunctionButton_ReleaseKeys(ModifierList, ModifierList.size() - 1); + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + + if (!Lgc_FunctionButton_SendWindowsKey(NormalKey, false)) + { + Lgc_FunctionButton_ReleaseKeys(ModifierList, ModifierList.size() - 1); + return false; + } + Sleep(LGC_FUNCTIONBUTTON_KEY_HOLD_DELAY_MS); + } + + Lgc_FunctionButton_ReleaseKeys(ModifierList, ModifierList.size() - 1); + return true; +} + +bool Lgc_FunctionButton_SendShortcutSequence( + const QVector>& CombinationList) +{ + for (int Index = 0; Index < CombinationList.size(); ++Index) + { + if (!Lgc_FunctionButton_SendKeyCombination(CombinationList.at(Index))) + { + return false; + } + if (Index + 1 < CombinationList.size()) + { + Sleep(LGC_FUNCTIONBUTTON_SEQUENCE_STEP_DELAY_MS); + } + } + return true; +} + +void Lgc_FunctionButton_RunKeyCombination( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString& TextStatus) +{ + const QString CombinationText = Feature.SequenceText.trimmed(); + if (CombinationText.isEmpty()) + { + TextStatus = QStringLiteral("%1 has no shortcut configured.") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + + QVector KeyList; + QString ErrorText; + if (!Lgc_FunctionButton_ParseKeyCombinationText( + CombinationText, + SourceUsage, + &KeyList, + &ErrorText)) + { + TextStatus = QStringLiteral("%1 has an invalid shortcut: %2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), ErrorText); + return; + } + + TextStatus = Lgc_FunctionButton_SendKeyCombination(KeyList) + ? QStringLiteral("%1 sent shortcut: %2") + .arg( + Lgc_FunctionButton_GetFeatureName(Feature), + Lgc_FunctionButton_FormatKeyCombination(KeyList)) + : QStringLiteral("%1 failed to send shortcut.") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); +} + +void Lgc_FunctionButton_RunKeySequence( + const Lgc_FunctionFeature_Definition& Feature, + quint16 SourceUsage, + QString& TextStatus) +{ + const QString SequenceText = Feature.SequenceText.trimmed(); + if (SequenceText.isEmpty()) + { + TextStatus = QStringLiteral("%1 has no shortcut sequence configured.") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + + QVector> CombinationList; + QString ErrorText; + if (!Lgc_FunctionButton_ParseShortcutSequenceText( + SequenceText, + SourceUsage, + &CombinationList, + &ErrorText)) + { + TextStatus = QStringLiteral("%1 has an invalid shortcut sequence: %2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), ErrorText); + return; + } + + TextStatus = Lgc_FunctionButton_SendShortcutSequence(CombinationList) + ? QStringLiteral("%1 sent shortcut sequence: %2") + .arg( + Lgc_FunctionButton_GetFeatureName(Feature), + Lgc_FunctionButton_FormatShortcutSequence(CombinationList)) + : QStringLiteral("%1 failed to send shortcut sequence.") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); +} + +void Lgc_FunctionButton_RunOpenWebsite( + const Lgc_FunctionFeature_Definition& Feature, + QString& TextStatus) +{ + const QString UrlText = Feature.WebsiteUrl.trimmed(); + const QUrl Url = QUrl::fromUserInput(UrlText); + if (UrlText.isEmpty() || !Url.isValid() || Url.isEmpty()) + { + TextStatus = QStringLiteral("%1 has an invalid URL.") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); + return; + } + + TextStatus = QDesktopServices::openUrl(Url) + ? QStringLiteral("%1 opened: %2") + .arg(Lgc_FunctionButton_GetFeatureName(Feature), Url.toString()) + : QStringLiteral("%1 failed to open URL.") + .arg(Lgc_FunctionButton_GetFeatureName(Feature)); +} diff --git a/LOGIC/Lgc_Nkro.cpp b/LOGIC/Lgc_Nkro.cpp deleted file mode 100644 index 0c128db..0000000 --- a/LOGIC/Lgc_Nkro.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "LOGIC/Lgc_Nkro.h" - -void Lgc_Nkro_Func_Parse(const QByteArray& ByteArray, Lgc_Nkro_Struct_Result* p_Result) -{ - // 当前调用链内部固定传有效结果对象,这里直接回到默认状态。 - *p_Result = Lgc_Nkro_Struct_Result(); - - // 没有新包时直接返回提示。 - if (ByteArray.isEmpty()) - { - p_Result->TextExplain = QStringLiteral("NKRO 端口没有收到数据。"); - return; - } - - // 先校验 report id。 - if (static_cast(ByteArray.at(0)) != Mid_Enum_ReportId_Nkro) - { - p_Result->TextExplain = QStringLiteral("这不是 report id 0x01。"); - return; - } - - p_Result->IsMatch = true; - - // 当前固件里的 0x01 包固定长度是 31 字节。 - if (ByteArray.size() != MID_CONST_PACKET_SIZE_NKRO) - { - p_Result->TextExplain = QStringLiteral("0x01 包长度不对。"); - return; - } - - p_Result->IsLengthOk = true; - - // 第 1 字节是 modifier。 - p_Result->Modifier = static_cast(ByteArray.at(1)); - - // 后面 29 字节是 usage 位图。 - p_Result->UsageBitmap = ByteArray.mid(2, MID_CONST_USAGE_BITMAP_SIZE); - - // 逐 bit 扫描位图,得到所有按下的 HID usage。 - for (quint16 Usage = 0; Usage <= MID_CONST_KEYBOARD_USAGE_MAX; ++Usage) - { - const int ByteIndex = Usage / 8; - const int BitIndex = Usage % 8; - const quint8 Value = static_cast(p_Result->UsageBitmap.at(ByteIndex)); - - if ((Value & static_cast(1U << BitIndex)) == 0) - { - continue; - } - - p_Result->UsageList.append(Usage); - p_Result->UsageTextList.append(Mid_Func_GetKeyboardUsageText(Usage)); - } - - // 这里整理的是适合调试窗口直接阅读的教学化文本。 - QStringList ExplainList; - ExplainList.append(QStringLiteral("0x01 NKRO 可见键盘态")); - ExplainList.append(QStringLiteral("修饰键:%1").arg(Mid_Func_GetModifierText(p_Result->Modifier))); - - if (p_Result->UsageTextList.isEmpty()) - { - ExplainList.append(QStringLiteral("按下:无")); - } - else - { - ExplainList.append(QStringLiteral("按下:%1").arg(p_Result->UsageTextList.join(QStringLiteral(", ")))); - } - - p_Result->TextExplain = ExplainList.join(QLatin1Char('\n')); -} diff --git a/LOGIC/Lgc_Nkro.h b/LOGIC/Lgc_Nkro.h deleted file mode 100644 index 383ea56..0000000 --- a/LOGIC/Lgc_Nkro.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include "MID/Mid_Def.h" - -/* - * 这份文件负责解析 report id 0x01 的 NKRO 包。 - * - * 这里解析的是“Windows 最终可见的键盘态”: - * - 会受下位机遮罩影响 - * - 不是 0x04 那种物理镜像流 - * - * 当前下位机结构固定为: - * [report_id(1) | modifier(1) | usage_bitmap(29)] - * 总长度 31 字节 - */ -struct Lgc_Nkro_Struct_Result -{ - // 首字节是否匹配 0x01。 - bool IsMatch = false; - - // 长度是否符合 31 字节。 - bool IsLengthOk = false; - - // 第 1 字节修饰键位图。 - quint8 Modifier = 0; - - // 第 2~30 字节 usage 位图原文。 - QByteArray UsageBitmap; - - // 展开后的 HID usage 列表。 - QVector UsageList; - - // 展开后的按键名称列表。 - QStringList UsageTextList; - - // 给调试窗口用的简短中文说明。 - QString TextExplain; -}; - -void Lgc_Nkro_Func_Parse(const QByteArray& ByteArray, Lgc_Nkro_Struct_Result* p_Result); diff --git a/LOGIC/Lgc_Vendor.cpp b/LOGIC/Lgc_Vendor.cpp deleted file mode 100644 index ae47cf9..0000000 --- a/LOGIC/Lgc_Vendor.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "LOGIC/Lgc_Vendor.h" - -void Lgc_Vendor_Func_Parse(const QByteArray& ByteArray, Lgc_Vendor_Struct_Result* p_Result) -{ - // 当前调用链内部固定传有效结果对象,这里直接回到默认状态。 - *p_Result = Lgc_Vendor_Struct_Result(); - - // 没有包时直接返回提示。 - if (ByteArray.isEmpty()) - { - p_Result->TextExplain = QStringLiteral("Vendor 端口没有收到数据。"); - return; - } - - // 第 0 字节不是 0x04,就不是当前要解析的 Vendor 镜像包。 - if (static_cast(ByteArray.at(0)) != Mid_Enum_ReportId_Vendor) - { - p_Result->TextExplain = QStringLiteral("这不是 report id 0x04。"); - return; - } - - p_Result->IsMatch = true; - - // 当前固件里的 0x04 固定长度也是 31 字节。 - if (ByteArray.size() != MID_CONST_PACKET_SIZE_VENDOR) - { - p_Result->TextExplain = QStringLiteral("0x04 包长度不对。"); - return; - } - - p_Result->IsLengthOk = true; - p_Result->VendorState.IsValid = true; - - // 第 1 字节是物理 modifier。 - p_Result->VendorState.Modifier = static_cast(ByteArray.at(1)); - - // 第 2~30 字节是物理 usage 位图。 - p_Result->VendorState.UsageBitmap = ByteArray.mid(2, MID_CONST_USAGE_BITMAP_SIZE); - - // 展开位图,得到当前物理按下的 usage 列表。 - for (quint16 Usage = 0; Usage <= MID_CONST_KEYBOARD_USAGE_MAX; ++Usage) - { - const int ByteIndex = Usage / 8; - const int BitIndex = Usage % 8; - const quint8 Value = static_cast(p_Result->VendorState.UsageBitmap.at(ByteIndex)); - - if ((Value & static_cast(1U << BitIndex)) == 0) - { - continue; - } - - p_Result->VendorState.UsageList.append(Usage); - p_Result->VendorState.UsageTextList.append(Mid_Func_GetKeyboardUsageText(Usage)); - } - - // 这里整理的是面向教学和调试窗口的中文解释。 - QStringList ExplainList; - ExplainList.append(QStringLiteral("0x04 Vendor 物理镜像态")); - ExplainList.append(QStringLiteral("物理修饰键:%1").arg(Mid_Func_GetModifierText(p_Result->VendorState.Modifier))); - - if (p_Result->VendorState.UsageTextList.isEmpty()) - { - ExplainList.append(QStringLiteral("物理按下:无")); - } - else - { - ExplainList.append(QStringLiteral("物理按下:%1").arg(p_Result->VendorState.UsageTextList.join(QStringLiteral(", ")))); - } - - p_Result->TextExplain = ExplainList.join(QLatin1Char('\n')); -} diff --git a/LOGIC/Lgc_Vendor.h b/LOGIC/Lgc_Vendor.h deleted file mode 100644 index 30914ff..0000000 --- a/LOGIC/Lgc_Vendor.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "MID/Mid_Def.h" - -/* - * 这份文件负责解析 report id 0x04 的 Vendor 包。 - * - * 当前 0x04 不是旧 VIA 命令协议,而是“键盘物理状态镜像流”。 - * 它的结构固定为: - * [report_id(1) | modifier(1) | usage_bitmap(29)] - * - * 也就是说: - * - 0x01 代表主机最终可见键盘态 - * - 0x04 代表下位机物理键盘态 - */ -struct Lgc_Vendor_Struct_Result -{ - // 拆出来的 Vendor 状态。 - Mid_Struct_VendorState VendorState; - - // 是否匹配到 report id 0x04。 - bool IsMatch = false; - - // 长度是否符合 31 字节。 - bool IsLengthOk = false; - - // 给调试窗口看的中文解释。 - QString TextExplain; -}; - -void Lgc_Vendor_Func_Parse(const QByteArray& ByteArray, Lgc_Vendor_Struct_Result* p_Result); diff --git a/MID/Mid_Ble.cpp b/MID/Mid_Ble.cpp new file mode 100644 index 0000000..8ff1909 --- /dev/null +++ b/MID/Mid_Ble.cpp @@ -0,0 +1,49 @@ +#include "MID/Mid_Ble.h" + +#include + +QString Mid_GetBleOpcodeText(quint8 Opcode) +{ + return QStringLiteral("Opcode 0x%1") + .arg(Opcode, 2, 16, QLatin1Char('0')) + .toUpper(); +} + +QString Mid_NormalizeBleAddress(const QString& Text) +{ + QString HexText; + HexText.reserve(Text.size()); + + for (const QChar Character : Text.trimmed()) + { + if (Character.isDigit() || + ((Character >= QLatin1Char('a')) && (Character <= QLatin1Char('f'))) || + ((Character >= QLatin1Char('A')) && (Character <= QLatin1Char('F')))) + { + HexText.append(Character.toUpper()); + } + } + + return HexText.size() == 12 ? HexText : QString(); +} + +QString Mid_FormatBleAddress(const QString& Address) +{ + const QString Normalized = Mid_NormalizeBleAddress(Address); + if (Normalized.isEmpty()) + { + return QString(); + } + + QStringList PartList; + for (int Index = 0; Index < Normalized.size(); Index += 2) + { + PartList.append(Normalized.mid(Index, 2)); + } + return PartList.join(QLatin1Char(':')); +} + +bool Mid_IsBleAddressValid(const QString& Address) +{ + return !Mid_NormalizeBleAddress(Address).isEmpty(); +} diff --git a/MID/Mid_Ble.h b/MID/Mid_Ble.h new file mode 100644 index 0000000..82f2b28 --- /dev/null +++ b/MID/Mid_Ble.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +// BLE raw packet opcode text helpers. +QString Mid_GetBleOpcodeText(quint8 Opcode); +QString Mid_NormalizeBleAddress(const QString& Text); +QString Mid_FormatBleAddress(const QString& Address); +bool Mid_IsBleAddressValid(const QString& Address); diff --git a/MID/Mid_Def.cpp b/MID/Mid_Def.cpp index 309f36a..80bd878 100644 --- a/MID/Mid_Def.cpp +++ b/MID/Mid_Def.cpp @@ -1,5 +1,7 @@ -#include "MID/Mid_Def.h" +#include "MID/Mid_Def.h" +#include +#include #include #include #include @@ -8,62 +10,47 @@ #pragma comment(lib, "hid.lib") #pragma comment(lib, "setupapi.lib") -/* - * MID 层:介于逻辑与 DRI 之间,负责描述 HID 设备匹配与数据格式。 - * 高密注释用于教学,逐条解释如何把固件接口映射为 Win32 API 调用。 - */ - -/* 构造 NKRO 接口的匹配条件(Usage Page / Usage 固定) */ -Mid_Struct_DeviceMatch Mid_Func_GetNkroMatch(const Mid_Struct_DeviceConfig& DeviceConfig) +namespace { - Mid_Struct_DeviceMatch Match; - Match.VendorId = DeviceConfig.VendorId; - Match.ProductId = DeviceConfig.ProductId; - Match.UsagePage = MID_CONST_USAGE_PAGE_NKRO; - Match.Usage = MID_CONST_USAGE_NKRO; - Match.Name = QStringLiteral("NKRO Keyboard Interface"); - return Match; + +QString Mid_GetDeviceInstanceId(HDEVINFO DeviceInfoSet, SP_DEVINFO_DATA* p_DeviceInfoData) +{ + DWORD NeedLength = 0; + SetupDiGetDeviceInstanceIdW( + DeviceInfoSet, + p_DeviceInfoData, + nullptr, + 0, + &NeedLength); + if (NeedLength == 0) + { + return QString(); + } + + QVector Buffer(static_cast(NeedLength) + 1, 0); + return SetupDiGetDeviceInstanceIdW( + DeviceInfoSet, + p_DeviceInfoData, + Buffer.data(), + static_cast(Buffer.size()), + nullptr) + ? QString::fromWCharArray(Buffer.constData()).trimmed() + : QString(); } -/* 构造 Consumer 接口的匹配条件 */ -Mid_Struct_DeviceMatch Mid_Func_GetConsumerMatch(const Mid_Struct_DeviceConfig& DeviceConfig) -{ - Mid_Struct_DeviceMatch Match; - Match.VendorId = DeviceConfig.VendorId; - Match.ProductId = DeviceConfig.ProductId; - Match.UsagePage = MID_CONST_USAGE_PAGE_CONSUMER; - Match.Usage = MID_CONST_USAGE_CONSUMER; - Match.Name = QStringLiteral("Consumer Interface"); - return Match; -} +} // namespace -/* 构造 Vendor(状态镜像)接口的匹配条件 */ -Mid_Struct_DeviceMatch Mid_Func_GetVendorMatch(const Mid_Struct_DeviceConfig& DeviceConfig) -{ - Mid_Struct_DeviceMatch Match; - Match.VendorId = DeviceConfig.VendorId; - Match.ProductId = DeviceConfig.ProductId; - Match.UsagePage = MID_CONST_USAGE_PAGE_VENDOR; - Match.Usage = MID_CONST_USAGE_VENDOR; - Match.Name = QStringLiteral("Vendor State Mirror Interface"); - return Match; -} - -/* - * Mid_Func_FindHidInterface:按照匹配条件扫描系统中的 HID 接口。 - * 步骤:1) 获取 HID GUID 2) SetupAPI 列举接口 3) CreateFile 查询属性 4) 命中后返回路径和报文长度。 - */ -bool Mid_Func_FindHidInterface( +// Find the HID interface that matches VID/PID plus usage page/usage. +bool Mid_FindHidInterface( const Mid_Struct_DeviceMatch& Match, QString* p_DevicePath, quint16* p_InputLength, - quint16* p_OutputLength) + quint16* p_OutputLength, + QString* p_DeviceInstanceId) { - /* Win32 提供的 HID GUID:列举所有 HID 接口都靠它 */ GUID HidGuid; HidD_GetHidGuid(&HidGuid); - /* 构建“当前存在 + 暴露接口”的设备集合 */ HDEVINFO DeviceInfoSet = SetupDiGetClassDevsW(&HidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (DeviceInfoSet == INVALID_HANDLE_VALUE) { @@ -78,25 +65,36 @@ bool Mid_Func_FindHidInterface( SetupDiEnumDeviceInterfaces(DeviceInfoSet, nullptr, &HidGuid, Index, &InterfaceData); ++Index) { - /* Query 设备路径前先得到所需缓冲区长度 */ DWORD NeedLength = 0; - SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, &InterfaceData, nullptr, 0, &NeedLength, nullptr); + SP_DEVINFO_DATA DeviceInfoData = {}; + DeviceInfoData.cbSize = sizeof(DeviceInfoData); + SetupDiGetDeviceInterfaceDetailW( + DeviceInfoSet, + &InterfaceData, + nullptr, + 0, + &NeedLength, + &DeviceInfoData); if (NeedLength == 0) { continue; } - /* 申请一段缓冲区存储 SP_DEVICE_INTERFACE_DETAIL_DATA_W */ QByteArray Buffer(static_cast(NeedLength), 0); auto* p_Detail = reinterpret_cast(Buffer.data()); p_Detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); - if (!SetupDiGetDeviceInterfaceDetailW(DeviceInfoSet, &InterfaceData, p_Detail, NeedLength, nullptr, nullptr)) + if (!SetupDiGetDeviceInterfaceDetailW( + DeviceInfoSet, + &InterfaceData, + p_Detail, + NeedLength, + nullptr, + &DeviceInfoData)) { continue; } - /* 只需 0 访问权限即可读取属性 */ HANDLE HandleQuery = CreateFileW( p_Detail->DevicePath, 0, @@ -114,15 +112,14 @@ bool Mid_Func_FindHidInterface( Attributes.Size = sizeof(Attributes); PHIDP_PREPARSED_DATA p_Preparsed = nullptr; HIDP_CAPS Caps = {}; - /* 通过 Attributes + Caps 对比 VID / PID / Usage Page / Usage */ - const bool IsMatch = + const bool IsExactMatch = HidD_GetAttributes(HandleQuery, &Attributes) && HidD_GetPreparsedData(HandleQuery, &p_Preparsed) && (HidP_GetCaps(p_Preparsed, &Caps) == HIDP_STATUS_SUCCESS) && - (Attributes.VendorID == Match.VendorId) && - (Attributes.ProductID == Match.ProductId) && (Caps.UsagePage == Match.UsagePage) && - (Caps.Usage == Match.Usage); + (Caps.Usage == Match.Usage) && + (Attributes.VendorID == Match.VendorId) && + (Attributes.ProductID == Match.ProductId); if (p_Preparsed != nullptr) { @@ -130,36 +127,43 @@ bool Mid_Func_FindHidInterface( } CloseHandle(HandleQuery); - if (!IsMatch) + if (IsExactMatch) { - continue; + if (p_DevicePath != nullptr) + { + *p_DevicePath = QString::fromWCharArray(p_Detail->DevicePath); + } + if (p_InputLength != nullptr) + { + *p_InputLength = Caps.InputReportByteLength; + } + if (p_OutputLength != nullptr) + { + *p_OutputLength = Caps.OutputReportByteLength; + } + if (p_DeviceInstanceId != nullptr) + { + *p_DeviceInstanceId = Mid_GetDeviceInstanceId(DeviceInfoSet, &DeviceInfoData); + } + + IsFound = true; + break; } - /* 命中:写回设备路径及报文长度 */ - if (p_DevicePath != nullptr) - { - *p_DevicePath = QString::fromWCharArray(p_Detail->DevicePath); - } - if (p_InputLength != nullptr) - { - *p_InputLength = Caps.InputReportByteLength; - } - if (p_OutputLength != nullptr) - { - *p_OutputLength = Caps.OutputReportByteLength; - } - - IsFound = true; - break; } - /* 枚举结束释放句柄 */ SetupDiDestroyDeviceInfoList(DeviceInfoSet); return IsFound; } -/* 调试输出:把字节数组格式化成“AA BB CC” */ -QString Mid_Func_GetHexText(const QByteArray& ByteArray) +bool Mid_IsBluetoothHidInstanceId(const QString& DeviceInstanceId) +{ + const QString UpperId = DeviceInstanceId.trimmed().toUpper(); + return UpperId.contains(QStringLiteral("BTHLEDEVICE")) || + UpperId.contains(QStringLiteral("{00001812-0000-1000-8000-00805F9B34FB}")); +} + +QString Mid_GetHexText(const QByteArray& ByteArray) { QStringList TextList; for (int Index = 0; Index < ByteArray.size(); ++Index) @@ -171,8 +175,7 @@ QString Mid_Func_GetHexText(const QByteArray& ByteArray) return TextList.join(QLatin1Char(' ')); } -/* 把 Modifier 位图翻译成人类可读的组合 */ -QString Mid_Func_GetModifierText(quint8 Modifier) +QString Mid_GetModifierText(quint8 Modifier) { QStringList TextList; if (Modifier == 0) @@ -191,8 +194,7 @@ QString Mid_Func_GetModifierText(quint8 Modifier) return TextList.join(QStringLiteral(", ")); } -/* 键盘 Usage -> 中文/英文标签,方便 UI 展示 */ -QString Mid_Func_GetKeyboardUsageText(quint16 Usage) +QString Mid_GetKeyboardUsageText(quint16 Usage) { switch (Usage) { @@ -226,8 +228,7 @@ QString Mid_Func_GetKeyboardUsageText(quint16 Usage) } } -/* Consumer Usage -> UI 标签 */ -QString Mid_Func_GetConsumerUsageText(quint16 Usage) +QString Mid_GetConsumerUsageText(quint16 Usage) { switch (Usage) { @@ -239,3 +240,4 @@ QString Mid_Func_GetConsumerUsageText(quint16 Usage) return QStringLiteral("未知 Consumer Usage"); } } + diff --git a/MID/Mid_Def.h b/MID/Mid_Def.h index d8ac1c4..0b0f6ba 100644 --- a/MID/Mid_Def.h +++ b/MID/Mid_Def.h @@ -1,86 +1,81 @@ -#pragma once +#pragma once #include #include -#include -#include -/* - * MID 层公共定义:设备配置、匹配条件、HID 报文常量等都集中于此。 - * 高密注释放在每个结构/函数附近,方便教学时直接引用。 - */ +// Shared protocol constants and raw packet definitions. -/* HID 报文 ID:用于区分 NKRO、Consumer、Vendor */ enum Mid_Enum_ReportId : quint8 { Mid_Enum_ReportId_None = 0x00, Mid_Enum_ReportId_Nkro = 0x01, Mid_Enum_ReportId_Consumer = 0x03, - Mid_Enum_ReportId_Vendor = 0x04 + Mid_Enum_ReportId_Vendor = 0x04, + Mid_Enum_ReportId_VendorCommand = 0x05 +}; + +enum Mid_Enum_RawPacketSource : quint8 +{ + Mid_Enum_RawPacketSource_None = 0, + Mid_Enum_RawPacketSource_UsbNkroRaw, + Mid_Enum_RawPacketSource_UsbConsumerHid, + Mid_Enum_RawPacketSource_UsbVendorHid, + Mid_Enum_RawPacketSource_BleGatt, + Mid_Enum_RawPacketSource_BleHidKeyboard, + Mid_Enum_RawPacketSource_BleHidConsumer, + Mid_Enum_RawPacketSource_BleHidVendor, + Mid_Enum_RawPacketSource_BleHidVendorCommand }; -/* 默认 VID/PID:教学用固件的 USB 身份 */ const quint16 MID_CONST_VENDOR_ID_DEFAULT = 0x1209; const quint16 MID_CONST_PRODUCT_ID_DEFAULT = 0x0001; -/* 上位机配置:允许 UI 快速切换 VID/PID */ struct Mid_Struct_DeviceConfig { quint16 VendorId = MID_CONST_VENDOR_ID_DEFAULT; quint16 ProductId = MID_CONST_PRODUCT_ID_DEFAULT; }; -/* 匹配条件:Win32 SetupAPI + HidP_Caps 的输入结构 */ struct Mid_Struct_DeviceMatch { quint16 VendorId = 0; quint16 ProductId = 0; quint16 UsagePage = 0; quint16 Usage = 0; - QString Name; }; -/* 统一的“原始包”封装:记录来源端口与字节内容 */ struct Mid_Struct_RawPacket { bool IsValid = false; + Mid_Enum_RawPacketSource Source = Mid_Enum_RawPacketSource_None; QByteArray ByteArray; QString PortName; }; -/* Vendor 状态:逻辑层解析后的结果(Modifier + Usage 列表) */ -struct Mid_Struct_VendorState -{ - bool IsValid = false; - quint8 Modifier = 0; - QByteArray UsageBitmap; - QVector UsageList; - QStringList UsageTextList; -}; - const quint16 MID_CONST_USAGE_PAGE_NKRO = 0x0001; const quint16 MID_CONST_USAGE_NKRO = 0x0006; const quint16 MID_CONST_USAGE_PAGE_CONSUMER = 0x000C; const quint16 MID_CONST_USAGE_CONSUMER = 0x0001; const quint16 MID_CONST_USAGE_PAGE_VENDOR = 0xFF00; const quint16 MID_CONST_USAGE_VENDOR = 0x0002; +const quint16 MID_CONST_USAGE_PAGE_VENDOR_COMMAND = 0xFF01; +const quint16 MID_CONST_USAGE_VENDOR_COMMAND = 0x0005; const int MID_CONST_KEYBOARD_USAGE_MAX = 0x00E7; const int MID_CONST_USAGE_BITMAP_SIZE = 29; const int MID_CONST_PACKET_SIZE_NKRO = 31; const int MID_CONST_PACKET_SIZE_VENDOR = 31; +const int MID_CONST_PACKET_SIZE_VENDOR_COMMAND = 10; +const int MID_CONST_PACKET_SIZE_VENDOR_COMMAND_DATA = 8; const int MID_CONST_PACKET_SIZE_CONSUMER = 3; - -/* 设备匹配函数:针对 NKRO / Consumer / Vendor */ -Mid_Struct_DeviceMatch Mid_Func_GetNkroMatch(const Mid_Struct_DeviceConfig& DeviceConfig); -Mid_Struct_DeviceMatch Mid_Func_GetConsumerMatch(const Mid_Struct_DeviceConfig& DeviceConfig); -Mid_Struct_DeviceMatch Mid_Func_GetVendorMatch(const Mid_Struct_DeviceConfig& DeviceConfig); -/* HID 接口枚举 & 辅助文本转换 */ -bool Mid_Func_FindHidInterface( +bool Mid_FindHidInterface( const Mid_Struct_DeviceMatch& Match, QString* p_DevicePath, quint16* p_InputLength, - quint16* p_OutputLength); -QString Mid_Func_GetHexText(const QByteArray& ByteArray); -QString Mid_Func_GetModifierText(quint8 Modifier); -QString Mid_Func_GetKeyboardUsageText(quint16 Usage); -QString Mid_Func_GetConsumerUsageText(quint16 Usage); + quint16* p_OutputLength, + QString* p_DeviceInstanceId = nullptr); +bool Mid_IsBluetoothHidInstanceId(const QString& DeviceInstanceId); +QString Mid_GetHexText(const QByteArray& ByteArray); +QString Mid_GetModifierText(quint8 Modifier); +QString Mid_GetKeyboardUsageText(quint16 Usage); +QString Mid_GetConsumerUsageText(quint16 Usage); + diff --git a/function_config.json b/function_config.json new file mode 100644 index 0000000..f1f3536 --- /dev/null +++ b/function_config.json @@ -0,0 +1,27 @@ +{ + "functions": [ + { + "description": "", + "id": 1, + "name": "Function 1", + "sequence_text": "", + "type": "website", + "usage_list": [ + 84, + 96 + ], + "website_url": "https://4399.com" + }, + { + "description": "", + "id": 2, + "name": "Function 2", + "sequence_text": "", + "type": "website", + "usage_list": [ + ], + "website_url": "https://4399.com" + } + ], + "version": 1 +} diff --git a/main.cpp b/main.cpp index 949a14b..2600a84 100644 --- a/main.cpp +++ b/main.cpp @@ -1,43 +1,15 @@ -#include "APP/APP_UIWindow.h" -#include "APP/APP_Theme.h" -#include #include -#include +#include "APP/APP_Theme.h" +#include "APP/APP_UIWindow.h" int main(int argc, char *argv[]) { - // 在创建 QApplication 之前开启高 DPI 缩放支持, - // 让界面在高分屏(如 125%、150%、200% 缩放)下显示更正常。 - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); + app.setPalette(APP::APP_Theme::App_Func_GetPalette()); + app.setFont(APP::APP_Theme::App_Func_GetBodyFont()); + app.setStyleSheet(APP::APP_Theme::App_Func_GetStyleSheet()); - // 在高 DPI 屏幕下使用更清晰的图片/图标资源, - // 避免图标被拉伸后发虚。 - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); - - // 创建 Qt 图形界面应用程序对象, - // argc 和 argv 用于接收命令行参数。 - QApplication app(argc, argv); - - // 统一使用 Fusion 风格,保证各标准控件外观更稳定。 - app.setStyle(QStyleFactory::create("Fusion")); - - // 统一套用暗色调色板,不需要写大段样式表字符串。 - app.setPalette(APP::APP_Theme::App_Func_GetPalette()); - - // 设置整个应用程序的默认字体, - // 所有控件如果没有单独指定字体,一般都会继承这里的设置。 - app.setFont(APP::APP_Theme::App_Func_GetBodyFont()); - - // 创建主窗口对象, - // 这里的 App_UIWindow 是你程序的主界面类。 - APP::App_UIWindow window; - - // 显示主窗口, - // 如果不调用 show(),窗口对象虽然创建了,但不会出现在屏幕上。 - window.show(); - - // 启动 Qt 事件循环, - // 程序会在这里持续处理鼠标、键盘、重绘、信号槽等事件, - // 直到窗口关闭后才会退出,并返回退出码。 - return app.exec(); + APP::App_UIWindow window; + window.show(); + return app.exec(); }