Files
0417_QT_code/DRI/Dri_Ble.cpp

818 lines
33 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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;
}