first push

This commit is contained in:
2026-04-03 09:26:10 +08:00
parent 2937a44e07
commit 025b88e366
41 changed files with 6842 additions and 0 deletions

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

52
DRI/Dri_Consumer.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include "DRI/Dri_Consumer.h"
void Dri_Consumer_Close(Dri_Consumer_Struct_Port* p_Port)
{
Dri_Hid_CloseReadPort(&p_Port->ReadPort);
}
bool Dri_Consumer_Init(
Dri_Consumer_Struct_Port* p_Port,
const Mid_Struct_DeviceConfig& DeviceConfig,
QString* p_TextStatus)
{
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_FindHidInterface(
Match,
&DevicePath,
&InputLength,
nullptr))
{
if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("Consumer interface was not found: 000C / 0001.");
}
return false;
}
return Dri_Hid_InitReadPort(
&p_Port->ReadPort,
DevicePath,
InputLength,
Mid_Enum_RawPacketSource_UsbConsumerHid,
QStringLiteral("Consumer"),
p_TextStatus);
}
bool Dri_Consumer_Read(
Dri_Consumer_Struct_Port* p_Port,
Mid_Struct_RawPacket* p_Packet,
QString* p_TextStatus)
{
return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus);
}

20
DRI/Dri_Consumer.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include "DRI/Dri_Hid.h"
// USB consumer report reader.
struct Dri_Consumer_Struct_Port
{
Dri_Hid_Struct_ReadPort ReadPort;
};
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);
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);

281
DRI/Dri_NkroRaw.cpp Normal file
View File

@@ -0,0 +1,281 @@
#include "DRI/Dri_NkroRaw.h"
#include <QtCore/QVector>
#include <Windows.h>
namespace
{
// Device filter and scancode-to-usage mapping.
QString Dri_NkroRaw_GetDevicePath(HANDLE DeviceHandle)
{
if (DeviceHandle == nullptr)
{
return QString();
}
UINT NeedChars = 0;
GetRawInputDeviceInfoW(DeviceHandle, RIDI_DEVICENAME, nullptr, &NeedChars);
if (NeedChars == 0)
{
return QString();
}
QVector<wchar_t> Buffer(static_cast<int>(NeedChars) + 1, 0);
if (GetRawInputDeviceInfoW(DeviceHandle, RIDI_DEVICENAME, Buffer.data(), &NeedChars) == static_cast<UINT>(-1))
{
return QString();
}
return QString::fromWCharArray(Buffer.constData()).trimmed();
}
bool Dri_NkroRaw_IsTargetDevice(const QString& DevicePath, const Mid_Struct_DeviceConfig& DeviceConfig)
{
if (DevicePath.isEmpty())
{
return false;
}
const QString UpperPath = DevicePath.toUpper();
return UpperPath.contains(QStringLiteral("VID_%1").arg(DeviceConfig.VendorId, 4, 16, QLatin1Char('0')).toUpper()) &&
UpperPath.contains(QStringLiteral("PID_%1").arg(DeviceConfig.ProductId, 4, 16, QLatin1Char('0')).toUpper());
}
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;
const USHORT ScanCode = Keyboard.MakeCode;
if (IsE1)
{
return 0;
}
if (IsE0)
{
switch (ScanCode)
{
case 0x35: return 0x0054;
case 0x1C: return 0x0058;
case 0x1D: return 0x00E4;
case 0x38: return 0x00E6;
case 0x5B: return 0x00E3;
case 0x5C: return 0x00E7;
default:
return 0;
}
}
switch (ScanCode)
{
case 0x45: return 0x0053;
case 0x37: return 0x0055;
case 0x4A: return 0x0056;
case 0x4E: return 0x0057;
case 0x47: return 0x005F;
case 0x48: return 0x0060;
case 0x49: return 0x0061;
case 0x4B: return 0x005C;
case 0x4C: return 0x005D;
case 0x4D: return 0x005E;
case 0x4F: return 0x0059;
case 0x50: return 0x005A;
case 0x51: return 0x005B;
case 0x52: return 0x0062;
case 0x53: return 0x0063;
case 0x1D: return 0x00E0;
case 0x2A: return 0x00E1;
case 0x36: return 0x00E5;
case 0x38: return 0x00E2;
default:
return 0;
}
}
} // namespace
void Dri_NkroRaw_Close(Dri_NkroRaw_Struct_Port* p_Port)
{
*p_Port = Dri_NkroRaw_Struct_Port();
}
bool Dri_NkroRaw_Init(
Dri_NkroRaw_Struct_Port* p_Port,
const Mid_Struct_DeviceConfig& DeviceConfig,
void* WindowHandle,
QString* p_TextStatus)
{
Dri_NkroRaw_Close(p_Port);
if (WindowHandle == nullptr)
{
if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("NKRO 原生输入链路打开失败:窗口句柄为空。");
}
return false;
}
RAWINPUTDEVICE Device = {};
Device.usUsagePage = MID_CONST_USAGE_PAGE_NKRO;
Device.usUsage = MID_CONST_USAGE_NKRO;
Device.dwFlags = RIDEV_INPUTSINK;
Device.hwndTarget = reinterpret_cast<HWND>(WindowHandle);
if (!RegisterRawInputDevices(&Device, 1, sizeof(Device)))
{
if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("NKRO 原生输入链路注册失败:%1").arg(GetLastError());
}
return false;
}
p_Port->IsOpened = true;
p_Port->WindowHandle = WindowHandle;
p_Port->DeviceConfig = DeviceConfig;
if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("已启用 NKRO 原生输入链路。");
}
return true;
}
bool Dri_NkroRaw_HandleMessage(
Dri_NkroRaw_Struct_Port* p_Port,
void* p_Message,
QString* p_TextStatus)
{
if (!p_Port->IsOpened || (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;
}
RAWINPUT* p_Input = reinterpret_cast<RAWINPUT*>(Buffer.data());
if (p_Input->header.dwType != RIM_TYPEKEYBOARD)
{
return false;
}
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_GetUsage(p_Input->data.keyboard);
if (Usage == 0)
{
return false;
}
const bool IsPressed = (p_Input->data.keyboard.Flags & RI_KEY_BREAK) == 0;
bool IsChanged = false;
if ((Usage >= 0x00E0) && (Usage <= 0x00E7))
{
const quint8 BitMask = static_cast<quint8>(1U << (Usage - 0x00E0));
const quint8 OldModifier = p_Port->Modifier;
if (IsPressed)
{
p_Port->Modifier = static_cast<quint8>(p_Port->Modifier | BitMask);
}
else
{
p_Port->Modifier = static_cast<quint8>(p_Port->Modifier & static_cast<quint8>(~BitMask));
}
IsChanged = (OldModifier != p_Port->Modifier);
}
else
{
const int ByteIndex = Usage / 8;
const quint8 BitMask = static_cast<quint8>(1U << (Usage % 8));
quint8 Value = static_cast<quint8>(p_Port->UsageBitmap.at(ByteIndex));
const bool OldPressed = (Value & BitMask) != 0;
if (OldPressed == IsPressed)
{
return false;
}
Value = IsPressed ? static_cast<quint8>(Value | BitMask) : static_cast<quint8>(Value & static_cast<quint8>(~BitMask));
p_Port->UsageBitmap[ByteIndex] = static_cast<char>(Value);
IsChanged = true;
}
if (!IsChanged)
{
return false;
}
if (p_Port->DevicePath != DevicePath)
{
p_Port->DevicePath = DevicePath;
if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("原生输入已命中目标设备:%1").arg(DevicePath);
}
}
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);
Packet.ByteArray[1] = static_cast<char>(p_Port->Modifier);
for (int Index = 0; Index < MID_CONST_USAGE_BITMAP_SIZE; ++Index)
{
Packet.ByteArray[2 + Index] = p_Port->UsageBitmap.at(Index);
}
p_Port->PacketQueue.append(Packet);
return true;
}
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(原生输入)");
if (!p_Port->IsOpened || p_Port->PacketQueue.isEmpty())
{
return false;
}
*p_Packet = p_Port->PacketQueue.takeFirst();
return p_Packet->IsValid;
}

31
DRI/Dri_NkroRaw.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include "MID/Mid_Def.h"
#include <QtCore/QByteArray>
#include <QtCore/QList>
#include <QtCore/QString>
// Windows RAWINPUT reader for the NKRO path.
struct Dri_NkroRaw_Struct_Port
{
bool IsOpened = false;
void* WindowHandle = nullptr;
Mid_Struct_DeviceConfig DeviceConfig;
quint8 Modifier = 0;
QByteArray UsageBitmap = QByteArray(MID_CONST_USAGE_BITMAP_SIZE, 0);
QList<Mid_Struct_RawPacket> PacketQueue;
QString DevicePath;
};
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);
bool Dri_NkroRaw_HandleMessage(Dri_NkroRaw_Struct_Port* p_Port,
void* p_Message,
QString* p_TextStatus);
bool Dri_NkroRaw_Read(Dri_NkroRaw_Struct_Port* p_Port,
Mid_Struct_RawPacket* p_Packet,
QString* p_TextStatus);

248
DRI/Dri_Vendor.cpp Normal file
View File

@@ -0,0 +1,248 @@
#include "DRI/Dri_Vendor.h"
#include <QtCore/QStringList>
namespace
{
void Dri_Vendor_CloseHandle(HANDLE* p_Handle)
{
if ((*p_Handle != nullptr) && (*p_Handle != INVALID_HANDLE_VALUE))
{
CloseHandle(*p_Handle);
}
*p_Handle = INVALID_HANDLE_VALUE;
}
bool Dri_Vendor_TryWrite(
HANDLE HandleWrite,
quint16 OutputLength,
const QByteArray& Packet,
const QString& SuccessText,
const QString& ErrorPrefix,
QString* p_TextStatus,
QStringList* p_ErrorList)
{
QString TextError;
if (Dri_Hid_WritePacket(HandleWrite, OutputLength, Packet, &TextError))
{
if (p_TextStatus != nullptr)
{
*p_TextStatus = SuccessText;
}
return true;
}
if (!TextError.isEmpty())
{
p_ErrorList->append(ErrorPrefix.arg(TextError));
}
return false;
}
} // namespace
void Dri_Vendor_Close(Dri_Vendor_Struct_Port* p_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_Init(
Dri_Vendor_Struct_Port* p_Port,
const Mid_Struct_DeviceConfig& DeviceConfig,
QString* p_TextStatus)
{
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 VendorOutputLength = 0;
if (!Mid_FindHidInterface(
VendorMatch,
&VendorPath,
&InputLength,
&VendorOutputLength,
&VendorInstanceId))
{
if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("Vendor interface was not found: FF00 / 0002.");
}
return false;
}
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))
{
return false;
}
p_Port->HandleWriteVendor = Dri_Hid_OpenWriteHandle(VendorPath, nullptr);
p_Port->VendorWriteOutputLength = VendorOutputLength;
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;
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_Hid_OpenWriteHandle(NkroPath, &TextError);
p_Port->NkroWriteOutputLength = NkroOutputLength;
if ((p_Port->HandleWriteNkro == INVALID_HANDLE_VALUE) && (p_TextStatus != nullptr))
{
*p_TextStatus = QStringLiteral("NKRO write handle failed: %1").arg(TextError);
}
}
else if (p_TextStatus != nullptr)
{
*p_TextStatus = QStringLiteral("NKRO write interface was not found.");
}
return true;
}
bool Dri_Vendor_Read(
Dri_Vendor_Struct_Port* p_Port,
Mid_Struct_RawPacket* p_Packet,
QString* p_TextStatus)
{
return Dri_Hid_Read(&p_Port->ReadPort, p_Packet, p_TextStatus);
}
bool Dri_Vendor_Write(
Dri_Vendor_Struct_Port* p_Port,
const QByteArray& ByteArray,
QString* p_TextStatus)
{
const QString RouteLabel = p_Port->IsBluetoothTransport
? QStringLiteral("Bluetooth Vendor")
: QStringLiteral("Vendor");
if (!p_Port->ReadPort.IsOpened)
{
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 ErrorList;
const quint8 ReportId = static_cast<quint8>(ByteArray.at(0));
if (ReportId == Mid_Enum_ReportId_VendorCommand)
{
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))
{
return true;
}
}
else
{
if (Dri_Vendor_TryWrite(
p_Port->HandleWriteNkro,
p_Port->NkroWriteOutputLength,
ByteArray,
QStringLiteral("Packet sent to NKRO write interface."),
QStringLiteral("NKRO write failed: %1"),
p_TextStatus,
&ErrorList))
{
return true;
}
if (Dri_Vendor_TryWrite(
p_Port->HandleWriteVendor,
p_Port->VendorWriteOutputLength,
ByteArray,
QStringLiteral("Packet sent to %1 write interface.").arg(RouteLabel),
RouteLabel + QStringLiteral(" write failed: %1"),
p_TextStatus,
&ErrorList))
{
return true;
}
}
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;
}

31
DRI/Dri_Vendor.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include "DRI/Dri_Hid.h"
// USB vendor reader plus three write routes: vendor, command, and NKRO.
struct Dri_Vendor_Struct_Port
{
Dri_Hid_Struct_ReadPort ReadPort;
HANDLE HandleWriteVendor = INVALID_HANDLE_VALUE;
HANDLE HandleWriteCommand = INVALID_HANDLE_VALUE;
HANDLE HandleWriteNkro = INVALID_HANDLE_VALUE;
quint16 VendorWriteOutputLength = 0;
quint16 CommandWriteOutputLength = 0;
quint16 NkroWriteOutputLength = 0;
bool IsBluetoothTransport = false;
};
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);
bool Dri_Vendor_Read(
Dri_Vendor_Struct_Port* p_Port,
Mid_Struct_RawPacket* p_Packet,
QString* p_TextStatus);
bool Dri_Vendor_Write(
Dri_Vendor_Struct_Port* p_Port,
const QByteArray& ByteArray,
QString* p_TextStatus);