Add a GUI option to select a bluetooth device

This commit is contained in:
Joshua Vandaële 2025-02-28 08:07:17 +01:00
parent 826625c7be
commit 4773a6f323
No known key found for this signature in database
GPG Key ID: 5E8F4E7EDBD390EA
8 changed files with 227 additions and 22 deletions

View File

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

View File

@ -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<u8>(LIBUSB_ENDPOINT_OUT) |
static_cast<u8>(LIBUSB_REQUEST_TYPE_CLASS) |
static_cast<u8>(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<IPCReply> BluetoothRealDevice::Open(const OpenRequest& request)
{
if (!m_context.IsValid())
@ -101,10 +99,7 @@ std::optional<IPCReply> 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::BluetoothDeviceInfo> BluetoothRealDevice::ListDevices()
{
std::vector<BluetoothDeviceInfo> 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)
{

View File

@ -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<BluetoothDeviceInfo> ListDevices();
private:
static constexpr u8 INTERFACE = 0x00;
// Arbitrarily chosen value that allows emulated software to send commands often enough

View File

@ -11,6 +11,14 @@
#include <string>
#include <utility>
#ifdef __LIBUSB__
#include <libusb.h>
#endif
#ifdef HAVE_LIBUDEV
#include <libudev.h>
#endif
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
@ -45,6 +53,68 @@ std::optional<IPCReply> 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<char*>(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);

View File

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

View File

@ -27,6 +27,7 @@ void ControllersWindow::showEvent(QShowEvent* event)
{
QDialog::showEvent(event);
m_wiimote_controllers->UpdateBluetoothAvailableStatus();
m_wiimote_controllers->RefreshBluetoothAdapters();
}
void ControllersWindow::CreateMainLayout()

View File

@ -13,6 +13,7 @@
#include <QRadioButton>
#include <QScreen>
#include <QVBoxLayout>
#include <QVariant>
#include <map>
#include <optional>
@ -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<IOS::HLE::BluetoothRealDevice::BluetoothDeviceInfo>();
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);

View File

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