diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c9ae497a4..f1ebba3240 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux") option(ENABLE_VTUNE "Enable Intel VTune integration for JIT code." OFF) if(NOT ANDROID) + option(ENABLE_HWDB "Enables the udev hardware database" ON) option(ENABLE_EVDEV "Enables the evdev controller backend" ON) endif() endif() @@ -571,6 +572,16 @@ if(OPROFILING) endif() endif() +if(ENABLE_HWDB) + find_package(LIBUDEV REQUIRED) + if(LIBUDEV_FOUND) + message(STATUS "libudev found, enabling hardware database") + add_definitions(-DHAVE_LIBUDEV=1) + else() + message(FATAL_ERROR "Couldn't find libudev. Can't build hardware database.\nDisable ENABLE_HWDB if you wish to build without hardware database support") + endif() +endif() + if(ENABLE_EVDEV) find_package(LIBUDEV REQUIRED) find_package(LIBEVDEV REQUIRED) diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp index e3a6a660ed..fae0c6e662 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.cpp @@ -29,6 +29,7 @@ #include "Core/Core.h" #include "Core/HW/Memmap.h" #include "Core/IOS/Device.h" +#include "Core/IOS/USB/Host.h" #include "Core/System.h" #include "VideoCommon/OnScreenDisplay.h" @@ -38,27 +39,17 @@ constexpr u8 REQUEST_TYPE = static_cast(LIBUSB_ENDPOINT_OUT) | static_cast(LIBUSB_REQUEST_TYPE_CLASS) | static_cast(LIBUSB_RECIPIENT_INTERFACE); -static bool IsWantedDevice(const libusb_device_descriptor& descriptor) -{ - const int vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); - const int pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); - if (vid == -1 || pid == -1) - return true; - return descriptor.idVendor == vid && descriptor.idProduct == pid; -} - -static bool IsBluetoothDevice(const libusb_interface_descriptor& descriptor) +static bool IsBluetoothDevice(const libusb_device_descriptor& descriptor) { constexpr u8 SUBCLASS = 0x01; constexpr u8 PROTOCOL_BLUETOOTH = 0x01; - if (Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID) != -1 && - Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID) != -1) - { - return true; - } - return descriptor.bInterfaceClass == LIBUSB_CLASS_WIRELESS && - descriptor.bInterfaceSubClass == SUBCLASS && - descriptor.bInterfaceProtocol == PROTOCOL_BLUETOOTH; + // Some devices misreport their class, so we avoid relying solely on descriptor checks and allow + // users to specify their own VID/PID. + return BluetoothRealDevice::IsConfiguredBluetoothDevice(descriptor.idVendor, + descriptor.idProduct) || + (descriptor.bDeviceClass == LIBUSB_CLASS_WIRELESS && + descriptor.bDeviceSubClass == SUBCLASS && + descriptor.bDeviceProtocol == PROTOCOL_BLUETOOTH); } BluetoothRealDevice::BluetoothRealDevice(EmulationKernel& ios, const std::string& device_name) @@ -82,6 +73,13 @@ BluetoothRealDevice::~BluetoothRealDevice() SaveLinkKeys(); } +bool BluetoothRealDevice::IsConfiguredBluetoothDevice(u16 vid, u16 pid) +{ + const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); + const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); + return configured_vid == vid && configured_pid == pid; +} + std::optional BluetoothRealDevice::Open(const OpenRequest& request) { if (!m_context.IsValid()) @@ -101,10 +99,7 @@ std::optional BluetoothRealDevice::Open(const OpenRequest& request) return true; } - const libusb_interface& interface = config_descriptor->interface[INTERFACE]; - const libusb_interface_descriptor& descriptor = interface.altsetting[0]; - if (IsBluetoothDevice(descriptor) && IsWantedDevice(device_descriptor) && - OpenDevice(device_descriptor, device)) + if (IsBluetoothDevice(device_descriptor) && OpenDevice(device_descriptor, device)) { unsigned char manufacturer[50] = {}, product[50] = {}, serial_number[50] = {}; const int manufacturer_ret = libusb_get_string_descriptor_ascii( @@ -677,6 +672,42 @@ bool BluetoothRealDevice::OpenDevice(const libusb_device_descriptor& device_desc return true; } +std::vector BluetoothRealDevice::ListDevices() +{ + std::vector device_list; + LibusbUtils::Context context; + + if (!context.IsValid()) + return {}; + + int result = context.GetDeviceList([&device_list](libusb_device* device) { + libusb_device_descriptor desc; + + auto [config_ret, config] = LibusbUtils::MakeConfigDescriptor(device, 0); + if (config_ret != LIBUSB_SUCCESS) + return true; + + if (libusb_get_device_descriptor(device, &desc) != LIBUSB_SUCCESS) + return true; + + if (IsBluetoothDevice(desc)) + { + const std::string device_name = + USBHost::GetDeviceNameFromVIDPID(desc.idVendor, desc.idProduct); + device_list.push_back({desc.idVendor, desc.idProduct, device_name}); + } + return true; + }); + + if (result < 0) + { + ERROR_LOG_FMT(IOS_USB, "Failed to get device list: {}", LibusbUtils::ErrorWrap(result)); + return device_list; + } + + return device_list; +} + // The callbacks are called from libusb code on a separate thread. void BluetoothRealDevice::HandleCtrlTransfer(libusb_transfer* tr) { diff --git a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h index 16002a848b..96a18c0617 100644 --- a/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h +++ b/Source/Core/Core/IOS/USB/Bluetooth/BTReal.h @@ -58,6 +58,17 @@ public: void HandleCtrlTransfer(libusb_transfer* finished_transfer); void HandleBulkOrIntrTransfer(libusb_transfer* finished_transfer); + static bool IsConfiguredBluetoothDevice(u16 vid, u16 pid); + + struct BluetoothDeviceInfo + { + u16 vid; + u16 pid; + std::string name; + }; + + static std::vector ListDevices(); + private: static constexpr u8 INTERFACE = 0x00; // Arbitrarily chosen value that allows emulated software to send commands often enough diff --git a/Source/Core/Core/IOS/USB/Host.cpp b/Source/Core/Core/IOS/USB/Host.cpp index e85c3a52f6..03598f0d00 100644 --- a/Source/Core/Core/IOS/USB/Host.cpp +++ b/Source/Core/Core/IOS/USB/Host.cpp @@ -11,6 +11,14 @@ #include #include +#ifdef __LIBUSB__ +#include +#endif + +#ifdef HAVE_LIBUDEV +#include +#endif + #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" #include "Common/Logging/Log.h" @@ -45,6 +53,68 @@ std::optional USBHost::Open(const OpenRequest& request) return IPCReply(IPC_SUCCESS); } +std::string USBHost::GetDeviceNameFromVIDPID(u16 vid, u16 pid) +{ + std::string device_name; +#ifdef __LIBUSB__ + LibusbUtils::Context context; + + if (!context.IsValid()) + return device_name; + + context.GetDeviceList([&device_name, vid, pid](libusb_device* device) { + libusb_device_descriptor desc; + + if (libusb_get_device_descriptor(device, &desc) != LIBUSB_SUCCESS) + return true; + + if (desc.idVendor == vid && desc.idProduct == pid) + { + libusb_device_handle* handle; + if (libusb_open(device, &handle) == LIBUSB_SUCCESS) + { + unsigned char buffer[256]; + if (desc.iProduct && + libusb_get_string_descriptor_ascii(handle, desc.iProduct, buffer, sizeof(buffer)) > 0) + { + device_name = reinterpret_cast(buffer); + libusb_close(handle); + } + } + return false; + } + return true; + }); + + if (!device_name.empty()) + return device_name; +#endif +#ifdef HAVE_LIBUDEV + udev* udev = udev_new(); + if (!udev) + return device_name; + + udev_hwdb* hwdb = udev_hwdb_new(udev); + if (hwdb) + { + const std::string modalias = fmt::format("usb:v{:04X}p{:04X}*", vid, pid); + udev_list_entry* entries = udev_hwdb_get_properties_list_entry(hwdb, modalias.c_str(), 0); + + if (entries) + { + udev_list_entry* device_name_entry = + udev_list_entry_get_by_name(entries, "ID_MODEL_FROM_DATABASE"); + if (device_name_entry) + { + device_name = udev_list_entry_get_value(device_name_entry); + } + } + udev_hwdb_unref(hwdb); + } +#endif + return device_name; +} + void USBHost::DoState(PointerWrap& p) { Device::DoState(p); diff --git a/Source/Core/Core/IOS/USB/Host.h b/Source/Core/Core/IOS/USB/Host.h index 6c1c488ee9..e457c2bc5b 100644 --- a/Source/Core/Core/IOS/USB/Host.h +++ b/Source/Core/Core/IOS/USB/Host.h @@ -32,6 +32,7 @@ public: void DoState(PointerWrap& p) override; void OnDevicesChanged(const USBScanner::DeviceMap& new_devices); + static std::string GetDeviceNameFromVIDPID(u16 vid, u16 pid); protected: enum class ChangeEvent diff --git a/Source/Core/DolphinQt/Config/ControllersWindow.cpp b/Source/Core/DolphinQt/Config/ControllersWindow.cpp index 2470910e8e..bb0e7a0489 100644 --- a/Source/Core/DolphinQt/Config/ControllersWindow.cpp +++ b/Source/Core/DolphinQt/Config/ControllersWindow.cpp @@ -27,6 +27,7 @@ void ControllersWindow::showEvent(QShowEvent* event) { QDialog::showEvent(event); m_wiimote_controllers->UpdateBluetoothAvailableStatus(); + m_wiimote_controllers->RefreshBluetoothAdapters(); } void ControllersWindow::CreateMainLayout() diff --git a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp index 9159402be2..69de3837ad 100644 --- a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp +++ b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -57,6 +58,50 @@ void WiimoteControllersWidget::UpdateBluetoothAvailableStatus() m_bluetooth_unavailable->setHidden(WiimoteReal::IsScannerReady()); } +void WiimoteControllersWidget::RefreshBluetoothAdapters() +{ + m_bluetooth_adapters->clear(); + + const int configured_vid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); + const int configured_pid = Config::Get(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); + bool found_configured_device = configured_vid == -1 || configured_pid == -1; + + m_bluetooth_adapters->addItem(tr("Automatic")); + + for (auto& device : IOS::HLE::BluetoothRealDevice::ListDevices()) + { + std::string name = device.name.empty() ? tr("Unknown Device").toStdString() : device.name; + QString device_info = + QString::fromStdString(fmt::format("{} ({:04x}:{:04x})", name, device.vid, device.pid)); + m_bluetooth_adapters->addItem(device_info, QVariant::fromValue(device)); + + if (!found_configured_device && + IOS::HLE::BluetoothRealDevice::IsConfiguredBluetoothDevice(device.vid, device.pid)) + { + found_configured_device = true; + m_bluetooth_adapters->setCurrentIndex(m_bluetooth_adapters->count() - 1); + } + } + + if (!found_configured_device) + { + const QString name = QLatin1Char{'['} + tr("disconnected") + QLatin1Char(']'); + const std::string name_str = name.toStdString(); + + IOS::HLE::BluetoothRealDevice::BluetoothDeviceInfo disconnected_device; + disconnected_device.vid = configured_vid; + disconnected_device.pid = configured_pid; + disconnected_device.name = name_str; + + QString device_info = QString::fromStdString( + fmt::format("{} ({:04x}:{:04x})", name_str, configured_vid, configured_pid)); + + m_bluetooth_adapters->insertSeparator(m_bluetooth_adapters->count()); + m_bluetooth_adapters->addItem(device_info, QVariant::fromValue(disconnected_device)); + m_bluetooth_adapters->setCurrentIndex(m_bluetooth_adapters->count() - 1); + } +} + static int GetRadioButtonIndicatorWidth() { const QStyle* style = QApplication::style(); @@ -104,6 +149,8 @@ void WiimoteControllersWidget::CreateLayout() m_wiimote_box->setLayout(m_wiimote_layout); m_wiimote_passthrough = new QRadioButton(tr("Passthrough a Bluetooth adapter")); + m_bluetooth_adapters_label = new QLabel(tr("Bluetooth adapter")); + m_bluetooth_adapters = new QComboBox(); m_wiimote_sync = new NonDefaultQPushButton(tr("Sync")); m_wiimote_reset = new NonDefaultQPushButton(tr("Reset")); m_wiimote_refresh = new NonDefaultQPushButton(tr("Refresh")); @@ -123,6 +170,10 @@ void WiimoteControllersWidget::CreateLayout() // Passthrough BT m_wiimote_layout->addWidget(m_wiimote_passthrough, m_wiimote_layout->rowCount(), 0, 1, -1); + int adapter_row = m_wiimote_layout->rowCount(); + m_wiimote_layout->addWidget(m_bluetooth_adapters_label, adapter_row, 1, 1, 1); + m_wiimote_layout->addWidget(m_bluetooth_adapters, adapter_row, 2, 1, 2); + int sync_row = m_wiimote_layout->rowCount(); m_wiimote_layout->addWidget(m_wiimote_pt_labels[0], sync_row, 1, 1, 2); m_wiimote_layout->addWidget(m_wiimote_sync, sync_row, 3); @@ -189,6 +240,8 @@ void WiimoteControllersWidget::ConnectWidgets() &WiimoteControllersWidget::SaveSettings); connect(m_wiimote_speaker_data, &QCheckBox::toggled, this, &WiimoteControllersWidget::SaveSettings); + connect(m_bluetooth_adapters, &QComboBox::activated, this, + &WiimoteControllersWidget::OnBluetoothPassthroughDeviceChanged); connect(m_wiimote_sync, &QPushButton::clicked, this, &WiimoteControllersWidget::OnBluetoothPassthroughSyncPressed); connect(m_wiimote_reset, &QPushButton::clicked, this, @@ -207,6 +260,25 @@ void WiimoteControllersWidget::ConnectWidgets() } } +void WiimoteControllersWidget::OnBluetoothPassthroughDeviceChanged(int index) +{ + // "Automatic" selection + if (index == 0) + { + Config::DeleteKey(Config::GetActiveLayerForConfig(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID), + Config::MAIN_BLUETOOTH_PASSTHROUGH_PID); + Config::DeleteKey(Config::GetActiveLayerForConfig(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID), + Config::MAIN_BLUETOOTH_PASSTHROUGH_VID); + return; + } + + auto device_info = m_bluetooth_adapters->itemData(index) + .value(); + + Config::SetBaseOrCurrent(Config::MAIN_BLUETOOTH_PASSTHROUGH_PID, device_info.pid); + Config::SetBaseOrCurrent(Config::MAIN_BLUETOOTH_PASSTHROUGH_VID, device_info.vid); +} + void WiimoteControllersWidget::OnBluetoothPassthroughResetPressed() { const auto ios = Core::System::GetInstance().GetIOS(); @@ -297,11 +369,15 @@ void WiimoteControllersWidget::LoadSettings(Core::State state) m_wiimote_passthrough->setEnabled(!running); const bool running_gc = running && !Core::System::GetInstance().IsWii(); + const bool running_wii = running && Core::System::GetInstance().IsWii(); const bool enable_passthrough = m_wiimote_passthrough->isChecked() && !running_gc; + const bool enable_adapter_selection = m_wiimote_passthrough->isChecked() && !running_wii; const bool enable_emu_bt = !m_wiimote_passthrough->isChecked() && !running_gc; const bool is_netplay = NetPlay::IsNetPlayRunning(); const bool running_netplay = running && is_netplay; + m_bluetooth_adapters_label->setEnabled(enable_adapter_selection); + m_bluetooth_adapters->setEnabled(enable_adapter_selection); m_wiimote_sync->setEnabled(enable_passthrough); m_wiimote_reset->setEnabled(enable_passthrough); diff --git a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h index d81e6f0653..bb2f50caad 100644 --- a/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h +++ b/Source/Core/DolphinQt/Config/WiimoteControllersWidget.h @@ -28,9 +28,11 @@ public: explicit WiimoteControllersWidget(QWidget* parent); void UpdateBluetoothAvailableStatus(); + void RefreshBluetoothAdapters(); private: void SaveSettings(); + void OnBluetoothPassthroughDeviceChanged(int index); void OnBluetoothPassthroughSyncPressed(); void OnBluetoothPassthroughResetPressed(); void OnWiimoteRefreshPressed(); @@ -50,6 +52,8 @@ private: QRadioButton* m_wiimote_emu; QRadioButton* m_wiimote_passthrough; + QLabel* m_bluetooth_adapters_label; + QComboBox* m_bluetooth_adapters; QPushButton* m_wiimote_sync; QPushButton* m_wiimote_reset; QCheckBox* m_wiimote_continuous_scanning;