WiimoteReal/IOLinux: Improvements, fixes, and code cleanups.

This commit is contained in:
Jordan Woyak
2025-09-19 02:27:46 -05:00
parent b2fef6ee1f
commit 38dc8ae3b6
3 changed files with 276 additions and 188 deletions

View File

@@ -4,16 +4,26 @@
#include "Core/HW/WiimoteReal/IOLinux.h"
#include <ranges>
#include <vector>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/l2cap.h>
#include <sys/eventfd.h>
#include <sys/ioctl.h>
#include <sys/poll.h>
#include <sys/signalfd.h>
#include <unistd.h>
#include "Common/CommonFuncs.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Common/Network.h"
#include "Common/ScopeGuard.h"
#include "Common/UnixUtil.h"
#include "Core/Config/MainSettings.h"
namespace WiimoteReal
@@ -21,68 +31,195 @@ namespace WiimoteReal
constexpr u16 L2CAP_PSM_HID_CNTL = 0x0011;
constexpr u16 L2CAP_PSM_HID_INTR = 0x0013;
WiimoteScannerLinux::WiimoteScannerLinux() : m_device_id(-1), m_device_sock(-1)
static void AddAutoConnectAddresses(std::vector<Wiimote*>& found_wiimotes)
{
std::string entries = Config::Get(Config::MAIN_WIIMOTE_AUTO_CONNECT_ADDRESSES);
for (auto& bt_address_str : SplitString(entries, ','))
{
const auto bt_addr = Common::StringToBluetoothAddress(bt_address_str);
if (!bt_addr.has_value())
{
WARN_LOG_FMT(WIIMOTE, "Bad Auto Connect Bluetooth Address: {}", bt_address_str);
continue;
}
Common::ToLower(&bt_address_str);
if (!IsNewWiimote(bt_address_str))
continue;
found_wiimotes.push_back(new WiimoteLinux(*bt_addr));
NOTICE_LOG_FMT(WIIMOTE, "Added Wiimote with fixed address ({}).", bt_address_str);
}
}
WiimoteScannerLinux::WiimoteScannerLinux()
{
Open();
}
bool WiimoteScannerLinux::Open()
{
if (IsReady())
return true;
// Get the id of the first Bluetooth device.
m_device_id = hci_get_route(nullptr);
if (m_device_id < 0)
{
NOTICE_LOG_FMT(WIIMOTE, "Bluetooth not found.");
return;
NOTICE_LOG_FMT(WIIMOTE, "Bluetooth not found. hci_get_route: {}", Common::LastStrerrorString());
return false;
}
// Create a socket to the device
m_device_sock = hci_open_dev(m_device_id);
if (m_device_sock < 0)
{
ERROR_LOG_FMT(WIIMOTE, "Unable to open Bluetooth.");
return;
ERROR_LOG_FMT(WIIMOTE, "Unable to open Bluetooth. hci_open_dev: {}",
Common::LastStrerrorString());
return false;
}
m_is_device_open.store(true, std::memory_order_relaxed);
return true;
}
WiimoteScannerLinux::~WiimoteScannerLinux()
{
if (IsReady())
close(m_device_sock);
Close();
}
void WiimoteScannerLinux::Close()
{
if (!IsReady())
return;
m_is_device_open.store(false, std::memory_order_relaxed);
close(std::exchange(m_device_sock, -1));
m_device_id = -1;
}
bool WiimoteScannerLinux::IsReady() const
{
return m_device_sock > 0;
return m_is_device_open.load(std::memory_order_relaxed);
}
struct InquiryRequest : hci_inquiry_req
{
static constexpr int MAX_INFOS = 255;
std::array<inquiry_info, MAX_INFOS> scan_infos;
};
static_assert(sizeof(InquiryRequest) ==
sizeof(hci_inquiry_req) + sizeof(inquiry_info) * InquiryRequest::MAX_INFOS);
// Returns 0 on success or some error number on failure.
static int HciInquiry(int device_socket, InquiryRequest* request)
{
const auto done_event = UnixUtil::CreateEventFD(0, 0);
Common::ScopeGuard close_guard([&] { close(done_event); });
int hci_inquiry_errorno = 0;
// Unplugging a BT adapter causes `hci_inquiry` to block forever (inside `ioctl`).
// Fortunately it does produce a signal on the socket so we can poll for that.
// Performing the inquiry on thread lets us interrupt `ioctl` if the socket signals.
// Using `pthread_cancel` with `std::thread` isn't technically correct so we use `pthread_create`.
UnixUtil::PThreadWrapper hci_inquiry_thread{[&] {
// We're manually doing the `ioctl` because `hci_inquiry` isn't pthread_cancel-safe.
// It uses `malloc` and whatnot.
const int ret = ioctl(device_socket, HCIINQUIRY, reinterpret_cast<unsigned long>(request));
if (ret < 0)
hci_inquiry_errorno = errno;
// Signal doneness to `poll`.
u64 val = 1;
write(done_event, &val, sizeof(val));
}};
Common::ScopeGuard join_guard([&] { pthread_join(hci_inquiry_thread.handle, nullptr); });
// Wait for the above thread or some socket signal.
std::array<pollfd, 2> pollfds{
pollfd{.fd = device_socket},
pollfd{.fd = done_event, .events = POLLIN},
};
UnixUtil::RetryOnEINTR(poll, pollfds.data(), pollfds.size(), -1);
if (pollfds[0].revents != 0)
{
ERROR_LOG_FMT(WIIMOTE, "HciInquiry device socket had error. Cancelling thread.");
// I am not entirely sure if this is foolproof, depending on where `ioctl` is internally?
// It's worked every time in testing though, and it's better than *always* freezing.
pthread_cancel(hci_inquiry_thread.handle);
return ENODEV;
}
return hci_inquiry_errorno;
}
void WiimoteScannerLinux::FindWiimotes(std::vector<Wiimote*>& found_wiimotes, Wiimote*& found_board)
{
WiimoteScannerLinux::AddAutoConnectAddresses(found_wiimotes);
int const wait_len = BLUETOOTH_INQUIRY_LENGTH;
int const max_infos = 255;
inquiry_info scan_infos[max_infos] = {};
auto* scan_infos_ptr = scan_infos;
found_board = nullptr;
// Use Limited Dedicated Inquiry Access Code (LIAC) to query, since third-party Wiimotes
// cannot be discovered without it.
const u8 lap[3] = {0x00, 0x8b, 0x9e};
// Scan for Bluetooth devices
int const found_devices =
hci_inquiry(m_device_id, wait_len, max_infos, lap, &scan_infos_ptr, IREQ_CACHE_FLUSH);
if (found_devices < 0)
if (!Open())
return;
AddAutoConnectAddresses(found_wiimotes);
InquiryRequest request{};
request.dev_id = m_device_id;
request.flags = IREQ_CACHE_FLUSH;
request.length = BLUETOOTH_INQUIRY_LENGTH;
request.num_rsp = InquiryRequest::MAX_INFOS;
// Use Limited Dedicated Inquiry Access Code (LIAC) like the Wii does.
// Third-party Wiimotes cannot be discovered without it.
std::ranges::copy(std::to_array({0x00, 0x8b, 0x9e}), request.lap);
const int hci_inquiry_result = HciInquiry(m_device_sock, &request);
switch (hci_inquiry_result)
{
ERROR_LOG_FMT(WIIMOTE, "Error searching for Bluetooth devices.");
case 0:
break;
case ENODEV:
Close();
[[fallthrough]];
default:
ERROR_LOG_FMT(WIIMOTE, "Error searching for Bluetooth devices: {}",
Common::StrerrorString(hci_inquiry_result));
return;
}
DEBUG_LOG_FMT(WIIMOTE, "Found {} Bluetooth device(s).", found_devices);
DEBUG_LOG_FMT(WIIMOTE, "Found {} Bluetooth device(s).", request.num_rsp);
// Display discovered devices
for (auto& scan_info : scan_infos | std::ranges::views::take(found_devices))
for (auto& scan_info : request.scan_infos | std::ranges::views::take(request.num_rsp))
{
// BT names are a maximum of 248 bytes apparently
char name[255] = {};
if (hci_read_remote_name(m_device_sock, &scan_info.bdaddr, sizeof(name), name, 1000) < 0)
const auto bdaddr_str =
BluetoothAddressToString(std::bit_cast<Common::BluetoothAddress>(scan_info.bdaddr));
// Did AddAutoConnectAddresses already add this remote?
const auto eq_this_bdaddr = [&](auto* wm) { return wm->GetId() == bdaddr_str; };
if (std::ranges::any_of(found_wiimotes, eq_this_bdaddr))
continue;
if (!IsNewWiimote(bdaddr_str))
{
ERROR_LOG_FMT(WIIMOTE, "Bluetooth read remote name failed.");
WARN_LOG_FMT(WIIMOTE, "Discovered already connected device: {}", bdaddr_str);
continue;
}
// The Wii can actually connect remotes with a 10 second name response time.
// We won't wait quite so long since we're doing this in a blocking manner right now.
// Note that waiting just 1 second can be problematic sometimes.
const int read_name_timeout_ms = 3000;
// BT names are a maximum of 248 bytes.
char name[255]{};
if (hci_read_remote_name_with_clock_offset(m_device_sock, &scan_info.bdaddr,
scan_info.pscan_rep_mode, scan_info.clock_offset,
sizeof(name), name, read_name_timeout_ms) < 0)
{
ERROR_LOG_FMT(WIIMOTE, "Bluetooth read remote name failed. hci_read_remote_name: {}",
Common::LastStrerrorString());
continue;
}
@@ -91,131 +228,91 @@ void WiimoteScannerLinux::FindWiimotes(std::vector<Wiimote*>& found_wiimotes, Wi
if (!IsValidDeviceName(name))
continue;
char bdaddr_str[18] = {};
ba2str(&scan_info.bdaddr, bdaddr_str);
if (!IsNewWiimote(bdaddr_str))
continue;
// Found a new device
Wiimote* wm = new WiimoteLinux(scan_info.bdaddr);
auto wm =
std::make_unique<WiimoteLinux>(std::bit_cast<Common::BluetoothAddress>(scan_info.bdaddr));
if (IsBalanceBoardName(name))
{
found_board = wm;
delete std::exchange(found_board, wm.release());
NOTICE_LOG_FMT(WIIMOTE, "Found balance board ({}).", bdaddr_str);
}
else
{
found_wiimotes.push_back(wm);
found_wiimotes.push_back(wm.release());
NOTICE_LOG_FMT(WIIMOTE, "Found Wiimote ({}).", bdaddr_str);
}
}
}
void WiimoteScannerLinux::AddAutoConnectAddresses(std::vector<Wiimote*>& found_wiimotes)
{
std::string entries = Config::Get(Config::MAIN_WIIMOTE_AUTO_CONNECT_ADDRESSES);
if (entries.empty())
return;
for (const auto& bt_address_str : SplitString(entries, ','))
{
bdaddr_t bt_addr;
if (str2ba(bt_address_str.c_str(), &bt_addr) < 0)
{
WARN_LOG_FMT(WIIMOTE, "Bad Known Bluetooth Address: {}", bt_address_str);
continue;
}
if (!IsNewWiimote(bt_address_str))
continue;
Wiimote* wm = new WiimoteLinux(bt_addr);
found_wiimotes.push_back(wm);
NOTICE_LOG_FMT(WIIMOTE, "Added Wiimote with fixed address ({}).", bt_address_str);
}
void WiimoteScannerLinux::Update()
{ // Nothing needed on Linux.
}
WiimoteLinux::WiimoteLinux(bdaddr_t bdaddr) : m_bdaddr(bdaddr)
void WiimoteScannerLinux::RequestStopSearching()
{ // Nothing needed on Linux.
}
WiimoteLinux::WiimoteLinux(Common::BluetoothAddress bdaddr)
: m_bdaddr{bdaddr}, m_wakeup_fd{UnixUtil::CreateEventFD(0, 0)}
{
m_really_disconnect = true;
m_cmd_sock = -1;
m_int_sock = -1;
int fds[2];
if (pipe(fds))
{
ERROR_LOG_FMT(WIIMOTE, "pipe failed");
abort();
}
m_wakeup_pipe_w = fds[1];
m_wakeup_pipe_r = fds[0];
}
WiimoteLinux::~WiimoteLinux()
{
Shutdown();
close(m_wakeup_pipe_w);
close(m_wakeup_pipe_r);
close(m_wakeup_fd);
}
std::string WiimoteLinux::GetId() const
{
return BluetoothAddressToString(m_bdaddr);
}
// Connect to a Wiimote with a known address.
bool WiimoteLinux::ConnectInternal()
{
sockaddr_l2 addr = {};
addr.l2_family = AF_BLUETOOTH;
addr.l2_bdaddr = m_bdaddr;
addr.l2_cid = 0;
sockaddr_l2 addr{
.l2_family = AF_BLUETOOTH,
.l2_bdaddr = std::bit_cast<bdaddr_t>(m_bdaddr),
.l2_cid = 0,
};
// Control channel
addr.l2_psm = htobs(L2CAP_PSM_HID_CNTL);
if ((m_cmd_sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)))
{
int retry = 0;
while (connect(m_cmd_sock, (sockaddr*)&addr, sizeof(addr)) < 0)
const auto open_channel = [&](u16 l2_psm) {
addr.l2_psm = htobs(l2_psm);
constexpr int total_tries = 3;
for (int i = 0; i != total_tries; ++i)
{
// If opening channel fails sleep and try again
if (retry == 3)
const int descriptor = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP);
if (descriptor == -1)
{
WARN_LOG_FMT(WIIMOTE, "Unable to connect control channel of Wiimote: {}", strerror(errno));
close(m_cmd_sock);
m_cmd_sock = -1;
return false;
WARN_LOG_FMT(WIIMOTE, "Failed to create L2CAP socket: {}", Common::LastStrerrorString());
return -1;
}
retry++;
sleep(1);
if (connect(descriptor, reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) == 0)
return descriptor;
// If connecting fails sleep and try again.
WARN_LOG_FMT(WIIMOTE, "Failed to connect L2CAP PSM({}) socket: {}", l2_psm,
Common::LastStrerrorString());
// A socket state is unspecified after connect() fails, so close and recreate it.
close(descriptor);
std::this_thread::sleep_for(std::chrono::milliseconds{500});
}
}
else
{
WARN_LOG_FMT(WIIMOTE, "Unable to open control socket to Wiimote: {}", strerror(errno));
return -1;
};
m_cmd_sock = open_channel(L2CAP_PSM_HID_CNTL);
if (m_cmd_sock == -1)
return false;
}
// Interrupt channel
addr.l2_psm = htobs(L2CAP_PSM_HID_INTR);
if ((m_int_sock = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)))
m_int_sock = open_channel(L2CAP_PSM_HID_INTR);
if (m_int_sock == -1)
{
int retry = 0;
while (connect(m_int_sock, (sockaddr*)&addr, sizeof(addr)) < 0)
{
// If opening channel fails sleep and try again
if (retry == 3)
{
WARN_LOG_FMT(WIIMOTE, "Unable to connect interrupt channel of Wiimote: {}",
strerror(errno));
close(m_int_sock);
close(m_cmd_sock);
m_int_sock = m_cmd_sock = -1;
return false;
}
retry++;
sleep(1);
}
}
else
{
WARN_LOG_FMT(WIIMOTE, "Unable to open interrupt socket to Wiimote: {}", strerror(errno));
close(m_cmd_sock);
m_int_sock = m_cmd_sock = -1;
close(std::exchange(m_cmd_sock, -1));
return false;
}
@@ -224,11 +321,8 @@ bool WiimoteLinux::ConnectInternal()
void WiimoteLinux::DisconnectInternal()
{
close(m_cmd_sock);
close(m_int_sock);
m_cmd_sock = -1;
m_int_sock = -1;
close(std::exchange(m_cmd_sock, -1));
close(std::exchange(m_int_sock, -1));
}
bool WiimoteLinux::IsConnected() const
@@ -238,10 +332,10 @@ bool WiimoteLinux::IsConnected() const
void WiimoteLinux::IOWakeup()
{
char c = 0;
if (write(m_wakeup_pipe_w, &c, 1) != 1)
u64 counter = 1;
if (write(m_wakeup_fd, &counter, sizeof(counter)) != sizeof(counter))
{
ERROR_LOG_FMT(WIIMOTE, "Unable to write to wakeup pipe.");
ERROR_LOG_FMT(WIIMOTE, "failed to write to wakeup eventfd: {}", Common::LastStrerrorString());
}
}
@@ -250,60 +344,47 @@ void WiimoteLinux::IOWakeup()
// zero = error
int WiimoteLinux::IORead(u8* buf)
{
std::array<pollfd, 2> pollfds = {};
std::array<pollfd, 2> pollfds{
pollfd{.fd = m_wakeup_fd, .events = POLLIN},
pollfd{.fd = m_int_sock, .events = POLLIN},
};
UnixUtil::RetryOnEINTR(poll, pollfds.data(), pollfds.size(), -1);
auto& poll_wakeup = pollfds[0];
poll_wakeup.fd = m_wakeup_pipe_r;
poll_wakeup.events = POLLIN;
auto& poll_sock = pollfds[1];
poll_sock.fd = m_int_sock;
poll_sock.events = POLLIN;
if (poll(pollfds.data(), pollfds.size(), -1) == -1)
// Handle IOWakeup.
if (pollfds[0].revents != 0)
{
ERROR_LOG_FMT(WIIMOTE, "Unable to poll Wiimote {} input socket.", m_index + 1);
return -1;
}
if (poll_wakeup.revents & POLLIN)
{
char c;
if (read(m_wakeup_pipe_r, &c, 1) != 1)
DEBUG_LOG_FMT(WIIMOTE, "IOWakeup");
u64 counter{};
if (read(m_wakeup_fd, &counter, sizeof(counter)) != sizeof(counter))
{
ERROR_LOG_FMT(WIIMOTE, "Unable to read from wakeup pipe.");
ERROR_LOG_FMT(WIIMOTE, "Failed to read from wakeup eventfd: {}",
Common::LastStrerrorString());
return 0;
}
return -1;
}
if (!(poll_sock.revents & POLLIN))
return -1;
// Read the pending message into the buffer
int r = read(m_int_sock, buf, MAX_PAYLOAD);
if (r == -1)
// Handle event on interrupt channel.
auto result = int(read(m_int_sock, buf, MAX_PAYLOAD));
if (result == -1)
{
// Error reading data
ERROR_LOG_FMT(WIIMOTE, "Receiving data from Wiimote {}.", m_index + 1);
if (errno == ENOTCONN)
{
// This can happen if the Bluetooth dongle is disconnected
ERROR_LOG_FMT(WIIMOTE,
"Bluetooth appears to be disconnected. "
"Wiimote {} will be disconnected.",
m_index + 1);
}
r = 0;
ERROR_LOG_FMT(WIIMOTE, "Wiimote {} read failed: {}", m_index + 1, Common::LastStrerrorString());
result = 0;
}
return r;
return result;
}
int WiimoteLinux::IOWrite(u8 const* buf, size_t len)
{
return write(m_int_sock, buf, (int)len);
auto result = int(write(m_int_sock, buf, int(len)));
if (result == -1)
{
ERROR_LOG_FMT(WIIMOTE, "Wiimote {} write failed: {}", m_index + 1,
Common::LastStrerrorString());
}
return result;
}
}; // namespace WiimoteReal
} // namespace WiimoteReal

View File

@@ -4,8 +4,10 @@
#pragma once
#if defined(__linux__) && HAVE_BLUEZ
#include <bluetooth/bluetooth.h>
#include <atomic>
#include "Common/Network.h"
#include "Core/HW/WiimoteReal/WiimoteReal.h"
namespace WiimoteReal
@@ -13,14 +15,10 @@ namespace WiimoteReal
class WiimoteLinux final : public Wiimote
{
public:
WiimoteLinux(bdaddr_t bdaddr);
explicit WiimoteLinux(Common::BluetoothAddress bdaddr);
~WiimoteLinux() override;
std::string GetId() const override
{
char bdaddr_str[18] = {};
ba2str(&m_bdaddr, bdaddr_str);
return bdaddr_str;
}
std::string GetId() const override;
protected:
bool ConnectInternal() override;
@@ -31,11 +29,10 @@ protected:
int IOWrite(u8 const* buf, size_t len) override;
private:
bdaddr_t m_bdaddr; // Bluetooth address
int m_cmd_sock; // Command socket
int m_int_sock; // Interrupt socket
int m_wakeup_pipe_w;
int m_wakeup_pipe_r;
const Common::BluetoothAddress m_bdaddr;
const int m_wakeup_fd{-1}; // Used to kick the read thread.
int m_cmd_sock{-1}; // Command socket
int m_int_sock{-1}; // Interrupt socket
};
class WiimoteScannerLinux final : public WiimoteScannerBackend
@@ -43,16 +40,23 @@ class WiimoteScannerLinux final : public WiimoteScannerBackend
public:
WiimoteScannerLinux();
~WiimoteScannerLinux() override;
bool IsReady() const override;
void FindWiimotes(std::vector<Wiimote*>&, Wiimote*&) override;
void Update() override {} // not needed on Linux
void RequestStopSearching() override {} // not needed on Linux
private:
int m_device_id;
int m_device_sock;
void Update() override;
void RequestStopSearching() override;
void AddAutoConnectAddresses(std::vector<Wiimote*>&);
private:
bool Open();
void Close();
int m_device_id{-1};
int m_device_sock{-1};
// FYI: Atomic because UI calls IsReady.
std::atomic<bool> m_is_device_open{};
};
} // namespace WiimoteReal
#else

View File

@@ -175,7 +175,10 @@ class WiimoteScannerBackend
{
public:
virtual ~WiimoteScannerBackend() = default;
// Note: Invoked from UI thread.
virtual bool IsReady() const = 0;
virtual void FindWiimotes(std::vector<Wiimote*>&, Wiimote*&) = 0;
// function called when not looking for more Wiimotes
virtual void Update() = 0;