#include "DRI/Dri_Ble.h" #include "DRI/Dri_Hid.h" #include "MID/Mid_Ble.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "BluetoothAPIs.lib") #pragma comment(lib, "hid.lib") #pragma comment(lib, "setupapi.lib") struct Dri_Ble_Struct_ServiceContext; struct Dri_Ble_Struct_HidPort; struct Dri_Ble_Struct_Subscription { Dri_Ble_Struct_Port* Port = nullptr; Dri_Ble_Struct_ServiceContext* Service = nullptr; BLUETOOTH_GATT_EVENT_HANDLE Event = nullptr; QString DeviceLabel; QString ServiceUuid; QString CharUuid; }; struct Dri_Ble_Struct_Context { QMutex Mutex; QList Queue; QVector Services; QVector Hids; std::atomic CallbackCount { 0 }; }; struct Dri_Ble_Struct_ServiceContext { Dri_Ble_Struct_Context* Ctx = nullptr; HANDLE Handle = INVALID_HANDLE_VALUE; QString Uuid; QVector Subs; }; struct Dri_Ble_Struct_HidPort { Dri_Hid_Struct_ReadPort ReadPort; HANDLE WriteHandle = INVALID_HANDLE_VALUE; quint16 UsagePage = 0; quint16 Usage = 0; quint16 OutputLength = 0; }; namespace { struct ServiceIf { QString Path; QString InstanceId; QString Uuid; }; struct DeviceIf { QString Path; QString InstanceId; QString Address; }; struct HidIf { QString Path; QString InstanceId; quint16 VendorId = 0; quint16 ProductId = 0; quint16 UsagePage = 0; quint16 Usage = 0; quint16 InputLength = 0; quint16 OutputLength = 0; }; struct PnpIf { QString Address; quint16 VendorId = 0; quint16 ProductId = 0; }; const QString kSvcHidBrace = QStringLiteral("{00001812-0000-1000-8000-00805F9B34FB}"); const QString kSvcCustom = QStringLiteral("0B7F5000-38D2-4F62-8F6F-36C4FD73A110"); const QString kSvcDis = QStringLiteral("0000180A-0000-1000-8000-00805F9B34FB"); const QString kChrPnpId = QStringLiteral("00002A50-0000-1000-8000-00805F9B34FB"); const quint16 kUsagePageKeyboard = 0x0001; const quint16 kUsageKeyboard = 0x0006; QString HResultText(HRESULT hr) { return QStringLiteral("0x%1").arg(static_cast(hr), 8, 16, QLatin1Char('0')).toUpper(); } bool IsCommandHid(quint16 page, quint16 usage) { return (page == MID_CONST_USAGE_PAGE_VENDOR_COMMAND) && (usage == MID_CONST_USAGE_VENDOR_COMMAND); } bool IsUsefulHid(quint16 page, quint16 usage) { return ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) || ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) || ((page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR)) || IsCommandHid(page, usage); } bool IsVendorHid(quint16 page, quint16 usage) { return (page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR); } Mid_Enum_RawPacketSource HidPacketSource(quint16 page, quint16 usage) { if ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) return Mid_Enum_RawPacketSource_BleHidKeyboard; if ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) return Mid_Enum_RawPacketSource_BleHidConsumer; if (IsVendorHid(page, usage)) return Mid_Enum_RawPacketSource_BleHidVendor; if (IsCommandHid(page, usage)) return Mid_Enum_RawPacketSource_BleHidVendorCommand; return Mid_Enum_RawPacketSource_None; } QString ExtractAddress(const QString& text) { auto m = QRegularExpression(QStringLiteral("([0-9A-F]{12})(?=[\\\\&_]|$)")).match(text.toUpper()); return m.hasMatch() ? m.captured(1) : QString(); } QString ServiceUuidFromId(const QString& id) { auto m = QRegularExpression(QStringLiteral("\\{([0-9A-Fa-f-]{36})\\}")).match(id); return m.hasMatch() ? m.captured(1).toUpper() : QString(); } QString DeviceLabel(const QString& addr) { return addr.isEmpty() ? QStringLiteral("BLE") : QStringLiteral("BLE-%1").arg(addr); } QString FormatVidPidLabel(quint16 vendorId, quint16 productId) { return QStringLiteral("0x%1:0x%2") .arg(vendorId, 4, 16, QLatin1Char('0')) .arg(productId, 4, 16, QLatin1Char('0')) .toUpper(); } QString PortName(const QString& dev, const QString& src) { const QString base = dev.isEmpty() ? QStringLiteral("Bluetooth") : QStringLiteral("Bluetooth(%1)").arg(dev); return src.isEmpty() ? base : QStringLiteral("%1/%2").arg(base, src); } QString HidSource(quint16 page, quint16 usage) { if ((page == kUsagePageKeyboard) && (usage == kUsageKeyboard)) return QStringLiteral("HID Keyboard"); if ((page == MID_CONST_USAGE_PAGE_CONSUMER) && (usage == MID_CONST_USAGE_CONSUMER)) return QStringLiteral("HID Consumer"); if ((page == MID_CONST_USAGE_PAGE_VENDOR) && (usage == MID_CONST_USAGE_VENDOR)) return QStringLiteral("HID Vendor"); if (IsCommandHid(page, usage)) return QStringLiteral("HID Vendor Command"); return QStringLiteral("HID UsagePage 0x%1 / Usage 0x%2").arg(page, 4, 16, QLatin1Char('0')).arg(usage, 4, 16, QLatin1Char('0')).toUpper(); } QString DeviceInstanceId(HDEVINFO set, SP_DEVINFO_DATA* info) { DWORD need = 0; SetupDiGetDeviceInstanceIdW(set, info, nullptr, 0, &need); if (need == 0) return QString(); QVector buf(static_cast(need) + 1, 0); return SetupDiGetDeviceInstanceIdW(set, info, buf.data(), static_cast(buf.size()), nullptr) ? QString::fromWCharArray(buf.constData()).trimmed() : QString(); } bool InterfacePath(HDEVINFO set, SP_DEVICE_INTERFACE_DATA* itf, SP_DEVINFO_DATA* info, QString* path) { DWORD need = 0; SetupDiGetDeviceInterfaceDetailW(set, itf, nullptr, 0, &need, info); if (need == 0) return false; QByteArray buf(static_cast(need), 0); auto* detail = reinterpret_cast(buf.data()); detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W); if (!SetupDiGetDeviceInterfaceDetailW(set, itf, detail, need, nullptr, info)) return false; *path = QString::fromWCharArray(detail->DevicePath); return true; } QUuid QtUuid(const BTH_LE_UUID& uuid) { if (uuid.IsShortUuid) return QUuid(QStringLiteral("{%1-0000-1000-8000-00805F9B34FB}").arg(uuid.Value.ShortUuid, 4, 16, QLatin1Char('0'))); const GUID& g = uuid.Value.LongUuid; return QUuid(g.Data1, g.Data2, g.Data3, g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3], g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7]); } QString CharText(const BTH_LE_GATT_CHARACTERISTIC& c) { const QString uuid = QtUuid(c.CharacteristicUuid).toString(QUuid::WithoutBraces).toUpper(); return uuid == QStringLiteral("00000000-0000-0000-0000-000000000000") ? QStringLiteral("HANDLE 0x%1").arg(c.CharacteristicValueHandle, 4, 16, QLatin1Char('0')).toUpper() : uuid; } HANDLE OpenService(const QString& path) { HANDLE handle = CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); return handle != INVALID_HANDLE_VALUE ? handle : CreateFileW(reinterpret_cast(path.utf16()), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); } void QueuePacket( Dri_Ble_Struct_Context* ctx, Mid_Enum_RawPacketSource source, const QString& portName, const QByteArray& bytes) { if ((ctx == nullptr) || bytes.isEmpty()) return; Mid_Struct_RawPacket packet; packet.IsValid = true; packet.Source = source; packet.PortName = portName; packet.ByteArray = bytes; QMutexLocker lock(&ctx->Mutex); ctx->Queue.append(packet); } QVector EnumServices() { QVector out; HDEVINFO set = SetupDiGetClassDevsW(&GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (set == INVALID_HANDLE_VALUE) return out; SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE, i, &itf); ++i) { SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; QString path; if (!InterfacePath(set, &itf, &info, &path)) continue; ServiceIf item; item.Path = path; item.InstanceId = DeviceInstanceId(set, &info); item.Uuid = ServiceUuidFromId(item.InstanceId); out.append(item); } SetupDiDestroyDeviceInfoList(set); return out; } QVector EnumBleDevices() { QVector out; HDEVINFO set = SetupDiGetClassDevsW( &GUID_BLUETOOTHLE_DEVICE_INTERFACE, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (set == INVALID_HANDLE_VALUE) return out; SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &GUID_BLUETOOTHLE_DEVICE_INTERFACE, i, &itf); ++i) { SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; QString path; if (!InterfacePath(set, &itf, &info, &path)) continue; DeviceIf item; item.Path = path; item.InstanceId = DeviceInstanceId(set, &info); item.Address = ExtractAddress(item.InstanceId); if (item.Address.isEmpty()) continue; out.append(item); } SetupDiDestroyDeviceInfoList(set); return out; } QVector EnumBleHids() { QVector out; GUID hidGuid; HidD_GetHidGuid(&hidGuid); HDEVINFO set = SetupDiGetClassDevsW(&hidGuid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); if (set == INVALID_HANDLE_VALUE) return out; SP_DEVICE_INTERFACE_DATA itf = { sizeof(SP_DEVICE_INTERFACE_DATA) }; for (DWORD i = 0; SetupDiEnumDeviceInterfaces(set, nullptr, &hidGuid, i, &itf); ++i) { SP_DEVINFO_DATA info = { sizeof(SP_DEVINFO_DATA) }; QString path; if (!InterfacePath(set, &itf, &info, &path)) continue; const QString id = DeviceInstanceId(set, &info).toUpper(); if (!id.contains(kSvcHidBrace)) continue; HANDLE handle = CreateFileW(reinterpret_cast(path.utf16()), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); if (handle == INVALID_HANDLE_VALUE) continue; HIDD_ATTRIBUTES attrs = {}; attrs.Size = sizeof(attrs); PHIDP_PREPARSED_DATA prep = nullptr; HIDP_CAPS caps = {}; const bool ok = HidD_GetAttributes(handle, &attrs) && HidD_GetPreparsedData(handle, &prep) && (HidP_GetCaps(prep, &caps) == HIDP_STATUS_SUCCESS) && IsUsefulHid(caps.UsagePage, caps.Usage) && (caps.InputReportByteLength > 0); if (prep != nullptr) HidD_FreePreparsedData(prep); CloseHandle(handle); if (!ok) continue; HidIf item; item.Path = path; item.InstanceId = id; item.VendorId = attrs.VendorID; item.ProductId = attrs.ProductID; item.UsagePage = caps.UsagePage; item.Usage = caps.Usage; item.InputLength = caps.InputReportByteLength; item.OutputLength = caps.OutputReportByteLength; out.append(item); } SetupDiDestroyDeviceInfoList(set); return out; } QVector Characteristics(HANDLE handle) { USHORT count = 0; HRESULT hr = BluetoothGATTGetCharacteristics(handle, nullptr, 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {}; QVector out(count); hr = BluetoothGATTGetCharacteristics(handle, nullptr, count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr)) return {}; out.resize(count); return out; } QVector Descriptors(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c) { USHORT count = 0; HRESULT hr = BluetoothGATTGetDescriptors(handle, const_cast(&c), 0, nullptr, &count, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return {}; QVector out(count); hr = BluetoothGATTGetDescriptors(handle, const_cast(&c), count, out.data(), &count, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr)) return {}; out.resize(count); return out; } bool ReadValue(HANDLE handle, const BTH_LE_GATT_CHARACTERISTIC& c, QByteArray* out) { USHORT need = 0; HRESULT hr = BluetoothGATTGetCharacteristicValue(handle, const_cast(&c), 0, nullptr, &need, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE); if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) return false; if (need == 0) return false; QByteArray buffer(static_cast(need), 0); auto* value = reinterpret_cast(buffer.data()); hr = BluetoothGATTGetCharacteristicValue(handle, const_cast(&c), need, value, nullptr, BLUETOOTH_GATT_FLAG_FORCE_READ_FROM_DEVICE); if (FAILED(hr)) return false; *out = QByteArray(reinterpret_cast(value->Data), static_cast(value->DataSize)); return true; } VOID CALLBACK OnGattEvent(BTH_LE_GATT_EVENT_TYPE type, PVOID out, PVOID context) { auto* sub = static_cast(context); if ((sub == nullptr) || (sub->Service == nullptr) || (sub->Service->Ctx == nullptr)) return; Dri_Ble_Struct_Context* ctx = sub->Service->Ctx; ctx->CallbackCount.fetch_add(1, std::memory_order_acq_rel); if ((type == CharacteristicValueChangedEvent) && (out != nullptr) && (sub->Port != nullptr)) { const auto* evt = static_cast(out); if ((evt->CharacteristicValue != nullptr) && (evt->CharacteristicValue->DataSize > 0)) QueuePacket(ctx, Mid_Enum_RawPacketSource_BleGatt, PortName(sub->DeviceLabel, QStringLiteral("SVC %1 / CHR %2").arg(sub->ServiceUuid, sub->CharUuid)), QByteArray(reinterpret_cast(evt->CharacteristicValue->Data), static_cast(evt->CharacteristicValue->DataSize))); } ctx->CallbackCount.fetch_sub(1, std::memory_order_acq_rel); } bool Subscribe(Dri_Ble_Struct_Port* port, Dri_Ble_Struct_ServiceContext* svc, const QString& deviceLabel, const BTH_LE_GATT_CHARACTERISTIC& c, QString* error) { auto* sub = new Dri_Ble_Struct_Subscription(); sub->Port = port; sub->Service = svc; sub->DeviceLabel = deviceLabel; sub->ServiceUuid = svc->Uuid; sub->CharUuid = CharText(c); BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION reg = {}; reg.NumCharacteristics = 1; reg.Characteristics[0] = c; HRESULT hr = BluetoothGATTRegisterEvent(svc->Handle, CharacteristicValueChangedEvent, ®, OnGattEvent, sub, &sub->Event, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr)) { if (error != nullptr) *error = QStringLiteral("BLE notify register failed: %1 / %2 / %3").arg(svc->Uuid, sub->CharUuid, HResultText(hr)); delete sub; return false; } bool configured = false; for (const BTH_LE_GATT_DESCRIPTOR& d : Descriptors(svc->Handle, c)) { if (d.DescriptorType != ClientCharacteristicConfiguration) continue; BTH_LE_GATT_DESCRIPTOR_VALUE value = {}; value.DescriptorType = ClientCharacteristicConfiguration; value.ClientCharacteristicConfiguration.IsSubscribeToNotification = c.IsNotifiable; value.ClientCharacteristicConfiguration.IsSubscribeToIndication = c.IsIndicatable; if (SUCCEEDED(BluetoothGATTSetDescriptorValue(svc->Handle, const_cast(&d), &value, BLUETOOTH_GATT_FLAG_NONE))) { configured = true; break; } } if (!configured) { if (sub->Event != nullptr) BluetoothGATTUnregisterEvent(sub->Event, BLUETOOTH_GATT_FLAG_NONE); if (error != nullptr) *error = QStringLiteral("BLE notify enable failed: %1 / %2").arg(svc->Uuid, sub->CharUuid); delete sub; return false; } svc->Subs.append(sub); return true; } bool AttachService(Dri_Ble_Struct_Port* port, Dri_Ble_Struct_Context* ctx, const ServiceIf& item, const QString& deviceLabel, int* charCount, int* subCount) { auto* svc = new Dri_Ble_Struct_ServiceContext(); svc->Ctx = ctx; svc->Uuid = item.Uuid; svc->Handle = OpenService(item.Path); if (svc->Handle == INVALID_HANDLE_VALUE) { delete svc; return false; } const auto list = Characteristics(svc->Handle); if (list.isEmpty()) { CloseHandle(svc->Handle); delete svc; return false; } bool keep = false; for (const BTH_LE_GATT_CHARACTERISTIC& c : list) { if (!c.IsReadable && !c.IsNotifiable && !c.IsIndicatable) continue; const QString charUuid = CharText(c); ++(*charCount); if (c.IsNotifiable || c.IsIndicatable) { if (Subscribe(port, svc, deviceLabel, c, nullptr)) { keep = true; ++(*subCount); } } if (c.IsReadable) { QByteArray bytes; if (ReadValue(svc->Handle, c, &bytes) && !bytes.isEmpty()) QueuePacket(ctx, Mid_Enum_RawPacketSource_BleGatt, PortName(deviceLabel, QStringLiteral("SVC %1 / CHR %2").arg(item.Uuid, charUuid)), bytes); } } if (keep) { ctx->Services.append(svc); return true; } CloseHandle(svc->Handle); delete svc; return true; } void CloseHidPort(Dri_Ble_Struct_HidPort* hid) { Dri_Hid_CloseReadPort(&hid->ReadPort); if ((hid->WriteHandle != nullptr) && (hid->WriteHandle != INVALID_HANDLE_VALUE)) CloseHandle(hid->WriteHandle); delete hid; } bool OpenHidPort(Dri_Ble_Struct_Context* ctx, const HidIf& item, const QString& deviceLabel) { auto* hid = new Dri_Ble_Struct_HidPort(); const QString source = HidSource(item.UsagePage, item.Usage); hid->UsagePage = item.UsagePage; hid->Usage = item.Usage; hid->OutputLength = item.OutputLength; QString textError; if (!Dri_Hid_InitReadPort( &hid->ReadPort, item.Path, item.InputLength, HidPacketSource(item.UsagePage, item.Usage), PortName(deviceLabel, source), &textError)) { delete hid; return false; } if (item.OutputLength > 0) { hid->WriteHandle = Dri_Hid_OpenWriteHandle(item.Path, nullptr); } ctx->Hids.append(hid); return true; } QString FormatAddressLabel(const QString& Address) { const QString Formatted = Mid_FormatBleAddress(Address); return Formatted.isEmpty() ? QStringLiteral("(unknown)") : Formatted; } bool ParsePnpIdValue(const QByteArray& bytes, quint16* vendorId, quint16* productId) { if (bytes.size() < 7) return false; const quint8 vendorSource = static_cast(bytes.at(0)); // Only accept USB-IF sourced PnP IDs so the fields map directly to USB VID/PID. if (vendorSource != 0x02U) return false; *vendorId = static_cast(bytes.at(1)) | (static_cast(static_cast(bytes.at(2))) << 8); *productId = static_cast(bytes.at(3)) | (static_cast(static_cast(bytes.at(4))) << 8); return true; } QVector EnumPnps(const QVector& deviceList, QStringList* p_DebugList) { QVector out; for (const DeviceIf& device : deviceList) { HANDLE handle = OpenService(device.Path); if (handle == INVALID_HANDLE_VALUE) { if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: 打开 BLE 设备失败 %1") .arg(FormatAddressLabel(device.Address))); } continue; } USHORT serviceCount = 0; HRESULT hr = BluetoothGATTGetServices( handle, 0, nullptr, &serviceCount, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) { if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: 枚举服务失败 %1 / hr=%2") .arg(FormatAddressLabel(device.Address), HResultText(hr))); } CloseHandle(handle); continue; } QVector services(serviceCount); hr = BluetoothGATTGetServices( handle, serviceCount, services.data(), &serviceCount, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr)) { if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: 读取服务列表失败 %1 / hr=%2") .arg(FormatAddressLabel(device.Address), HResultText(hr))); } CloseHandle(handle); continue; } services.resize(serviceCount); QByteArray valueBytes; bool found = false; quint16 vendorId = 0; quint16 productId = 0; QStringList serviceUuidList; QStringList charUuidList; bool hasPnpChar = false; for (const BTH_LE_GATT_SERVICE& service : services) { const QUuid serviceUuid = QtUuid(service.ServiceUuid); const QString serviceUuidText = serviceUuid.toString(QUuid::WithoutBraces).toUpper(); serviceUuidList.append(serviceUuidText); USHORT charCount = 0; hr = BluetoothGATTGetCharacteristics( handle, const_cast(&service), 0, nullptr, &charCount, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_MORE_DATA))) { if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: 枚举 180A 特征失败 %1 / hr=%2") .arg(FormatAddressLabel(device.Address), HResultText(hr))); } break; } QVector chars(charCount); hr = BluetoothGATTGetCharacteristics( handle, const_cast(&service), charCount, chars.data(), &charCount, BLUETOOTH_GATT_FLAG_NONE); if (FAILED(hr)) { if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: 读取 180A 特征失败 %1 / hr=%2") .arg(FormatAddressLabel(device.Address), HResultText(hr))); } break; } chars.resize(charCount); for (const BTH_LE_GATT_CHARACTERISTIC& characteristic : chars) { const QString charUuid = CharText(characteristic); charUuidList.append(QStringLiteral("%1/%2").arg(serviceUuidText, charUuid)); if (charUuid != kChrPnpId) continue; hasPnpChar = true; if (!ReadValue(handle, characteristic, &valueBytes)) { if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: 发现 2A50 但读取失败 %1 / svc=%2") .arg(FormatAddressLabel(device.Address), serviceUuidText)); } break; } found = ParsePnpIdValue(valueBytes, &vendorId, &productId); if (p_DebugList != nullptr) { p_DebugList->append( QStringLiteral("PnP: %1 / svc=%2 / raw=%3 / parsed=%4") .arg(FormatAddressLabel(device.Address)) .arg(serviceUuidText) .arg(Mid_GetHexText(valueBytes)) .arg(found ? FormatVidPidLabel(vendorId, productId) : QStringLiteral("invalid"))); } break; } break; } CloseHandle(handle); if (!hasPnpChar && (p_DebugList != nullptr)) { p_DebugList->append( QStringLiteral("PnP: 未发现 2A50 %1 / services=%2 / chars=%3") .arg(FormatAddressLabel(device.Address)) .arg(serviceUuidList.join(QStringLiteral(", "))) .arg(charUuidList.join(QStringLiteral(", ")))); } if (!found) continue; bool exists = false; for (const PnpIf& item : out) { if (item.Address == device.Address) { exists = true; break; } } if (exists) continue; PnpIf item; item.Address = device.Address; item.VendorId = vendorId; item.ProductId = productId; out.append(item); } return out; } QStringList CollectPnpAddresses( const QVector& pnpList, quint16 vendorId, quint16 productId) { QStringList addressList; for (const PnpIf& pnp : pnpList) { if ((pnp.VendorId == vendorId) && (pnp.ProductId == productId) && !addressList.contains(pnp.Address)) { addressList.append(pnp.Address); } } return addressList; } QStringList CollectHidAddresses(const QVector& HidList) { QStringList AddressList; for (const HidIf& Hid : HidList) { const QString Address = ExtractAddress(Hid.InstanceId); if (!Address.isEmpty() && !AddressList.contains(Address)) AddressList.append(Address); } return AddressList; } QString BuildCandidateSummary( const QVector& pnpList, const QVector& HidList, const QVector& ServiceList, const QStringList& AllowedAddressList) { QHash HidCountMap; QHash GattCountMap; QHash PnpTextMap; for (const PnpIf& pnp : pnpList) { PnpTextMap.insert( pnp.Address, FormatVidPidLabel(pnp.VendorId, pnp.ProductId)); } for (const HidIf& Hid : HidList) { const QString Address = ExtractAddress(Hid.InstanceId); if (!Address.isEmpty()) ++HidCountMap[Address]; } for (const ServiceIf& Service : ServiceList) { if (Service.Uuid != kSvcCustom) continue; const QString Address = ExtractAddress(Service.InstanceId); if (!Address.isEmpty() && AllowedAddressList.contains(Address)) ++GattCountMap[Address]; } QStringList AddressList = HidCountMap.keys(); for (const QString& Address : GattCountMap.keys()) if (!AddressList.contains(Address)) AddressList.append(Address); if (AddressList.isEmpty()) return QStringLiteral("无"); std::sort(AddressList.begin(), AddressList.end()); QStringList SummaryList; for (const QString& Address : AddressList) { SummaryList.append( QStringLiteral("%1 [pnp=%2, hid=%3, gatt=%4]") .arg(FormatAddressLabel(Address)) .arg(PnpTextMap.value(Address, QStringLiteral("unknown"))) .arg(HidCountMap.value(Address)) .arg(GattCountMap.value(Address))); } return SummaryList.join(QLatin1Char('\n')); } QString BuildPnpSummary( const QVector& pnpList, quint16 vendorId, quint16 productId) { QStringList lineList; for (const PnpIf& pnp : pnpList) { lineList.append( QStringLiteral("%1 -> %2") .arg(FormatAddressLabel(pnp.Address)) .arg(FormatVidPidLabel(pnp.VendorId, pnp.ProductId))); } if (lineList.isEmpty()) { return QStringLiteral( "PnP 扫描结果:未发现任何 DIS/PnP ID。"); } QString summary = QStringLiteral( "PnP 扫描结果(目标 %1):\n%2") .arg(FormatVidPidLabel(vendorId, productId)) .arg(lineList.join(QLatin1Char('\n'))); return summary; } QString BuildPnpProbeSummary(const QStringList& debugList) { if (debugList.isEmpty()) { return QStringLiteral("PnP 探测日志:无。"); } return QStringLiteral("PnP 探测日志:\n%1").arg(debugList.join(QLatin1Char('\n'))); } } // namespace void Dri_Ble_Close(Dri_Ble_Struct_Port* port) { Dri_Ble_Struct_Context* ctx = port->p_Context; if (ctx != nullptr) { for (Dri_Ble_Struct_HidPort* hid : ctx->Hids) CloseHidPort(hid); for (Dri_Ble_Struct_ServiceContext* svc : ctx->Services) for (Dri_Ble_Struct_Subscription* sub : svc->Subs) { sub->Port = nullptr; if (sub->Event != nullptr) BluetoothGATTUnregisterEvent(sub->Event, BLUETOOTH_GATT_FLAG_NONE); } while (ctx->CallbackCount.load(std::memory_order_acquire) > 0) Sleep(1); for (Dri_Ble_Struct_ServiceContext* svc : ctx->Services) { for (Dri_Ble_Struct_Subscription* sub : svc->Subs) delete sub; if ((svc->Handle != nullptr) && (svc->Handle != INVALID_HANDLE_VALUE)) CloseHandle(svc->Handle); delete svc; } delete ctx; } *port = Dri_Ble_Struct_Port(); } bool Dri_Ble_Init(Dri_Ble_Struct_Port* port, const Mid_Struct_DeviceConfig& deviceConfig, QString* textStatus) { Dri_Ble_Close(port); port->IsOpened = true; port->p_Context = new Dri_Ble_Struct_Context(); const QVector devices = EnumBleDevices(); const QVector hids = EnumBleHids(); const QVector services = EnumServices(); QStringList pnpDebugList; const QVector pnps = EnumPnps(devices, &pnpDebugList); const QStringList HidAddressList = CollectPnpAddresses( pnps, deviceConfig.VendorId, deviceConfig.ProductId); QString targetAddress; QString SelectStatus; if (HidAddressList.size() == 1) { targetAddress = HidAddressList.first(); } else { SelectStatus = HidAddressList.isEmpty() ? QStringLiteral("未检测到匹配 PnP ID %1 的 BLE 设备。") .arg(FormatVidPidLabel(deviceConfig.VendorId, deviceConfig.ProductId)) : QStringLiteral("检测到多个匹配 PnP ID %1 的 BLE 设备,无法唯一绑定。") .arg(FormatVidPidLabel(deviceConfig.VendorId, deviceConfig.ProductId)); } QString deviceLabel = targetAddress.isEmpty() ? QString() : DeviceLabel(targetAddress); int charCount = 0; int subCount = 0; if (!targetAddress.isEmpty()) { for (const ServiceIf& item : services) { if ((item.Uuid == kSvcCustom) && (ExtractAddress(item.InstanceId) == targetAddress)) AttachService(port, port->p_Context, item, deviceLabel, &charCount, &subCount); } } if (deviceLabel.isEmpty() && !targetAddress.isEmpty()) deviceLabel = DeviceLabel(targetAddress); int hidCount = 0; if (!targetAddress.isEmpty()) { for (const HidIf& hid : hids) { if ((ExtractAddress(hid.InstanceId) == targetAddress) && OpenHidPort(port->p_Context, hid, deviceLabel)) ++hidCount; } } if (deviceLabel.isEmpty() && !hids.isEmpty()) deviceLabel = QStringLiteral("BLE"); port->IsConnected = (charCount > 0) || (hidCount > 0); return port->IsConnected; } bool Dri_Ble_Read(Dri_Ble_Struct_Port* port, Mid_Struct_RawPacket* packet, QString*) { *packet = Mid_Struct_RawPacket(); packet->PortName = QStringLiteral("Bluetooth"); if (!port->IsOpened || (port->p_Context == nullptr)) return false; for (Dri_Ble_Struct_HidPort* hid : port->p_Context->Hids) { Mid_Struct_RawPacket hidPacket; if (Dri_Hid_Read(&hid->ReadPort, &hidPacket, nullptr)) { QMutexLocker lock(&port->p_Context->Mutex); port->p_Context->Queue.append(hidPacket); } } QMutexLocker lock(&port->p_Context->Mutex); if (port->p_Context->Queue.isEmpty()) return false; *packet = port->p_Context->Queue.takeFirst(); return packet->IsValid; } bool Dri_Ble_Write(Dri_Ble_Struct_Port* port, const QByteArray& bytes, QString* textStatus) { if (!port->IsOpened || (port->p_Context == nullptr)) { if (textStatus != nullptr) *textStatus = QStringLiteral("Bluetooth port is not open, packet send was skipped."); return false; } if (bytes.isEmpty()) { if (textStatus != nullptr) *textStatus = QStringLiteral("Bluetooth packet is empty."); return false; } const auto TryWrite = [&](auto MatchFunc, const QString& OkText) { for (Dri_Ble_Struct_HidPort* hid : port->p_Context->Hids) { if (!MatchFunc(hid->UsagePage, hid->Usage) || (hid->WriteHandle == INVALID_HANDLE_VALUE)) continue; if (Dri_Hid_WritePacket(hid->WriteHandle, hid->OutputLength, bytes, nullptr)) { if (textStatus != nullptr) *textStatus = OkText; return true; } } return false; }; if (TryWrite(IsCommandHid, QStringLiteral("Bluetooth Vendor command packet sent."))) return true; if (TryWrite(IsVendorHid, QStringLiteral("Bluetooth Vendor packet sent."))) return true; if (textStatus != nullptr) *textStatus = QStringLiteral("No writable Bluetooth Vendor endpoint was found."); return false; }