Push layered Qt host source files

This commit is contained in:
2026-04-17 16:25:19 +08:00
parent b576d3f19d
commit 89b23b2291
58 changed files with 10349 additions and 2461 deletions

View File

@@ -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

View File

@@ -1,5 +1,8 @@
<Solution>
<Configurations>
<BuildType Name="Debug" />
<BuildType Name="Re" />
<BuildType Name="Release" />
<Platform Name="x64" />
</Configurations>
<Project Path="20260320_new_keyboard.vcxproj" />

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="18.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
@@ -15,19 +15,21 @@
<Keyword>QtVS_v304</Keyword>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">10.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">10.0</WindowsTargetPlatformVersion>
<QtMsBuild Condition="'$(QtMsBuild)'=='' AND Exists('$(LocalAppData)\QtMsBuild\qt.targets')">$(LocalAppData)\QtMsBuild</QtMsBuild>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')">$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
<QtMsBuild Condition="'$(QtMsBuild)'=='' AND Exists('$(LocalAppData)\\QtMsBuild\\qt.targets')"
>$(LocalAppData)\QtMsBuild</QtMsBuild>
<QtMsBuild Condition="'$(QtMsBuild)'=='' OR !Exists('$(QtMsBuild)\qt.targets')"
>$(MSBuildProjectDirectory)\QtMsBuild</QtMsBuild>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<PlatformToolset>v145</PlatformToolset>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
@@ -36,18 +38,32 @@
<ImportGroup Condition="Exists('$(QtMsBuild)\qt_defaults.props')">
<Import Project="$(QtMsBuild)\qt_defaults.props" />
</ImportGroup>
<!-- 兼容 QTDIR/QT 指向 Qt 根目录或 bin 目录,并在本机已安装 Qt 但未注册到 Qt VS Tools 时提供兜底路径。 -->
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'" Label="QtSettings">
<QtInstall>D:\Qt\5.13.1\msvc2015_64</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtInstall Condition="'$(QTDIR)' != '' AND Exists('$(QTDIR)\\qmake.exe')">$([System.IO.Path]::GetDirectoryName('$(QTDIR)'))</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND '$(QTDIR)' != '' AND Exists('$(QTDIR)\\bin\\qmake.exe')">$(QTDIR)</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND '$(QT)' != '' AND Exists('$(QT)\\qmake.exe')">$([System.IO.Path]::GetDirectoryName('$(QT)'))</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND '$(QT)' != '' AND Exists('$(QT)\\bin\\qmake.exe')">$(QT)</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND Exists('D:\\App\\Qt\\5.13.1\\msvc2015_64\\bin\\qmake.exe')">D:\App\Qt\5.13.1\msvc2015_64</QtInstall>
<QtInstall Condition="'$(QtInstall)' == ''">5.13.1_msvc2015_64</QtInstall>
<QtModules>core;gui;widgets;serialport;bluetooth</QtModules>
<QtBuildConfig>debug</QtBuildConfig>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'" Label="QtSettings">
<QtInstall>D:\Qt\5.13.1\msvc2015_64</QtInstall>
<QtModules>core;gui;widgets</QtModules>
<QtInstall Condition="'$(QTDIR)' != '' AND Exists('$(QTDIR)\\qmake.exe')">$([System.IO.Path]::GetDirectoryName('$(QTDIR)'))</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND '$(QTDIR)' != '' AND Exists('$(QTDIR)\\bin\\qmake.exe')">$(QTDIR)</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND '$(QT)' != '' AND Exists('$(QT)\\qmake.exe')">$([System.IO.Path]::GetDirectoryName('$(QT)'))</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND '$(QT)' != '' AND Exists('$(QT)\\bin\\qmake.exe')">$(QT)</QtInstall>
<QtInstall Condition="'$(QtInstall)' == '' AND Exists('D:\\App\\Qt\\5.13.1\\msvc2015_64\\bin\\qmake.exe')">D:\App\Qt\5.13.1\msvc2015_64</QtInstall>
<QtInstall Condition="'$(QtInstall)' == ''">5.13.1_msvc2015_64</QtInstall>
<QtModules>core;gui;widgets;serialport;bluetooth</QtModules>
<QtBuildConfig>release</QtBuildConfig>
</PropertyGroup>
<Target Name="QtMsBuildNotFound" BeforeTargets="CustomBuild;ClCompile" Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High" Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
<Target Name="QtMsBuildNotFound"
BeforeTargets="CustomBuild;ClCompile"
Condition="!Exists('$(QtMsBuild)\qt.targets') or !Exists('$(QtMsBuild)\qt.props')">
<Message Importance="High"
Text="QtMsBuild: could not locate qt.targets, qt.props; project may not build correctly." />
</Target>
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
@@ -97,42 +113,38 @@
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="APP\APP_GlassCard.cpp" />
<ClCompile Include="APP\APP_KeyButton.cpp" />
<ClCompile Include="APP\APP_KeypadModel.cpp" />
<ClCompile Include="APP\APP_Theme.cpp" />
<ClCompile Include="APP\APP_UIWindow.cpp" />
<ClCompile Include="DRI\Dri_Consumer.cpp" />
<ClCompile Include="DRI\Dri_NkroRaw.cpp" />
<ClCompile Include="DRI\Dri_Vendor.cpp" />
<ClCompile Include="DEBUG\Debug_Panel.cpp" />
<ClCompile Include="LOGIC\Lgc_Consumer.cpp" />
<ClCompile Include="LOGIC\Lgc_Func_Button.cpp" />
<ClCompile Include="LOGIC\Lgc_Core.cpp" />
<ClCompile Include="LOGIC\Lgc_Nkro.cpp" />
<ClCompile Include="LOGIC\Lgc_Vendor.cpp" />
<ClCompile Include="MID\Mid_Def.cpp" />
<ClCompile Include="main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="APP\APP_GlassCard.h" />
<ClInclude Include="APP\APP_KeyButton.h" />
<ClInclude Include="APP\APP_KeypadModel.h" />
<ClInclude Include="APP\APP_Theme.h" />
<ClInclude Include="APP\APP_UIWindow.h" />
<ClInclude Include="DRI\Dri_Consumer.h" />
<ClInclude Include="DRI\Dri_NkroRaw.h" />
<ClInclude Include="DRI\Dri_Vendor.h" />
<ClInclude Include="DEBUG\Debug_Config.h" />
<ClInclude Include="DEBUG\Debug_Panel.h" />
<ClInclude Include="LOGIC\Lgc_Consumer.h" />
<ClInclude Include="LOGIC\Lgc_Func_Button.h" />
<ClInclude Include="LOGIC\Lgc_Core.h" />
<ClInclude Include="LOGIC\Lgc_Nkro.h" />
<ClInclude Include="LOGIC\Lgc_Vendor.h" />
<ClInclude Include="MID\Mid_Def.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="APP\APP_GlassCard.cpp"/>
<ClCompile Include="APP\APP_KeyButton.cpp"/>
<ClCompile Include="APP\APP_KeypadModel.cpp"/>
<ClCompile Include="APP\APP_Theme.cpp"/>
<ClCompile Include="APP\APP_UIWindow.cpp"/>
<ClCompile Include="APP\APP_UIWindow_Feature.cpp"/>
<ClCompile Include="APP\APP_UIWindow_Record.cpp"/>
<ClCompile Include="COM\Com_Protocol.cpp"/>
<ClCompile Include="DRI\Dri_Cdc.cpp"/>
<ClCompile Include="DRI\Dri_Nus.cpp"/>
<ClCompile Include="LOGIC\Lgc_Core.cpp"/>
<ClCompile Include="LOGIC\Lgc_Core_Command.cpp"/>
<ClCompile Include="LOGIC\Lgc_Core_Config.cpp"/>
<ClCompile Include="LOGIC\Lgc_Core_Input.cpp"/>
<ClCompile Include="LOGIC\Lgc_Func_Button.cpp"/>
<ClCompile Include="LOGIC\Lgc_Func_Button_Parse.cpp"/>
<ClCompile Include="LOGIC\Lgc_Func_Button_Run.cpp"/>
<ClCompile Include="main.cpp"/>
<ClInclude Include="APP\APP_GlassCard.h"/>
<ClInclude Include="APP\APP_KeyButton.h"/>
<ClInclude Include="APP\APP_KeypadModel.h"/>
<ClInclude Include="APP\APP_Theme.h"/>
<ClInclude Include="APP\APP_UIWindow.h"/>
<ClInclude Include="APP\APP_UIWindow_Private.h"/>
<ClInclude Include="COM\Com_Def.h"/>
<ClInclude Include="COM\Com_Protocol.h"/>
<ClInclude Include="DRI\Dri_Cdc.h"/>
<ClInclude Include="DRI\Dri_Nus.h"/>
<ClInclude Include="LOGIC\Lgc_Core.h"/>
<ClInclude Include="LOGIC\Lgc_Core_Private.h"/>
<ClInclude Include="LOGIC\Lgc_Func_Button.h"/>
<ClInclude Include="LOGIC\Lgc_Func_Button_Private.h"/>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Condition="Exists('$(QtMsBuild)\qt.targets')">

View File

@@ -6,46 +6,32 @@
<Extensions>qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<UniqueIdentifier>{93995380-89BD-4B04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Form Files">
<UniqueIdentifier>{99349809-55BA-4b9d-BF79-8FDBB0286EB3}</UniqueIdentifier>
<Extensions>ui</Extensions>
</Filter>
<Filter Include="Source Files\APP">
<Filter Include="Source Files\\APP">
<UniqueIdentifier>{A64D7E2E-38B0-4FC7-A5E5-86BE065B5E14}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\LOGIC">
<UniqueIdentifier>{A8E15F2D-A7FA-4303-B845-0E6A75B5906B}</UniqueIdentifier>
<Filter Include="Source Files\\COM">
<UniqueIdentifier>{D7D98364-D54B-4AA0-8C30-2F059606DE39}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\DRI">
<Filter Include="Source Files\\DRI">
<UniqueIdentifier>{80F53A7E-0DA9-44B4-85ED-69D404BDE88A}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\MID">
<UniqueIdentifier>{0D0A047D-7113-47DE-9A05-D7BA3B12F9E2}</UniqueIdentifier>
<Filter Include="Source Files\\LOGIC">
<UniqueIdentifier>{A8E15F2D-A7FA-4303-B845-0E6A75B5906B}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\DEBUG">
<UniqueIdentifier>{3D7D0A5D-F0CB-49A6-A6D0-FA66F58B3111}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\APP">
<Filter Include="Header Files\\APP">
<UniqueIdentifier>{2B601A06-899A-4D88-932D-9F6ED625B1E8}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\LOGIC">
<UniqueIdentifier>{72C4499C-5806-4BBC-869D-0E4B33D867B6}</UniqueIdentifier>
<Filter Include="Header Files\\COM">
<UniqueIdentifier>{D5F327AA-D1B8-4154-BE13-FA8C94BE8425}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\DRI">
<Filter Include="Header Files\\DRI">
<UniqueIdentifier>{E72E5301-BE63-408E-BD35-F817FD4A0D0E}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\MID">
<UniqueIdentifier>{7CB18420-5F64-4E2A-81CA-6A5E2BA4CFC8}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files\DEBUG">
<UniqueIdentifier>{7814B0B6-EA6A-49D3-9B6B-BC144B1AF55F}</UniqueIdentifier>
<Filter Include="Header Files\\LOGIC">
<UniqueIdentifier>{72C4499C-5806-4BBC-869D-0E4B33D867B6}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
@@ -67,36 +53,42 @@
<ClCompile Include="APP\APP_UIWindow.cpp">
<Filter>Source Files\APP</Filter>
</ClCompile>
<ClCompile Include="DRI\Dri_Consumer.cpp">
<ClCompile Include="APP\APP_UIWindow_Feature.cpp">
<Filter>Source Files\APP</Filter>
</ClCompile>
<ClCompile Include="APP\APP_UIWindow_Record.cpp">
<Filter>Source Files\APP</Filter>
</ClCompile>
<ClCompile Include="COM\Com_Protocol.cpp">
<Filter>Source Files\COM</Filter>
</ClCompile>
<ClCompile Include="DRI\Dri_Cdc.cpp">
<Filter>Source Files\DRI</Filter>
</ClCompile>
<ClCompile Include="DRI\Dri_NkroRaw.cpp">
<ClCompile Include="DRI\Dri_Nus.cpp">
<Filter>Source Files\DRI</Filter>
</ClCompile>
<ClCompile Include="DRI\Dri_Vendor.cpp">
<Filter>Source Files\DRI</Filter>
<ClCompile Include="LOGIC\Lgc_Core.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="DEBUG\Debug_Panel.cpp">
<Filter>Source Files\DEBUG</Filter>
<ClCompile Include="LOGIC\Lgc_Core_Command.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="LOGIC\Lgc_Consumer.cpp">
<ClCompile Include="LOGIC\Lgc_Core_Config.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="LOGIC\Lgc_Core_Input.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="LOGIC\Lgc_Func_Button.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="LOGIC\Lgc_Core.cpp">
<ClCompile Include="LOGIC\Lgc_Func_Button_Parse.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="LOGIC\Lgc_Nkro.cpp">
<ClCompile Include="LOGIC\Lgc_Func_Button_Run.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="LOGIC\Lgc_Vendor.cpp">
<Filter>Source Files\LOGIC</Filter>
</ClCompile>
<ClCompile Include="MID\Mid_Def.cpp">
<Filter>Source Files\MID</Filter>
</ClCompile>
<ClInclude Include="APP\APP_GlassCard.h">
<Filter>Header Files\APP</Filter>
</ClInclude>
@@ -112,38 +104,32 @@
<ClInclude Include="APP\APP_UIWindow.h">
<Filter>Header Files\APP</Filter>
</ClInclude>
<ClInclude Include="DRI\Dri_Consumer.h">
<ClInclude Include="APP\APP_UIWindow_Private.h">
<Filter>Header Files\APP</Filter>
</ClInclude>
<ClInclude Include="COM\Com_Def.h">
<Filter>Header Files\COM</Filter>
</ClInclude>
<ClInclude Include="COM\Com_Protocol.h">
<Filter>Header Files\COM</Filter>
</ClInclude>
<ClInclude Include="DRI\Dri_Cdc.h">
<Filter>Header Files\DRI</Filter>
</ClInclude>
<ClInclude Include="DRI\Dri_NkroRaw.h">
<ClInclude Include="DRI\Dri_Nus.h">
<Filter>Header Files\DRI</Filter>
</ClInclude>
<ClInclude Include="DRI\Dri_Vendor.h">
<Filter>Header Files\DRI</Filter>
<ClInclude Include="LOGIC\Lgc_Core.h">
<Filter>Header Files\LOGIC</Filter>
</ClInclude>
<ClInclude Include="DEBUG\Debug_Config.h">
<Filter>Header Files\DEBUG</Filter>
</ClInclude>
<ClInclude Include="DEBUG\Debug_Panel.h">
<Filter>Header Files\DEBUG</Filter>
</ClInclude>
<ClInclude Include="LOGIC\Lgc_Consumer.h">
<ClInclude Include="LOGIC\Lgc_Core_Private.h">
<Filter>Header Files\LOGIC</Filter>
</ClInclude>
<ClInclude Include="LOGIC\Lgc_Func_Button.h">
<Filter>Header Files\LOGIC</Filter>
</ClInclude>
<ClInclude Include="LOGIC\Lgc_Core.h">
<ClInclude Include="LOGIC\Lgc_Func_Button_Private.h">
<Filter>Header Files\LOGIC</Filter>
</ClInclude>
<ClInclude Include="LOGIC\Lgc_Nkro.h">
<Filter>Header Files\LOGIC</Filter>
</ClInclude>
<ClInclude Include="LOGIC\Lgc_Vendor.h">
<Filter>Header Files\LOGIC</Filter>
</ClInclude>
<ClInclude Include="MID\Mid_Def.h">
<Filter>Header Files\MID</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@@ -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

View File

@@ -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;
};

View File

@@ -4,12 +4,9 @@
#include <QtCore/QtMath>
#include <QtGui/QPainter>
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);
}

View File

@@ -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;
};

View File

@@ -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_KeyInfo>& APP_KeypadModel::App_Func_GetKeyList() const
@@ -45,11 +30,6 @@ const QVector<APP_KeyInfo>& APP_KeypadModel::App_Func_GetKeyList() const
return appKeyList;
}
const QVector<APP_KeyInfo>& 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<quint16
for (quint16 Usage : UsageList)
{
const QString KeyId = App_Func_GetKeyIdFromUsage(App_Func_MapUsageForUi(Usage));
const QString KeyId = App_Func_GetKeyIdFromUsage(Usage);
if (!KeyId.isEmpty() && !appPressedKeyIdList.contains(KeyId))
{
appPressedKeyIdList.append(KeyId);
@@ -97,39 +103,4 @@ void APP_KeypadModel::App_Func_SetPressedKeysFromUsageList(const QVector<quint16
}
}
void APP_KeypadModel::App_Func_SetSwapUsagePair(quint16 UsageLeft, quint16 UsageRight, bool IsEnabled)
{
appIsSwapOn = IsEnabled;
appSwapUsageLeft = UsageLeft;
appSwapUsageRight = UsageRight;
}
quint16 APP_KeypadModel::App_Func_MapUsageForUi(quint16 Usage) const
{
if (!appIsSwapOn || (appSwapUsageLeft == 0) || (appSwapUsageRight == 0) || (appSwapUsageLeft == appSwapUsageRight))
{
return Usage;
}
if (Usage == appSwapUsageLeft)
{
return appSwapUsageRight;
}
return (Usage == appSwapUsageRight) ? appSwapUsageLeft : Usage;
}
QString APP_KeypadModel::App_Func_GetKeyIdFromUsage(quint16 Usage) const
{
for (const APP_KeyInfo& KeyInfo : appKeyList)
{
if (KeyInfo.usage == Usage)
{
return KeyInfo.id;
}
}
return QString();
}
} // namespace APP

View File

@@ -33,26 +33,19 @@ public:
APP_KeypadModel();
const QVector<APP_KeyInfo>& App_Func_GetKeyList() const;
const QVector<APP_KeyInfo>& 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<quint16>& 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<APP_KeyInfo> appKeyList;
QVector<APP_KeyInfo> appFunctionKeyList;
QStringList appPressedKeyIdList;
bool appNumLockOn = false;
bool appIsSwapOn = false;
quint16 appSwapUsageLeft = 0;
quint16 appSwapUsageRight = 0;
};
} // namespace APP

View File

@@ -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();
}

View File

@@ -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);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,130 +1,117 @@
#pragma once
#include "APP/APP_KeypadModel.h"
#include "DEBUG/Debug_Config.h"
#include "LOGIC/Lgc_Core.h"
#include <QtCore/QHash>
#include <QtCore/QSet>
#include <QtCore/QTimer>
#include <QtWidgets/QWidget>
#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<QString, APP_KeyButton*> appKeypadButtonMap;
// 功能键按钮映射表:键名 -> 按钮对象。
QHash<QString, APP_KeyButton*> 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<QString> appSequenceRecordingPressedKeySet;
#if APP_ENABLE_DEBUG_WINDOW
// 调试面板实例。
DEBUG::Debug_Panel* appDebugPanel = nullptr;
#endif
// 周期轮询逻辑层的定时器。
QTimer appTimerPoll;
// APP 层持有的逻辑总状态。
QTimer appTimerAutoRefreshDevice;
Lgc_Core_Struct_State appLgcState;
};

View File

@@ -0,0 +1,291 @@
#include "APP/APP_UIWindow.h"
#include "APP/APP_KeyButton.h"
#include "APP/APP_UIWindow_Private.h"
#include <QtCore/QSignalBlocker>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QStackedWidget>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QTableWidgetItem>
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<int> 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<Lgc_FunctionFeature_Type>(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<int>(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

View File

@@ -0,0 +1,12 @@
#pragma once
#include "LOGIC/Lgc_Func_Button.h"
#include <QtCore/QString>
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

427
APP/APP_UIWindow_Record.cpp Normal file
View File

@@ -0,0 +1,427 @@
#include "APP/APP_UIWindow.h"
#include "APP/APP_UIWindow_Private.h"
#include <QtCore/QByteArray>
#include <QtCore/QSignalBlocker>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QMenu>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTableWidget>
#include <Windows.h>
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<QString>& 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<QString>& 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<MSG*>(p_Message);
if (p_Msg->message != WM_INPUT)
{
return false;
}
UINT NeedSize = 0;
GetRawInputData(
reinterpret_cast<HRAWINPUT>(p_Msg->lParam),
RID_INPUT,
nullptr,
&NeedSize,
sizeof(RAWINPUTHEADER));
if (NeedSize == 0)
{
return false;
}
QByteArray Buffer(static_cast<int>(NeedSize), 0);
if (GetRawInputData(
reinterpret_cast<HRAWINPUT>(p_Msg->lParam),
RID_INPUT,
Buffer.data(),
&NeedSize,
sizeof(RAWINPUTHEADER)) == static_cast<UINT>(-1))
{
return false;
}
const RAWINPUT* p_Input = reinterpret_cast<const RAWINPUT*>(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<char16_t>(VirtualKey)));
}
if ((VirtualKey >= '0') && (VirtualKey <= '9'))
{
return QString(QChar(static_cast<char16_t>(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<int> 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

35
COM/Com_Def.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "COM/Com_Protocol.h"
#include <QtCore/QByteArray>
#include <QtCore/QString>
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;
};

975
COM/Com_Protocol.cpp Normal file
View File

@@ -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<quint8>(Bytes.at(Offset++));
Value |= static_cast<quint64>(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<quint32>(Key >> 3);
*p_WireType = static_cast<quint8>(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<quint64>(Bytes.size())) ||
(*p_Offset > Bytes.size() - static_cast<int>(Length)))
{
return false;
}
*p_Value = Bytes.mid(*p_Offset, static_cast<int>(Length));
*p_Offset += static_cast<int>(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<quint32>(
static_cast<quint8>(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<quint64>(
static_cast<quint8>(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<quint8>(Value & 0x7FU);
Value >>= 7;
if (Value != 0)
{
Byte = static_cast<quint8>(Byte | 0x80U);
}
p_Bytes->append(static_cast<char>(Byte));
} while (Value != 0);
}
void Com_Protocol_AppendKey(QByteArray* p_Bytes, quint32 FieldNumber, quint8 WireType)
{
Com_Protocol_AppendVarint(
p_Bytes,
(static_cast<quint64>(FieldNumber) << 3) | static_cast<quint64>(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<quint32>(
(static_cast<quint32>(Value) << 1) ^ static_cast<quint32>(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<char>((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<char>((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<quint64>(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<quint32>(Value);
break;
case 2:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->VendorId = static_cast<quint32>(Value);
break;
case 3:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->ProductId = static_cast<quint32>(Value);
break;
case 4:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->FirmwareMajor = static_cast<quint32>(Value);
break;
case 5:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->FirmwareMinor = static_cast<quint32>(Value);
break;
case 6:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->CapabilityFlags = static_cast<quint32>(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<quint16>(Value);
HasLegacyUsage = true;
break;
case 2:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->Action = static_cast<quint32>(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<quint32>(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<quint32>(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<quint32>(Value);
break;
case 2:
if ((WireType != kProtoWireVarint) ||
!Com_Protocol_ReadVarint(MessageBytes, &Offset, &Value))
{
return false;
}
p_Message->ErrorCode = static_cast<quint32>(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<quint8>(1U << (Usage - kUsageModifierFirst));
return true;
}
if (Usage <= kUsageNormalLast)
{
*p_ByteIndex = 1 + static_cast<int>(Usage / 8U);
*p_BitMask = static_cast<quint8>(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<quint8>(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<quint8>(p_UsageBitmap->at(ByteIndex));
if (IsPressed)
{
ByteValue = static_cast<quint8>(ByteValue | BitMask);
}
else
{
ByteValue = static_cast<quint8>(ByteValue & ~BitMask);
}
(*p_UsageBitmap)[ByteIndex] = static_cast<char>(ByteValue);
return true;
}
QVector<quint16> Com_Protocol_BuildPressedUsageList(const QByteArray& UsageBitmap)
{
QVector<quint16> 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;
}

117
COM/Com_Protocol.h Normal file
View File

@@ -0,0 +1,117 @@
#pragma once
#include <QtCore/QByteArray>
#include <QtCore/QVector>
#include <QtGlobal>
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<quint16> Com_Protocol_BuildPressedUsageList(const QByteArray& UsageBitmap);

817
DRI/Dri_Ble.cpp Normal file
View File

@@ -0,0 +1,817 @@
#include "DRI/Dri_Ble.h"
#include "DRI/Dri_Hid.h"
#include "MID/Mid_Ble.h"
#include <QtCore/QHash>
#include <QtCore/QList>
#include <QtCore/QMutex>
#include <QtCore/QMutexLocker>
#include <QtCore/QRegularExpression>
#include <QtCore/QStringList>
#include <QtCore/QUuid>
#include <Windows.h>
#include <SetupAPI.h>
#include <bluetoothleapis.h>
#include <bthledef.h>
#include <hidsdi.h>
#include <hidpi.h>
#include <atomic>
#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<Mid_Struct_RawPacket> Queue;
QVector<Dri_Ble_Struct_ServiceContext*> Services;
QVector<Dri_Ble_Struct_HidPort*> Hids;
std::atomic<long> CallbackCount { 0 };
};
struct Dri_Ble_Struct_ServiceContext
{
Dri_Ble_Struct_Context* Ctx = nullptr;
HANDLE Handle = INVALID_HANDLE_VALUE;
QString Uuid;
QVector<Dri_Ble_Struct_Subscription*> 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<quint32>(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<wchar_t> buf(static_cast<int>(need) + 1, 0);
return SetupDiGetDeviceInstanceIdW(set, info, buf.data(), static_cast<DWORD>(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<int>(need), 0);
auto* detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(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<LPCWSTR>(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<LPCWSTR>(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<ServiceIf> EnumServices()
{
QVector<ServiceIf> 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<DeviceIf> EnumBleDevices()
{
QVector<DeviceIf> 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<HidIf> EnumBleHids()
{
QVector<HidIf> 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<LPCWSTR>(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<BTH_LE_GATT_CHARACTERISTIC> 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<BTH_LE_GATT_CHARACTERISTIC> out(count);
hr = BluetoothGATTGetCharacteristics(handle, nullptr, count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE);
if (FAILED(hr)) return {};
out.resize(count); return out;
}
QVector<BTH_LE_GATT_DESCRIPTOR> Descriptors(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c)
{
USHORT count = 0;
HRESULT hr = BluetoothGATTGetDescriptors(handle, const_cast<PBTH_LE_GATT_CHARACTERISTIC>(&c), 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE);
if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {};
QVector<BTH_LE_GATT_DESCRIPTOR> out(count);
hr = BluetoothGATTGetDescriptors(handle, const_cast<PBTH_LE_GATT_CHARACTERISTIC>(&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<PBTH_LE_GATT_CHARACTERISTIC>(&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<int>(need), 0);
auto* value = reinterpret_cast<PBTH_LE_GATT_CHARACTERISTIC_VALUE>(buffer.data());
hr = BluetoothGATTGetCharacteristicValue(handle, const_cast<PBTH_LE_GATT_CHARACTERISTIC>(&c), need, value, nullptr, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE);
if (FAILED(hr)) return false;
*out = QByteArray(reinterpret_cast<const char*>(value->Data), static_cast<int>(value->DataSize));
return true;
}
VOID CALLBACK OnGattEvent(BTH_LE_GATT_EVENT_TYPE type, PVOID out, PVOID context)
{
auto* sub = static_cast<Dri_Ble_Struct_Subscription*>(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<const BLUETOOTH_GATT_VALUE_CHANGED_EVENT*>(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<const char*>(evt->CharacteristicValue->Data), static_cast<int>(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, &reg, 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<PBTH_LE_GATT_DESCRIPTOR>(&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<quint8>(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<quint8>(bytes.at(1)) |
(static_cast<quint16>(static_cast<quint8>(bytes.at(2))) << 8);
*productId =
static_cast<quint8>(bytes.at(3)) |
(static_cast<quint16>(static_cast<quint8>(bytes.at(4))) << 8);
return true;
}
QVector<PnpIf> EnumPnps(const QVector<DeviceIf>& deviceList, QStringList* p_DebugList)
{
QVector<PnpIf> 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<BTH_LE_GATT_SERVICE> 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<PBTH_LE_GATT_SERVICE>(&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<BTH_LE_GATT_CHARACTERISTIC> chars(charCount);
hr = BluetoothGATTGetCharacteristics(
handle,
const_cast<PBTH_LE_GATT_SERVICE>(&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<PnpIf>& 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<HidIf>& 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<PnpIf>& pnpList,
const QVector<HidIf>& HidList,
const QVector<ServiceIf>& ServiceList,
const QStringList& AllowedAddressList)
{
QHash<QString, int> HidCountMap;
QHash<QString, int> GattCountMap;
QHash<QString, QString> 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<PnpIf>& 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<DeviceIf> devices = EnumBleDevices();
const QVector<HidIf> hids = EnumBleHids();
const QVector<ServiceIf> services = EnumServices();
QStringList pnpDebugList;
const QVector<PnpIf> 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;
}

32
DRI/Dri_Ble.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QString>
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);

623
DRI/Dri_Cdc.cpp Normal file
View File

@@ -0,0 +1,623 @@
#include "DRI/Dri_Cdc.h"
#include "COM/Com_Protocol.h"
#include <QtCore/QDir>
#include <QtCore/QQueue>
#include <QtCore/QStringList>
#include <QtCore/QVector>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
struct Dri_Cdc_Struct_Candidate
{
QSerialPort* p_SerialPort = nullptr;
QString PortName;
QString EndpointId;
QString SystemLocation;
QByteArray PendingBytes;
QQueue<Com_Struct_RawPacket> PacketQueue;
};
struct Dri_Cdc_Struct_Context
{
QVector<Dri_Cdc_Struct_Candidate> 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<QSerialPortInfo> 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;
}

40
DRI/Dri_Cdc.h Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include "COM/Com_Def.h"
#include <QtCore/QString>
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);

View File

@@ -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<LPCWSTR>(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<int>(BytesRead));
}
Dri_Consumer_Func_BeginRead(p_Port, p_TextStatus);
return p_Packet->IsValid;
return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus);
}

View File

@@ -1,35 +1,20 @@
#pragma once
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QByteArray>
#include <QtCore/QString>
#include <Windows.h>
#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);

189
DRI/Dri_Hid.cpp Normal file
View File

@@ -0,0 +1,189 @@
#include "DRI/Dri_Hid.h"
#include <hidsdi.h>
#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<LPCWSTR>(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<int>(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<LPCWSTR>(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<LPCWSTR>(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<ULONG>(Buffer.size()))) return true;
const DWORD SetReportError = GetLastError();
DWORD BytesWritten = 0;
if (WriteFile(HandleWrite, Buffer.constData(), static_cast<DWORD>(Buffer.size()), &BytesWritten, nullptr) && (BytesWritten == static_cast<DWORD>(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<ULONG>(Buffer.size()))) return true;
const DWORD PaddedSetReportError = GetLastError();
BytesWritten = 0;
if (WriteFile(HandleWrite, Buffer.constData(), static_cast<DWORD>(Buffer.size()), &BytesWritten, nullptr) && (BytesWritten == static_cast<DWORD>(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;
}

41
DRI/Dri_Hid.h Normal file
View File

@@ -0,0 +1,41 @@
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QByteArray>
#include <QtCore/QString>
#include <Windows.h>
// 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);

View File

@@ -1,4 +1,4 @@
#include "DRI/Dri_NkroRaw.h"
#include "DRI/Dri_NkroRaw.h"
#include <QtCore/QVector>
#include <Windows.h>
@@ -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<char>(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;
}

View File

@@ -1,40 +1,31 @@
#pragma once
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QByteArray>
#include <QtCore/QList>
#include <QtCore/QString>
/*
* 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<Mid_Struct_RawPacket> 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);

920
DRI/Dri_Nus.cpp Normal file
View File

@@ -0,0 +1,920 @@
#include "DRI/Dri_Nus.h"
#include "COM/Com_Protocol.h"
#include <QtBluetooth/QBluetoothDeviceDiscoveryAgent>
#include <QtBluetooth/QBluetoothDeviceInfo>
#include <QtBluetooth/QBluetoothUuid>
#include <QtBluetooth/QLowEnergyCharacteristic>
#include <QtBluetooth/QLowEnergyController>
#include <QtBluetooth/QLowEnergyDescriptor>
#include <QtBluetooth/QLowEnergyService>
#include <QtCore/QCoreApplication>
#include <QtCore/QQueue>
#include <QtCore/QTimer>
#include <QtCore/QVector>
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<Dri_Nus_Struct_Candidate> CandidateList;
int CurrentCandidateIndex = -1;
int LockedCandidateIndex = -1;
bool IsDiscoveryFinished = false;
bool HasTargetService = false;
QLowEnergyCharacteristic WriteCharacteristic;
QLowEnergyCharacteristic NotifyCharacteristic;
QLowEnergyDescriptor NotifyDescriptor;
QQueue<Com_Struct_RawPacket> 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<void (QLowEnergyService::*)(QLowEnergyService::ServiceError)>(
&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<int>(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<void (QLowEnergyController::*)(QLowEnergyController::Error)>(
&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<void (QBluetoothDeviceDiscoveryAgent::*)(QBluetoothDeviceDiscoveryAgent::Error)>(
&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;
}

42
DRI/Dri_Nus.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include "COM/Com_Def.h"
#include <QtCore/QString>
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);

View File

@@ -1,338 +1,248 @@
#include "DRI/Dri_Vendor.h"
#include "DRI/Dri_Vendor.h"
#include <hidsdi.h>
#pragma comment(lib, "hid.lib")
#include <QtCore/QStringList>
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<LPCWSTR>(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=%1ZeroAccess=%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())
QString TextError;
if (Dri_Hid_WritePacket(HandleWrite, OutputLength, Packet, &TextError))
{
return false;
if (p_TextStatus != nullptr)
{
*p_TextStatus = SuccessText;
}
QByteArray Buffer = Packet;
if ((OutputLength > 0) && (Buffer.size() < OutputLength))
{
Buffer.append(OutputLength - Buffer.size(), 0);
}
if (HidD_SetOutputReport(HandleWrite, Buffer.data(), static_cast<ULONG>(Buffer.size())))
{
return true;
}
const DWORD SetReportError = GetLastError();
DWORD BytesWritten = 0;
if (WriteFile(
HandleWrite,
Buffer.constData(),
static_cast<DWORD>(Buffer.size()),
&BytesWritten,
nullptr) &&
(BytesWritten == static_cast<DWORD>(Buffer.size())))
if (!TextError.isEmpty())
{
return true;
}
if (p_TextError != nullptr)
{
*p_TextError = QStringLiteral("SetOutputReport=%1WriteFile=%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<int>(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<quint8>(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())
{
Errors.append(ErrorPrefix.arg(TextError));
}
return false;
};
if (TryWriteTo(
else
{
if (Dri_Vendor_TryWrite(
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")))
ByteArray,
QStringLiteral("Packet sent to NKRO write interface."),
QStringLiteral("NKRO write failed: %1"),
p_TextStatus,
&ErrorList))
{
return true;
}
Dri_Vendor_Func_SetStatus(
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,
Errors.isEmpty()
? QStringLiteral("没有可用的写句柄,掩码发送未执行。")
: Errors.join(QStringLiteral("\n")));
&ErrorList))
{
return true;
}
}
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;
}

View File

@@ -1,42 +1,31 @@
#pragma once
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QByteArray>
#include <QtCore/QString>
#include <Windows.h>
#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);

View File

@@ -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<quint8>(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<quint8>(ByteArray.at(1)) |
(static_cast<quint16>(static_cast<quint8>(ByteArray.at(2))) << 8);
// 当前项目里只保留简短解释,避免调试日志过于冗长。
p_Result->TextExplain = QStringLiteral("0x03 Consumer%1")
.arg(Mid_Func_GetConsumerUsageText(p_Result->Usage));
}

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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 <QtCore/QByteArray>
#include <QtCore/QSet>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QVector>
/*
* 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<quint16> VisibleUsageList;
/* 物理按键:直接来自硬件,供 Swap/功能逻辑判断 */
bool IsPhysicalKeyStateValid = false;
quint8 PhysicalModifier = 0;
QVector<quint16> PhysicalUsageList;
QVector<quint16> 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<quint16> 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<quint16> VisibleUsageList;
bool IsPhysicalKeyStateValid = false;
QVector<quint16> 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<int> 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);

813
LOGIC/Lgc_Core_Command.cpp Normal file
View File

@@ -0,0 +1,813 @@
#include "LOGIC/Lgc_Core_Private.h"
#include <QtCore/QDateTime>
#include <QtCore/QStringList>
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<quint16>& 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<char>(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<quint64>(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<char>(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<quint64>(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"));
}

366
LOGIC/Lgc_Core_Config.cpp Normal file
View File

@@ -0,0 +1,366 @@
#include "LOGIC/Lgc_Core_Private.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QSaveFile>
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<quint16> Lgc_Core_GetUsageListForFeature(
const Lgc_Core_Struct_State* p_State,
int FeatureId)
{
QVector<quint16> 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<int>(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<quint16>(Usage)))
{
p_Config->UsageFeatureIdMap.insert(static_cast<quint16>(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;
}

320
LOGIC/Lgc_Core_Control.cpp Normal file
View File

@@ -0,0 +1,320 @@
#include "LOGIC/Lgc_Core_Private.h"
#include <QtCore/QDateTime>
#include <QtCore/QStringList>
#include <QtCore/QTimeZone>
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<quint8>(1U << (Usage % 8));
const quint8 ByteValue = static_cast<quint8>(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<quint8>(1U << (Usage % 8));
quint8 ByteValue = static_cast<quint8>(p_Bitmap->at(ByteIndex));
if (IsEnabled)
{
ByteValue = static_cast<quint8>(ByteValue | BitMask);
}
else
{
ByteValue = static_cast<quint8>(ByteValue & ~BitMask);
}
(*p_Bitmap)[ByteIndex] = static_cast<char>(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<char>(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<char>(Mid_Enum_ReportId_VendorCommand);
Packet[1] = static_cast<char>(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<char>(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<quint16> 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<char>((static_cast<quint64>(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<char>(ThemeColor.Red);
Payload[1] = static_cast<char>(ThemeColor.Green);
Payload[2] = static_cast<char>(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)));
}

702
LOGIC/Lgc_Core_Input.cpp Normal file
View File

@@ -0,0 +1,702 @@
#include "LOGIC/Lgc_Core_Private.h"
#include <QtCore/QDateTime>
#include <QtCore/QStringList>
#include <Windows.h>
namespace
{
constexpr quint32 kProtocolVersionCurrent = 1U;
constexpr qint64 kBitmapInitialSendDelayMs = 300;
constexpr qint64 kBitmapRetryDelayMs = 200;
constexpr int kBitmapRetryMaxCount = 5;
QString Lgc_Core_FormatUsageListText(const QVector<quint16>& 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<quint16>& 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<Com_Enum_ProtocolType>(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<quint32>(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<quint32>(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<quint16> 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<quint32>(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<quint32>(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;
}

54
LOGIC/Lgc_Core_Private.h Normal file
View File

@@ -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);

View File

@@ -1,20 +1,59 @@
#include "LOGIC/Lgc_Func_Button.h"
#include "LOGIC/Lgc_Core.h"
#include <QtCore/QUrl>
#include <QtCore/QVector>
#include <QtGui/QDesktopServices>
#include <Windows.h>
#include "LOGIC/Lgc_Func_Button_Private.h"
#include <QtCore/QStringList>
#include <algorithm>
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<quint16> Lgc_FunctionButton_GetConfigurableUsages()
{
if (Text.isEmpty())
{
return false;
}
QVector<INPUT> 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<UINT>(InputList.size()), InputList.data(), sizeof(INPUT)) ==
static_cast<UINT>(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<int> Lgc_FunctionButton_GetFeatureIdList(const Lgc_FunctionButton_Config& Config)
{
const QString Text = p_State->FunctionButtonConfig.MacroText.trimmed();
if (Text.isEmpty())
QVector<int> 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;
}
}

View File

@@ -1,19 +1,67 @@
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QHash>
#include <QtCore/QString>
#include <QtCore/QVector>
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<int, Lgc_FunctionFeature_Definition> FeatureMap;
QHash<quint16, int> UsageFeatureIdMap;
};
QString Lgc_FunctionButton_GetUsageShortText(quint16 Usage);
QString Lgc_FunctionButton_GetFeatureTypeText(Lgc_FunctionFeature_Type Type);
QVector<quint16> Lgc_FunctionButton_GetConfigurableUsages();
QVector<int> 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);

View File

@@ -0,0 +1,278 @@
#include "LOGIC/Lgc_Func_Button_Private.h"
#include <QtCore/QRegularExpression>
#include <QtCore/QStringList>
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<Lgc_FunctionButton_Struct_SequenceKey>* 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<WORD>(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<WORD>(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<Lgc_FunctionButton_Struct_SequenceKey>* 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<Lgc_FunctionButton_Struct_SequenceKey>& 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<QVector<Lgc_FunctionButton_Struct_SequenceKey>>* p_CombinationList,
QString* p_ErrorText)
{
p_CombinationList->clear();
if (p_ErrorText != nullptr)
{
p_ErrorText->clear();
}
for (const QString& SegmentText : Lgc_FunctionButton_SplitShortcutSegments(Text))
{
QVector<Lgc_FunctionButton_Struct_SequenceKey> 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<QVector<Lgc_FunctionButton_Struct_SequenceKey>>& CombinationList)
{
QStringList TextList;
for (const auto& Combination : CombinationList)
{
TextList.append(Lgc_FunctionButton_FormatKeyCombination(Combination));
}
return TextList.join(QStringLiteral(" -> "));
}

View File

@@ -0,0 +1,61 @@
#pragma once
#include "LOGIC/Lgc_Func_Button.h"
#include <QtCore/QVector>
#include <Windows.h>
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<Lgc_FunctionButton_Struct_SequenceKey>* p_KeyList,
QString* p_ErrorText);
QString Lgc_FunctionButton_FormatKeyCombination(
const QVector<Lgc_FunctionButton_Struct_SequenceKey>& KeyList);
bool Lgc_FunctionButton_ParseShortcutSequenceText(
const QString& Text,
quint16 SourceUsage,
QVector<QVector<Lgc_FunctionButton_Struct_SequenceKey>>* p_CombinationList,
QString* p_ErrorText);
QString Lgc_FunctionButton_FormatShortcutSequence(
const QVector<QVector<Lgc_FunctionButton_Struct_SequenceKey>>& CombinationList);
bool Lgc_FunctionButton_SendKeyCombination(
const QVector<Lgc_FunctionButton_Struct_SequenceKey>& KeyList);
bool Lgc_FunctionButton_SendShortcutSequence(
const QVector<QVector<Lgc_FunctionButton_Struct_SequenceKey>>& 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);

View File

@@ -0,0 +1,258 @@
#include "LOGIC/Lgc_Func_Button_Private.h"
#include <QtCore/QUrl>
#include <QtGui/QDesktopServices>
#include <Windows.h>
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<Lgc_FunctionButton_Struct_WindowsKey>& 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<Lgc_FunctionButton_Struct_SequenceKey>& KeyList)
{
QVector<Lgc_FunctionButton_Struct_WindowsKey> ModifierList;
QVector<Lgc_FunctionButton_Struct_WindowsKey> 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<QVector<Lgc_FunctionButton_Struct_SequenceKey>>& 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<Lgc_FunctionButton_Struct_SequenceKey> 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<QVector<Lgc_FunctionButton_Struct_SequenceKey>> 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));
}

View File

@@ -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<quint8>(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<quint8>(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<quint8>(p_Result->UsageBitmap.at(ByteIndex));
if ((Value & static_cast<quint8>(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'));
}

View File

@@ -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<quint16> UsageList;
// 展开后的按键名称列表。
QStringList UsageTextList;
// 给调试窗口用的简短中文说明。
QString TextExplain;
};
void Lgc_Nkro_Func_Parse(const QByteArray& ByteArray, Lgc_Nkro_Struct_Result* p_Result);

View File

@@ -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<quint8>(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<quint8>(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<quint8>(p_Result->VendorState.UsageBitmap.at(ByteIndex));
if ((Value & static_cast<quint8>(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'));
}

View File

@@ -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);

49
MID/Mid_Ble.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "MID/Mid_Ble.h"
#include <QtCore/QStringList>
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();
}

9
MID/Mid_Ble.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include <QtCore/QString>
// 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);

View File

@@ -1,5 +1,7 @@
#include "MID/Mid_Def.h"
#include "MID/Mid_Def.h"
#include <QtCore/QStringList>
#include <QtCore/QVector>
#include <Windows.h>
#include <SetupAPI.h>
#include <hidsdi.h>
@@ -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<wchar_t> Buffer(static_cast<int>(NeedLength) + 1, 0);
return SetupDiGetDeviceInstanceIdW(
DeviceInfoSet,
p_DeviceInfoData,
Buffer.data(),
static_cast<DWORD>(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<int>(NeedLength), 0);
auto* p_Detail = reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_W*>(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,12 +127,8 @@ bool Mid_Func_FindHidInterface(
}
CloseHandle(HandleQuery);
if (!IsMatch)
if (IsExactMatch)
{
continue;
}
/* 命中:写回设备路径及报文长度 */
if (p_DevicePath != nullptr)
{
*p_DevicePath = QString::fromWCharArray(p_Detail->DevicePath);
@@ -148,18 +141,29 @@ bool Mid_Func_FindHidInterface(
{
*p_OutputLength = Caps.OutputReportByteLength;
}
if (p_DeviceInstanceId != nullptr)
{
*p_DeviceInstanceId = Mid_GetDeviceInstanceId(DeviceInfoSet, &DeviceInfoData);
}
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");
}
}

View File

@@ -1,86 +1,81 @@
#pragma once
#pragma once
#include <QtCore/QByteArray>
#include <QtCore/QString>
#include <QtCore/QStringList>
#include <QtCore/QVector>
/*
* 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<quint16> 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);

27
function_config.json Normal file
View File

@@ -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
}

View File

@@ -1,43 +1,15 @@
#include "APP/APP_UIWindow.h"
#include "APP/APP_Theme.h"
#include <QtCore/QCoreApplication>
#include <QtWidgets/QApplication>
#include <QtWidgets/QStyleFactory>
#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);
// 在高 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.setStyleSheet(APP::APP_Theme::App_Func_GetStyleSheet());
// 创建主窗口对象,
// 这里的 App_UIWindow 是你程序的主界面类。
APP::App_UIWindow window;
// 显示主窗口,
// 如果不调用 show(),窗口对象虽然创建了,但不会出现在屏幕上。
window.show();
// 启动 Qt 事件循环,
// 程序会在这里持续处理鼠标、键盘、重绘、信号槽等事件,
// 直到窗口关闭后才会退出,并返回退出码。
return app.exec();
}