diff --git a/Source/Core/Core/HW/WiimoteReal/IOLinux.cpp b/Source/Core/Core/HW/WiimoteReal/IOLinux.cpp index 9361515d922..3fe5f09403a 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOLinux.cpp +++ b/Source/Core/Core/HW/WiimoteReal/IOLinux.cpp @@ -4,16 +4,26 @@ #include "Core/HW/WiimoteReal/IOLinux.h" #include +#include #include #include #include #include + +#include +#include #include +#include + #include +#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& 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 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(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 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& 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(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& 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(std::bit_cast(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& 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(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(&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 pollfds = {}; + std::array 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 diff --git a/Source/Core/Core/HW/WiimoteReal/IOLinux.h b/Source/Core/Core/HW/WiimoteReal/IOLinux.h index 0f84659a39c..9f03dbd066b 100644 --- a/Source/Core/Core/HW/WiimoteReal/IOLinux.h +++ b/Source/Core/Core/HW/WiimoteReal/IOLinux.h @@ -4,8 +4,10 @@ #pragma once #if defined(__linux__) && HAVE_BLUEZ -#include +#include + +#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*&) 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&); +private: + bool Open(); + void Close(); + + int m_device_id{-1}; + int m_device_sock{-1}; + + // FYI: Atomic because UI calls IsReady. + std::atomic m_is_device_open{}; }; + } // namespace WiimoteReal #else diff --git a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h index ee43588f511..091532cea9c 100644 --- a/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h +++ b/Source/Core/Core/HW/WiimoteReal/WiimoteReal.h @@ -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*&) = 0; // function called when not looking for more Wiimotes virtual void Update() = 0;