mirror of
https://github.com/patchzyy/wiicompiled
synced 2026-10-20 05:33:23 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f1bdb4c2a | |||
| 0d0ae2d8f7 | |||
| fd5e0331bd | |||
| 209fba7f64 |
@@ -102,6 +102,7 @@ jobs:
|
||||
mkw_nand_settings_tests \
|
||||
mkw_sc_serial_tests \
|
||||
mkw_input_expr_tests \
|
||||
mkw_bluetooth_controller_tests \
|
||||
mkw_macos_native_compile \
|
||||
mkw_macos_context_abi_tests \
|
||||
mkw_macos_host_context_tests \
|
||||
|
||||
@@ -63,6 +63,16 @@ target_include_directories(mkw_input_expr_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}
|
||||
target_compile_features(mkw_input_expr_tests PRIVATE cxx_std_17)
|
||||
add_test(NAME mkw_input_expr_tests COMMAND mkw_input_expr_tests)
|
||||
|
||||
# The emulated Bluetooth controller is pure host code.
|
||||
add_executable(mkw_bluetooth_controller_tests
|
||||
"${MKW_TEST_RUNTIME_DIR}/tests/bluetooth_controller_tests.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_controller.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_remote_link.cpp"
|
||||
"${MKW_TEST_RUNTIME_DIR}/src/hle/bluetooth/bt_sdp.cpp")
|
||||
target_include_directories(mkw_bluetooth_controller_tests PRIVATE "${MKW_TEST_RUNTIME_DIR}/include")
|
||||
target_compile_features(mkw_bluetooth_controller_tests PRIVATE cxx_std_20)
|
||||
add_test(NAME mkw_bluetooth_controller_tests COMMAND mkw_bluetooth_controller_tests)
|
||||
|
||||
# HostContext deliberately keeps the platform-specific context primitive out
|
||||
# of fiber_manager.cpp. Exercise the Linux libco handoff directly so future
|
||||
# refactors cannot silently remove its headers, implementation, or link edge.
|
||||
|
||||
@@ -206,7 +206,7 @@ function(mkw_configure_product target)
|
||||
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} PRIVATE
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp)
|
||||
dbghelp user32 winmm ws2_32 iphlpapi secur32 crypt32 windowsapp bthprops)
|
||||
|
||||
set_target_properties(${target} PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
|
||||
@@ -66,7 +66,7 @@ function(mkw_runtime_link_dependencies target scope)
|
||||
aurora::gx aurora::pad aurora::si aurora::vi aurora::mtx)
|
||||
target_link_libraries(${target} ${scope} mkw_platform mkw::pugixml mkw::toml11 mkw::cryptopp mkw::mbedtls)
|
||||
if(MKW_PLATFORM_WINDOWS)
|
||||
target_link_libraries(${target} ${scope} shell32 windowsapp)
|
||||
target_link_libraries(${target} ${scope} shell32 windowsapp bthprops)
|
||||
elseif(MKW_PLATFORM_LINUX OR MKW_PLATFORM_MACOS_X86_64)
|
||||
# ${CMAKE_DL_LIBS} for music_attenuation.cpp's dlopen of libdbus-1 (MPRIS
|
||||
# media monitoring). Empty on platforms where dl* is already in libc/libSystem.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// HCI and L2CAP fields are little-endian.
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
inline uint16_t Le16(const uint8_t* p) {
|
||||
return static_cast<uint16_t>(p[0] | (p[1] << 8));
|
||||
}
|
||||
|
||||
inline void PutLe16(std::vector<uint8_t>& out, uint16_t v) {
|
||||
out.push_back(static_cast<uint8_t>(v));
|
||||
out.push_back(static_cast<uint8_t>(v >> 8));
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
// Emulated Bluetooth controller behind the Wii's internal USB module
|
||||
// (/dev/usb/oh1/57e/305). The game's own Broadcom stack (BTE), WUD, WPAD and KPAD
|
||||
// talk HCI to it; each of the four virtual Wii Remotes it hosts is backed by a
|
||||
// physical remote on the PC (see RemoteHost), whose HID reports are passed
|
||||
// through unchanged. Pure host code: no guest memory, so it is unit-testable.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
// Bluetooth device address in HCI (least significant byte first) order.
|
||||
using BdAddr = std::array<uint8_t, 6>;
|
||||
|
||||
inline constexpr size_t kRemoteSlots = 4;
|
||||
|
||||
enum class LinkState : uint8_t {
|
||||
Down, // no baseband connection to the game
|
||||
Connecting, // baseband up, HID channels being opened
|
||||
Ready, // HID control and interrupt channels open
|
||||
};
|
||||
|
||||
// The physical side of the virtual remotes. Called from the thread that drives
|
||||
// the Controller; implementations synchronize with their own I/O threads.
|
||||
class RemoteHost {
|
||||
public:
|
||||
virtual ~RemoteHost() = default;
|
||||
// A physical remote is attached to `slot` and can carry a connection.
|
||||
virtual bool IsAttached(size_t slot) const = 0;
|
||||
// True once each time the remote on `slot` asks to connect (it was just
|
||||
// attached, or a button was pressed while it had no connection).
|
||||
virtual bool ConsumeWake(size_t slot) = 0;
|
||||
// Next input report (report id first, no HID header), false when none.
|
||||
virtual bool PopInputReport(size_t slot, std::vector<uint8_t>& report) = 0;
|
||||
// Output report from the game (report id first, no HID header).
|
||||
virtual void SendOutputReport(size_t slot, const uint8_t* report, size_t size) = 0;
|
||||
virtual void OnLinkStateChanged(size_t slot, LinkState state) = 0;
|
||||
// The game ended a working connection; a real remote powers off here.
|
||||
virtual void OnGuestDisconnect(size_t slot) = 0;
|
||||
// The game is searching for remotes (its 1+2 / reconnect screens).
|
||||
virtual void OnGuestInquiry(bool active) = 0;
|
||||
// "Nintendo RVL-CNT-01" or "Nintendo RVL-CNT-01-TR".
|
||||
virtual const char* DeviceName(size_t slot) const = 0;
|
||||
};
|
||||
|
||||
class RemoteLink;
|
||||
|
||||
class Controller {
|
||||
public:
|
||||
explicit Controller(RemoteHost& host);
|
||||
~Controller();
|
||||
Controller(const Controller&) = delete;
|
||||
Controller& operator=(const Controller&) = delete;
|
||||
|
||||
// The guest opened / closed the USB device.
|
||||
void Open(uint64_t nowUs);
|
||||
void Close();
|
||||
bool IsOpen() const { return m_open; }
|
||||
|
||||
// Raw HCI command packet (opcode first, no H4 byte).
|
||||
void ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs);
|
||||
// Raw ACL packet from the guest (handle first).
|
||||
void ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs);
|
||||
// Advances timers, connection attempts, input forwarding and flow control.
|
||||
void Tick(uint64_t nowUs);
|
||||
|
||||
bool HasEvent() const { return !m_events.empty(); }
|
||||
bool PopEvent(std::vector<uint8_t>& event);
|
||||
// ACL data is only handed out once every queued event has been delivered,
|
||||
// so the guest always sees a connection before data on it.
|
||||
bool PopAcl(std::vector<uint8_t>& packet);
|
||||
size_t AclQueued() const { return m_acl.size(); }
|
||||
|
||||
LinkState SlotLinkState(size_t slot) const;
|
||||
bool PageScanEnabled() const { return (m_scanEnable & 0x02) != 0; }
|
||||
bool InquiryActive() const { return m_inquiry.active; }
|
||||
|
||||
static BdAddr LocalAddress();
|
||||
static BdAddr RemoteAddress(size_t slot);
|
||||
static uint16_t ConnectionHandle(size_t slot);
|
||||
|
||||
// Used by RemoteLink.
|
||||
void QueueEvent(std::vector<uint8_t> event);
|
||||
void QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame);
|
||||
RemoteHost& Host() { return m_host; }
|
||||
uint32_t PageTimeoutUs() const { return static_cast<uint32_t>(m_pageTimeoutSlots) * 625u; }
|
||||
|
||||
private:
|
||||
struct StoredKey {
|
||||
BdAddr addr{};
|
||||
std::array<uint8_t, 16> key{};
|
||||
};
|
||||
struct Inquiry {
|
||||
bool active = false;
|
||||
uint64_t endUs = 0;
|
||||
uint8_t maxResponses = 0;
|
||||
uint8_t reported = 0;
|
||||
std::array<bool, kRemoteSlots> slotReported{};
|
||||
};
|
||||
struct Reassembly {
|
||||
std::vector<uint8_t> frame;
|
||||
size_t expected = 0;
|
||||
};
|
||||
// A reply the controller sends after a delay (page / name request timeouts).
|
||||
struct PendingEvent {
|
||||
uint64_t dueUs = 0;
|
||||
std::vector<uint8_t> event;
|
||||
};
|
||||
|
||||
void ResetState(bool keepStoredKeys);
|
||||
void CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params);
|
||||
void CommandStatus(uint16_t opcode, uint8_t status);
|
||||
void TickInquiry(uint64_t nowUs);
|
||||
void FinishInquiry();
|
||||
void SendNumberOfCompletedPackets();
|
||||
int SlotForAddress(const BdAddr& addr) const;
|
||||
int SlotForHandle(uint16_t handle) const;
|
||||
const StoredKey* FindStoredKey(const BdAddr& addr) const;
|
||||
|
||||
void HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
|
||||
void HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs);
|
||||
void HandleInformational(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void HandleVendor(uint16_t opcode, const uint8_t* p, size_t n);
|
||||
void UnknownCommand(uint16_t opcode);
|
||||
|
||||
RemoteHost& m_host;
|
||||
bool m_open = false;
|
||||
uint8_t m_scanEnable = 0;
|
||||
uint16_t m_pageTimeoutSlots = 0x2000;
|
||||
uint8_t m_inquiryMode = 0;
|
||||
std::array<uint8_t, 3> m_classOfDevice{};
|
||||
std::array<char, 248> m_localName{};
|
||||
Inquiry m_inquiry;
|
||||
bool m_hostInquiryNotified = false;
|
||||
std::vector<StoredKey> m_storedKeys;
|
||||
std::array<std::unique_ptr<RemoteLink>, kRemoteSlots> m_links;
|
||||
std::array<uint16_t, kRemoteSlots> m_completedPackets{};
|
||||
std::array<Reassembly, kRemoteSlots> m_reassembly;
|
||||
std::array<bool, kRemoteSlots> m_authPending{};
|
||||
std::vector<PendingEvent> m_pending;
|
||||
std::deque<std::vector<uint8_t>> m_events;
|
||||
std::deque<std::vector<uint8_t>> m_acl;
|
||||
};
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
|
||||
// One emulated Wii Remote as seen from the controller: its baseband connection,
|
||||
// L2CAP channels (SDP, HID control, HID interrupt) and the HID transaction layer
|
||||
// that carries the physical remote's reports.
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth {
|
||||
|
||||
class RemoteLink {
|
||||
public:
|
||||
enum class Baseband : uint8_t {
|
||||
Idle,
|
||||
Requesting, // Connection Request sent, waiting for the guest to accept
|
||||
Paging, // the guest paged this remote (Create Connection)
|
||||
Connected,
|
||||
};
|
||||
|
||||
RemoteLink(Controller& controller, size_t slot);
|
||||
|
||||
// Drops every connection without telling the guest (controller reset/close).
|
||||
void Reset();
|
||||
void Tick(uint64_t nowUs);
|
||||
|
||||
// Remote side wants a connection and should be offered to the guest's inquiry.
|
||||
bool Discoverable() const;
|
||||
bool ConnectWanted() const { return m_connectWanted; }
|
||||
|
||||
// Guest accepted / rejected this remote's Connection Request.
|
||||
bool IsRequesting() const { return m_baseband == Baseband::Requesting; }
|
||||
void OnAccepted(uint8_t role);
|
||||
void OnRejected(uint8_t reason, uint64_t nowUs);
|
||||
// Guest paged this remote. Returns false when the remote cannot answer.
|
||||
bool OnPaged(uint64_t nowUs);
|
||||
// Baseband went down. `byGuest` = the guest's HCI Disconnect.
|
||||
void OnDisconnected(bool byGuest, uint64_t nowUs);
|
||||
// The physical remote vanished while connected: tells the guest.
|
||||
void DropFromRemote(uint8_t reason);
|
||||
|
||||
void ReceiveL2cap(const uint8_t* frame, size_t size);
|
||||
|
||||
Baseband BasebandState() const { return m_baseband; }
|
||||
LinkState State() const { return m_state; }
|
||||
|
||||
private:
|
||||
struct Channel {
|
||||
uint16_t psm = 0;
|
||||
uint16_t localCid = 0;
|
||||
uint16_t remoteCid = 0;
|
||||
uint16_t remoteMtu = 672;
|
||||
bool weInitiated = false;
|
||||
bool connected = false; // connection response success seen/sent
|
||||
bool configSent = false; // our Configuration Request is out
|
||||
bool localConfigured = false; // the guest accepted our configuration
|
||||
bool remoteConfigured = false; // we accepted the guest's configuration
|
||||
uint8_t connectIdent = 0;
|
||||
uint8_t configIdent = 0;
|
||||
uint64_t requestUs = 0;
|
||||
std::vector<uint8_t> pendingOptions; // continued Configuration Request
|
||||
bool Open() const { return connected && localConfigured && remoteConfigured; }
|
||||
};
|
||||
|
||||
Channel* FindByLocal(uint16_t cid);
|
||||
Channel* FindByPsm(uint16_t psm);
|
||||
uint16_t AllocateCid() const;
|
||||
uint8_t NextIdent();
|
||||
|
||||
void SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data);
|
||||
void SendData(const Channel& channel, const std::vector<uint8_t>& payload);
|
||||
void RequestChannel(uint16_t psm, uint64_t nowUs);
|
||||
void SendConfigRequest(Channel& channel);
|
||||
void HandleSignalling(const uint8_t* data, size_t size);
|
||||
void HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n);
|
||||
void HandleDisconnectionResponse(const uint8_t* p, size_t n);
|
||||
void HandleChannelData(Channel& channel, const uint8_t* data, size_t size);
|
||||
void HandleHidControl(Channel& channel, const uint8_t* data, size_t size);
|
||||
void UpdateState();
|
||||
void SetState(LinkState state);
|
||||
void ForwardInput();
|
||||
void SendInput(const Channel& interrupt, const std::vector<uint8_t>& report);
|
||||
void ClearConnection();
|
||||
|
||||
Controller& m_controller;
|
||||
size_t m_slot;
|
||||
Baseband m_baseband = Baseband::Idle;
|
||||
LinkState m_state = LinkState::Down;
|
||||
bool m_connectWanted = false;
|
||||
bool m_guestInitiated = false;
|
||||
bool m_everReady = false;
|
||||
uint8_t m_ident = 0;
|
||||
uint64_t m_stateSinceUs = 0;
|
||||
uint64_t m_retryAtUs = 0;
|
||||
uint64_t m_nowUs = 0;
|
||||
std::vector<Channel> m_channels;
|
||||
std::vector<uint8_t> m_report;
|
||||
};
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// SDP server of an emulated Wii Remote: the HID service record (0x00010000)
|
||||
// and the Device ID record (0x00010001) a real RVL-CNT-01 publishes.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Bluetooth::Sdp {
|
||||
|
||||
// The Wii Remote's HID report descriptor, as the game also embeds it.
|
||||
const std::vector<uint8_t>& WiiRemoteHidDescriptor();
|
||||
|
||||
// Answers one SDP request PDU. `maxResponse` bounds the PDU returned (the
|
||||
// guest's L2CAP MTU on the SDP channel).
|
||||
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse);
|
||||
|
||||
} // namespace Bluetooth::Sdp
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
// IOS side of the emulated Bluetooth module (/dev/usb/oh1/57e/305): routes the
|
||||
// guest's USB control / bulk / interrupt transfers to Bluetooth::Controller and
|
||||
// completes the held event and ACL reads from the alarm pump.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct CpuContext;
|
||||
|
||||
// Returns the fd for the Bluetooth device path, 0 when `path` is not ours.
|
||||
int32_t Bluetooth_HLE_OpenDevice(const char* path);
|
||||
bool Bluetooth_HLE_IsFd(uint32_t fd);
|
||||
// Cancels the held reads (their callbacks are queued) and closes the device.
|
||||
int32_t Bluetooth_HLE_Close(uint32_t fd);
|
||||
// True when the request completed now with `result`; false when it is held
|
||||
// until data arrives (its callback is queued later by the pump).
|
||||
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
|
||||
uint32_t callback, uint32_t callbackArg, int32_t& result);
|
||||
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr);
|
||||
// Runs the controller and completes held reads. Emulation thread only.
|
||||
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu);
|
||||
@@ -41,42 +41,3 @@ inline GuestStatus Encode(const Fields& fields)
|
||||
}
|
||||
|
||||
} // namespace PadStatusContract
|
||||
|
||||
namespace WpadContract {
|
||||
|
||||
inline constexpr std::size_t kChannelCount = 4;
|
||||
inline constexpr int32_t kStatusDisabled = 0;
|
||||
inline constexpr int32_t kStatusReady = 3;
|
||||
inline constexpr int32_t kErrorNoController = -1;
|
||||
inline constexpr int32_t kErrorNotReady = -2;
|
||||
inline constexpr int32_t kErrorBadChannel = -6;
|
||||
inline constexpr int32_t kExtensionCore = 0;
|
||||
|
||||
class State {
|
||||
public:
|
||||
void Initialize() { m_initialized = true; }
|
||||
bool IsInitialized() const { return m_initialized; }
|
||||
int32_t GetLibraryStatus() const { return m_initialized ? kStatusReady : kStatusDisabled; }
|
||||
|
||||
int32_t GetDataFormat(uint32_t chan) const
|
||||
{
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? 0 : kErrorNotReady;
|
||||
}
|
||||
|
||||
int32_t SetDataFormat(uint32_t chan, int32_t format) const
|
||||
{
|
||||
(void)format;
|
||||
if (chan >= kChannelCount) {
|
||||
return kErrorBadChannel;
|
||||
}
|
||||
return m_initialized ? kErrorNoController : kErrorNotReady;
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
} // namespace WpadContract
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// HW_GPIOB_OUT, the GPIO outputs the PPC owns. WPAD sets bit 8 to power the
|
||||
// sensor bar; there is nothing behind it on a PC, so the register only latches.
|
||||
inline constexpr uint32_t kHollywoodGpioBOut = 0xCD0000C0u;
|
||||
|
||||
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value);
|
||||
bool HollywoodGpio_Write32(uint32_t address, uint32_t value);
|
||||
@@ -57,26 +57,16 @@ struct RuntimeUserConfig {
|
||||
std::optional<bool> audioMuted;
|
||||
std::optional<bool> audioMixWorker;
|
||||
std::optional<bool> attenuateMusicWhenMediaPlays;
|
||||
// Real Wii Remotes (with or without Nunchuk / Classic Controller) and Wii U Pro
|
||||
// Controllers paired over Bluetooth, driven by SDL's HIDAPI Wii driver. The driver
|
||||
// is opt-in on SDL's side, so this decides whether the runtime turns it on.
|
||||
// Real Wii Remotes passed through to the game's own Bluetooth stack. Off hands
|
||||
// Nintendo controllers (e.g. the Wii U Pro Controller) to SDL as plain gamepads.
|
||||
std::optional<bool> wiiRemotes;
|
||||
// Keep re-enumerating Bluetooth HID devices while no Wii controller is connected
|
||||
// (Dolphin's "continuous scanning"), so a remote that dropped or was switched on
|
||||
// after launch shows up without restarting.
|
||||
std::optional<bool> wiiContinuousScan;
|
||||
// Accelerometer zero-point correction for the Bluetooth Wii Remote, in g and in
|
||||
// SDL's sensor frame (x right, y out of the button face, z towards the user).
|
||||
// SDL's Wii driver falls back to a nominal zero point when its read of the
|
||||
// remote's calibration block times out (common over Bluetooth), so this is
|
||||
// measured in the overlay with the remote at rest. Stored per remote under
|
||||
// [controller.wii_accel_offsets]; the old shared
|
||||
// wii_accel_offset_* keys are only detected, to report that they are ignored.
|
||||
std::map<std::string, std::array<double, 3>> wiiAccelOffsets;
|
||||
bool hasLegacyWiiAccelOffset = false;
|
||||
// Debugging aid: append every KPAD sample of the Bluetooth remote (raw and
|
||||
// corrected accelerometer, buttons) to wii_accel_trace.csv next to Config.toml.
|
||||
std::optional<bool> wiiAccelTrace;
|
||||
// Look for remotes in pairing mode (1+2 / SYNC) while none is connected.
|
||||
std::optional<bool> wiiAutoPair;
|
||||
// The console's Wii Remote settings (SYSCONF BT.BAR, BT.SENS, BT.SPKV, BT.MOT).
|
||||
std::optional<bool> wiiSensorBarAbove;
|
||||
std::optional<int32_t> wiiIrSensitivity;
|
||||
std::optional<int32_t> wiiSpeakerVolume;
|
||||
std::optional<bool> wiiRumble;
|
||||
std::optional<bool> networkEnabled;
|
||||
std::optional<bool> discordPresenceEnabled;
|
||||
// The application ID of the WiiCompiled Discord application. This is only
|
||||
@@ -382,28 +372,6 @@ inline void AppendOverlayRoots(RuntimeUserConfig& config, const std::string& roo
|
||||
}
|
||||
|
||||
// Reads every supported setting out of a parsed Config.toml document.
|
||||
// [x, y, z] of finite numbers (integers allowed), else nullopt.
|
||||
inline std::optional<std::array<double, 3>> ParseAccelOffset(const toml::value& value) {
|
||||
if (!value.is_array() || value.as_array().size() != 3) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::array<double, 3> offset{};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
const toml::value& element = value.as_array()[i];
|
||||
if (element.is_floating()) {
|
||||
offset[i] = element.as_floating();
|
||||
} else if (element.is_integer()) {
|
||||
offset[i] = static_cast<double>(element.as_integer());
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (!std::isfinite(offset[i])) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
RuntimeUserConfig config;
|
||||
|
||||
@@ -427,13 +395,6 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.controllerExpressions[key] = value.as_string();
|
||||
}
|
||||
}
|
||||
if (section->contains("wii_accel_offsets") && section->at("wii_accel_offsets").is_table()) {
|
||||
for (const auto& [key, value] : section->at("wii_accel_offsets").as_table()) {
|
||||
if (const auto offset = ParseAccelOffset(value)) {
|
||||
config.wiiAccelOffsets[key] = *offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
config.widescreen = FindConfigValue<bool>(document, "video", "widescreen");
|
||||
@@ -494,11 +455,17 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
|
||||
config.attenuateMusicWhenMediaPlays =
|
||||
FindConfigValue<bool>(document, "audio", "attenuate_music_when_media_plays");
|
||||
config.wiiRemotes = FindConfigValue<bool>(document, "controller", "wii_remotes");
|
||||
config.wiiContinuousScan = FindConfigValue<bool>(document, "controller", "wii_continuous_scan");
|
||||
for (const char* key : {"wii_accel_offset_x", "wii_accel_offset_y", "wii_accel_offset_z"}) {
|
||||
config.hasLegacyWiiAccelOffset |= FindConfigValue<double>(document, "controller", key).value_or(0.0) != 0.0;
|
||||
config.wiiAutoPair = FindConfigValue<bool>(document, "controller", "wii_auto_pair");
|
||||
if (const auto bar = FindConfigValue<std::string>(document, "controller", "wii_sensor_bar")) {
|
||||
if (*bar == "above" || *bar == "below") config.wiiSensorBarAbove = *bar == "above";
|
||||
}
|
||||
config.wiiAccelTrace = FindConfigValue<bool>(document, "controller", "wii_accel_trace");
|
||||
if (const auto value = FindConfigInt(document, "controller", "wii_ir_sensitivity"); value && *value >= 1 && *value <= 5) {
|
||||
config.wiiIrSensitivity = *value;
|
||||
}
|
||||
if (const auto value = FindConfigInt(document, "controller", "wii_speaker_volume"); value && *value >= 0 && *value <= 127) {
|
||||
config.wiiSpeakerVolume = *value;
|
||||
}
|
||||
config.wiiRumble = FindConfigValue<bool>(document, "controller", "wii_rumble");
|
||||
config.networkEnabled = FindConfigValue<bool>(document, "network", "enabled");
|
||||
config.discordPresenceEnabled = FindConfigValue<bool>(document, "discord", "enabled");
|
||||
config.discordClientId = FindConfigValue<std::string>(document, "discord", "client_id");
|
||||
@@ -849,51 +816,61 @@ inline bool AttenuateMusicWhenMediaPlays(bool fallback = false) {
|
||||
return Get().attenuateMusicWhenMediaPlays.value_or(fallback);
|
||||
}
|
||||
|
||||
// Bluetooth Wii Remotes / Wii U Pro Controllers. Read once before SDL's joystick
|
||||
// subsystem comes up, so a change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = true) {
|
||||
// Bluetooth Wii Remotes. Read once before SDL's joystick subsystem comes up, so a
|
||||
// change only takes effect on the next launch.
|
||||
inline bool WiiRemotesEnabled(bool fallback = false) {
|
||||
return Get().wiiRemotes.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the Bluetooth Wii Remote driver switch.
|
||||
inline bool SetWiiRemotesEnabled(bool value) {
|
||||
Mutable().wiiRemotes = value;
|
||||
return WriteSetting("controller", "wii_remotes", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Whether to keep rescanning Bluetooth while no Wii controller is connected.
|
||||
inline bool WiiContinuousScanEnabled(bool fallback = false) {
|
||||
return Get().wiiContinuousScan.value_or(fallback);
|
||||
inline bool WiiAutoPairEnabled(bool fallback = true) {
|
||||
return Get().wiiAutoPair.value_or(fallback);
|
||||
}
|
||||
|
||||
// Persists the continuous scanning switch.
|
||||
inline bool SetWiiContinuousScanEnabled(bool value) {
|
||||
Mutable().wiiContinuousScan = value;
|
||||
return WriteSetting("controller", "wii_continuous_scan", value ? "true" : "false");
|
||||
inline bool SetWiiAutoPairEnabled(bool value) {
|
||||
Mutable().wiiAutoPair = value;
|
||||
return WriteSetting("controller", "wii_auto_pair", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Stored correction of the remote identified by `remoteKey`, if it has one.
|
||||
inline std::optional<std::array<double, 3>> WiiAccelOffsetFor(const std::string& remoteKey) {
|
||||
const auto it = Get().wiiAccelOffsets.find(remoteKey);
|
||||
if (it == Get().wiiAccelOffsets.end()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return it->second;
|
||||
// Defaults are the console's.
|
||||
inline bool WiiSensorBarAbove(bool fallback = false) {
|
||||
return Get().wiiSensorBarAbove.value_or(fallback);
|
||||
}
|
||||
|
||||
// Whether to write the per-frame accelerometer trace (off unless asked for).
|
||||
inline bool WiiAccelTraceEnabled(bool fallback = false) {
|
||||
return Get().wiiAccelTrace.value_or(fallback);
|
||||
inline bool SetWiiSensorBarAbove(bool value) {
|
||||
Mutable().wiiSensorBarAbove = value;
|
||||
return WriteSetting("controller", "wii_sensor_bar", value ? "\"above\"" : "\"below\"");
|
||||
}
|
||||
|
||||
// Persists the accelerometer correction of one remote. `remoteKey` must be a bare TOML key.
|
||||
inline bool SetWiiAccelOffsetFor(const std::string& remoteKey, const std::array<double, 3>& offset) {
|
||||
Mutable().wiiAccelOffsets[remoteKey] = offset;
|
||||
// Always float literals, so a whole-number offset does not come back as a TOML integer.
|
||||
std::ostringstream formatted;
|
||||
formatted << std::fixed << std::setprecision(4) << '[' << offset[0] << ", " << offset[1] << ", " << offset[2]
|
||||
<< ']';
|
||||
return WriteSetting("controller.wii_accel_offsets", remoteKey, formatted.str());
|
||||
inline int32_t WiiIrSensitivity(int32_t fallback = 3) {
|
||||
return Get().wiiIrSensitivity.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiIrSensitivity(int32_t value) {
|
||||
Mutable().wiiIrSensitivity = value;
|
||||
return WriteSetting("controller", "wii_ir_sensitivity", std::to_string(value));
|
||||
}
|
||||
|
||||
inline int32_t WiiSpeakerVolume(int32_t fallback = 0x59) {
|
||||
return Get().wiiSpeakerVolume.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiSpeakerVolume(int32_t value) {
|
||||
Mutable().wiiSpeakerVolume = value;
|
||||
return WriteSetting("controller", "wii_speaker_volume", std::to_string(value));
|
||||
}
|
||||
|
||||
inline bool WiiRumbleEnabled(bool fallback = true) {
|
||||
return Get().wiiRumble.value_or(fallback);
|
||||
}
|
||||
|
||||
inline bool SetWiiRumbleEnabled(bool value) {
|
||||
Mutable().wiiRumble = value;
|
||||
return WriteSetting("controller", "wii_rumble", value ? "true" : "false");
|
||||
}
|
||||
|
||||
// Target frame rate for frame interpolation, or 0 to disable it.
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
struct PADStatus;
|
||||
|
||||
// Real Wii Remotes paired over Bluetooth.
|
||||
//
|
||||
// SDL 3 ships a HIDAPI driver for them (Wii Remote alone, with Nunchuk, with a
|
||||
// Classic Controller, and the Wii U Pro Controller) that exposes each as a
|
||||
// regular SDL gamepad; it is off by default on SDL's side and ConfigureSdlHints
|
||||
// turns it on. A remote, alone or with a Nunchuk or Classic Controller, is
|
||||
// handed to the game as a real Wii Remote with that extension: the KPAD/WPAD
|
||||
// HLE builds a KPADStatus (and the WPADCLStatus behind KPADGetUnifiedWpadStatus)
|
||||
// from it every frame (see ReadKpadSample), so the game's own code does wheelies,
|
||||
// tricks, Wii Wheel steering and the Classic Controller layout, and plugging an
|
||||
// extension in or out mid-game switches control scheme like on the console. Only
|
||||
// the Wii U Pro Controller, which has no Wii-era equivalent, goes through
|
||||
// aurora's PAD layer as a GameCube pad.
|
||||
namespace WiiRemoteInput {
|
||||
|
||||
enum class Kind : uint8_t {
|
||||
NotWii,
|
||||
Remote, // Wii Remote with no extension
|
||||
RemoteWithNunchuk,
|
||||
RemoteWithClassic,
|
||||
WiiUPro,
|
||||
};
|
||||
|
||||
// Must run before SDL's joystick subsystem is initialized (aurora does that
|
||||
// inside aurora_initialize); SDL only consults the hint on its first device scan.
|
||||
void ConfigureSdlHints(bool enabled);
|
||||
|
||||
// Classifies a gamepad by the name SDL's Wii driver reports for it.
|
||||
Kind KindForName(const char* gamepadName);
|
||||
// Classification of the SDL gamepad currently assigned to a game port.
|
||||
Kind KindForPort(uint32_t port);
|
||||
const char* KindLabel(Kind kind);
|
||||
|
||||
// One frame of a Wii Remote in the units KPAD uses.
|
||||
struct KpadSample {
|
||||
uint32_t hold = 0; // WPAD button bits (WPAD_BUTTON_*), Nunchuk C/Z included
|
||||
float acc[3] = {}; // remote accelerometer in g, KPAD frame (rest: y = -1)
|
||||
bool hasNunchuk = false;
|
||||
float stick[2] = {}; // Nunchuk stick, -1..1, +y up
|
||||
float nunchukAcc[3] = {};
|
||||
bool hasClassic = false;
|
||||
uint32_t clHold = 0; // WPAD_CL_BUTTON_* bits
|
||||
float clLStick[2] = {}; // Classic sticks, -1..1, +y up
|
||||
float clRStick[2] = {};
|
||||
int16_t clLStickRaw[2] = {}; // as WPADCLStatus reports them: -512..511, centre 0, +y up
|
||||
int16_t clRStickRaw[2] = {};
|
||||
uint8_t clTriggerL = 0; // 0..255; SDL only exposes the digital click
|
||||
uint8_t clTriggerR = 0;
|
||||
};
|
||||
|
||||
// What the game should see on `chan`: the controller SDL has there right now,
|
||||
// or, for a few seconds after a remote vanished, the kind it had. SDL's driver
|
||||
// destroys and re-creates the joystick when an extension is plugged in or out,
|
||||
// and the console never disconnects for that, so the gap is papered over with
|
||||
// neutral input instead of a "communications interrupted" prompt.
|
||||
Kind EffectiveKind(uint32_t chan);
|
||||
// True when the game reads `chan` through KPAD: a Wii Remote alone, with a
|
||||
// Nunchuk or with a Classic Controller (live or within the swap grace period).
|
||||
bool IsRemoteChannel(uint32_t chan);
|
||||
// Reads the current state of the remote on `chan`; false when IsRemoteChannel
|
||||
// is false. During the swap grace period the sample is neutral.
|
||||
bool ReadKpadSample(uint32_t chan, KpadSample& sample);
|
||||
|
||||
// Remote accelerometer for the overlay readout: the SDL sample in g after the
|
||||
// zero-point correction (x right across the face, y out of the button face, z
|
||||
// towards the user) and the KPAD vector built from it. False when the port has
|
||||
// no remote or SDL has not delivered a sample yet.
|
||||
bool ReadAccelDebug(uint32_t chan, float sdlG[3], float kpadAcc[3]);
|
||||
|
||||
// Residual zero-point trim on top of SDL's factory-calibrated decode, stored per
|
||||
// physical remote under [controller.wii_accel_offsets]. Expects the remote lying
|
||||
// still, buttons up; Poll() averages about a second and stores it for that remote.
|
||||
void StartAccelCalibration(uint32_t chan);
|
||||
// Forgets the stored correction of the remote on `chan`.
|
||||
void ClearAccelCalibration(uint32_t chan);
|
||||
bool IsAccelCalibrating(uint32_t chan);
|
||||
// 0..1 while a calibration is collecting samples.
|
||||
float AccelCalibrationProgress();
|
||||
// Correction in effect for the remote on `chan` (zeros when uncalibrated), or
|
||||
// nullopt when the port has no remote.
|
||||
std::optional<std::array<double, 3>> ReadAccelOffset(uint32_t chan);
|
||||
// Outcome of the last calibration attempt on `chan` for the overlay, or nullptr.
|
||||
const char* AccelCalibrationMessage(uint32_t chan);
|
||||
|
||||
// Reports "no controller" on the GameCube side for every port served through
|
||||
// KPAD, so the game never sees the same remote twice. Runs after aurora's PADRead.
|
||||
void HideRemotesFromPad(PADStatus* statuses, uint32_t count);
|
||||
|
||||
// Dolphin-style continuous scanning. SDL's HIDAPI Wii driver drops a remote on
|
||||
// a failed Bluetooth read or an extension change and never re-adds it on its
|
||||
// own. Poll() runs once per PADRead and, while no Wii controller is present,
|
||||
// periodically forces SDL to re-enumerate HIDAPI by toggling the driver hint.
|
||||
void Poll();
|
||||
// Forces one re-enumeration right now (settings overlay "Rescan now").
|
||||
void RescanNow();
|
||||
// True while Poll() is looking for a remote (no Wii controller connected).
|
||||
bool IsScanning();
|
||||
// True where looking means periodic rescans; elsewhere Poll() waits for hotplug.
|
||||
bool PeriodicRescanEnabled();
|
||||
// Rescans issued since a Wii controller was last seen.
|
||||
uint32_t ScanCount();
|
||||
|
||||
} // namespace WiiRemoteInput
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// The Wii's extension controller encryption: the game writes a random key to
|
||||
// the extension, which then XORs and offsets every byte it reports.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace Wiimote::ExtCrypto {
|
||||
|
||||
struct Tables {
|
||||
std::array<uint8_t, 8> ft{};
|
||||
std::array<uint8_t, 8> sb{};
|
||||
void Encrypt(uint8_t* data, uint32_t address, size_t length) const;
|
||||
void Decrypt(uint8_t* data, uint32_t address, size_t length) const;
|
||||
};
|
||||
|
||||
// Tables for the key the game wrote to registers 0x40..0x4F, or nullopt when it
|
||||
// does not come from the first-party key schedule.
|
||||
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData);
|
||||
|
||||
} // namespace Wiimote::ExtCrypto
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// Follows the game's conversation with a remote's extension (type, calibration,
|
||||
// encryption key) so its data can be put at rest while the overlay owns input.
|
||||
|
||||
#include "wiimote_ext_crypto.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace Wiimote {
|
||||
|
||||
class ExtensionState {
|
||||
public:
|
||||
void ObserveOutput(const uint8_t* report, size_t size);
|
||||
void ObserveInput(const uint8_t* report, size_t size);
|
||||
// Centres the sticks and releases the buttons of the extension data in a
|
||||
// data report, leaving its motion data alone.
|
||||
void Neutralize(std::vector<uint8_t>& report) const;
|
||||
|
||||
private:
|
||||
enum class Type : uint8_t { Unknown, Nunchuk, Classic };
|
||||
|
||||
void Reset();
|
||||
uint8_t Calibration(size_t index, uint8_t fallback) const;
|
||||
|
||||
Type m_type = Type::Unknown;
|
||||
uint8_t m_format = 0;
|
||||
bool m_encrypted = false;
|
||||
std::array<uint8_t, 16> m_key{};
|
||||
std::optional<ExtCrypto::Tables> m_tables;
|
||||
std::array<uint8_t, 16> m_calibration{};
|
||||
bool m_haveCalibration = false;
|
||||
uint32_t m_readAddress = 0;
|
||||
};
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
// Physical Wii Remotes on the PC. Each one is attached to one of the four
|
||||
// virtual remotes of the emulated Bluetooth controller and its HID reports are
|
||||
// passed through unchanged, so the game's own WPAD/KPAD drive it (IR, speaker,
|
||||
// rumble, extensions, battery).
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace Wiimote {
|
||||
|
||||
struct SlotInfo {
|
||||
bool attached = false;
|
||||
// The game disconnected it and the platform could not power it off; any
|
||||
// button wakes it up again.
|
||||
bool asleep = false;
|
||||
Bluetooth::LinkState link = Bluetooth::LinkState::Down;
|
||||
std::string model; // "Wii Remote" / "Wii Remote Plus"
|
||||
std::string extension; // empty when none
|
||||
int battery = -1; // 0..4 like the game's battery bars, -1 unknown
|
||||
};
|
||||
|
||||
struct PairingInfo {
|
||||
bool supported = false; // this platform can discover and pair remotes itself
|
||||
bool searching = false;
|
||||
std::string status; // last pairing outcome, for the overlay
|
||||
};
|
||||
|
||||
// Must run before SDL's joystick subsystem starts (aurora_initialize).
|
||||
void ConfigureSdlHints(bool enabled);
|
||||
// Starts device scanning when Wii Remotes are enabled.
|
||||
void Initialize();
|
||||
// Powers the remotes off, like a console shutting down, and stops the threads.
|
||||
void Shutdown();
|
||||
|
||||
Bluetooth::RemoteHost& Host();
|
||||
|
||||
SlotInfo Slot(size_t slot);
|
||||
PairingInfo Pairing();
|
||||
// The console's SYNC button: look for remotes in pairing mode for a while.
|
||||
void StartPairing();
|
||||
// Ends the game's connection to the remote on `slot` and powers it off.
|
||||
void Disconnect(size_t slot);
|
||||
// Setup problems worth showing (kernel driver, missing permissions), else empty.
|
||||
std::string PlatformNotice();
|
||||
// Connected remotes waiting for a free player slot.
|
||||
size_t WaitingRemotes();
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
// Host Bluetooth services the Wii Remote manager needs from the OS: finding and
|
||||
// pairing remotes in pairing mode, naming a HID device, and dropping a remote's
|
||||
// connection (which powers it off, as a console does).
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
struct SDL_hid_device_info;
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
|
||||
struct Capabilities {
|
||||
bool pairing = false;
|
||||
bool powerOff = false;
|
||||
};
|
||||
|
||||
Capabilities Query();
|
||||
|
||||
// One search for remotes in pairing mode (red SYNC or 1+2); pairs and connects
|
||||
// each one found. Blocks for a few seconds. Returns the number connected.
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status);
|
||||
|
||||
// Disconnects the remote; it switches itself off. False when not possible.
|
||||
bool PowerOff(const std::string& address);
|
||||
|
||||
// Bluetooth address of a HID device as 12 lowercase hex digits, or empty.
|
||||
std::string AddressFor(const SDL_hid_device_info& info);
|
||||
|
||||
// Bluetooth name ("Nintendo RVL-CNT-01-TR", ...) or the HID product string.
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info);
|
||||
|
||||
// False only when the OS knows the remote is not connected right now.
|
||||
bool MaybeConnected(const std::string& address);
|
||||
|
||||
// False when the OS driver would fight over the device (Linux hid-wiimote).
|
||||
bool Usable(const SDL_hid_device_info& info);
|
||||
|
||||
// Setup problems worth showing in the overlay, else empty.
|
||||
std::string Notice();
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
@@ -0,0 +1,951 @@
|
||||
#include "bluetooth/bt_controller.h"
|
||||
|
||||
#include "bluetooth/bt_bytes.h"
|
||||
#include "bluetooth/bt_remote_link.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace Bluetooth {
|
||||
namespace HciDetail {
|
||||
|
||||
|
||||
constexpr uint16_t kOgfLinkControl = 0x01;
|
||||
constexpr uint16_t kOgfLinkPolicy = 0x02;
|
||||
constexpr uint16_t kOgfControllerBaseband = 0x03;
|
||||
constexpr uint16_t kOgfInformational = 0x04;
|
||||
constexpr uint16_t kOgfStatus = 0x05;
|
||||
constexpr uint16_t kOgfVendor = 0x3F;
|
||||
|
||||
constexpr uint8_t kEventInquiryComplete = 0x01;
|
||||
constexpr uint8_t kEventInquiryResult = 0x02;
|
||||
constexpr uint8_t kEventConnectionComplete = 0x03;
|
||||
constexpr uint8_t kEventDisconnectionComplete = 0x05;
|
||||
constexpr uint8_t kEventAuthenticationComplete = 0x06;
|
||||
constexpr uint8_t kEventRemoteNameComplete = 0x07;
|
||||
constexpr uint8_t kEventEncryptionChange = 0x08;
|
||||
constexpr uint8_t kEventRemoteFeatures = 0x0B;
|
||||
constexpr uint8_t kEventRemoteVersion = 0x0C;
|
||||
constexpr uint8_t kEventCommandComplete = 0x0E;
|
||||
constexpr uint8_t kEventCommandStatus = 0x0F;
|
||||
constexpr uint8_t kEventRoleChange = 0x12;
|
||||
constexpr uint8_t kEventNumberOfCompletedPackets = 0x13;
|
||||
constexpr uint8_t kEventModeChange = 0x14;
|
||||
constexpr uint8_t kEventReturnLinkKeys = 0x15;
|
||||
constexpr uint8_t kEventPinCodeRequest = 0x16;
|
||||
constexpr uint8_t kEventLinkKeyRequest = 0x17;
|
||||
constexpr uint8_t kEventLinkKeyNotification = 0x18;
|
||||
constexpr uint8_t kEventClockOffset = 0x1C;
|
||||
constexpr uint8_t kEventPacketTypeChanged = 0x1D;
|
||||
constexpr uint8_t kEventInquiryResultWithRssi = 0x22;
|
||||
constexpr uint8_t kEventExtendedInquiryResult = 0x2F;
|
||||
|
||||
constexpr uint8_t kStatusSuccess = 0x00;
|
||||
constexpr uint8_t kStatusUnknownCommand = 0x01;
|
||||
constexpr uint8_t kStatusUnknownConnection = 0x02;
|
||||
constexpr uint8_t kStatusPageTimeout = 0x04;
|
||||
constexpr uint8_t kStatusAuthenticationFailure = 0x05;
|
||||
constexpr uint8_t kStatusKeyMissing = 0x06;
|
||||
constexpr uint8_t kStatusUnsupportedFeature = 0x11;
|
||||
constexpr uint8_t kReasonLocalHostTerminated = 0x16;
|
||||
|
||||
// What a Wii's BCM2045 reports (the version makes WUD apply its usual patch).
|
||||
constexpr BdAddr kLocalAddress = {0x11, 0x02, 0x19, 0x79, 0x00, 0xFF};
|
||||
constexpr std::array<uint8_t, 8> kLocalFeatures = {0xFF, 0xFF, 0x8D, 0xFE, 0x9B, 0xF9, 0x00, 0x80};
|
||||
// Never advertise more than 10 ACL buffers: the stack keeps the count in a u8.
|
||||
constexpr uint16_t kAclBufferSize = 339;
|
||||
constexpr uint16_t kAclBufferCount = 10;
|
||||
constexpr uint8_t kScoBufferSize = 64;
|
||||
|
||||
// A real RVL-CNT-01.
|
||||
constexpr std::array<uint8_t, 8> kRemoteFeatures = {0xBC, 0x02, 0x04, 0x38, 0x08, 0x00, 0x00, 0x00};
|
||||
constexpr uint8_t kRemoteLmpVersion = 0x02;
|
||||
constexpr uint16_t kRemoteManufacturer = 0x000F;
|
||||
constexpr uint16_t kRemoteLmpSubversion = 0x0229;
|
||||
constexpr uint16_t kRemoteClockOffset = 0x3818;
|
||||
constexpr uint8_t kInquiryRssi = 0xD8; // -40 dBm
|
||||
constexpr uint64_t kInquiryUnitUs = 1'280'000;
|
||||
constexpr uint16_t kMaxStoredKeys = 0x00FF;
|
||||
constexpr uint8_t kKeysPerEvent = 11;
|
||||
|
||||
void PutAddr(std::vector<uint8_t>& out, const BdAddr& addr) {
|
||||
out.insert(out.end(), addr.begin(), addr.end());
|
||||
}
|
||||
|
||||
BdAddr ReadAddr(const uint8_t* p) {
|
||||
BdAddr addr{};
|
||||
std::memcpy(addr.data(), p, addr.size());
|
||||
return addr;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Event(uint8_t code, const std::vector<uint8_t>& params) {
|
||||
std::vector<uint8_t> event;
|
||||
event.reserve(2 + params.size());
|
||||
event.push_back(code);
|
||||
event.push_back(static_cast<uint8_t>(params.size()));
|
||||
event.insert(event.end(), params.begin(), params.end());
|
||||
return event;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 16> SlotLinkKey(size_t slot) {
|
||||
std::array<uint8_t, 16> key{};
|
||||
for (size_t i = 0; i < key.size(); ++i) {
|
||||
key[i] = static_cast<uint8_t>(0xA0 + slot + i * 0x11);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace HciDetail
|
||||
|
||||
using namespace HciDetail;
|
||||
|
||||
Controller::Controller(RemoteHost& host) : m_host(host) {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
m_links[slot] = std::make_unique<RemoteLink>(*this, slot);
|
||||
m_storedKeys.push_back({RemoteAddress(slot), SlotLinkKey(slot)});
|
||||
}
|
||||
ResetState(true);
|
||||
}
|
||||
|
||||
Controller::~Controller() = default;
|
||||
|
||||
BdAddr Controller::LocalAddress() {
|
||||
return kLocalAddress;
|
||||
}
|
||||
|
||||
BdAddr Controller::RemoteAddress(size_t slot) {
|
||||
return {0x11, 0x02, 0x19, 0x79, 0x00, static_cast<uint8_t>(slot)};
|
||||
}
|
||||
|
||||
uint16_t Controller::ConnectionHandle(size_t slot) {
|
||||
return static_cast<uint16_t>(0x0100 + slot);
|
||||
}
|
||||
|
||||
void Controller::Open(uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
ResetState(true);
|
||||
m_open = true;
|
||||
}
|
||||
|
||||
void Controller::Close() {
|
||||
ResetState(true);
|
||||
m_open = false;
|
||||
}
|
||||
|
||||
void Controller::ResetState(bool keepStoredKeys) {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
RemoteLink& link = *m_links[slot];
|
||||
// A reset drops the radio links: the remotes lose their connection.
|
||||
if (link.State() == LinkState::Ready) {
|
||||
m_host.OnGuestDisconnect(slot);
|
||||
}
|
||||
link.Reset();
|
||||
}
|
||||
if (!keepStoredKeys) {
|
||||
m_storedKeys.clear();
|
||||
}
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
m_inquiry = {};
|
||||
m_scanEnable = 0;
|
||||
m_pageTimeoutSlots = 0x2000;
|
||||
m_inquiryMode = 0;
|
||||
m_classOfDevice = {};
|
||||
m_localName = {};
|
||||
m_completedPackets = {};
|
||||
for (Reassembly& r : m_reassembly) r = {};
|
||||
m_events.clear();
|
||||
m_acl.clear();
|
||||
m_pending.clear();
|
||||
m_authPending = {};
|
||||
}
|
||||
|
||||
LinkState Controller::SlotLinkState(size_t slot) const {
|
||||
return slot < kRemoteSlots ? m_links[slot]->State() : LinkState::Down;
|
||||
}
|
||||
|
||||
int Controller::SlotForAddress(const BdAddr& addr) const {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (RemoteAddress(slot) == addr) return static_cast<int>(slot);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Controller::SlotForHandle(uint16_t handle) const {
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (ConnectionHandle(slot) == (handle & 0x0FFF) &&
|
||||
m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected) {
|
||||
return static_cast<int>(slot);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const Controller::StoredKey* Controller::FindStoredKey(const BdAddr& addr) const {
|
||||
for (const StoredKey& key : m_storedKeys) {
|
||||
if (key.addr == addr) return &key;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Controller::QueueEvent(std::vector<uint8_t> event) {
|
||||
m_events.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void Controller::QueueAcl(uint16_t handle, const std::vector<uint8_t>& l2capFrame) {
|
||||
std::vector<uint8_t> packet;
|
||||
packet.reserve(4 + l2capFrame.size());
|
||||
// Packet boundary 0x2: first (and only) fragment, automatically flushable.
|
||||
PutLe16(packet, static_cast<uint16_t>((handle & 0x0FFF) | 0x2000));
|
||||
PutLe16(packet, static_cast<uint16_t>(l2capFrame.size()));
|
||||
packet.insert(packet.end(), l2capFrame.begin(), l2capFrame.end());
|
||||
m_acl.push_back(std::move(packet));
|
||||
}
|
||||
|
||||
bool Controller::PopEvent(std::vector<uint8_t>& event) {
|
||||
if (m_events.empty()) return false;
|
||||
event = std::move(m_events.front());
|
||||
m_events.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Controller::PopAcl(std::vector<uint8_t>& packet) {
|
||||
if (!m_events.empty() || m_acl.empty()) return false;
|
||||
packet = std::move(m_acl.front());
|
||||
m_acl.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Controller::CommandComplete(uint16_t opcode, const std::vector<uint8_t>& params) {
|
||||
std::vector<uint8_t> body = {0x01};
|
||||
PutLe16(body, opcode);
|
||||
body.insert(body.end(), params.begin(), params.end());
|
||||
QueueEvent(Event(kEventCommandComplete, body));
|
||||
}
|
||||
|
||||
void Controller::CommandStatus(uint16_t opcode, uint8_t status) {
|
||||
std::vector<uint8_t> body = {status, 0x01};
|
||||
PutLe16(body, opcode);
|
||||
QueueEvent(Event(kEventCommandStatus, body));
|
||||
}
|
||||
|
||||
void Controller::UnknownCommand(uint16_t opcode) {
|
||||
std::cerr << "[bluetooth] unsupported HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::ExecuteCommand(const uint8_t* packet, size_t size, uint64_t nowUs) {
|
||||
if (size < 3) return;
|
||||
const uint16_t opcode = Le16(packet);
|
||||
const size_t length = packet[2];
|
||||
if (3 + length > size) {
|
||||
std::cerr << "[bluetooth] truncated HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
return;
|
||||
}
|
||||
const uint8_t* p = packet + 3;
|
||||
switch (opcode >> 10) {
|
||||
case kOgfLinkControl: HandleLinkControl(opcode, p, length, nowUs); break;
|
||||
case kOgfLinkPolicy: HandleLinkPolicy(opcode, p, length); break;
|
||||
case kOgfControllerBaseband: HandleControllerBaseband(opcode, p, length, nowUs); break;
|
||||
case kOgfInformational: HandleInformational(opcode, p, length); break;
|
||||
case kOgfStatus: HandleStatusParameters(opcode, p, length); break;
|
||||
case kOgfVendor: HandleVendor(opcode, p, length); break;
|
||||
default: UnknownCommand(opcode); break;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleLinkControl(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
|
||||
const auto handleSlot = [&](size_t offset) { return n >= offset + 2 ? SlotForHandle(Le16(p + offset)) : -1; };
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: { // Inquiry
|
||||
if (n < 5) break;
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
m_inquiry = {};
|
||||
m_inquiry.active = true;
|
||||
m_inquiry.endUs = nowUs + std::max<uint8_t>(p[3], 1) * kInquiryUnitUs;
|
||||
m_inquiry.maxResponses = p[4];
|
||||
if (!m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(true);
|
||||
m_hostInquiryNotified = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0002: // Inquiry Cancel
|
||||
m_inquiry = {};
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0003: // Periodic Inquiry Mode
|
||||
case 0x0004: // Exit Periodic Inquiry Mode
|
||||
CommandComplete(opcode, {kStatusUnsupportedFeature});
|
||||
return;
|
||||
case 0x0005: { // Create Connection
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_links[slot]->OnPaged(nowUs)) {
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
} else {
|
||||
std::vector<uint8_t> body = {kStatusPageTimeout, 0x00, 0x00};
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventConnectionComplete, body)});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0006: { // Disconnect
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 3) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(kReasonLocalHostTerminated);
|
||||
QueueEvent(Event(kEventDisconnectionComplete, body));
|
||||
m_completedPackets[slot] = 0;
|
||||
m_reassembly[slot] = {};
|
||||
m_authPending[slot] = false;
|
||||
m_links[slot]->OnDisconnected(true, nowUs);
|
||||
return;
|
||||
}
|
||||
case 0x0008: { // Create Connection Cancel
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
|
||||
return e.event[0] == kEventConnectionComplete && ReadAddr(&e.event[5]) == addr;
|
||||
});
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
|
||||
: kStatusSuccess)};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
if (it != m_pending.end()) {
|
||||
std::vector<uint8_t> complete = it->event;
|
||||
complete[2] = kStatusUnknownConnection;
|
||||
m_pending.erase(it);
|
||||
QueueEvent(std::move(complete));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0009: { // Accept Connection Request
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const uint8_t role = p[6];
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_links[slot]->IsRequesting()) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
if (role == 0x00) {
|
||||
std::vector<uint8_t> change = {kStatusSuccess};
|
||||
PutAddr(change, addr);
|
||||
change.push_back(0x00);
|
||||
QueueEvent(Event(kEventRoleChange, change));
|
||||
}
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
m_completedPackets[slot] = 0;
|
||||
m_reassembly[slot] = {};
|
||||
m_links[slot]->OnAccepted(role);
|
||||
return;
|
||||
}
|
||||
case 0x000A: { // Reject Connection Request
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_links[slot]->IsRequesting()) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {p[6], 0x00, 0x00};
|
||||
PutAddr(body, addr);
|
||||
body.insert(body.end(), {0x01, 0x00});
|
||||
QueueEvent(Event(kEventConnectionComplete, body));
|
||||
m_links[slot]->OnRejected(p[6], nowUs);
|
||||
return;
|
||||
}
|
||||
case 0x000B: { // Link Key Request Reply
|
||||
if (n < 22) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
m_authPending[slot] = false;
|
||||
const bool match = std::equal(p + 6, p + 22, SlotLinkKey(slot).begin());
|
||||
std::vector<uint8_t> auth = {static_cast<uint8_t>(match ? kStatusSuccess : kStatusKeyMissing)};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000C: { // Link Key Request Negative Reply
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
std::vector<uint8_t> request;
|
||||
PutAddr(request, addr);
|
||||
QueueEvent(Event(kEventPinCodeRequest, request));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000D: { // PIN Code Request Reply
|
||||
if (n < 23) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot < 0 || !m_authPending[slot]) return;
|
||||
m_authPending[slot] = false;
|
||||
// A remote takes the host's address (SYNC button) or its own (1+2) as PIN.
|
||||
const uint8_t pinLength = p[6];
|
||||
const uint8_t* pin = p + 7;
|
||||
const BdAddr local = LocalAddress();
|
||||
const bool valid = pinLength == 6 && (std::equal(pin, pin + 6, local.begin()) ||
|
||||
std::equal(pin, pin + 6, addr.begin()));
|
||||
if (valid) {
|
||||
std::vector<uint8_t> notification;
|
||||
PutAddr(notification, addr);
|
||||
const auto key = SlotLinkKey(slot);
|
||||
notification.insert(notification.end(), key.begin(), key.end());
|
||||
notification.push_back(0x00); // combination key
|
||||
QueueEvent(Event(kEventLinkKeyNotification, notification));
|
||||
}
|
||||
std::vector<uint8_t> auth = {static_cast<uint8_t>(valid ? kStatusSuccess : kStatusAuthenticationFailure)};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
return;
|
||||
}
|
||||
case 0x000E: { // PIN Code Request Negative Reply
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
const int slot = SlotForAddress(addr);
|
||||
if (slot >= 0 && m_authPending[slot]) {
|
||||
m_authPending[slot] = false;
|
||||
std::vector<uint8_t> auth = {kStatusKeyMissing};
|
||||
PutLe16(auth, ConnectionHandle(slot));
|
||||
QueueEvent(Event(kEventAuthenticationComplete, auth));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x000F: { // Change Connection Packet Type
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 4) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, Le16(p + 2));
|
||||
QueueEvent(Event(kEventPacketTypeChanged, body));
|
||||
return;
|
||||
}
|
||||
case 0x0011: { // Authentication Requested
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
m_authPending[slot] = true;
|
||||
std::vector<uint8_t> request;
|
||||
PutAddr(request, RemoteAddress(slot));
|
||||
QueueEvent(Event(kEventLinkKeyRequest, request));
|
||||
return;
|
||||
}
|
||||
case 0x0013: { // Set Connection Encryption
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0 || n < 3) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(p[2]);
|
||||
QueueEvent(Event(kEventEncryptionChange, body));
|
||||
return;
|
||||
}
|
||||
case 0x0019: { // Remote Name Request
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
const int slot = SlotForAddress(addr);
|
||||
const bool reachable = slot >= 0 && (m_links[slot]->BasebandState() == RemoteLink::Baseband::Connected ||
|
||||
m_links[slot]->Discoverable());
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(reachable ? kStatusSuccess : kStatusPageTimeout)};
|
||||
PutAddr(body, addr);
|
||||
std::array<char, 248> name{};
|
||||
if (reachable) {
|
||||
std::strncpy(name.data(), m_host.DeviceName(slot), name.size() - 1);
|
||||
}
|
||||
body.insert(body.end(), name.begin(), name.end());
|
||||
if (reachable) {
|
||||
QueueEvent(Event(kEventRemoteNameComplete, body));
|
||||
} else {
|
||||
m_pending.push_back({nowUs + PageTimeoutUs(), Event(kEventRemoteNameComplete, body)});
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x001A: { // Remote Name Request Cancel
|
||||
if (n < 6) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
auto it = std::find_if(m_pending.begin(), m_pending.end(), [&](const PendingEvent& e) {
|
||||
return e.event[0] == kEventRemoteNameComplete && ReadAddr(&e.event[3]) == addr;
|
||||
});
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(it == m_pending.end() ? kStatusUnknownConnection
|
||||
: kStatusSuccess)};
|
||||
PutAddr(body, addr);
|
||||
CommandComplete(opcode, body);
|
||||
if (it != m_pending.end()) {
|
||||
std::vector<uint8_t> complete = it->event;
|
||||
complete[2] = kStatusUnknownConnection;
|
||||
m_pending.erase(it);
|
||||
QueueEvent(std::move(complete));
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x001B: { // Read Remote Supported Features
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.insert(body.end(), kRemoteFeatures.begin(), kRemoteFeatures.end());
|
||||
QueueEvent(Event(kEventRemoteFeatures, body));
|
||||
return;
|
||||
}
|
||||
case 0x001D: { // Read Remote Version Information
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
body.push_back(kRemoteLmpVersion);
|
||||
PutLe16(body, kRemoteManufacturer);
|
||||
PutLe16(body, kRemoteLmpSubversion);
|
||||
QueueEvent(Event(kEventRemoteVersion, body));
|
||||
return;
|
||||
}
|
||||
case 0x001F: { // Read Clock Offset
|
||||
const int slot = handleSlot(0);
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
QueueEvent(Event(kEventClockOffset, body));
|
||||
return;
|
||||
}
|
||||
case 0x0007: // Add SCO Connection
|
||||
case 0x0028: // Setup Synchronous Connection
|
||||
case 0x0029: // Accept Synchronous Connection Request
|
||||
case 0x002A: // Reject Synchronous Connection Request
|
||||
CommandStatus(opcode, kStatusUnsupportedFeature);
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::HandleLinkPolicy(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
const auto modeChange = [&](uint8_t mode, uint16_t interval) {
|
||||
if (slot < 0) {
|
||||
CommandStatus(opcode, kStatusUnknownConnection);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, ConnectionHandle(static_cast<size_t>(slot)));
|
||||
body.push_back(mode);
|
||||
PutLe16(body, interval);
|
||||
QueueEvent(Event(kEventModeChange, body));
|
||||
};
|
||||
const auto handleReply = [&](std::vector<uint8_t> extra) {
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
body.insert(body.end(), extra.begin(), extra.end());
|
||||
CommandComplete(opcode, body);
|
||||
};
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: modeChange(0x01, n >= 4 ? Le16(p + 2) : 0); return; // Hold Mode
|
||||
case 0x0003: modeChange(0x02, n >= 4 ? Le16(p + 2) : 0); return; // Sniff Mode
|
||||
case 0x0004: modeChange(0x00, 0); return; // Exit Sniff Mode
|
||||
case 0x0005: modeChange(0x03, n >= 4 ? Le16(p + 2) : 0); return; // Park State
|
||||
case 0x0006: modeChange(0x00, 0); return; // Exit Park State
|
||||
case 0x0009: handleReply({0x00}); return; // Role Discovery
|
||||
case 0x000B: { // Switch Role
|
||||
if (n < 7) break;
|
||||
CommandStatus(opcode, kStatusSuccess);
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), p, p + 7);
|
||||
QueueEvent(Event(kEventRoleChange, body));
|
||||
return;
|
||||
}
|
||||
case 0x000C: handleReply({0x05, 0x00}); return; // Read Link Policy Settings
|
||||
case 0x000D: handleReply({}); return; // Write Link Policy Settings
|
||||
case 0x000E: CommandComplete(opcode, {kStatusSuccess, 0x05, 0x00}); return; // Read Default Link Policy
|
||||
case 0x000F: CommandComplete(opcode, {kStatusSuccess}); return; // Write Default Link Policy
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
CommandStatus(opcode, kStatusUnknownCommand);
|
||||
}
|
||||
|
||||
void Controller::HandleControllerBaseband(uint16_t opcode, const uint8_t* p, size_t n, uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
const auto handleReply = [&](std::vector<uint8_t> extra) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
body.insert(body.end(), extra.begin(), extra.end());
|
||||
CommandComplete(opcode, body);
|
||||
};
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0003: // Reset
|
||||
ResetState(true);
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x000D: { // Read Stored Link Key
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const bool readAll = p[6] == 0x01;
|
||||
std::vector<StoredKey> keys;
|
||||
for (const StoredKey& key : m_storedKeys) {
|
||||
if (readAll || key.addr == addr) keys.push_back(key);
|
||||
}
|
||||
for (size_t start = 0; start < keys.size(); start += kKeysPerEvent) {
|
||||
const size_t count = std::min<size_t>(kKeysPerEvent, keys.size() - start);
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(count)};
|
||||
for (size_t i = start; i < start + count; ++i) {
|
||||
PutAddr(body, keys[i].addr);
|
||||
body.insert(body.end(), keys[i].key.begin(), keys[i].key.end());
|
||||
}
|
||||
QueueEvent(Event(kEventReturnLinkKeys, body));
|
||||
}
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, kMaxStoredKeys);
|
||||
PutLe16(body, static_cast<uint16_t>(keys.size()));
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0011: { // Write Stored Link Key
|
||||
if (n < 1) break;
|
||||
const uint8_t count = p[0];
|
||||
if (n < 1 + count * size_t{22}) break;
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
const uint8_t* entry = p + 1 + i * 22;
|
||||
StoredKey key{ReadAddr(entry), {}};
|
||||
std::copy(entry + 6, entry + 22, key.key.begin());
|
||||
auto existing = std::find_if(m_storedKeys.begin(), m_storedKeys.end(),
|
||||
[&](const StoredKey& k) { return k.addr == key.addr; });
|
||||
if (existing != m_storedKeys.end()) {
|
||||
*existing = key;
|
||||
} else if (m_storedKeys.size() < kMaxStoredKeys) {
|
||||
m_storedKeys.push_back(key);
|
||||
}
|
||||
}
|
||||
CommandComplete(opcode, {kStatusSuccess, count});
|
||||
return;
|
||||
}
|
||||
case 0x0012: { // Delete Stored Link Key
|
||||
if (n < 7) break;
|
||||
const BdAddr addr = ReadAddr(p);
|
||||
const bool deleteAll = p[6] == 0x01;
|
||||
const size_t before = m_storedKeys.size();
|
||||
m_storedKeys.erase(std::remove_if(m_storedKeys.begin(), m_storedKeys.end(),
|
||||
[&](const StoredKey& k) { return deleteAll || k.addr == addr; }),
|
||||
m_storedKeys.end());
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, static_cast<uint16_t>(before - m_storedKeys.size()));
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0013: // Change Local Name
|
||||
if (n < m_localName.size()) break;
|
||||
std::memcpy(m_localName.data(), p, m_localName.size());
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0014: { // Read Local Name
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), m_localName.begin(), m_localName.end());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0018: // Write Page Timeout
|
||||
if (n < 2) break;
|
||||
m_pageTimeoutSlots = Le16(p);
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0019: // Read Scan Enable
|
||||
CommandComplete(opcode, {kStatusSuccess, m_scanEnable});
|
||||
return;
|
||||
case 0x001A: // Write Scan Enable
|
||||
if (n < 1) break;
|
||||
m_scanEnable = p[0];
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0023: // Read Class of Device
|
||||
CommandComplete(opcode, {kStatusSuccess, m_classOfDevice[0], m_classOfDevice[1], m_classOfDevice[2]});
|
||||
return;
|
||||
case 0x0024: // Write Class of Device
|
||||
if (n < 3) break;
|
||||
std::copy(p, p + 3, m_classOfDevice.begin());
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0045: // Write Inquiry Mode
|
||||
if (n < 1) break;
|
||||
m_inquiryMode = p[0];
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0028: // Write Automatic Flush Timeout
|
||||
case 0x0037: // Write Link Supervision Timeout
|
||||
handleReply({});
|
||||
return;
|
||||
case 0x0036: // Read Link Supervision Timeout
|
||||
handleReply({0x80, 0x0C});
|
||||
return;
|
||||
case 0x0001: // Set Event Mask
|
||||
case 0x0005: // Set Event Filter
|
||||
case 0x000A: // Write PIN Type
|
||||
case 0x0016: // Write Connection Accept Timeout
|
||||
case 0x001C: // Write Page Scan Activity
|
||||
case 0x001E: // Write Inquiry Scan Activity
|
||||
case 0x0020: // Write Authentication Enable
|
||||
case 0x0022: // Write Encryption Mode
|
||||
case 0x0031: // Set Controller To Host Flow Control
|
||||
case 0x0033: // Host Buffer Size
|
||||
case 0x003A: // Write Current IAC LAP
|
||||
case 0x003F: // Set AFH Host Channel Classification
|
||||
case 0x0043: // Write Inquiry Scan Type
|
||||
case 0x0047: // Write Page Scan Type
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
return;
|
||||
case 0x0035: // Host Number Of Completed Packets: no event
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
std::cerr << "[bluetooth] short HCI command 0x" << std::hex << opcode << std::dec << std::endl;
|
||||
CommandComplete(opcode, {kStatusUnknownCommand});
|
||||
}
|
||||
|
||||
void Controller::HandleInformational(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
(void)p;
|
||||
(void)n;
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0001: { // Read Local Version Information
|
||||
std::vector<uint8_t> body = {kStatusSuccess, 0x03};
|
||||
PutLe16(body, 0x40A7);
|
||||
body.push_back(0x03);
|
||||
PutLe16(body, 0x000F);
|
||||
PutLe16(body, 0x430E);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0003: { // Read Local Supported Features
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
body.insert(body.end(), kLocalFeatures.begin(), kLocalFeatures.end());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0005: { // Read Buffer Size
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutLe16(body, kAclBufferSize);
|
||||
body.push_back(kScoBufferSize);
|
||||
PutLe16(body, kAclBufferCount);
|
||||
PutLe16(body, 0);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
case 0x0009: { // Read BD_ADDR
|
||||
std::vector<uint8_t> body = {kStatusSuccess};
|
||||
PutAddr(body, LocalAddress());
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleStatusParameters(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
const int slot = n >= 2 ? SlotForHandle(Le16(p)) : -1;
|
||||
std::vector<uint8_t> body = {static_cast<uint8_t>(slot < 0 ? kStatusUnknownConnection : kStatusSuccess)};
|
||||
PutLe16(body, n >= 2 ? Le16(p) : 0);
|
||||
switch (opcode & 0x3FF) {
|
||||
case 0x0003: // Get Link Quality
|
||||
body.push_back(0xFF);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
case 0x0005: // Read RSSI: 0 = inside the golden receive power range
|
||||
body.push_back(0x00);
|
||||
CommandComplete(opcode, body);
|
||||
return;
|
||||
default:
|
||||
UnknownCommand(opcode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::HandleVendor(uint16_t opcode, const uint8_t* p, size_t n) {
|
||||
(void)p;
|
||||
(void)n;
|
||||
// WUD's firmware patch upload (0xFC4C writes, 0xFC4F install) and friends.
|
||||
CommandComplete(opcode, {kStatusSuccess});
|
||||
}
|
||||
|
||||
void Controller::ReceiveAcl(const uint8_t* packet, size_t size, uint64_t nowUs) {
|
||||
(void)nowUs;
|
||||
if (size < 4) return;
|
||||
const uint16_t header = Le16(packet);
|
||||
const uint16_t length = Le16(packet + 2);
|
||||
if (length != size - 4) {
|
||||
std::cerr << "[bluetooth] dropped malformed ACL packet" << std::endl;
|
||||
return;
|
||||
}
|
||||
const int slot = SlotForHandle(header & 0x0FFF);
|
||||
if (slot < 0) return;
|
||||
++m_completedPackets[slot];
|
||||
const uint8_t boundary = static_cast<uint8_t>((header >> 12) & 0x3);
|
||||
Reassembly& r = m_reassembly[slot];
|
||||
const uint8_t* data = packet + 4;
|
||||
if (boundary == 0x1) {
|
||||
if (r.expected == 0) return;
|
||||
r.frame.insert(r.frame.end(), data, data + length);
|
||||
} else {
|
||||
r.frame.assign(data, data + length);
|
||||
r.expected = length >= 2 ? static_cast<size_t>(Le16(data)) + 4 : 0;
|
||||
}
|
||||
if (r.expected != 0 && r.frame.size() >= r.expected) {
|
||||
const std::vector<uint8_t> frame(r.frame.begin(), r.frame.begin() + static_cast<std::ptrdiff_t>(r.expected));
|
||||
r = {};
|
||||
m_links[slot]->ReceiveL2cap(frame.data(), frame.size());
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::Tick(uint64_t nowUs) {
|
||||
if (!m_open) return;
|
||||
for (size_t i = 0; i < m_pending.size();) {
|
||||
if (nowUs >= m_pending[i].dueUs) {
|
||||
QueueEvent(std::move(m_pending[i].event));
|
||||
m_pending.erase(m_pending.begin() + static_cast<std::ptrdiff_t>(i));
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
TickInquiry(nowUs);
|
||||
for (auto& link : m_links) {
|
||||
link->Tick(nowUs);
|
||||
}
|
||||
SendNumberOfCompletedPackets();
|
||||
}
|
||||
|
||||
void Controller::TickInquiry(uint64_t nowUs) {
|
||||
if (!m_inquiry.active) return;
|
||||
if (nowUs >= m_inquiry.endUs ||
|
||||
(m_inquiry.maxResponses != 0 && m_inquiry.reported >= m_inquiry.maxResponses)) {
|
||||
FinishInquiry();
|
||||
return;
|
||||
}
|
||||
// One result per tick, lowest slot first, so remotes keep their order.
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (m_inquiry.slotReported[slot] || !m_links[slot]->Discoverable()) continue;
|
||||
m_inquiry.slotReported[slot] = true;
|
||||
++m_inquiry.reported;
|
||||
const BdAddr addr = RemoteAddress(slot);
|
||||
std::vector<uint8_t> body = {0x01};
|
||||
PutAddr(body, addr);
|
||||
body.push_back(0x01); // page scan repetition mode R1
|
||||
if (m_inquiryMode == 0) {
|
||||
body.insert(body.end(), {0x00, 0x00, 0x04, 0x25, 0x00});
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
QueueEvent(Event(kEventInquiryResult, body));
|
||||
} else {
|
||||
body.insert(body.end(), {0x00, 0x04, 0x25, 0x00});
|
||||
PutLe16(body, kRemoteClockOffset);
|
||||
body.push_back(kInquiryRssi);
|
||||
if (m_inquiryMode == 2) {
|
||||
std::array<uint8_t, 240> eir{};
|
||||
const char* name = m_host.DeviceName(slot);
|
||||
const size_t nameLength = std::min<size_t>(std::strlen(name), eir.size() - 2);
|
||||
eir[0] = static_cast<uint8_t>(nameLength + 1);
|
||||
eir[1] = 0x09; // complete local name
|
||||
std::memcpy(&eir[2], name, nameLength);
|
||||
body.insert(body.end(), eir.begin(), eir.end());
|
||||
QueueEvent(Event(kEventExtendedInquiryResult, body));
|
||||
} else {
|
||||
QueueEvent(Event(kEventInquiryResultWithRssi, body));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::FinishInquiry() {
|
||||
m_inquiry = {};
|
||||
QueueEvent(Event(kEventInquiryComplete, {kStatusSuccess}));
|
||||
if (m_hostInquiryNotified) {
|
||||
m_host.OnGuestInquiry(false);
|
||||
m_hostInquiryNotified = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::SendNumberOfCompletedPackets() {
|
||||
std::vector<uint8_t> body = {0};
|
||||
for (size_t slot = 0; slot < kRemoteSlots; ++slot) {
|
||||
if (m_completedPackets[slot] == 0) continue;
|
||||
PutLe16(body, ConnectionHandle(slot));
|
||||
PutLe16(body, m_completedPackets[slot]);
|
||||
m_completedPackets[slot] = 0;
|
||||
++body[0];
|
||||
}
|
||||
if (body[0] != 0) {
|
||||
QueueEvent(Event(kEventNumberOfCompletedPackets, body));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,567 @@
|
||||
#include "bluetooth/bt_remote_link.h"
|
||||
|
||||
#include "bluetooth/bt_bytes.h"
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
|
||||
namespace Bluetooth {
|
||||
namespace L2capDetail {
|
||||
|
||||
constexpr uint16_t kSignallingCid = 0x0001;
|
||||
constexpr uint16_t kFirstDynamicCid = 0x0040;
|
||||
|
||||
constexpr uint16_t kPsmSdp = 0x0001;
|
||||
constexpr uint16_t kPsmHidControl = 0x0011;
|
||||
constexpr uint16_t kPsmHidInterrupt = 0x0013;
|
||||
|
||||
constexpr uint8_t kCommandReject = 0x01;
|
||||
constexpr uint8_t kConnectionRequest = 0x02;
|
||||
constexpr uint8_t kConnectionResponse = 0x03;
|
||||
constexpr uint8_t kConfigurationRequest = 0x04;
|
||||
constexpr uint8_t kConfigurationResponse = 0x05;
|
||||
constexpr uint8_t kDisconnectionRequest = 0x06;
|
||||
constexpr uint8_t kDisconnectionResponse = 0x07;
|
||||
constexpr uint8_t kEchoRequest = 0x08;
|
||||
constexpr uint8_t kEchoResponse = 0x09;
|
||||
constexpr uint8_t kInformationRequest = 0x0A;
|
||||
constexpr uint8_t kInformationResponse = 0x0B;
|
||||
|
||||
constexpr uint16_t kResultSuccess = 0x0000;
|
||||
constexpr uint16_t kResultPending = 0x0001;
|
||||
constexpr uint16_t kResultPsmNotSupported = 0x0002;
|
||||
constexpr uint16_t kResultNoResources = 0x0004;
|
||||
|
||||
constexpr uint8_t kOptionMtu = 0x01;
|
||||
constexpr uint8_t kOptionFlushTimeout = 0x02;
|
||||
// A real remote asks for this MTU on its own channels.
|
||||
constexpr uint16_t kRemoteMtu = 185;
|
||||
|
||||
constexpr uint8_t kHidHandshake = 0x0;
|
||||
constexpr uint8_t kHidControl = 0x1;
|
||||
constexpr uint8_t kHidSetReport = 0x5;
|
||||
constexpr uint8_t kHidData = 0xA;
|
||||
constexpr uint8_t kHidParamOutput = 0x2;
|
||||
constexpr uint8_t kHidHandshakeSuccess = 0x00;
|
||||
constexpr uint8_t kHidHandshakeUnsupported = 0x03;
|
||||
constexpr uint8_t kHidVirtualCableUnplug = 0x5;
|
||||
constexpr uint8_t kHidDataInput = 0xA1;
|
||||
|
||||
constexpr uint64_t kRequestTimeoutUs = 10'000'000;
|
||||
constexpr uint64_t kRetryDelayUs = 3'000'000;
|
||||
constexpr uint8_t kReasonRemoteUserTerminated = 0x13;
|
||||
// Output reports carry at most 21 payload bytes; anything longer is not a Wii Remote report.
|
||||
constexpr size_t kMaxReportSize = 22;
|
||||
constexpr size_t kInputBacklog = 8;
|
||||
|
||||
} // namespace L2capDetail
|
||||
|
||||
using namespace L2capDetail;
|
||||
|
||||
RemoteLink::RemoteLink(Controller& controller, size_t slot) : m_controller(controller), m_slot(slot) {}
|
||||
|
||||
void RemoteLink::Reset() {
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
m_retryAtUs = 0;
|
||||
}
|
||||
|
||||
void RemoteLink::ClearConnection() {
|
||||
m_channels.clear();
|
||||
m_guestInitiated = false;
|
||||
m_everReady = false;
|
||||
}
|
||||
|
||||
bool RemoteLink::Discoverable() const {
|
||||
return m_baseband == Baseband::Idle && m_connectWanted && m_controller.Host().IsAttached(m_slot);
|
||||
}
|
||||
|
||||
void RemoteLink::Tick(uint64_t nowUs) {
|
||||
m_nowUs = nowUs;
|
||||
RemoteHost& host = m_controller.Host();
|
||||
if (m_baseband == Baseband::Idle) {
|
||||
if (host.ConsumeWake(m_slot)) {
|
||||
m_connectWanted = true;
|
||||
m_retryAtUs = 0;
|
||||
}
|
||||
if (!host.IsAttached(m_slot)) {
|
||||
m_connectWanted = false;
|
||||
}
|
||||
if (m_connectWanted && m_controller.PageScanEnabled() && nowUs >= m_retryAtUs) {
|
||||
std::vector<uint8_t> event = {0x04, 10};
|
||||
const BdAddr addr = Controller::RemoteAddress(m_slot);
|
||||
event.insert(event.end(), addr.begin(), addr.end());
|
||||
// Class of device 0x002504: peripheral, joystick.
|
||||
event.insert(event.end(), {0x04, 0x25, 0x00, 0x01});
|
||||
m_controller.QueueEvent(std::move(event));
|
||||
m_baseband = Baseband::Requesting;
|
||||
m_stateSinceUs = nowUs;
|
||||
}
|
||||
} else if (m_baseband == Baseband::Requesting) {
|
||||
if (nowUs - m_stateSinceUs > kRequestTimeoutUs) {
|
||||
m_baseband = Baseband::Idle;
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
} else if (m_baseband == Baseband::Connected) {
|
||||
if (!host.IsAttached(m_slot)) {
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_connectWanted = false;
|
||||
return;
|
||||
}
|
||||
if (!m_guestInitiated) {
|
||||
const Channel* control = FindByPsm(kPsmHidControl);
|
||||
if (control == nullptr) {
|
||||
RequestChannel(kPsmHidControl, nowUs);
|
||||
} else if (control->Open() && FindByPsm(kPsmHidInterrupt) == nullptr) {
|
||||
RequestChannel(kPsmHidInterrupt, nowUs);
|
||||
}
|
||||
}
|
||||
for (const Channel& channel : m_channels) {
|
||||
const bool waiting = (channel.weInitiated && !channel.connected) ||
|
||||
(channel.configSent && !channel.localConfigured);
|
||||
if (waiting && nowUs - channel.requestUs > kRequestTimeoutUs) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": L2CAP channel 0x" << std::hex
|
||||
<< channel.psm << std::dec << " did not open, dropping the connection" << std::endl;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ForwardInput();
|
||||
}
|
||||
|
||||
void RemoteLink::ForwardInput() {
|
||||
RemoteHost& host = m_controller.Host();
|
||||
if (m_state != LinkState::Ready) {
|
||||
while (host.PopInputReport(m_slot, m_report)) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
|
||||
while (m_controller.AclQueued() < kInputBacklog && host.PopInputReport(m_slot, m_report)) {
|
||||
if (m_report.empty() || m_report.size() > kMaxReportSize) continue;
|
||||
SendInput(*interrupt, m_report);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::SendInput(const Channel& interrupt, const std::vector<uint8_t>& report) {
|
||||
std::vector<uint8_t> payload;
|
||||
payload.reserve(report.size() + 1);
|
||||
payload.push_back(kHidDataInput);
|
||||
payload.insert(payload.end(), report.begin(), report.end());
|
||||
if (payload.size() <= interrupt.remoteMtu) {
|
||||
SendData(interrupt, payload);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void RemoteLink::OnAccepted(uint8_t role) {
|
||||
(void)role;
|
||||
m_baseband = Baseband::Connected;
|
||||
m_guestInitiated = false;
|
||||
m_everReady = false;
|
||||
m_stateSinceUs = m_nowUs;
|
||||
SetState(LinkState::Connecting);
|
||||
}
|
||||
|
||||
void RemoteLink::OnRejected(uint8_t reason, uint64_t nowUs) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": connection rejected (0x" << std::hex
|
||||
<< static_cast<int>(reason) << std::dec << ")" << std::endl;
|
||||
m_baseband = Baseband::Idle;
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
|
||||
bool RemoteLink::OnPaged(uint64_t nowUs) {
|
||||
if (!Discoverable()) {
|
||||
return false;
|
||||
}
|
||||
m_nowUs = nowUs;
|
||||
m_baseband = Baseband::Connected;
|
||||
m_guestInitiated = true;
|
||||
m_everReady = false;
|
||||
m_stateSinceUs = nowUs;
|
||||
SetState(LinkState::Connecting);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RemoteLink::OnDisconnected(bool byGuest, uint64_t nowUs) {
|
||||
const bool wasReady = m_everReady;
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
if (byGuest && wasReady) {
|
||||
m_connectWanted = false;
|
||||
m_controller.Host().OnGuestDisconnect(m_slot);
|
||||
} else {
|
||||
m_retryAtUs = nowUs + kRetryDelayUs;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::DropFromRemote(uint8_t reason) {
|
||||
std::vector<uint8_t> event = {0x05, 4, 0x00};
|
||||
PutLe16(event, Controller::ConnectionHandle(m_slot));
|
||||
event.push_back(reason);
|
||||
m_controller.QueueEvent(std::move(event));
|
||||
ClearConnection();
|
||||
m_baseband = Baseband::Idle;
|
||||
SetState(LinkState::Down);
|
||||
}
|
||||
|
||||
void RemoteLink::SetState(LinkState state) {
|
||||
if (m_state == state) return;
|
||||
m_state = state;
|
||||
if (state == LinkState::Ready) {
|
||||
m_everReady = true;
|
||||
m_connectWanted = false;
|
||||
}
|
||||
m_controller.Host().OnLinkStateChanged(m_slot, state);
|
||||
}
|
||||
|
||||
void RemoteLink::UpdateState() {
|
||||
if (m_baseband != Baseband::Connected) {
|
||||
SetState(LinkState::Down);
|
||||
return;
|
||||
}
|
||||
const Channel* control = FindByPsm(kPsmHidControl);
|
||||
const Channel* interrupt = FindByPsm(kPsmHidInterrupt);
|
||||
const bool ready = control != nullptr && interrupt != nullptr && control->Open() && interrupt->Open();
|
||||
SetState(ready ? LinkState::Ready : LinkState::Connecting);
|
||||
}
|
||||
|
||||
RemoteLink::Channel* RemoteLink::FindByLocal(uint16_t cid) {
|
||||
for (Channel& channel : m_channels) {
|
||||
if (channel.localCid == cid) return &channel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RemoteLink::Channel* RemoteLink::FindByPsm(uint16_t psm) {
|
||||
for (Channel& channel : m_channels) {
|
||||
if (channel.psm == psm) return &channel;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint16_t RemoteLink::AllocateCid() const {
|
||||
uint16_t cid = kFirstDynamicCid;
|
||||
while (std::any_of(m_channels.begin(), m_channels.end(), [&](const Channel& c) { return c.localCid == cid; })) {
|
||||
++cid;
|
||||
}
|
||||
return cid;
|
||||
}
|
||||
|
||||
uint8_t RemoteLink::NextIdent() {
|
||||
m_ident = static_cast<uint8_t>(m_ident + 1);
|
||||
if (m_ident == 0) m_ident = 1;
|
||||
return m_ident;
|
||||
}
|
||||
|
||||
void RemoteLink::SendSignal(uint8_t code, uint8_t ident, const std::vector<uint8_t>& data) {
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(8 + data.size());
|
||||
PutLe16(frame, static_cast<uint16_t>(4 + data.size()));
|
||||
PutLe16(frame, kSignallingCid);
|
||||
frame.push_back(code);
|
||||
frame.push_back(ident);
|
||||
PutLe16(frame, static_cast<uint16_t>(data.size()));
|
||||
frame.insert(frame.end(), data.begin(), data.end());
|
||||
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
|
||||
}
|
||||
|
||||
void RemoteLink::SendData(const Channel& channel, const std::vector<uint8_t>& payload) {
|
||||
std::vector<uint8_t> frame;
|
||||
frame.reserve(4 + payload.size());
|
||||
PutLe16(frame, static_cast<uint16_t>(payload.size()));
|
||||
PutLe16(frame, channel.remoteCid);
|
||||
frame.insert(frame.end(), payload.begin(), payload.end());
|
||||
m_controller.QueueAcl(Controller::ConnectionHandle(m_slot), frame);
|
||||
}
|
||||
|
||||
void RemoteLink::RequestChannel(uint16_t psm, uint64_t nowUs) {
|
||||
Channel channel;
|
||||
channel.psm = psm;
|
||||
channel.localCid = AllocateCid();
|
||||
channel.weInitiated = true;
|
||||
channel.connectIdent = NextIdent();
|
||||
channel.requestUs = nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, psm);
|
||||
PutLe16(data, channel.localCid);
|
||||
SendSignal(kConnectionRequest, channel.connectIdent, data);
|
||||
m_channels.push_back(channel);
|
||||
}
|
||||
|
||||
void RemoteLink::SendConfigRequest(Channel& channel) {
|
||||
channel.configSent = true;
|
||||
channel.configIdent = NextIdent();
|
||||
channel.requestUs = m_nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, channel.remoteCid);
|
||||
PutLe16(data, 0);
|
||||
data.push_back(kOptionMtu);
|
||||
data.push_back(2);
|
||||
PutLe16(data, kRemoteMtu);
|
||||
SendSignal(kConfigurationRequest, channel.configIdent, data);
|
||||
}
|
||||
|
||||
void RemoteLink::ReceiveL2cap(const uint8_t* frame, size_t size) {
|
||||
if (m_baseband != Baseband::Connected || size < 4) return;
|
||||
const uint16_t length = Le16(frame);
|
||||
const uint16_t cid = Le16(frame + 2);
|
||||
if (length != size - 4) {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": dropped malformed L2CAP frame" << std::endl;
|
||||
return;
|
||||
}
|
||||
if (cid == kSignallingCid) {
|
||||
HandleSignalling(frame + 4, length);
|
||||
} else if (Channel* channel = FindByLocal(cid)) {
|
||||
HandleChannelData(*channel, frame + 4, length);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleSignalling(const uint8_t* data, size_t size) {
|
||||
while (size >= 4) {
|
||||
const uint8_t code = data[0];
|
||||
const uint8_t ident = data[1];
|
||||
const uint16_t length = Le16(data + 2);
|
||||
if (static_cast<size_t>(length) + 4 > size) {
|
||||
return;
|
||||
}
|
||||
const uint8_t* p = data + 4;
|
||||
switch (code) {
|
||||
case kConnectionRequest: HandleConnectionRequest(ident, p, length); break;
|
||||
case kConnectionResponse: HandleConnectionResponse(ident, p, length); break;
|
||||
case kConfigurationRequest: HandleConfigurationRequest(ident, p, length); break;
|
||||
case kConfigurationResponse: HandleConfigurationResponse(ident, p, length); break;
|
||||
case kDisconnectionRequest: HandleDisconnectionRequest(ident, p, length); break;
|
||||
case kDisconnectionResponse: HandleDisconnectionResponse(p, length); break;
|
||||
case kEchoRequest: SendSignal(kEchoResponse, ident, std::vector<uint8_t>(p, p + length)); break;
|
||||
case kInformationRequest: {
|
||||
if (length >= 2) {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, Le16(p));
|
||||
PutLe16(reply, 0x0001); // not supported
|
||||
SendSignal(kInformationResponse, ident, reply);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case kCommandReject:
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game rejected L2CAP command "
|
||||
<< static_cast<int>(ident) << std::endl;
|
||||
break;
|
||||
case kEchoResponse:
|
||||
case kInformationResponse:
|
||||
break;
|
||||
default: {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, 0x0000); // command not understood
|
||||
SendSignal(kCommandReject, ident, reply);
|
||||
break;
|
||||
}
|
||||
}
|
||||
data += 4 + length;
|
||||
size -= 4 + length;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t psm = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
uint16_t result = kResultSuccess;
|
||||
uint16_t dcid = 0;
|
||||
if (psm != kPsmSdp && psm != kPsmHidControl && psm != kPsmHidInterrupt) {
|
||||
result = kResultPsmNotSupported;
|
||||
} else if (FindByPsm(psm) != nullptr) {
|
||||
// A real remote refuses a second channel on the same PSM.
|
||||
result = kResultNoResources;
|
||||
} else {
|
||||
Channel channel;
|
||||
channel.psm = psm;
|
||||
channel.localCid = AllocateCid();
|
||||
channel.remoteCid = scid;
|
||||
channel.connected = true;
|
||||
dcid = channel.localCid;
|
||||
m_channels.push_back(channel);
|
||||
}
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, scid);
|
||||
PutLe16(reply, result);
|
||||
PutLe16(reply, 0);
|
||||
SendSignal(kConnectionResponse, ident, reply);
|
||||
if (result == kResultSuccess) {
|
||||
SendConfigRequest(*FindByLocal(dcid));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConnectionResponse(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 8) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
const uint16_t result = Le16(p + 4);
|
||||
Channel* channel = FindByLocal(scid);
|
||||
if (channel == nullptr || !channel->weInitiated || channel->connected || channel->connectIdent != ident) {
|
||||
return;
|
||||
}
|
||||
if (result == kResultSuccess) {
|
||||
channel->remoteCid = dcid;
|
||||
channel->connected = true;
|
||||
SendConfigRequest(*channel);
|
||||
} else if (result == kResultPending) {
|
||||
channel->requestUs = m_nowUs;
|
||||
} else {
|
||||
std::cerr << "[bluetooth] Wii Remote " << (m_slot + 1) << ": the game refused L2CAP PSM 0x" << std::hex
|
||||
<< channel->psm << " (result 0x" << result << ")" << std::dec << std::endl;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
m_retryAtUs = m_nowUs + kRetryDelayUs;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConfigurationRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t flags = Le16(p + 2);
|
||||
Channel* channel = FindByLocal(dcid);
|
||||
if (channel == nullptr) {
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, 0x0002); // invalid CID
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, 0);
|
||||
SendSignal(kCommandReject, ident, reply);
|
||||
return;
|
||||
}
|
||||
channel->pendingOptions.insert(channel->pendingOptions.end(), p + 4, p + n);
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, channel->remoteCid);
|
||||
if (flags & 1) {
|
||||
PutLe16(reply, 1);
|
||||
PutLe16(reply, kResultSuccess);
|
||||
SendSignal(kConfigurationResponse, ident, reply);
|
||||
return;
|
||||
}
|
||||
PutLe16(reply, 0);
|
||||
PutLe16(reply, kResultSuccess);
|
||||
const std::vector<uint8_t>& options = channel->pendingOptions;
|
||||
for (size_t i = 0; i + 2 <= options.size();) {
|
||||
const uint8_t type = options[i] & 0x7F;
|
||||
const uint8_t length = options[i + 1];
|
||||
if (i + 2 + length > options.size()) break;
|
||||
if (type == kOptionMtu && length == 2) {
|
||||
channel->remoteMtu = Le16(&options[i + 2]);
|
||||
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
|
||||
} else if (type == kOptionFlushTimeout && length == 2) {
|
||||
reply.insert(reply.end(), options.begin() + static_cast<std::ptrdiff_t>(i),
|
||||
options.begin() + static_cast<std::ptrdiff_t>(i + 4));
|
||||
}
|
||||
i += 2 + length;
|
||||
}
|
||||
channel->pendingOptions.clear();
|
||||
channel->remoteConfigured = true;
|
||||
SendSignal(kConfigurationResponse, ident, reply);
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleConfigurationResponse(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 6) return;
|
||||
const uint16_t scid = Le16(p);
|
||||
const uint16_t flags = Le16(p + 2);
|
||||
const uint16_t result = Le16(p + 4);
|
||||
Channel* channel = FindByLocal(scid);
|
||||
if (channel == nullptr || !channel->configSent || channel->configIdent != ident) return;
|
||||
if (flags & 1) {
|
||||
return;
|
||||
}
|
||||
if (result == kResultSuccess) {
|
||||
channel->localConfigured = true;
|
||||
UpdateState();
|
||||
return;
|
||||
}
|
||||
// Our only option is the MTU; offer the default instead.
|
||||
channel->configIdent = NextIdent();
|
||||
channel->requestUs = m_nowUs;
|
||||
std::vector<uint8_t> data;
|
||||
PutLe16(data, channel->remoteCid);
|
||||
PutLe16(data, 0);
|
||||
SendSignal(kConfigurationRequest, channel->configIdent, data);
|
||||
}
|
||||
|
||||
void RemoteLink::HandleDisconnectionRequest(uint8_t ident, const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t dcid = Le16(p);
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
std::vector<uint8_t> reply;
|
||||
PutLe16(reply, dcid);
|
||||
PutLe16(reply, scid);
|
||||
SendSignal(kDisconnectionResponse, ident, reply);
|
||||
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
|
||||
[&](const Channel& c) { return c.localCid == dcid; }),
|
||||
m_channels.end());
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleDisconnectionResponse(const uint8_t* p, size_t n) {
|
||||
if (n < 4) return;
|
||||
const uint16_t scid = Le16(p + 2);
|
||||
m_channels.erase(std::remove_if(m_channels.begin(), m_channels.end(),
|
||||
[&](const Channel& c) { return c.localCid == scid; }),
|
||||
m_channels.end());
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
void RemoteLink::HandleChannelData(Channel& channel, const uint8_t* data, size_t size) {
|
||||
if (size == 0) return;
|
||||
if (channel.psm == kPsmSdp) {
|
||||
const std::vector<uint8_t> response = Sdp::HandleRequest(data, size, channel.remoteMtu);
|
||||
SendData(channel, response);
|
||||
return;
|
||||
}
|
||||
if (channel.psm == kPsmHidControl) {
|
||||
HandleHidControl(channel, data, size);
|
||||
return;
|
||||
}
|
||||
const uint8_t type = data[0] >> 4;
|
||||
const uint8_t param = data[0] & 0x0F;
|
||||
if (type == kHidData && param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteLink::HandleHidControl(Channel& channel, const uint8_t* data, size_t size) {
|
||||
const uint8_t type = data[0] >> 4;
|
||||
const uint8_t param = data[0] & 0x0F;
|
||||
switch (type) {
|
||||
case kHidHandshake:
|
||||
return;
|
||||
case kHidControl:
|
||||
if (param == kHidVirtualCableUnplug) {
|
||||
const bool wasReady = m_everReady;
|
||||
DropFromRemote(kReasonRemoteUserTerminated);
|
||||
if (wasReady) {
|
||||
m_connectWanted = false;
|
||||
m_controller.Host().OnGuestDisconnect(m_slot);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case kHidSetReport:
|
||||
SendData(channel, {kHidHandshakeSuccess});
|
||||
if ((param & 0x3) == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
return;
|
||||
case kHidData:
|
||||
if (param == kHidParamOutput && size >= 2 && size - 1 <= kMaxReportSize) {
|
||||
m_controller.Host().SendOutputReport(m_slot, data + 1, size - 1);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
SendData(channel, {kHidHandshakeUnsupported});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth
|
||||
@@ -0,0 +1,438 @@
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace Bluetooth::Sdp {
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
|
||||
constexpr uint8_t kErrorResponse = 0x01;
|
||||
constexpr uint8_t kServiceSearchRequest = 0x02;
|
||||
constexpr uint8_t kServiceSearchResponse = 0x03;
|
||||
constexpr uint8_t kServiceAttributeRequest = 0x04;
|
||||
constexpr uint8_t kServiceAttributeResponse = 0x05;
|
||||
constexpr uint8_t kServiceSearchAttributeRequest = 0x06;
|
||||
constexpr uint8_t kServiceSearchAttributeResponse = 0x07;
|
||||
|
||||
constexpr uint16_t kErrInvalidHandle = 0x0002;
|
||||
constexpr uint16_t kErrInvalidSyntax = 0x0003;
|
||||
constexpr uint16_t kErrInvalidPduSize = 0x0004;
|
||||
constexpr uint16_t kErrInvalidContinuation = 0x0005;
|
||||
|
||||
void PutBe16(Bytes& out, uint16_t v) {
|
||||
out.push_back(static_cast<uint8_t>(v >> 8));
|
||||
out.push_back(static_cast<uint8_t>(v));
|
||||
}
|
||||
|
||||
void PutBe32(Bytes& out, uint32_t v) {
|
||||
PutBe16(out, static_cast<uint16_t>(v >> 16));
|
||||
PutBe16(out, static_cast<uint16_t>(v));
|
||||
}
|
||||
|
||||
uint16_t Be16(const uint8_t* p) {
|
||||
return static_cast<uint16_t>((p[0] << 8) | p[1]);
|
||||
}
|
||||
|
||||
uint32_t Be32(const uint8_t* p) {
|
||||
return (static_cast<uint32_t>(Be16(p)) << 16) | Be16(p + 2);
|
||||
}
|
||||
|
||||
// Data element constructors (Bluetooth Core, Vol 3 Part B, 3.2).
|
||||
Bytes U8(uint8_t v) { return {0x08, v}; }
|
||||
Bytes U16(uint16_t v) { Bytes b{0x09}; PutBe16(b, v); return b; }
|
||||
Bytes U32(uint32_t v) { Bytes b{0x0A}; PutBe32(b, v); return b; }
|
||||
Bytes Uuid16(uint16_t v) { Bytes b{0x19}; PutBe16(b, v); return b; }
|
||||
Bytes Bool(bool v) { return {0x28, static_cast<uint8_t>(v ? 1 : 0)}; }
|
||||
|
||||
Bytes WithLength(uint8_t type, size_t length, bool wide = false) {
|
||||
if (length <= 0xFF && !wide) {
|
||||
return {static_cast<uint8_t>((type << 3) | 5), static_cast<uint8_t>(length)};
|
||||
}
|
||||
Bytes b{static_cast<uint8_t>((type << 3) | 6)};
|
||||
PutBe16(b, static_cast<uint16_t>(length));
|
||||
return b;
|
||||
}
|
||||
|
||||
Bytes Text(const Bytes& raw) {
|
||||
Bytes b = WithLength(4, raw.size());
|
||||
b.insert(b.end(), raw.begin(), raw.end());
|
||||
return b;
|
||||
}
|
||||
|
||||
Bytes Text(std::string_view s) {
|
||||
return Text(Bytes(s.begin(), s.end()));
|
||||
}
|
||||
|
||||
Bytes Seq(std::initializer_list<Bytes> items) {
|
||||
size_t length = 0;
|
||||
for (const Bytes& item : items) length += item.size();
|
||||
Bytes b = WithLength(6, length);
|
||||
for (const Bytes& item : items) b.insert(b.end(), item.begin(), item.end());
|
||||
return b;
|
||||
}
|
||||
|
||||
using Record = std::vector<std::pair<uint16_t, Bytes>>;
|
||||
|
||||
const Bytes kHidDescriptor = {
|
||||
0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x10, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95,
|
||||
0x01, 0x06, 0x00, 0xFF, 0x09, 0x01, 0x91, 0x00, 0x85, 0x11, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x12, 0x95, 0x02, 0x09, 0x01, 0x91, 0x00, 0x85, 0x13, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x14, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x15, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x16, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x17, 0x95, 0x06, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x18, 0x95, 0x15, 0x09, 0x01, 0x91, 0x00, 0x85, 0x19, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00,
|
||||
0x85, 0x1A, 0x95, 0x01, 0x09, 0x01, 0x91, 0x00, 0x85, 0x20, 0x95, 0x06, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x21, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x22, 0x95, 0x04, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x30, 0x95, 0x02, 0x09, 0x01, 0x81, 0x00, 0x85, 0x31, 0x95, 0x05, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x32, 0x95, 0x0A, 0x09, 0x01, 0x81, 0x00, 0x85, 0x33, 0x95, 0x11, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x34, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x35, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x36, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x37, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x3D, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0x85, 0x3E, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00,
|
||||
0x85, 0x3F, 0x95, 0x15, 0x09, 0x01, 0x81, 0x00, 0xC0,
|
||||
};
|
||||
|
||||
constexpr uint32_t kHidRecordHandle = 0x00010000;
|
||||
constexpr uint32_t kDeviceIdRecordHandle = 0x00010001;
|
||||
|
||||
const Record& HidRecord() {
|
||||
static const Record record = {
|
||||
{0x0000, U32(kHidRecordHandle)},
|
||||
{0x0001, Seq({Uuid16(0x1124)})},
|
||||
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0011)}), Seq({Uuid16(0x0011)})})},
|
||||
{0x0005, Seq({Uuid16(0x1002)})},
|
||||
{0x0006, Seq({U16(0x656E), U16(0x006A), U16(0x0100)})},
|
||||
{0x0009, Seq({Seq({Uuid16(0x1124), U16(0x0100)})})},
|
||||
{0x000D, Seq({Seq({Seq({Uuid16(0x0100), U16(0x0013)}), Seq({Uuid16(0x0011)})})})},
|
||||
{0x0100, Text("Nintendo RVL-CNT-01")},
|
||||
{0x0101, Text("Nintendo RVL-CNT-01")},
|
||||
{0x0102, Text("Nintendo")},
|
||||
{0x0200, U16(0x0100)},
|
||||
{0x0201, U16(0x0111)},
|
||||
{0x0202, U8(0x04)},
|
||||
{0x0203, U8(0x33)},
|
||||
{0x0204, Bool(false)},
|
||||
{0x0205, Bool(true)},
|
||||
{0x0206, Seq({Seq({U8(0x22), Text(kHidDescriptor)})})},
|
||||
{0x0207, Seq({Seq({U16(0x0409), U16(0x0100)})})},
|
||||
{0x0208, Bool(false)},
|
||||
{0x0209, Bool(true)},
|
||||
{0x020A, Bool(true)},
|
||||
{0x020B, U16(0x0100)},
|
||||
{0x020C, U16(0x0C80)},
|
||||
{0x020D, Bool(false)},
|
||||
{0x020E, Bool(false)},
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
const Record& DeviceIdRecord() {
|
||||
static const Record record = {
|
||||
{0x0000, U32(kDeviceIdRecordHandle)},
|
||||
{0x0001, Seq({Uuid16(0x1200)})},
|
||||
{0x0004, Seq({Seq({Uuid16(0x0100), U16(0x0001)}), Seq({Uuid16(0x0001)})})},
|
||||
{0x0005, Seq({Uuid16(0x1002)})},
|
||||
{0x0009, Seq({Seq({Uuid16(0x1200), U16(0x0100)})})},
|
||||
{0x0200, U16(0x0100)},
|
||||
{0x0201, U16(0x057E)},
|
||||
{0x0202, U16(0x0306)},
|
||||
{0x0203, U16(0x0600)},
|
||||
{0x0204, Bool(true)},
|
||||
{0x0205, U16(0x0002)},
|
||||
};
|
||||
return record;
|
||||
}
|
||||
|
||||
struct NamedRecord {
|
||||
uint32_t handle;
|
||||
const Record& (*get)();
|
||||
};
|
||||
constexpr std::array<NamedRecord, 2> kRecords = {{
|
||||
{kHidRecordHandle, HidRecord},
|
||||
{kDeviceIdRecordHandle, DeviceIdRecord},
|
||||
}};
|
||||
|
||||
using Uuid = std::array<uint8_t, 16>;
|
||||
|
||||
Uuid FromShort(uint32_t value) {
|
||||
// Bluetooth base UUID 00000000-0000-1000-8000-00805F9B34FB.
|
||||
Uuid u = {0, 0, 0, 0, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB};
|
||||
u[0] = static_cast<uint8_t>(value >> 24);
|
||||
u[1] = static_cast<uint8_t>(value >> 16);
|
||||
u[2] = static_cast<uint8_t>(value >> 8);
|
||||
u[3] = static_cast<uint8_t>(value);
|
||||
return u;
|
||||
}
|
||||
|
||||
// One parsed data element header: type, header size and payload size.
|
||||
struct ElementHeader {
|
||||
uint8_t type = 0;
|
||||
size_t headerSize = 0;
|
||||
size_t dataSize = 0;
|
||||
};
|
||||
|
||||
std::optional<ElementHeader> ParseHeader(const uint8_t* p, size_t n) {
|
||||
if (n == 0) return std::nullopt;
|
||||
ElementHeader h;
|
||||
h.type = static_cast<uint8_t>(p[0] >> 3);
|
||||
const uint8_t sizeIndex = p[0] & 7;
|
||||
if (h.type == 0) {
|
||||
h.headerSize = 1;
|
||||
h.dataSize = 0;
|
||||
} else if (sizeIndex <= 4) {
|
||||
h.headerSize = 1;
|
||||
h.dataSize = size_t{1} << sizeIndex;
|
||||
} else if (sizeIndex == 5) {
|
||||
if (n < 2) return std::nullopt;
|
||||
h.headerSize = 2;
|
||||
h.dataSize = p[1];
|
||||
} else if (sizeIndex == 6) {
|
||||
if (n < 3) return std::nullopt;
|
||||
h.headerSize = 3;
|
||||
h.dataSize = Be16(p + 1);
|
||||
} else {
|
||||
if (n < 5) return std::nullopt;
|
||||
h.headerSize = 5;
|
||||
h.dataSize = Be32(p + 1);
|
||||
}
|
||||
if (h.headerSize + h.dataSize > n) return std::nullopt;
|
||||
return h;
|
||||
}
|
||||
|
||||
std::optional<Uuid> ReadUuid(const uint8_t* p, const ElementHeader& h) {
|
||||
if (h.type != 3) return std::nullopt;
|
||||
const uint8_t* d = p + h.headerSize;
|
||||
if (h.dataSize == 2) return FromShort(Be16(d));
|
||||
if (h.dataSize == 4) return FromShort(Be32(d));
|
||||
if (h.dataSize == 16) {
|
||||
Uuid u{};
|
||||
std::copy(d, d + 16, u.begin());
|
||||
return u;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void CollectUuids(const uint8_t* p, size_t n, std::vector<Uuid>& out) {
|
||||
while (n > 0) {
|
||||
const auto h = ParseHeader(p, n);
|
||||
if (!h) return;
|
||||
if (h->type == 6 || h->type == 7) {
|
||||
CollectUuids(p + h->headerSize, h->dataSize, out);
|
||||
} else if (const auto uuid = ReadUuid(p, *h)) {
|
||||
out.push_back(*uuid);
|
||||
}
|
||||
const size_t total = h->headerSize + h->dataSize;
|
||||
p += total;
|
||||
n -= total;
|
||||
}
|
||||
}
|
||||
|
||||
bool RecordMatches(const Record& record, const std::vector<Uuid>& pattern) {
|
||||
std::vector<Uuid> present;
|
||||
for (const auto& [id, value] : record) CollectUuids(value.data(), value.size(), present);
|
||||
return std::all_of(pattern.begin(), pattern.end(), [&](const Uuid& wanted) {
|
||||
return std::find(present.begin(), present.end(), wanted) != present.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Parses a data element sequence at `p`; advances `p`/`n` past it.
|
||||
std::optional<std::pair<const uint8_t*, size_t>> TakeSequence(const uint8_t*& p, size_t& n) {
|
||||
const auto h = ParseHeader(p, n);
|
||||
if (!h || (h->type != 6)) return std::nullopt;
|
||||
const std::pair<const uint8_t*, size_t> body{p + h->headerSize, h->dataSize};
|
||||
p += h->headerSize + h->dataSize;
|
||||
n -= h->headerSize + h->dataSize;
|
||||
return body;
|
||||
}
|
||||
|
||||
std::optional<std::vector<Uuid>> ParsePattern(const uint8_t*& p, size_t& n) {
|
||||
const auto body = TakeSequence(p, n);
|
||||
if (!body) return std::nullopt;
|
||||
std::vector<Uuid> pattern;
|
||||
const uint8_t* q = body->first;
|
||||
size_t m = body->second;
|
||||
while (m > 0) {
|
||||
const auto h = ParseHeader(q, m);
|
||||
if (!h) return std::nullopt;
|
||||
const auto uuid = ReadUuid(q, *h);
|
||||
if (!uuid) return std::nullopt;
|
||||
pattern.push_back(*uuid);
|
||||
q += h->headerSize + h->dataSize;
|
||||
m -= h->headerSize + h->dataSize;
|
||||
}
|
||||
if (pattern.empty() || pattern.size() > 12) return std::nullopt;
|
||||
return pattern;
|
||||
}
|
||||
|
||||
using IdRange = std::pair<uint16_t, uint16_t>;
|
||||
|
||||
std::optional<std::vector<IdRange>> ParseAttributeIds(const uint8_t*& p, size_t& n) {
|
||||
const auto body = TakeSequence(p, n);
|
||||
if (!body) return std::nullopt;
|
||||
std::vector<IdRange> ranges;
|
||||
const uint8_t* q = body->first;
|
||||
size_t m = body->second;
|
||||
while (m > 0) {
|
||||
const auto h = ParseHeader(q, m);
|
||||
if (!h || h->type != 1) return std::nullopt;
|
||||
const uint8_t* d = q + h->headerSize;
|
||||
if (h->dataSize == 2) {
|
||||
ranges.emplace_back(Be16(d), Be16(d));
|
||||
} else if (h->dataSize == 4) {
|
||||
ranges.emplace_back(Be16(d), Be16(d + 2));
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
q += h->headerSize + h->dataSize;
|
||||
m -= h->headerSize + h->dataSize;
|
||||
}
|
||||
if (ranges.empty()) return std::nullopt;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Continuation state is the byte offset into the complete response (2 bytes BE).
|
||||
std::optional<size_t> ParseContinuation(const uint8_t* p, size_t n) {
|
||||
if (n < 1 || p[0] + size_t{1} != n) return std::nullopt;
|
||||
if (p[0] == 0) return 0;
|
||||
if (p[0] != 2) return std::nullopt;
|
||||
return Be16(p + 1);
|
||||
}
|
||||
|
||||
Bytes AttributeList(const Record& record, const std::vector<IdRange>& ranges) {
|
||||
Bytes inner;
|
||||
for (const auto& [id, value] : record) {
|
||||
const bool wanted = std::any_of(ranges.begin(), ranges.end(),
|
||||
[id = id](const IdRange& r) { return id >= r.first && id <= r.second; });
|
||||
if (!wanted) continue;
|
||||
const Bytes idElement = U16(id);
|
||||
inner.insert(inner.end(), idElement.begin(), idElement.end());
|
||||
inner.insert(inner.end(), value.begin(), value.end());
|
||||
}
|
||||
// A real remote always sends attribute lists with a 16-bit length.
|
||||
Bytes list = WithLength(6, inner.size(), true);
|
||||
list.insert(list.end(), inner.begin(), inner.end());
|
||||
return list;
|
||||
}
|
||||
|
||||
Bytes Pdu(uint8_t id, uint16_t tid, const Bytes& params) {
|
||||
Bytes out{id};
|
||||
PutBe16(out, tid);
|
||||
PutBe16(out, static_cast<uint16_t>(params.size()));
|
||||
out.insert(out.end(), params.begin(), params.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
Bytes Error(uint16_t tid, uint16_t code) {
|
||||
Bytes params;
|
||||
PutBe16(params, code);
|
||||
return Pdu(kErrorResponse, tid, params);
|
||||
}
|
||||
|
||||
// Emits the slice of `full` starting at `offset` that fits, plus continuation.
|
||||
Bytes Chunked(uint8_t responseId, uint16_t tid, const Bytes& full, size_t offset, uint16_t maxBytes,
|
||||
size_t maxResponse) {
|
||||
// PDU header 5, byte count 2, continuation up to 3.
|
||||
const size_t room = maxResponse > 10 ? maxResponse - 10 : 1;
|
||||
const size_t limit = std::max<size_t>(1, std::min<size_t>(maxBytes, room));
|
||||
const size_t count = std::min(limit, full.size() - offset);
|
||||
Bytes params;
|
||||
PutBe16(params, static_cast<uint16_t>(count));
|
||||
params.insert(params.end(), full.begin() + static_cast<std::ptrdiff_t>(offset),
|
||||
full.begin() + static_cast<std::ptrdiff_t>(offset + count));
|
||||
const size_t next = offset + count;
|
||||
if (next < full.size()) {
|
||||
params.push_back(2);
|
||||
PutBe16(params, static_cast<uint16_t>(next));
|
||||
} else {
|
||||
params.push_back(0);
|
||||
}
|
||||
return Pdu(responseId, tid, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const std::vector<uint8_t>& WiiRemoteHidDescriptor() {
|
||||
return kHidDescriptor;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> HandleRequest(const uint8_t* pdu, size_t size, size_t maxResponse) {
|
||||
if (size < 5) {
|
||||
return Error(0, kErrInvalidPduSize);
|
||||
}
|
||||
const uint8_t id = pdu[0];
|
||||
const uint16_t tid = Be16(pdu + 1);
|
||||
const uint16_t paramLength = Be16(pdu + 3);
|
||||
if (paramLength != size - 5) {
|
||||
return Error(tid, kErrInvalidPduSize);
|
||||
}
|
||||
const uint8_t* p = pdu + 5;
|
||||
size_t n = paramLength;
|
||||
|
||||
switch (id) {
|
||||
case kServiceSearchRequest: {
|
||||
const auto pattern = ParsePattern(p, n);
|
||||
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
|
||||
const uint16_t maxCount = Be16(p);
|
||||
if (!ParseContinuation(p + 2, n - 2)) return Error(tid, kErrInvalidContinuation);
|
||||
Bytes handles;
|
||||
uint16_t found = 0;
|
||||
for (const NamedRecord& record : kRecords) {
|
||||
if (found < maxCount && RecordMatches(record.get(), *pattern)) {
|
||||
PutBe32(handles, record.handle);
|
||||
++found;
|
||||
}
|
||||
}
|
||||
Bytes params;
|
||||
PutBe16(params, found);
|
||||
PutBe16(params, found);
|
||||
params.insert(params.end(), handles.begin(), handles.end());
|
||||
params.push_back(0);
|
||||
return Pdu(kServiceSearchResponse, tid, params);
|
||||
}
|
||||
case kServiceAttributeRequest: {
|
||||
if (n < 6) return Error(tid, kErrInvalidSyntax);
|
||||
const uint32_t handle = Be32(p);
|
||||
const uint16_t maxBytes = Be16(p + 4);
|
||||
p += 6;
|
||||
n -= 6;
|
||||
const auto ranges = ParseAttributeIds(p, n);
|
||||
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
|
||||
const auto record = std::find_if(kRecords.begin(), kRecords.end(),
|
||||
[&](const NamedRecord& r) { return r.handle == handle; });
|
||||
if (record == kRecords.end()) return Error(tid, kErrInvalidHandle);
|
||||
const Bytes full = AttributeList(record->get(), *ranges);
|
||||
const auto offset = ParseContinuation(p, n);
|
||||
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
|
||||
return Chunked(kServiceAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
|
||||
}
|
||||
case kServiceSearchAttributeRequest: {
|
||||
const auto pattern = ParsePattern(p, n);
|
||||
if (!pattern || n < 2) return Error(tid, kErrInvalidSyntax);
|
||||
const uint16_t maxBytes = Be16(p);
|
||||
p += 2;
|
||||
n -= 2;
|
||||
const auto ranges = ParseAttributeIds(p, n);
|
||||
if (!ranges || maxBytes < 7) return Error(tid, kErrInvalidSyntax);
|
||||
Bytes lists;
|
||||
for (const NamedRecord& record : kRecords) {
|
||||
if (RecordMatches(record.get(), *pattern)) {
|
||||
const Bytes list = AttributeList(record.get(), *ranges);
|
||||
lists.insert(lists.end(), list.begin(), list.end());
|
||||
}
|
||||
}
|
||||
Bytes full = WithLength(6, lists.size());
|
||||
full.insert(full.end(), lists.begin(), lists.end());
|
||||
const auto offset = ParseContinuation(p, n);
|
||||
if (!offset || *offset > full.size()) return Error(tid, kErrInvalidContinuation);
|
||||
return Chunked(kServiceSearchAttributeResponse, tid, full, *offset, maxBytes, maxResponse);
|
||||
}
|
||||
default:
|
||||
return Error(tid, kErrInvalidSyntax);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Bluetooth::Sdp
|
||||
@@ -0,0 +1,126 @@
|
||||
// SYSCONF items behind the Wii Remote stack. The runtime never parses a
|
||||
// SYSCONF (SCCheckStatus is HLE'd), so these accessors serve the Bluetooth items
|
||||
// directly: BT.DINF registers the four virtual remotes of the emulated
|
||||
// controller, and the console settings come from Config.toml.
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace BluetoothSysconf {
|
||||
|
||||
constexpr uint32_t kDeviceInfoSize = 0x461;
|
||||
constexpr uint32_t kCmpDeviceInfoSize = 0x205;
|
||||
constexpr uint32_t kDeviceEntrySize = 0x46;
|
||||
// [0..9] registered remotes, [10..13] the remote last connected on each channel.
|
||||
constexpr uint32_t kActiveEntriesStart = 10;
|
||||
constexpr const char kRemoteName[] = "Nintendo RVL-CNT-01";
|
||||
|
||||
void Fill(uint32_t buffer, uint32_t size, uint8_t value) {
|
||||
std::memset(Memory::GetPointer(buffer, size), value, size);
|
||||
}
|
||||
|
||||
void WriteEntry(uint32_t buffer, uint32_t index, size_t slot) {
|
||||
const uint32_t entry = buffer + 1 + index * kDeviceEntrySize;
|
||||
// SYSCONF keeps addresses most significant byte first.
|
||||
const Bluetooth::BdAddr addr = Bluetooth::Controller::RemoteAddress(slot);
|
||||
for (size_t i = 0; i < addr.size(); ++i) {
|
||||
Memory::Write8(entry + static_cast<uint32_t>(i), addr[addr.size() - 1 - i]);
|
||||
}
|
||||
std::memcpy(Memory::GetPointer(entry + 6, sizeof(kRemoteName)), kRemoteName, sizeof(kRemoteName));
|
||||
}
|
||||
|
||||
void SetReturn(CpuContext* ctx, uint32_t value) {
|
||||
ctx->gpr[3] = value;
|
||||
}
|
||||
|
||||
} // namespace BluetoothSysconf
|
||||
|
||||
extern "C" void SCGetBtDeviceInfoArray_HLE(CpuContext* ctx) {
|
||||
const uint32_t buffer = ctx->gpr[3];
|
||||
try {
|
||||
BluetoothSysconf::Fill(buffer, BluetoothSysconf::kDeviceInfoSize, 0);
|
||||
Memory::Write8(buffer, static_cast<uint8_t>(Bluetooth::kRemoteSlots));
|
||||
for (size_t slot = 0; slot < Bluetooth::kRemoteSlots; ++slot) {
|
||||
BluetoothSysconf::WriteEntry(buffer, static_cast<uint32_t>(slot), slot);
|
||||
BluetoothSysconf::WriteEntry(buffer, BluetoothSysconf::kActiveEntriesStart + static_cast<uint32_t>(slot), slot);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "SCGetBtDeviceInfoArray", e);
|
||||
BluetoothSysconf::SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1ED0, SCGetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// WUD writes the list back after reconciling it with the controller's link
|
||||
// keys, and WPAD whenever a channel's remote changes. The virtual remotes are
|
||||
// fixed, so the list is rebuilt on every boot.
|
||||
extern "C" void SCSetBtDeviceInfoArray_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EDC, SCSetBtDeviceInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// Remotes paired with 1+2 on the game's own sync screens. The virtual remotes
|
||||
// are all registered, so the list is always empty.
|
||||
extern "C" void SCGetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
|
||||
try {
|
||||
BluetoothSysconf::Fill(ctx->gpr[3], BluetoothSysconf::kCmpDeviceInfoSize, 0);
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "SCGetBtCmpDevInfoArray", e);
|
||||
BluetoothSysconf::SetReturn(ctx, 0);
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EE8, SCGetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCSetBtCmpDevInfoArray_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1EF4, SCSetBtCmpDevInfoArray_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetBtDpdSensibility_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiIrSensitivity(), 1, 5)));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1F00, SCGetBtDpdSensibility_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadMotorMode_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiRumbleEnabled() ? 1u : 0u);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1F68, SCGetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// The Home Menu's vibration switch, saved by WPADSaveConfig.
|
||||
extern "C" void SCSetWpadMotorMode_HLE(CpuContext* ctx) {
|
||||
const bool enabled = (ctx->gpr[3] & 0xFF) == 1;
|
||||
if (enabled != RuntimeConfigFile::WiiRumbleEnabled()) {
|
||||
RuntimeConfigFile::SetWiiRumbleEnabled(enabled);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1FBC, SCSetWpadMotorMode_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadSensorBarPosition_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, RuntimeConfigFile::WiiSensorBarAbove() ? 1u : 0u);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B1FC4, SCGetWpadSensorBarPosition_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
extern "C" void SCGetWpadSpeakerVolume_HLE(CpuContext* ctx) {
|
||||
BluetoothSysconf::SetReturn(ctx, static_cast<uint32_t>(std::clamp(RuntimeConfigFile::WiiSpeakerVolume(), 0, 0x7F)));
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B2018, SCGetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
|
||||
|
||||
// The Home Menu's speaker volume, saved by WPADSaveConfig.
|
||||
extern "C" void SCSetWpadSpeakerVolume_HLE(CpuContext* ctx) {
|
||||
const int32_t volume = std::min<int32_t>(static_cast<int32_t>(ctx->gpr[3] & 0xFF), 0x7F);
|
||||
if (volume != RuntimeConfigFile::WiiSpeakerVolume()) {
|
||||
RuntimeConfigFile::SetWiiSpeakerVolume(volume);
|
||||
}
|
||||
BluetoothSysconf::SetReturn(ctx, 1);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE_VOID(801B206C, SCSetWpadSpeakerVolume_HLE, (CpuContext* ctx), (ctx));
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "hle/bluetooth_hle.h"
|
||||
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "hle/runtime_parse_helpers.h"
|
||||
#include "memory.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
|
||||
namespace BluetoothUsb {
|
||||
|
||||
constexpr const char* kDevicePath = "/dev/usb/oh1/57e/305";
|
||||
constexpr int32_t kDeviceFd = 5;
|
||||
constexpr int32_t kIpcSuccess = 0;
|
||||
constexpr int32_t kIpcEinval = -4;
|
||||
|
||||
constexpr uint32_t kCtrlMsg = 0;
|
||||
constexpr uint32_t kBulkMsg = 1;
|
||||
constexpr uint32_t kIntrMsg = 2;
|
||||
|
||||
constexpr uint8_t kEndpointEvents = 0x81;
|
||||
constexpr uint8_t kEndpointAclIn = 0x82;
|
||||
constexpr uint8_t kEndpointAclOut = 0x02;
|
||||
|
||||
// The guest copies each delivered packet into a GKI buffer that its 2 ms
|
||||
// Bluetooth tick frees; a pool it cannot refill drops the packet, so ACL data is
|
||||
// paced. Plenty for four remotes reporting at 200 Hz.
|
||||
constexpr double kAclPacketsPerMs = 3.0;
|
||||
constexpr double kAclBurst = 6.0;
|
||||
|
||||
struct HeldRead {
|
||||
bool active = false;
|
||||
uint32_t buffer = 0;
|
||||
uint32_t length = 0;
|
||||
uint32_t callback = 0;
|
||||
uint32_t callbackArg = 0;
|
||||
};
|
||||
|
||||
struct Device {
|
||||
bool open = false;
|
||||
std::unique_ptr<Bluetooth::Controller> controller;
|
||||
HeldRead eventRead;
|
||||
HeldRead aclRead;
|
||||
double aclTokens = kAclBurst;
|
||||
uint64_t lastRefillUs = 0;
|
||||
};
|
||||
|
||||
Device& Dev() {
|
||||
static Device device;
|
||||
return device;
|
||||
}
|
||||
|
||||
uint64_t NowUs() {
|
||||
using namespace std::chrono;
|
||||
return static_cast<uint64_t>(duration_cast<microseconds>(steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
Bluetooth::Controller& ControllerFor(Device& device) {
|
||||
if (!device.controller) {
|
||||
device.controller = std::make_unique<Bluetooth::Controller>(Wiimote::Host());
|
||||
}
|
||||
return *device.controller;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> ReadGuest(uint32_t address, uint32_t length) {
|
||||
std::vector<uint8_t> data(length);
|
||||
if (length != 0) {
|
||||
std::memcpy(data.data(), Memory::GetPointer(address, length), length);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void Complete(HeldRead& read, const std::vector<uint8_t>& data) {
|
||||
const uint32_t size = static_cast<uint32_t>(std::min<size_t>(data.size(), read.length));
|
||||
if (size < data.size()) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: packet of " << data.size() << " bytes truncated to " << size << std::endl;
|
||||
}
|
||||
if (size != 0) {
|
||||
std::memcpy(Memory::GetPointer(read.buffer, size), data.data(), size);
|
||||
}
|
||||
read.active = false;
|
||||
NandQueueIosCallback(read.callback, static_cast<int32_t>(size), read.callbackArg);
|
||||
}
|
||||
|
||||
// Hands queued events and ACL data to whichever reads the guest has armed.
|
||||
bool Deliver(Device& device, uint64_t nowUs) {
|
||||
Bluetooth::Controller& controller = ControllerFor(device);
|
||||
bool delivered = false;
|
||||
std::vector<uint8_t> packet;
|
||||
if (device.eventRead.active && controller.PopEvent(packet)) {
|
||||
Complete(device.eventRead, packet);
|
||||
delivered = true;
|
||||
}
|
||||
if (nowUs > device.lastRefillUs) {
|
||||
device.aclTokens = std::min(kAclBurst, device.aclTokens + (nowUs - device.lastRefillUs) / 1000.0 * kAclPacketsPerMs);
|
||||
device.lastRefillUs = nowUs;
|
||||
}
|
||||
if (device.aclRead.active && device.aclTokens >= 1.0 && controller.PopAcl(packet)) {
|
||||
device.aclTokens -= 1.0;
|
||||
Complete(device.aclRead, packet);
|
||||
delivered = true;
|
||||
}
|
||||
return delivered;
|
||||
}
|
||||
|
||||
bool Hold(HeldRead& read, uint32_t buffer, uint32_t length, uint32_t callback, uint32_t callbackArg) {
|
||||
if (length != 0) {
|
||||
Memory::GetPointer(buffer, length);
|
||||
}
|
||||
if (read.active) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: a second read on the same endpoint replaced the first" << std::endl;
|
||||
NandQueueIosCallback(read.callback, kIpcEinval, read.callbackArg);
|
||||
}
|
||||
read = {true, buffer, length, callback, callbackArg};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Executes a transfer. Returns true with `result` when it completed now.
|
||||
bool Transfer(uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr, uint32_t callback,
|
||||
uint32_t callbackArg, bool async, int32_t& result) {
|
||||
Device& device = Dev();
|
||||
Bluetooth::Controller& controller = ControllerFor(device);
|
||||
const uint64_t nowUs = NowUs();
|
||||
try {
|
||||
if (cmd == kCtrlMsg && numIn == 6 && numOut == 1) {
|
||||
const auto requestType = RuntimeHle::ReadIoVector(vectorPtr, 0);
|
||||
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 4);
|
||||
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 6);
|
||||
const uint8_t bmRequestType = Memory::Read8(requestType.address);
|
||||
// wLength is little-endian in the request block.
|
||||
const uint16_t wLength = static_cast<uint16_t>(Memory::Read8(lengthVector.address) |
|
||||
(Memory::Read8(lengthVector.address + 1) << 8));
|
||||
if (bmRequestType != 0x20 || wLength > data.size) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unexpected control transfer type 0x" << std::hex
|
||||
<< static_cast<int>(bmRequestType) << std::dec << std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
const std::vector<uint8_t> command = ReadGuest(data.address, wLength);
|
||||
controller.ExecuteCommand(command.data(), command.size(), nowUs);
|
||||
result = wLength;
|
||||
return true;
|
||||
}
|
||||
if ((cmd == kBulkMsg || cmd == kIntrMsg) && numIn == 2 && numOut == 1) {
|
||||
const auto endpointVector = RuntimeHle::ReadIoVector(vectorPtr, 0);
|
||||
const auto lengthVector = RuntimeHle::ReadIoVector(vectorPtr, 1);
|
||||
const auto data = RuntimeHle::ReadIoVector(vectorPtr, 2);
|
||||
const uint8_t endpoint = Memory::Read8(endpointVector.address);
|
||||
// Bulk / interrupt lengths are big-endian.
|
||||
const uint16_t length = Memory::Read16(lengthVector.address);
|
||||
if (length > data.size) {
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
if (cmd == kBulkMsg && endpoint == kEndpointAclOut) {
|
||||
const std::vector<uint8_t> packet = ReadGuest(data.address, length);
|
||||
controller.ReceiveAcl(packet.data(), packet.size(), nowUs);
|
||||
result = length;
|
||||
return true;
|
||||
}
|
||||
if (async && cmd == kBulkMsg && endpoint == kEndpointAclIn) {
|
||||
Hold(device.aclRead, data.address, length, callback, callbackArg);
|
||||
Deliver(device, nowUs);
|
||||
return false;
|
||||
}
|
||||
if (async && cmd == kIntrMsg && endpoint == kEndpointEvents) {
|
||||
Hold(device.eventRead, data.address, length, callback, callbackArg);
|
||||
Deliver(device, nowUs);
|
||||
return false;
|
||||
}
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported transfer cmd " << cmd << " endpoint 0x" << std::hex
|
||||
<< static_cast<int>(endpoint) << std::dec << (async ? "" : " (sync)") << std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "Bluetooth transfer", e);
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: unsupported ioctlv " << cmd << " (" << numIn << " in, " << numOut << " out)"
|
||||
<< std::endl;
|
||||
result = kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace BluetoothUsb
|
||||
|
||||
int32_t Bluetooth_HLE_OpenDevice(const char* path) {
|
||||
if (path == nullptr || std::strcmp(path, BluetoothUsb::kDevicePath) != 0) {
|
||||
return 0;
|
||||
}
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
if (device.open) {
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: device opened twice" << std::endl;
|
||||
}
|
||||
device.open = true;
|
||||
device.eventRead = {};
|
||||
device.aclRead = {};
|
||||
device.aclTokens = BluetoothUsb::kAclBurst;
|
||||
device.lastRefillUs = BluetoothUsb::NowUs();
|
||||
BluetoothUsb::ControllerFor(device).Open(device.lastRefillUs);
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " opened" << std::endl;
|
||||
return BluetoothUsb::kDeviceFd;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_IsFd(uint32_t fd) {
|
||||
return static_cast<int32_t>(fd) == BluetoothUsb::kDeviceFd;
|
||||
}
|
||||
|
||||
int32_t Bluetooth_HLE_Close(uint32_t fd) {
|
||||
if (!Bluetooth_HLE_IsFd(fd)) {
|
||||
return BluetoothUsb::kIpcEinval;
|
||||
}
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
// IOS cancels the transfers still queued on a device it closes.
|
||||
for (BluetoothUsb::HeldRead* read : {&device.eventRead, &device.aclRead}) {
|
||||
if (read->active) {
|
||||
read->active = false;
|
||||
NandQueueIosCallback(read->callback, BluetoothUsb::kIpcEinval, read->callbackArg);
|
||||
}
|
||||
}
|
||||
BluetoothUsb::ControllerFor(device).Close();
|
||||
device.open = false;
|
||||
RT_LOG(RT_TAG_HLE) << "Bluetooth: " << BluetoothUsb::kDevicePath << " closed" << std::endl;
|
||||
return BluetoothUsb::kIpcSuccess;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_IoctlvAsync(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr,
|
||||
uint32_t callback, uint32_t callbackArg, int32_t& result) {
|
||||
(void)fd;
|
||||
if (!BluetoothUsb::Dev().open) {
|
||||
result = BluetoothUsb::kIpcEinval;
|
||||
return true;
|
||||
}
|
||||
return BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, callback, callbackArg, true, result);
|
||||
}
|
||||
|
||||
int32_t Bluetooth_HLE_Ioctlv(uint32_t fd, uint32_t cmd, uint32_t numIn, uint32_t numOut, uint32_t vectorPtr) {
|
||||
(void)fd;
|
||||
if (!BluetoothUsb::Dev().open) {
|
||||
return BluetoothUsb::kIpcEinval;
|
||||
}
|
||||
int32_t result = BluetoothUsb::kIpcEinval;
|
||||
BluetoothUsb::Transfer(cmd, numIn, numOut, vectorPtr, 0, 0, false, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Bluetooth_HLE_ProcessCompletions(CpuContext* cpu) {
|
||||
(void)cpu;
|
||||
BluetoothUsb::Device& device = BluetoothUsb::Dev();
|
||||
if (!device.open) {
|
||||
return false;
|
||||
}
|
||||
const uint64_t nowUs = BluetoothUsb::NowUs();
|
||||
try {
|
||||
BluetoothUsb::ControllerFor(device).Tick(nowUs);
|
||||
return BluetoothUsb::Deliver(device, nowUs);
|
||||
} catch (const Memory::AccessViolation& e) {
|
||||
LogMemoryError(RT_TAG_HLE, "Bluetooth delivery", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "hle/hollywood_gpio.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace HollywoodGpio {
|
||||
|
||||
constexpr uint32_t kSensorBarPower = 0x100;
|
||||
|
||||
std::atomic<uint32_t> g_gpioBOut{0};
|
||||
|
||||
} // namespace HollywoodGpio
|
||||
|
||||
bool HollywoodGpio_Read32(uint32_t address, uint32_t& value) {
|
||||
if (address != kHollywoodGpioBOut) {
|
||||
return false;
|
||||
}
|
||||
value = HollywoodGpio::g_gpioBOut.load(std::memory_order_relaxed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HollywoodGpio_Write32(uint32_t address, uint32_t value) {
|
||||
if (address != kHollywoodGpioBOut) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t previous = HollywoodGpio::g_gpioBOut.exchange(value, std::memory_order_relaxed);
|
||||
if ((previous ^ value) & HollywoodGpio::kSensorBarPower) {
|
||||
RT_LOG(RT_TAG_HLE) << "Sensor bar power " << ((value & HollywoodGpio::kSensorBarPower) ? "on" : "off") << std::endl;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "input_bindings.h"
|
||||
#include "memory.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// KPAD HLE fed by a real Bluetooth Wii Remote. The game calls KPADRead once per
|
||||
// frame with room for 16 KPADStatus entries and only looks at entry 0; with a
|
||||
// Classic Controller it also calls KPADGetUnifiedWpadStatus for the raw
|
||||
// WPADCLStatus (buttons, sticks and triggers of the extension).
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kKpadStatusSize = 0x84;
|
||||
|
||||
// KPADStatus field offsets (RVL SDK).
|
||||
constexpr uint32_t kHold = 0x00, kTrig = 0x04, kRelease = 0x08, kAcc = 0x0C, kAccValue = 0x18,
|
||||
kAccSpeed = 0x1C, kPos = 0x20, kAccVertical = 0x54, kDevType = 0x5C, kWpadErr = 0x5D,
|
||||
kDpdValidFg = 0x5E, kDataFormat = 0x5F, kFsStick = 0x60, kFsAcc = 0x68, kFsAccValue = 0x74,
|
||||
kFsAccSpeed = 0x78;
|
||||
// KPADStatus.ex_status.cl (KPADEXStatus, Classic Controller view).
|
||||
constexpr uint32_t kClHold = 0x60, kClTrig = 0x64, kClRelease = 0x68, kClLStick = 0x6C, kClRStick = 0x74,
|
||||
kClLTrigger = 0x7C, kClRTrigger = 0x80;
|
||||
|
||||
// KPADUnifiedWpadStatus: WPADStatus / WPADFSStatus / WPADCLStatus union, then fmt.
|
||||
constexpr uint32_t kUnifiedSize = 0x38;
|
||||
constexpr uint32_t kUButton = 0x00, kUAccX = 0x02, kUAccY = 0x04, kUAccZ = 0x06, kUObj = 0x08, kUDev = 0x28,
|
||||
kUErr = 0x29, kUFsStickX = 0x2A, kUFsStickY = 0x2B, kUFsAccX = 0x2C, kUFsAccY = 0x2E,
|
||||
kUFsAccZ = 0x30, kUClButton = 0x2A, kUClLStickX = 0x2C, kUClLStickY = 0x2E, kUClRStickX = 0x30,
|
||||
kUClRStickY = 0x32, kUClTriggerL = 0x34, kUClTriggerR = 0x35, kUFmt = 0x36;
|
||||
|
||||
// WPAD device types (WPAD_DEV_*) and the data formats KPAD runs each of them
|
||||
// in (WPAD_FMT_*_ACC_DPD): the values KPADStatus.dev_type / data_format and
|
||||
// KPADUnifiedWpadStatus.dev / fmt carry on the console.
|
||||
constexpr uint8_t kDevCore = 0;
|
||||
constexpr uint8_t kDevFreestyle = 1;
|
||||
constexpr uint8_t kDevClassic = 2;
|
||||
constexpr uint8_t kFmtCoreAccDpd = 2;
|
||||
constexpr uint8_t kFmtFreestyleAccDpd = 5;
|
||||
constexpr uint8_t kFmtClassicAccDpd = 8;
|
||||
constexpr int8_t kWpadErrNone = 0;
|
||||
constexpr int8_t kWpadErrNoController = -1;
|
||||
|
||||
// Raw accelerometer as WPADStatus carries it: 10 bits, 0x200 at 0 g, 100 per g.
|
||||
constexpr float kRawAccZero = 512.0f;
|
||||
constexpr float kRawAccPerG = 100.0f;
|
||||
|
||||
struct ChannelState {
|
||||
uint32_t prevHold = 0;
|
||||
uint32_t prevClHold = 0;
|
||||
float prevAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
float prevFsAcc[3] = {0.0f, -1.0f, 0.0f};
|
||||
};
|
||||
|
||||
std::array<ChannelState, 4> g_channels{};
|
||||
|
||||
// Euclidean length of a 3-vector.
|
||||
float Length(const float* v) {
|
||||
return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
||||
}
|
||||
|
||||
// Euclidean distance between two 3-vectors.
|
||||
float Distance(const float* a, const float* b) {
|
||||
const float d[3] = {a[0] - b[0], a[1] - b[1], a[2] - b[2]};
|
||||
return Length(d);
|
||||
}
|
||||
|
||||
// Writes three big-endian floats to guest memory.
|
||||
void WriteVec3(uint32_t addr, const float* v) {
|
||||
Memory::WriteFloat32(addr, v[0]);
|
||||
Memory::WriteFloat32(addr + 4, v[1]);
|
||||
Memory::WriteFloat32(addr + 8, v[2]);
|
||||
}
|
||||
|
||||
// Zeroes `count` consecutive floats in guest memory.
|
||||
void WriteZeroFloats(uint32_t addr, uint32_t count) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
Memory::WriteFloat32(addr + i * 4, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Fills one KPADStatus at `addr` and returns the number of valid entries (1),
|
||||
// or writes an "unplugged" status and returns 0.
|
||||
int32_t WriteStatus(uint32_t chan, uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
|
||||
ChannelState& state = g_channels[chan];
|
||||
if (sample == nullptr) {
|
||||
state = {};
|
||||
Memory::Write32(addr + kHold, 0);
|
||||
Memory::Write32(addr + kTrig, 0);
|
||||
Memory::Write32(addr + kRelease, 0);
|
||||
Memory::Write8(addr + kDevType, kDevCore);
|
||||
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNoController));
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t hold = sample->hold;
|
||||
Memory::Write32(addr + kHold, hold);
|
||||
Memory::Write32(addr + kTrig, hold & ~state.prevHold);
|
||||
Memory::Write32(addr + kRelease, state.prevHold & ~hold);
|
||||
state.prevHold = hold;
|
||||
|
||||
WriteVec3(addr + kAcc, sample->acc);
|
||||
Memory::WriteFloat32(addr + kAccValue, Length(sample->acc));
|
||||
Memory::WriteFloat32(addr + kAccSpeed, Distance(sample->acc, state.prevAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevAcc[i] = sample->acc[i];
|
||||
|
||||
// No IR pointer: pos .. acc_vertical zeroed and dpd_valid_fg clear, which
|
||||
// the game treats as "pointing away from the screen".
|
||||
WriteZeroFloats(addr + kPos, (kAccVertical + 8 - kPos) / 4);
|
||||
Memory::Write8(addr + kDpdValidFg, 0);
|
||||
|
||||
const uint8_t devType = sample->hasClassic ? kDevClassic : sample->hasNunchuk ? kDevFreestyle : kDevCore;
|
||||
const uint8_t dataFormat =
|
||||
sample->hasClassic ? kFmtClassicAccDpd : sample->hasNunchuk ? kFmtFreestyleAccDpd : kFmtCoreAccDpd;
|
||||
Memory::Write8(addr + kDevType, devType);
|
||||
Memory::Write8(addr + kWpadErr, static_cast<uint8_t>(kWpadErrNone));
|
||||
Memory::Write8(addr + kDataFormat, dataFormat);
|
||||
|
||||
if (sample->hasClassic) {
|
||||
const uint32_t clHold = sample->clHold;
|
||||
Memory::Write32(addr + kClHold, clHold);
|
||||
Memory::Write32(addr + kClTrig, clHold & ~state.prevClHold);
|
||||
Memory::Write32(addr + kClRelease, state.prevClHold & ~clHold);
|
||||
state.prevClHold = clHold;
|
||||
Memory::WriteFloat32(addr + kClLStick, sample->clLStick[0]);
|
||||
Memory::WriteFloat32(addr + kClLStick + 4, sample->clLStick[1]);
|
||||
Memory::WriteFloat32(addr + kClRStick, sample->clRStick[0]);
|
||||
Memory::WriteFloat32(addr + kClRStick + 4, sample->clRStick[1]);
|
||||
Memory::WriteFloat32(addr + kClLTrigger, sample->clTriggerL / 255.0f);
|
||||
Memory::WriteFloat32(addr + kClRTrigger, sample->clTriggerR / 255.0f);
|
||||
} else if (sample->hasNunchuk) {
|
||||
state.prevClHold = 0;
|
||||
Memory::WriteFloat32(addr + kFsStick, sample->stick[0]);
|
||||
Memory::WriteFloat32(addr + kFsStick + 4, sample->stick[1]);
|
||||
WriteVec3(addr + kFsAcc, sample->nunchukAcc);
|
||||
Memory::WriteFloat32(addr + kFsAccValue, Length(sample->nunchukAcc));
|
||||
Memory::WriteFloat32(addr + kFsAccSpeed, Distance(sample->nunchukAcc, state.prevFsAcc));
|
||||
for (int i = 0; i < 3; ++i) state.prevFsAcc[i] = sample->nunchukAcc[i];
|
||||
} else {
|
||||
state.prevClHold = 0;
|
||||
WriteZeroFloats(addr + kFsStick, (kKpadStatusSize - kFsStick) / 4);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// One accelerometer axis of KPAD's g vector back to the 10-bit raw WPAD value.
|
||||
uint16_t RawAcc(float g) {
|
||||
const float raw = kRawAccZero + g * kRawAccPerG;
|
||||
return static_cast<uint16_t>(std::clamp(raw, 0.0f, 1023.0f));
|
||||
}
|
||||
|
||||
// Fills one KPADUnifiedWpadStatus at `addr` from the sample: the WPADStatus core
|
||||
// (remote buttons, raw accelerometer, no IR objects), then the Nunchuk or Classic
|
||||
// Controller tail, then the data format.
|
||||
void WriteUnifiedStatus(uint32_t addr, const WiiRemoteInput::KpadSample* sample) {
|
||||
for (uint32_t offset = 0; offset < kUnifiedSize; offset += 4) {
|
||||
Memory::Write32(addr + offset, 0);
|
||||
}
|
||||
if (sample == nullptr) {
|
||||
Memory::Write8(addr + kUDev, kDevCore);
|
||||
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNoController));
|
||||
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
|
||||
return;
|
||||
}
|
||||
Memory::Write16(addr + kUButton, static_cast<uint16_t>(sample->hold & 0xFFFF));
|
||||
// KPAD's acc is (-wiiX, -wiiZ, wiiY); WPADStatus keeps the remote's own axes.
|
||||
Memory::Write16(addr + kUAccX, RawAcc(-sample->acc[0]));
|
||||
Memory::Write16(addr + kUAccY, RawAcc(sample->acc[2]));
|
||||
Memory::Write16(addr + kUAccZ, RawAcc(-sample->acc[1]));
|
||||
// No IR: every DPDObject invalid (x/y at the sensor's out-of-range value).
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
Memory::Write16(addr + kUObj + i * 8, 0x3FF);
|
||||
Memory::Write16(addr + kUObj + i * 8 + 2, 0x3FF);
|
||||
}
|
||||
Memory::Write8(addr + kUErr, static_cast<uint8_t>(kWpadErrNone));
|
||||
if (sample->hasClassic) {
|
||||
Memory::Write8(addr + kUDev, kDevClassic);
|
||||
Memory::Write16(addr + kUClButton, static_cast<uint16_t>(sample->clHold & 0xFFFF));
|
||||
Memory::Write16(addr + kUClLStickX, static_cast<uint16_t>(sample->clLStickRaw[0]));
|
||||
Memory::Write16(addr + kUClLStickY, static_cast<uint16_t>(sample->clLStickRaw[1]));
|
||||
Memory::Write16(addr + kUClRStickX, static_cast<uint16_t>(sample->clRStickRaw[0]));
|
||||
Memory::Write16(addr + kUClRStickY, static_cast<uint16_t>(sample->clRStickRaw[1]));
|
||||
Memory::Write8(addr + kUClTriggerL, sample->clTriggerL);
|
||||
Memory::Write8(addr + kUClTriggerR, sample->clTriggerR);
|
||||
Memory::Write8(addr + kUFmt, kFmtClassicAccDpd);
|
||||
} else if (sample->hasNunchuk) {
|
||||
Memory::Write8(addr + kUDev, kDevFreestyle);
|
||||
// WPADFSStatus: 8-bit stick (centre 128) and 10-bit Nunchuk accelerometer.
|
||||
Memory::Write8(addr + kUFsStickX,
|
||||
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[0] * 100.0f, 0.0f, 255.0f)));
|
||||
Memory::Write8(addr + kUFsStickY,
|
||||
static_cast<uint8_t>(std::clamp(128.0f + sample->stick[1] * 100.0f, 0.0f, 255.0f)));
|
||||
Memory::Write16(addr + kUFsAccX, RawAcc(-sample->nunchukAcc[0]));
|
||||
Memory::Write16(addr + kUFsAccY, RawAcc(sample->nunchukAcc[2]));
|
||||
Memory::Write16(addr + kUFsAccZ, RawAcc(-sample->nunchukAcc[1]));
|
||||
Memory::Write8(addr + kUFmt, kFmtFreestyleAccDpd);
|
||||
} else {
|
||||
Memory::Write8(addr + kUDev, kDevCore);
|
||||
Memory::Write8(addr + kUFmt, kFmtCoreAccDpd);
|
||||
}
|
||||
}
|
||||
|
||||
// Neutral sticks and no buttons while an overlay owns input; the remote stays connected.
|
||||
bool ReadSample(uint32_t chan, WiiRemoteInput::KpadSample& sample) {
|
||||
if (!WiiRemoteInput::ReadKpadSample(chan, sample)) {
|
||||
return false;
|
||||
}
|
||||
if (InputBindings::InputBlocked()) {
|
||||
sample.hold = 0;
|
||||
sample.clHold = 0;
|
||||
sample.stick[0] = sample.stick[1] = 0.0f;
|
||||
sample.clLStick[0] = sample.clLStick[1] = 0.0f;
|
||||
sample.clRStick[0] = sample.clRStick[1] = 0.0f;
|
||||
sample.clLStickRaw[0] = sample.clLStickRaw[1] = 0;
|
||||
sample.clRStickRaw[0] = sample.clRStickRaw[1] = 0;
|
||||
sample.clTriggerL = sample.clTriggerR = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// KPADRead: fills KPADStatus[0] for `chan` from the Bluetooth remote, returns the entry count.
|
||||
extern "C" int32_t KPAD__Read_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
|
||||
{
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
const bool have = ReadSample(chan, sample);
|
||||
try {
|
||||
return WriteStatus(chan, statusPtr, have ? &sample : nullptr);
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(80197380, KPAD__Read_HLE, int32_t, (uint32_t chan, uint32_t statusPtr, uint32_t count),
|
||||
(chan, statusPtr, count));
|
||||
|
||||
// KPADGetUnifiedWpadStatus: the raw WPAD status behind KPADStatus. The game
|
||||
// reads the Classic Controller's buttons, sticks and triggers from here. The
|
||||
// SDK fills `count` entries with the channel's recent samples (the game asks for
|
||||
// as many as it asked KPADRead for and looks at entry 0); with one sample per
|
||||
// frame here, every entry gets the current one.
|
||||
extern "C" int32_t KPAD__GetUnifiedWpadStatus_HLE(uint32_t chan, uint32_t statusPtr, uint32_t count)
|
||||
{
|
||||
constexpr uint32_t kMaxEntries = 16; // KPAD_MAX_READ_BUFS
|
||||
if (chan >= g_channels.size() || statusPtr == 0 || count == 0) {
|
||||
return 0;
|
||||
}
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
const bool have = ReadSample(chan, sample);
|
||||
try {
|
||||
const uint32_t entries = std::min(count, kMaxEntries);
|
||||
for (uint32_t i = 0; i < entries; ++i) {
|
||||
WriteUnifiedStatus(statusPtr + i * kUnifiedSize, have ? &sample : nullptr);
|
||||
}
|
||||
} catch (const Memory::AccessViolation&) {
|
||||
return 0;
|
||||
}
|
||||
return have ? 1 : 0;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(8019812C, KPAD__GetUnifiedWpadStatus_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t statusPtr, uint32_t count), (chan, statusPtr, count));
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "input_bindings.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
@@ -113,13 +112,7 @@ extern "C" uint32_t PAD__Read_HLE(uint32_t statusPtr)
|
||||
}
|
||||
|
||||
PADStatus statuses[PAD_CHANMAX]{};
|
||||
// Keep looking for a Bluetooth Wii Remote that dropped out (or was turned on late).
|
||||
WiiRemoteInput::Poll();
|
||||
uint32_t rumbleMask = PADRead(statuses);
|
||||
// Wii Remotes reach the game through KPAD, not as GameCube pads. This also
|
||||
// applies while input is blocked (overlay open) so the port does not flip
|
||||
// between "connected" and "no controller" every time the overlay toggles.
|
||||
WiiRemoteInput::HideRemotesFromPad(statuses, PAD_CHANMAX);
|
||||
|
||||
FillTriggersHeldByButtons(statuses);
|
||||
InputBindings::Apply(statuses);
|
||||
|
||||
@@ -1,217 +0,0 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/controller_status_contract.h"
|
||||
#include "wii_remote_input.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <dolphin/pad.h>
|
||||
|
||||
extern "C" bool PAD_HLE_RumbleEnabled();
|
||||
void NandQueueIosCallback(uint32_t callbackPtr, int32_t result, uint32_t callbackArg);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kDefaultWorkMemSize = 0x20000;
|
||||
constexpr uint8_t kDefaultDpdSensitivity = 3;
|
||||
constexpr int32_t kStatusOk = 0;
|
||||
|
||||
struct WpadStubState {
|
||||
bool initSubRan = false;
|
||||
bool simpleSyncActive = false;
|
||||
uint32_t syncDeviceCallback = 0;
|
||||
uint32_t workMemSize = kDefaultWorkMemSize;
|
||||
uint8_t dpdSensitivity = kDefaultDpdSensitivity;
|
||||
WpadContract::State contract{};
|
||||
};
|
||||
|
||||
WpadStubState g_state{};
|
||||
|
||||
void InvokeWpadCallback(uint32_t callback, uint32_t chan, int32_t result)
|
||||
{
|
||||
if (callback == 0) {
|
||||
return;
|
||||
}
|
||||
if (!TranslatedFunctionRegistry::FindByAddressPtr(callback)) {
|
||||
return;
|
||||
}
|
||||
auto& cpu = GetPersistentCpuContext();
|
||||
cpu.gpr[3] = chan;
|
||||
cpu.gpr[4] = static_cast<uint32_t>(result);
|
||||
InvokeIndirectCpu(callback, &cpu);
|
||||
}
|
||||
|
||||
int32_t InitializeWpadLibrary()
|
||||
{
|
||||
g_state.contract.Initialize();
|
||||
return kStatusOk;
|
||||
}
|
||||
|
||||
// The async WPAD entry points all report their outcome to the guest callback and
|
||||
// then return the same value.
|
||||
int32_t CompleteWpadRequest(uint32_t chan, uint32_t callback, int32_t result)
|
||||
{
|
||||
InvokeWpadCallback(callback, chan, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int32_t WPADGetStatus_HLE()
|
||||
{
|
||||
// RVL::WPADGetStatus takes no channel arg; it reports global WUD library state.
|
||||
// Reading r3 here would leak a caller's stale register value into the result.
|
||||
return g_state.contract.GetLibraryStatus();
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF64C, WPADGetStatus_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" uint32_t WPADGetDpdSensitivity_HLE()
|
||||
{
|
||||
return static_cast<uint32_t>(g_state.dpdSensitivity);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C329C, WPADGetDpdSensitivity_HLE, uint32_t, (), ());
|
||||
|
||||
extern "C" int32_t WPADInitSub_HLE()
|
||||
{
|
||||
if (!g_state.initSubRan) {
|
||||
g_state.initSubRan = true;
|
||||
return InitializeWpadLibrary();
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF3B4, WPADInitSub_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" int32_t WPADInit_HLE()
|
||||
{
|
||||
return InitializeWpadLibrary();
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF5C4, WPADInit_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" int32_t WUDGetStatus_HLE()
|
||||
{
|
||||
return WpadContract::kStatusReady;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801CDB84, WUDGetStatus_HLE, int32_t, (), ());
|
||||
|
||||
// WPADGetDataFormat: the format KPAD runs the attached extension in (WPAD_FMT_*_ACC_DPD).
|
||||
// WPADClampStick keys off it. Can't reuse the translated SDK implementation: it dereferences
|
||||
// Bluetooth control blocks that HLE'd WPADInit never constructs.
|
||||
extern "C" int32_t WPADGetDataFormat_HLE(uint32_t chan)
|
||||
{
|
||||
const int32_t status = g_state.contract.GetDataFormat(chan);
|
||||
if (status < 0) {
|
||||
return status;
|
||||
}
|
||||
switch (WiiRemoteInput::EffectiveKind(chan)) {
|
||||
case WiiRemoteInput::Kind::RemoteWithNunchuk: return 5;
|
||||
case WiiRemoteInput::Kind::RemoteWithClassic: return 8;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B54, WPADGetDataFormat_HLE, int32_t, (uint32_t chan), (chan));
|
||||
|
||||
// WPADSetDataFormat: records the per-channel data format the game asked for.
|
||||
extern "C" int32_t WPADSetDataFormat_HLE(uint32_t chan, int32_t format)
|
||||
{
|
||||
return g_state.contract.SetDataFormat(chan, format);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0B9C, WPADSetDataFormat_HLE, int32_t, (uint32_t chan, int32_t format), (chan, format));
|
||||
|
||||
// WPADProbe: reports the extension type of a Bluetooth remote on `chan`, or no controller.
|
||||
extern "C" int32_t WPADProbe_HLE(uint32_t chan, uint32_t typePtr)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return WpadContract::kErrorBadChannel;
|
||||
}
|
||||
|
||||
// Drive the rescan state machine here too: a reconnect probe can arrive
|
||||
// before the next PADRead, and only Poll() brings a dropped remote back.
|
||||
WiiRemoteInput::Poll();
|
||||
|
||||
// A real Bluetooth remote: WPAD_DEV_CORE (0) for a bare remote,
|
||||
// WPAD_DEV_FREESTYLE (1) with a Nunchuk, WPAD_DEV_CLASSIC (2) with a Classic
|
||||
// Controller. The game reads the type from here (not from
|
||||
// KPADStatus.dev_type) to pick its control scheme, and re-reads it when it
|
||||
// changes, which is what makes an extension swap mid-game work like on the
|
||||
// console. EffectiveKind keeps the last type through SDL's re-creation of
|
||||
// the joystick after a swap.
|
||||
const WiiRemoteInput::Kind kind = WiiRemoteInput::EffectiveKind(chan);
|
||||
if (WiiRemoteInput::IsRemoteChannel(chan)) {
|
||||
if (typePtr != 0) {
|
||||
uint32_t type = WpadContract::kExtensionCore;
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithNunchuk) type = 1u;
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) type = 2u;
|
||||
Memory::Write32(typePtr, type);
|
||||
}
|
||||
return kStatusOk;
|
||||
}
|
||||
if (typePtr != 0) {
|
||||
Memory::Write32(typePtr, WpadContract::kExtensionCore);
|
||||
}
|
||||
return WpadContract::kErrorNoController;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0990, WPADProbe_HLE, int32_t, (uint32_t chan, uint32_t typePtr), (chan, typePtr));
|
||||
|
||||
extern "C" void WPADControlMotor_HLE(uint32_t chan, uint32_t command)
|
||||
{
|
||||
const bool on = command != 0 && PAD_HLE_RumbleEnabled();
|
||||
PADControlMotor(chan, on ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0EC4, WPADControlMotor_HLE, void, (uint32_t chan, uint32_t command), (chan, command));
|
||||
|
||||
extern "C" int32_t WPADGetInfoAsync_HLE(uint32_t chan, uint32_t infoPtr, uint32_t callback)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorBadChannel);
|
||||
}
|
||||
|
||||
(void)infoPtr;
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorNoController);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0CA4, WPADGetInfoAsync_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t infoPtr, uint32_t callback), (chan, infoPtr, callback));
|
||||
|
||||
extern "C" int32_t WPADControlLed_HLE(uint32_t chan, uint32_t ledMask, uint32_t callback)
|
||||
{
|
||||
if (chan >= WpadContract::kChannelCount) {
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorBadChannel);
|
||||
}
|
||||
(void)ledMask;
|
||||
return CompleteWpadRequest(chan, callback, WpadContract::kErrorNoController);
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801C0FF8, WPADControlLed_HLE, int32_t,
|
||||
(uint32_t chan, uint32_t ledMask, uint32_t callback), (chan, ledMask, callback));
|
||||
|
||||
extern "C" int32_t WPADStartSimpleSync_HLE()
|
||||
{
|
||||
if (g_state.simpleSyncActive) {
|
||||
return 0;
|
||||
}
|
||||
g_state.simpleSyncActive = true;
|
||||
return 1;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF634, WPADStartSimpleSync_HLE, int32_t, (), ()); // WUDStartSyncSimple
|
||||
PPC_NATIVE_OVERRIDE(801BF638, WPADStartSimpleSync_HLE, int32_t, (), ()); // WPADStartSimpleSync (HBM)
|
||||
|
||||
// due to multiplayer controller screen hle this to avoid startsyncdevice to return fail every frame
|
||||
// causing you to get softlocked in the game
|
||||
extern "C" int32_t WPADStopSimpleSync_HLE()
|
||||
{
|
||||
if (g_state.simpleSyncActive) {
|
||||
g_state.simpleSyncActive = false;
|
||||
const uint32_t callback = g_state.syncDeviceCallback;
|
||||
if (callback != 0 && TranslatedFunctionRegistry::FindByAddressPtr(callback)) {
|
||||
NandQueueIosCallback(callback, 1, 0); // callback(WUD_SYNC_DONE, devicesSynced=0)
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF63C, WPADStopSimpleSync_HLE, int32_t, (), ());
|
||||
|
||||
extern "C" uint32_t WPADSetSyncDeviceCallback_HLE(uint32_t callback)
|
||||
{
|
||||
const uint32_t previous = g_state.syncDeviceCallback;
|
||||
g_state.syncDeviceCallback = callback;
|
||||
return previous;
|
||||
}
|
||||
PPC_NATIVE_OVERRIDE(801BF640, WPADSetSyncDeviceCallback_HLE, uint32_t, (uint32_t callback), (callback));
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "hle_stubs.h"
|
||||
#include "memory.h"
|
||||
#include "hle/bluetooth_hle.h"
|
||||
#include "hle/net/network.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -120,6 +121,10 @@ extern "C" void IOS_OpenAsync_HLE(CpuContext* ctx) {
|
||||
const uint32_t callbackArg = ctx->gpr[6];
|
||||
const char* path = pathPtr ? (const char*)Memory::GetPointer(pathPtr) : "(null)";
|
||||
if (pathPtr) {
|
||||
if (const int32_t btFd = Bluetooth_HLE_OpenDevice(path)) {
|
||||
CompleteAsync(ctx, callback, btFd, callbackArg);
|
||||
return;
|
||||
}
|
||||
if (const int32_t netFd = Network_HLE_OpenDevice(path, mode)) {
|
||||
CompleteAsync(ctx, callback, netFd, callbackArg);
|
||||
return;
|
||||
@@ -135,6 +140,11 @@ extern "C" void IOS_CloseAsync_HLE(CpuContext* ctx) {
|
||||
const uint32_t fd = ctx->gpr[3];
|
||||
const uint32_t callback = ctx->gpr[4];
|
||||
const uint32_t callbackArg = ctx->gpr[5];
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
const int32_t result = Bluetooth_HLE_Close(fd);
|
||||
CompleteAsync(ctx, callback, result, callbackArg);
|
||||
return;
|
||||
}
|
||||
if (Network_HLE_IsFd(fd)) {
|
||||
const int32_t result = Network_HLE_Close(fd);
|
||||
CompleteAsync(ctx, callback, result, callbackArg);
|
||||
@@ -179,6 +189,15 @@ extern "C" void IOS_IoctlvAsync_HLE(CpuContext* ctx)
|
||||
const uint32_t vectorPtr = ctx->gpr[7];
|
||||
const uint32_t callback = ctx->gpr[8];
|
||||
const uint32_t callbackArg = ctx->gpr[9];
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
int32_t result = 0;
|
||||
if (Bluetooth_HLE_IoctlvAsync(fd, cmd, numIn, numOut, vectorPtr, callback, callbackArg, result)) {
|
||||
CompleteAsync(ctx, callback, result, callbackArg);
|
||||
} else {
|
||||
SetIosReturn(ctx, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (TryDeferredNetworkAsync(
|
||||
ctx, fd, callback, callbackArg,
|
||||
[&] {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "hle_stubs.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "runtime_log.h"
|
||||
#include "hle/bluetooth_hle.h"
|
||||
#include "hle/net/network.h"
|
||||
#include "guest_small_data.h"
|
||||
#include "os_internal.h"
|
||||
@@ -237,6 +238,10 @@ bool ProcessAlarmQueue(CpuContext* cpu, int maxToProcess)
|
||||
handledAny = true;
|
||||
completionNeedsReschedule = true;
|
||||
}
|
||||
if (Bluetooth_HLE_ProcessCompletions(cpu)) {
|
||||
handledAny = true;
|
||||
completionNeedsReschedule = true;
|
||||
}
|
||||
|
||||
if (NandProcessPendingCallbacks(cpu, maxToProcess)) {
|
||||
handledAny = true;
|
||||
|
||||
@@ -80,33 +80,16 @@ extern "C" void OSFatal_HLE_801a4ec4(CpuContext* ctx) {
|
||||
|
||||
REGISTER_NATIVE_FUNCTION_AS(0x801A4EC4, OSFatal_HLE_801a4ec4, "OSFatal_HLE_801a4ec4");
|
||||
|
||||
extern "C" void GKI_delay_HLE_801301b4(CpuContext* ctx)
|
||||
extern "C" void GKI_delay_HLE_801301b4()
|
||||
{
|
||||
const uint32_t delayMs = ctx ? static_cast<uint32_t>(ctx->gpr[3]) : 0;
|
||||
const uint32_t sleepMs = delayMs == 0 ? 1u : std::min(delayMs, 10u);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||||
// The guest's GKI_delay is empty: BTA_Init spins on it while the Bluetooth
|
||||
// module answers through IPC interrupts and the 2 ms BTU alarm. Service both
|
||||
// here and let the wall clock advance.
|
||||
OS_HLE_ProcessAlarms(32);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
extern "C" uint32_t BTM_IsDeviceUp_HLE_8013a300(CpuContext* ctx)
|
||||
{
|
||||
// Force Bluetooth stack to "up" to avoid endless polling loops while we lack
|
||||
// real hardware bring-up.
|
||||
constexpr uint32_t kBtmCbBase = 0x80336278u;
|
||||
constexpr uint32_t kDevStateOffset = 0x64Eu;
|
||||
try {
|
||||
::Memory::Write8(kBtmCbBase + kDevStateOffset, 5u);
|
||||
} catch (const ::Memory::AccessViolation&) {
|
||||
// Ignore; best-effort write
|
||||
}
|
||||
|
||||
if (ctx) {
|
||||
ctx->gpr[3] = 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
PPC_NATIVE_OVERRIDE_VOID(801301B4, GKI_delay_HLE_801301b4, (CpuContext* ctx), (ctx));
|
||||
PPC_NATIVE_OVERRIDE(8013A300, BTM_IsDeviceUp_HLE_8013a300, uint32_t, (CpuContext* ctx), (ctx));
|
||||
PPC_NATIVE_OVERRIDE_VOID(801301B4, GKI_delay_HLE_801301b4, (), ());
|
||||
|
||||
// Serial Interface (SI) - GameCube controller ports; stubbed since we don't emulate the MMIO.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "memory.h"
|
||||
#include "nand_path.h"
|
||||
#include "nand_save_probe.h"
|
||||
#include "hle/bluetooth_hle.h"
|
||||
#include "hle/net/network.h"
|
||||
#include "recomp_mod_loader.h"
|
||||
#include "runtime_config.h"
|
||||
|
||||
@@ -382,6 +382,9 @@ extern "C" int32_t NAND_IOS_Open_HLE(uint32_t pathPtr, uint32_t mode) {
|
||||
if (const int32_t netFd = Network_HLE_OpenDevice(path, mode)) {
|
||||
return netFd;
|
||||
}
|
||||
if (const int32_t btFd = Bluetooth_HLE_OpenDevice(path)) {
|
||||
return btFd;
|
||||
}
|
||||
if (std::strcmp(path, "/dev/dolphin") == 0) {
|
||||
return DOLPHIN_DEV_FD;
|
||||
}
|
||||
@@ -452,6 +455,9 @@ extern "C" int32_t NAND_IOS_Close_HLE(uint32_t fd) {
|
||||
if (Network_HLE_IsFd(fd)) {
|
||||
return Network_HLE_Close(fd);
|
||||
}
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
return Bluetooth_HLE_Close(fd);
|
||||
}
|
||||
|
||||
auto* handle = GetHandle(fd);
|
||||
if (!handle) {
|
||||
@@ -566,6 +572,10 @@ extern "C" int32_t NAND_IOS_Ioctl_HLE(
|
||||
LogNandWarning("IOS_Ioctl", "/dev/sha does not support scalar ioctl cmd=%u", cmd);
|
||||
return ISFS_EINVAL;
|
||||
}
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
LogNandWarning("IOS_Ioctl", "/dev/usb/oh1 does not support scalar ioctl cmd=%u", cmd);
|
||||
return -4;
|
||||
}
|
||||
if (fd == DOLPHIN_DEV_FD) {
|
||||
LogNandWarning("IOS_Ioctl", "/dev/dolphin does not support scalar ioctl cmd=%u", cmd);
|
||||
return ISFS_EINVAL;
|
||||
@@ -1032,6 +1042,9 @@ extern "C" int32_t NAND_IOS_Ioctlv_HLE(
|
||||
if (Network_HLE_IsFd(fd)) {
|
||||
return Network_HLE_Ioctlv(fd, cmd, numIn, numOut, vectorPtr);
|
||||
}
|
||||
if (Bluetooth_HLE_IsFd(fd)) {
|
||||
return Bluetooth_HLE_Ioctlv(fd, cmd, numIn, numOut, vectorPtr);
|
||||
}
|
||||
|
||||
if (GetShaHandle(static_cast<int32_t>(fd))) {
|
||||
return HandleShaIoctlv(static_cast<int32_t>(fd), cmd, numIn, numOut, vectorPtr);
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
#include "system_bridge.h"
|
||||
#include "ppc_runtime.h"
|
||||
#include "aurora_events.h"
|
||||
#include "wii_remote_input.h"
|
||||
#include "wiimote_host.h"
|
||||
#include "discord_presence.h"
|
||||
#include "fiber_manager.h"
|
||||
#include "hle_stubs.h"
|
||||
@@ -1430,10 +1430,9 @@ int RuntimeMain(int argc, char** argv) {
|
||||
}
|
||||
const AuroraBackend requestedBackend = auroraConfig.desiredBackend;
|
||||
|
||||
// SDL only reads its Wii driver hint when the joystick subsystem starts, which
|
||||
// aurora_initialize does; a Bluetooth Wii Remote paired before launch must be
|
||||
// visible on that first scan.
|
||||
WiiRemoteInput::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled(true));
|
||||
// SDL reads its device hints when the joystick subsystem starts, which
|
||||
// aurora_initialize does.
|
||||
Wiimote::ConfigureSdlHints(RuntimeConfigFile::WiiRemotesEnabled());
|
||||
|
||||
const AuroraInfo auroraInfo = aurora_initialize(0, nullptr, &auroraConfig);
|
||||
if (auroraInfo.initializationStatus != AURORA_INITIALIZATION_SUCCESS) {
|
||||
@@ -1450,6 +1449,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
<< std::endl;
|
||||
}
|
||||
aurora_set_frame_worker_wait_callback(ServiceGuestTimingDuringAuroraFrameWait);
|
||||
Wiimote::Initialize();
|
||||
GxGuestWrite::InstallAuroraHooks();
|
||||
UpdateMkwDynamicAspectSurface(auroraInfo.windowSize.native_fb_width,
|
||||
auroraInfo.windowSize.native_fb_height);
|
||||
@@ -1493,6 +1493,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
// Shutdown fiber system
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wiimote::Shutdown();
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
SetRuntimeExitCodeImpl(0);
|
||||
@@ -1511,6 +1512,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wiimote::Shutdown();
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
@@ -1523,6 +1525,7 @@ int RuntimeMain(int argc, char** argv) {
|
||||
SetRuntimeExitCodeImpl(1);
|
||||
Fiber::GuestFiberManager::Shutdown();
|
||||
WindowPlacementPersistence::Flush(true);
|
||||
Wiimote::Shutdown();
|
||||
aurora_shutdown();
|
||||
DiscordPresence::Shutdown();
|
||||
ShutdownProcessTranscript();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "memory.h"
|
||||
#include "hle/hollywood_gpio.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -643,6 +644,9 @@ uint16_t MemoryInline::Read16Slow(uint32_t addr) {
|
||||
|
||||
uint32_t MemoryInline::Read32Slow(uint32_t addr) {
|
||||
if (IsMmioAddress(addr)) {
|
||||
if (uint32_t value = 0; HollywoodGpio_Read32(addr, value)) {
|
||||
return value;
|
||||
}
|
||||
ThrowMmioReadBlocked(addr, sizeof(uint32_t));
|
||||
}
|
||||
ResolveDeferredReads(addr, sizeof(uint32_t));
|
||||
@@ -708,6 +712,9 @@ void MemoryInline::Write32Slow(uint32_t addr, uint32_t val) {
|
||||
return;
|
||||
}
|
||||
if (IsMmioAddress(addr)) {
|
||||
if (HollywoodGpio_Write32(addr, val)) {
|
||||
return;
|
||||
}
|
||||
throw Memory::AccessViolation(addr, sizeof(val), "MMIO write blocked (non-GPU)");
|
||||
}
|
||||
WriteScalar(addr, val);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wii_remote_input.h"
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <SDL3/SDL_events.h>
|
||||
@@ -259,9 +259,9 @@ void ApplyConfiguredMappings() {
|
||||
// they describe whatever pad the user set them up with (usually an Xbox
|
||||
// layout: a = south). A Wii U Pro Controller has a fixed, known layout
|
||||
// (A on the east position) that aurora already maps by name; applying
|
||||
// the shared bindings on top swaps A/B and X/Y. (Wii Remotes with any
|
||||
// extension never reach the PAD layer: the game reads them through KPAD.)
|
||||
if (WiiRemoteInput::KindForPort(port) == WiiRemoteInput::Kind::WiiUPro) {
|
||||
// the shared bindings on top swaps A/B and X/Y.
|
||||
if (const char* name = PADGetNameForControllerIndex(static_cast<uint32_t>(controllerIndex));
|
||||
name != nullptr && std::string_view(name).find("Wii U Pro") != std::string_view::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -295,47 +295,11 @@ void ApplyConfiguredMappings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled(true);
|
||||
bool g_wiiContinuousScan = RuntimeConfigFile::WiiContinuousScanEnabled(false);
|
||||
bool g_wiiRemotesEnabled = RuntimeConfigFile::WiiRemotesEnabled();
|
||||
bool g_wiiAutoPair = RuntimeConfigFile::WiiAutoPairEnabled(true);
|
||||
bool g_wiiSensorBarAbove = RuntimeConfigFile::WiiSensorBarAbove(false);
|
||||
int g_wiiIrSensitivity = RuntimeConfigFile::WiiIrSensitivity(3);
|
||||
|
||||
// Accelerometer readout and zero-point calibration for a bare remote / remote + Nunchuk.
|
||||
void DrawWiiRemoteAccelerometer(uint32_t port) {
|
||||
float sdlG[3] = {};
|
||||
float kpad[3] = {};
|
||||
if (WiiRemoteInput::ReadAccelDebug(port, sdlG, kpad)) {
|
||||
ImGui::Text("KPAD acc: x %+.2f y %+.2f z %+.2f g", kpad[0], kpad[1], kpad[2]);
|
||||
ImGui::TextDisabled("Flat, buttons up: (0, -1, 0). Sideways as a wheel: (1, 0, 0); z follows the turn.");
|
||||
} else {
|
||||
ImGui::TextDisabled("No accelerometer data yet.");
|
||||
}
|
||||
// Residual per-axis bias left after SDL's factory-calibrated decode, measured
|
||||
// here with the remote at rest.
|
||||
if (WiiRemoteInput::IsAccelCalibrating(port)) {
|
||||
ImGui::ProgressBar(WiiRemoteInput::AccelCalibrationProgress(), ImVec2(220.0f, 0.0f), "Hold still...");
|
||||
} else if (ImGui::Button("Calibrate (remote lying flat, buttons up)")) {
|
||||
WiiRemoteInput::StartAccelCalibration(port);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Put the remote down on a flat surface with the buttons facing up and do not touch it\n"
|
||||
"for about two seconds. Corrects the steering offset of a remote held sideways.");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
const auto stored = WiiRemoteInput::ReadAccelOffset(port);
|
||||
const std::array<double, 3> offset = stored.value_or(std::array<double, 3>{});
|
||||
const bool calibrated = offset[0] != 0.0 || offset[1] != 0.0 || offset[2] != 0.0;
|
||||
ImGui::BeginDisabled(!calibrated || WiiRemoteInput::IsAccelCalibrating(port));
|
||||
if (ImGui::Button("Clear calibration")) {
|
||||
WiiRemoteInput::ClearAccelCalibration(port);
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (const char* message = WiiRemoteInput::AccelCalibrationMessage(port)) {
|
||||
ImGui::TextWrapped("%s", message);
|
||||
} else if (calibrated) {
|
||||
ImGui::TextDisabled("Stored offset: x %+.3f y %+.3f z %+.3f g", offset[0], offset[1], offset[2]);
|
||||
} else {
|
||||
ImGui::TextDisabled("No trim stored.");
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float kControllerMenuWidth = 460.0f;
|
||||
|
||||
@@ -343,116 +307,101 @@ void PushMenuTextWrap() {
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
}
|
||||
|
||||
// Driver switch, pairing help and scanning; applies to every port.
|
||||
std::string WiiRemoteLabel(const Wiimote::SlotInfo& info) {
|
||||
std::string label = info.model;
|
||||
if (!info.extension.empty()) {
|
||||
label += " + " + info.extension;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
// Pairing, the console's Wii Remote settings and the driver switch; applies to every remote.
|
||||
void DrawWiiRemoteSetup() {
|
||||
if (!ImGui::CollapsingHeader("Wii Remote setup (Bluetooth)")) {
|
||||
return;
|
||||
}
|
||||
ImGui::Indent();
|
||||
PushMenuTextWrap();
|
||||
#if defined(_WIN32)
|
||||
ImGui::TextUnformatted("1. Windows Settings > Bluetooth > Add device.");
|
||||
#elif defined(__APPLE__)
|
||||
ImGui::TextUnformatted("1. System Settings > Bluetooth.");
|
||||
#else
|
||||
ImGui::TextUnformatted("1. Open your system's Bluetooth settings.");
|
||||
#endif
|
||||
ImGui::TextUnformatted("2. Press 1+2 (or the red SYNC button) on the remote. Leave the PIN empty.");
|
||||
ImGui::TextDisabled("Paired before? Press 1+2 to turn it on. It then appears in the player list.");
|
||||
const std::string notice = Wiimote::PlatformNotice();
|
||||
if (!notice.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.25f, 1.0f), "%s", notice.c_str());
|
||||
}
|
||||
const Wiimote::PairingInfo pairing = Wiimote::Pairing();
|
||||
if (pairing.supported) {
|
||||
ImGui::TextUnformatted("Press the red SYNC button (or 1 and 2) on the Wii Remote, like on a Wii.");
|
||||
ImGui::TextDisabled("Paired before? Just press any button on it.");
|
||||
} else if (g_wiiRemotesEnabled) {
|
||||
ImGui::TextUnformatted("Pair the Wii Remote in your system's Bluetooth settings, then press any button on it.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
// The driver hint is only read at launch, so a rescan after the user turned
|
||||
// the setting off would still re-enumerate Wii devices in this session.
|
||||
ImGui::BeginDisabled(!g_wiiRemotesEnabled);
|
||||
if (ImGui::Button("Scan now")) {
|
||||
WiiRemoteInput::RescanNow();
|
||||
ImGui::BeginDisabled(!pairing.supported);
|
||||
if (ImGui::Button("Pair a Wii Remote")) {
|
||||
Wiimote::StartPairing();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
if (WiiRemoteInput::IsScanning()) {
|
||||
ImGui::TextDisabled("Scanning... (%u so far) - press 1+2 on the remote", WiiRemoteInput::ScanCount());
|
||||
} else {
|
||||
ImGui::TextDisabled("Not scanning");
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Searches for a minute, like pressing SYNC on the console.");
|
||||
}
|
||||
if (ImGui::Checkbox("Keep scanning for Wii Remotes", &g_wiiContinuousScan)) {
|
||||
RuntimeConfigFile::SetWiiContinuousScanEnabled(g_wiiContinuousScan);
|
||||
ImGui::SameLine();
|
||||
if (pairing.searching) {
|
||||
ImGui::TextDisabled("Searching...");
|
||||
} else if (!pairing.status.empty()) {
|
||||
ImGui::TextDisabled("%s", pairing.status.c_str());
|
||||
}
|
||||
if (const size_t waiting = Wiimote::WaitingRemotes(); waiting != 0) {
|
||||
ImGui::TextDisabled("%zu more Wii Remote%s connected; joins when a player leaves.", waiting,
|
||||
waiting == 1 ? "" : "s");
|
||||
}
|
||||
if (ImGui::Checkbox("Look for Wii Remotes automatically", &g_wiiAutoPair)) {
|
||||
RuntimeConfigFile::SetWiiAutoPairEnabled(g_wiiAutoPair);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("While no Wii controller is connected, re-check Bluetooth every 2 seconds so a\n"
|
||||
"remote that dropped out (\"Communications with the controller have been\n"
|
||||
"interrupted\") or was turned on after launch comes back by itself.");
|
||||
ImGui::SetTooltip("While no Wii Remote is connected, keep searching for one in pairing mode.\n"
|
||||
"Searching can briefly disturb Bluetooth headphones.");
|
||||
}
|
||||
if (ImGui::Checkbox("Use Wii Remotes / Wii U Pro Controllers", &g_wiiRemotesEnabled)) {
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
int sensorBar = g_wiiSensorBarAbove ? 1 : 0;
|
||||
if (ImGui::Combo("Sensor bar", &sensorBar, "Below TV\0Above TV\0")) {
|
||||
g_wiiSensorBarAbove = sensorBar == 1;
|
||||
RuntimeConfigFile::SetWiiSensorBarAbove(g_wiiSensorBarAbove);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Takes effect the next time the game starts.");
|
||||
}
|
||||
ImGui::SetNextItemWidth(160.0f);
|
||||
if (ImGui::SliderInt("Pointer sensitivity", &g_wiiIrSensitivity, 1, 5, "%d", ImGuiSliderFlags_AlwaysClamp)) {
|
||||
RuntimeConfigFile::SetWiiIrSensitivity(g_wiiIrSensitivity);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("The sensor bar sensitivity from the Wii settings.\nTakes effect the next time the game starts.");
|
||||
}
|
||||
if (ImGui::Checkbox("Use Wii Remotes", &g_wiiRemotesEnabled)) {
|
||||
RuntimeConfigFile::SetWiiRemotesEnabled(g_wiiRemotesEnabled);
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Takes effect on the next launch. Turn this off if you use a Mayflash DolphinBar.");
|
||||
ImGui::SetTooltip("Takes effect on the next launch. Turn this off to use a Wii U Pro Controller as a gamepad.");
|
||||
}
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
void DrawWiiDiagnostics(uint32_t port, WiiRemoteInput::Kind kind) {
|
||||
if (kind == WiiRemoteInput::Kind::RemoteWithClassic) {
|
||||
WiiRemoteInput::KpadSample sample;
|
||||
if (WiiRemoteInput::ReadKpadSample(port, sample)) {
|
||||
// WPAD_CL_BUTTON_* bits, in the game's own layout (no mapping involved).
|
||||
const auto held = [&](uint32_t bit, const char* on, const char* off) { return (sample.clHold & bit) ? on : off; };
|
||||
ImGui::Text("Classic: %s %s %s %s %s %s %s %s %s %s %s %s %s %s", held(0x0010, "A", "a"),
|
||||
held(0x0040, "B", "b"), held(0x0008, "X", "x"), held(0x0020, "Y", "y"), held(0x2000, "L", "l"),
|
||||
held(0x0200, "R", "r"), held(0x0080, "ZL", "zl"), held(0x0004, "ZR", "zr"),
|
||||
held(0x0400, "PLUS", "plus"), held(0x1000, "MINUS", "minus"), held(0x0001, "UP", "up"),
|
||||
held(0x4000, "DOWN", "down"), held(0x0002, "LEFT", "left"), held(0x8000, "RIGHT", "right"));
|
||||
ImGui::Text("Sticks: L %+.2f %+.2f (WPAD %+d %+d) R %+.2f %+.2f (WPAD %+d %+d)", sample.clLStick[0],
|
||||
sample.clLStick[1], static_cast<int>(sample.clLStickRaw[0]),
|
||||
static_cast<int>(sample.clLStickRaw[1]), sample.clRStick[0], sample.clRStick[1],
|
||||
static_cast<int>(sample.clRStickRaw[0]), static_cast<int>(sample.clRStickRaw[1]));
|
||||
ImGui::TextDisabled("Capitals = held.");
|
||||
}
|
||||
// The Wii Remote the game has on this player's channel.
|
||||
void DrawWiiRemoteDetails(uint32_t port, const Wiimote::SlotInfo& info) {
|
||||
ImGui::Text("%s", WiiRemoteLabel(info).c_str());
|
||||
if (info.battery >= 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Battery %d/4", info.battery);
|
||||
}
|
||||
if (kind == WiiRemoteInput::Kind::WiiUPro) {
|
||||
if (SDL_Gamepad* gamepad = SDL_GetGamepadFromPlayerIndex(static_cast<int>(port))) {
|
||||
// SDL's Wii driver posts the D-pad as joystick buttons 11-14 (the
|
||||
// SDL_GAMEPAD_BUTTON_DPAD_* values) while its default HIDAPI mapping
|
||||
// expects a hat, so SDL_GetGamepadButton never sees them; read the
|
||||
// joystick directly, like the fallback in aurora's PADRead does.
|
||||
SDL_Joystick* joystick = SDL_GetGamepadJoystick(gamepad);
|
||||
const auto rawButton = [&](int index) {
|
||||
return joystick != nullptr && SDL_GetJoystickButton(joystick, index);
|
||||
};
|
||||
ImGui::Text("Raw D-pad: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_DPAD_UP) ? "UP" : "up",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN) ? "DOWN" : "down",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT) ? "LEFT" : "left",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT) ? "RIGHT" : "right");
|
||||
ImGui::Text("Raw face buttons: %s %s %s %s", rawButton(SDL_GAMEPAD_BUTTON_EAST) ? "A" : "a",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_SOUTH) ? "B" : "b", rawButton(SDL_GAMEPAD_BUTTON_NORTH) ? "X" : "x",
|
||||
rawButton(SDL_GAMEPAD_BUTTON_WEST) ? "Y" : "y");
|
||||
ImGui::Text("Raw ZL/ZR: %d / %d (pressed above 0)",
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER),
|
||||
SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER));
|
||||
ImGui::TextDisabled("Capitals = held. A button that never turns to capitals is not reaching SDL.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Selected port holds a Wii controller: no mapping, only motion calibration and diagnostics.
|
||||
void DrawWiiPortDetails(uint32_t port, WiiRemoteInput::Kind kind) {
|
||||
PushMenuTextWrap();
|
||||
if (kind == WiiRemoteInput::Kind::WiiUPro) {
|
||||
ImGui::TextUnformatted("Uses Nintendo's own layout, buttons work as labelled. Nothing to set up.");
|
||||
} else {
|
||||
if (info.asleep) {
|
||||
ImGui::TextUnformatted("Disconnected by the game. Press any button on it to reconnect.");
|
||||
} else if (info.link == Bluetooth::LinkState::Ready) {
|
||||
ImGui::TextUnformatted("The game reads it like a real Wii Remote. Nothing to set up.");
|
||||
} else {
|
||||
ImGui::TextUnformatted("Connecting to the game...");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
const bool remote = kind == WiiRemoteInput::Kind::Remote || kind == WiiRemoteInput::Kind::RemoteWithNunchuk ||
|
||||
kind == WiiRemoteInput::Kind::RemoteWithClassic;
|
||||
if (remote && ImGui::CollapsingHeader("Motion calibration")) {
|
||||
ImGui::Indent();
|
||||
DrawWiiRemoteAccelerometer(port);
|
||||
ImGui::Unindent();
|
||||
}
|
||||
if ((kind == WiiRemoteInput::Kind::RemoteWithClassic || kind == WiiRemoteInput::Kind::WiiUPro) &&
|
||||
ImGui::TreeNode("Diagnostics")) {
|
||||
DrawWiiDiagnostics(port, kind);
|
||||
ImGui::TreePop();
|
||||
if (!info.asleep && ImGui::Button("Disconnect")) {
|
||||
Wiimote::Disconnect(port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,6 +895,8 @@ void DrawPlayerRow(uint32_t port) {
|
||||
preview = "Keyboard and mouse";
|
||||
} else if (assigned >= 0) {
|
||||
preview = ControllerLabel(static_cast<uint32_t>(assigned));
|
||||
} else if (const Wiimote::SlotInfo remote = Wiimote::Slot(port); remote.attached) {
|
||||
preview = WiiRemoteLabel(remote);
|
||||
}
|
||||
|
||||
ImGui::PushID(static_cast<int>(port));
|
||||
@@ -994,21 +945,29 @@ void DrawControllerSettings() {
|
||||
for (uint32_t port = 0; port < PAD_MAX_CONTROLLERS; ++port) {
|
||||
DrawPlayerRow(port);
|
||||
}
|
||||
if (PADCount() == 0) {
|
||||
bool anyRemote = false;
|
||||
for (uint32_t player = 0; player < PAD_MAX_CONTROLLERS; ++player) {
|
||||
anyRemote |= Wiimote::Slot(player).attached;
|
||||
}
|
||||
if (PADCount() == 0 && !anyRemote) {
|
||||
ImGui::TextDisabled("No controller detected. Plug one in, or pair a Wii Remote below.");
|
||||
}
|
||||
|
||||
const uint32_t port = static_cast<uint32_t>(g_controllerPort);
|
||||
ImGui::SeparatorText(("Player " + std::to_string(port + 1)).c_str());
|
||||
const WiiRemoteInput::Kind kind = WiiRemoteInput::KindForPort(port);
|
||||
const Wiimote::SlotInfo remote = Wiimote::Slot(port);
|
||||
if (remote.attached) {
|
||||
DrawWiiRemoteDetails(port, remote);
|
||||
if (KeyboardActive(port) || PADGetIndexForPort(port) >= 0) {
|
||||
ImGui::Spacing();
|
||||
}
|
||||
}
|
||||
if (KeyboardActive(port)) {
|
||||
DrawKeyboardSettings(port);
|
||||
} else if (PADGetIndexForPort(port) < 0) {
|
||||
ImGui::TextDisabled("No device. Pick one in the list above.");
|
||||
} else if (kind != WiiRemoteInput::Kind::NotWii || WiiRemoteInput::IsRemoteChannel(port)) {
|
||||
ImGui::Text("%s", WiiRemoteInput::KindLabel(kind == WiiRemoteInput::Kind::NotWii ? WiiRemoteInput::Kind::Remote
|
||||
: kind));
|
||||
DrawWiiPortDetails(port, kind);
|
||||
if (!remote.attached) {
|
||||
ImGui::TextDisabled("No device. Pick one in the list above.");
|
||||
}
|
||||
} else {
|
||||
ImGui::TextUnformatted("Ready to play.");
|
||||
if (ImGui::CollapsingHeader("Customize buttons")) {
|
||||
@@ -1606,6 +1565,7 @@ void ReleaseControllers() noexcept {
|
||||
constexpr std::array<uint32_t, PAD_MAX_CONTROLLERS> stopAll{
|
||||
PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD, PAD_MOTOR_STOP_HARD};
|
||||
PADControlAllMotors(stopAll.data());
|
||||
Wiimote::Shutdown();
|
||||
// SDL hands LED and rumble reports to its own HIDAPI sender thread rather
|
||||
// than writing them here, so without this the process dies before the
|
||||
// controller ever receives them.
|
||||
@@ -1616,11 +1576,6 @@ void Draw() noexcept {
|
||||
// Wait for the frame worker's DONE phase: it has replayed the previous frame's ImGui draw lists
|
||||
// and started the next ImGui frame, so all overlay callers can now safely issue ImGui commands.
|
||||
aurora_wait_for_frame_worker();
|
||||
// Also drive the Wii Remote rescan from here: PADRead runs it too, but this
|
||||
// runs once per presented frame whatever the game is doing (e.g. sitting in
|
||||
// its "communications interrupted" prompt without polling pads). Same guest
|
||||
// thread as PADRead, so no concurrent access to the scanner's state.
|
||||
WiiRemoteInput::Poll();
|
||||
ApplyConfiguredMappings();
|
||||
PersistDisplayModeIfChanged();
|
||||
UpdateCursorAutoHide();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,260 @@
|
||||
// Wii Remote extension encryption (first-party key schedule). Ported from
|
||||
// Dolphin (Source/Core/Core/HW/WiimoteEmu/Encryption.cpp, GPL-2.0-or-later),
|
||||
// based on Hector Martin's reverse engineering.
|
||||
|
||||
#include "wiimote_ext_crypto.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
|
||||
namespace Wiimote::ExtCrypto {
|
||||
namespace Detail {
|
||||
|
||||
using SBox = std::array<uint8_t, 256>;
|
||||
|
||||
constexpr SBox kKeygenSbox = {
|
||||
0x70, 0x51, 0x03, 0x86, 0x40, 0x0d, 0x4f, 0xeb, 0x3e, 0xcc, 0xd1, 0x87, 0x35, 0xbd, 0xf5, 0x0b,
|
||||
0x5e, 0xd0, 0xf8, 0xf2, 0xd5, 0xe2, 0x6c, 0x31, 0x0c, 0xad, 0xfc, 0x21, 0xc3, 0x78, 0xc1, 0x06,
|
||||
0xc2, 0x4c, 0x55, 0xe6, 0x4a, 0x34, 0x48, 0x11, 0x1e, 0xda, 0xe7, 0x1a, 0x84, 0xa0, 0x96, 0xa7,
|
||||
0xe3, 0x7f, 0xaf, 0x63, 0x9c, 0xfa, 0x23, 0x5b, 0x79, 0xc8, 0x9e, 0xba, 0xb2, 0xc9, 0x22, 0x12,
|
||||
0x4b, 0xb3, 0xa1, 0xb6, 0x32, 0x49, 0xa2, 0xe1, 0x89, 0x39, 0x10, 0x66, 0xc5, 0x07, 0x8f, 0x54,
|
||||
0xea, 0x91, 0xca, 0x3f, 0xf9, 0x19, 0xf0, 0xd7, 0x46, 0xbc, 0x28, 0x1b, 0x61, 0xe8, 0x2f, 0x6a,
|
||||
0xae, 0x9d, 0xf6, 0x4e, 0x09, 0x14, 0x77, 0x4d, 0xdb, 0x1f, 0x2e, 0x7b, 0x7c, 0xf1, 0x43, 0xa3,
|
||||
0x00, 0xb8, 0x13, 0x8c, 0x85, 0xb9, 0x29, 0x75, 0x88, 0xfd, 0xd2, 0x56, 0x1c, 0x50, 0x97, 0x41,
|
||||
0xe5, 0x3b, 0x60, 0xb5, 0xc0, 0x64, 0xee, 0x98, 0xd6, 0x2d, 0x25, 0xa4, 0xaa, 0xcd, 0x7d, 0xa8,
|
||||
0x83, 0xc6, 0xab, 0xbe, 0x44, 0x99, 0x26, 0x3c, 0xce, 0x9f, 0xbf, 0xd3, 0xcb, 0x76, 0x7a, 0x7e,
|
||||
0x82, 0x01, 0x8a, 0x9a, 0x80, 0x1d, 0x0e, 0xb0, 0x5c, 0xd4, 0x38, 0x62, 0xf4, 0x30, 0xe0, 0x8e,
|
||||
0x53, 0xb7, 0x02, 0x57, 0xac, 0xa6, 0x52, 0x0a, 0x6d, 0x92, 0x65, 0x17, 0x24, 0x33, 0x45, 0x72,
|
||||
0x74, 0xb1, 0xb4, 0xf7, 0x5d, 0xed, 0x2c, 0xff, 0x47, 0x37, 0x5a, 0x90, 0xbb, 0xdf, 0x2a, 0x16,
|
||||
0x59, 0x95, 0xd9, 0xc4, 0x27, 0x67, 0x73, 0xc7, 0x68, 0xfe, 0xa5, 0xdd, 0x6b, 0x5f, 0x93, 0xd8,
|
||||
0xec, 0x05, 0x3a, 0x8d, 0x6e, 0xfb, 0x3d, 0xa9, 0x69, 0x36, 0xf3, 0x94, 0xde, 0xef, 0x15, 0x6f,
|
||||
0x8b, 0x9b, 0x08, 0x0f, 0xdc, 0x81, 0x18, 0x20, 0x04, 0xe4, 0x71, 0xcf, 0xe9, 0x2b, 0x42, 0x58,
|
||||
};
|
||||
|
||||
constexpr std::array<SBox, 8> kSboxes = {{
|
||||
{
|
||||
0x01, 0xa0, 0xa9, 0x62, 0xd6, 0x3f, 0x85, 0xa7, 0xb6, 0xd4, 0xfa, 0x15, 0x66, 0x17, 0x09,
|
||||
0xbd, 0x5d, 0x14, 0x34, 0x26, 0x59, 0x72, 0x91, 0x54, 0x06, 0x4f, 0xf8, 0xb0, 0x5b, 0x74,
|
||||
0x93, 0x99, 0x8c, 0xf2, 0x45, 0xcd, 0xea, 0x4e, 0xad, 0x10, 0x4a, 0xe5, 0xca, 0xee, 0xdf,
|
||||
0xc6, 0x6f, 0x9f, 0x88, 0x8e, 0x02, 0xcc, 0x08, 0xa8, 0x77, 0x94, 0x6d, 0x21, 0xb1, 0x28,
|
||||
0xe4, 0x39, 0x79, 0x96, 0x60, 0x71, 0x81, 0x16, 0x2e, 0xe6, 0x78, 0xb9, 0xc4, 0x46, 0x9a,
|
||||
0x42, 0xae, 0xb7, 0x7c, 0x43, 0xb3, 0x22, 0x1a, 0x86, 0xc2, 0x32, 0x3d, 0x2d, 0x9c, 0xd2,
|
||||
0x29, 0xe9, 0x63, 0x9b, 0xd1, 0x31, 0x38, 0x5e, 0x1e, 0x36, 0x41, 0xbb, 0x03, 0x18, 0x2b,
|
||||
0x3e, 0xbf, 0x68, 0x61, 0xfc, 0x52, 0xc0, 0xde, 0xe0, 0x0a, 0x58, 0x13, 0x5a, 0x00, 0xbe,
|
||||
0x1c, 0x90, 0x0e, 0x53, 0x12, 0xfd, 0xe2, 0x6e, 0xba, 0xce, 0x24, 0x27, 0x44, 0x7f, 0x87,
|
||||
0xa3, 0xa1, 0xd5, 0x50, 0x40, 0xe3, 0xf9, 0x83, 0xf7, 0xc7, 0xa2, 0x35, 0xc8, 0xdb, 0x19,
|
||||
0xab, 0x2f, 0x11, 0x25, 0xed, 0x33, 0x9e, 0x55, 0xe1, 0x48, 0xaf, 0x73, 0x84, 0xda, 0x2a,
|
||||
0xaa, 0x51, 0xeb, 0x9d, 0x95, 0xb2, 0xcb, 0xe7, 0x70, 0x80, 0xfe, 0x4c, 0x65, 0x04, 0xef,
|
||||
0xc5, 0xf1, 0xc3, 0x3a, 0xb4, 0xf5, 0x5f, 0x23, 0x89, 0xdd, 0x30, 0xa5, 0x8b, 0xd3, 0xf6,
|
||||
0xdc, 0x4d, 0x64, 0xd7, 0xf0, 0x8f, 0xec, 0x56, 0x37, 0x5c, 0xa4, 0x0d, 0x07, 0x76, 0x8a,
|
||||
0x2c, 0x0b, 0xb5, 0xd8, 0xc1, 0x1f, 0xe8, 0x3b, 0xf4, 0x4b, 0x1b, 0x47, 0x6c, 0x49, 0x67,
|
||||
0x7b, 0x92, 0xcf, 0x75, 0x7e, 0x20, 0xd9, 0x7d, 0x3c, 0x97, 0x7a, 0xd0, 0x05, 0x6b, 0x0f,
|
||||
0x1d, 0xfb, 0x82, 0x98, 0x57, 0x8d, 0xf3, 0x6a, 0xbc, 0xac, 0xc9, 0xa6, 0xff, 0xb8, 0x69,
|
||||
0x0c,
|
||||
},
|
||||
{
|
||||
0x4c, 0x4d, 0x72, 0x07, 0x5a, 0x49, 0x33, 0x8d, 0xa2, 0xab, 0x46, 0x3d, 0x63, 0x0d, 0xa0,
|
||||
0x97, 0xff, 0xf0, 0xf5, 0xfa, 0xc0, 0xe9, 0xdb, 0x62, 0xe4, 0xe1, 0x74, 0x43, 0xdc, 0x86,
|
||||
0x18, 0x29, 0x37, 0xf4, 0x06, 0xe2, 0xed, 0x6f, 0x90, 0x48, 0x1e, 0x2d, 0x1d, 0xea, 0x73,
|
||||
0x94, 0x54, 0xdf, 0x25, 0xf6, 0x47, 0x27, 0xd9, 0x11, 0x77, 0xc9, 0x84, 0x1c, 0x5b, 0x5c,
|
||||
0x51, 0x81, 0xa6, 0x22, 0x3e, 0x24, 0x96, 0xc8, 0x8a, 0xec, 0x82, 0x7c, 0x09, 0xb8, 0x45,
|
||||
0x4a, 0x57, 0xbb, 0x2f, 0x50, 0x75, 0x8e, 0x61, 0x70, 0x8c, 0x6c, 0xaf, 0xd0, 0xfd, 0xb4,
|
||||
0x1b, 0xae, 0xde, 0xfe, 0x3b, 0xb5, 0x36, 0xbd, 0x55, 0x01, 0x0e, 0x9c, 0x41, 0x56, 0x5f,
|
||||
0xb3, 0x26, 0x03, 0x83, 0xba, 0x13, 0x4b, 0xca, 0xc5, 0x0a, 0xf8, 0x60, 0xa5, 0xb9, 0xc7,
|
||||
0xc3, 0x98, 0x32, 0xfb, 0x12, 0xf9, 0xa7, 0x92, 0xaa, 0x68, 0xf3, 0x78, 0x7e, 0x05, 0x20,
|
||||
0x21, 0x02, 0xe8, 0xbf, 0xf2, 0xb0, 0x59, 0x8f, 0xd2, 0xcb, 0x87, 0x65, 0x15, 0xf1, 0x1a,
|
||||
0xb2, 0x30, 0xad, 0xee, 0x58, 0xa3, 0x8b, 0x66, 0x1f, 0x2c, 0xd7, 0x5d, 0x19, 0x85, 0xa8,
|
||||
0xe6, 0xd3, 0x6b, 0xa1, 0x0c, 0x91, 0x93, 0x6a, 0x5e, 0x0b, 0x79, 0xe3, 0xdd, 0x00, 0x4f,
|
||||
0x3c, 0x89, 0x6e, 0x71, 0x69, 0xa9, 0xac, 0x40, 0xe5, 0x99, 0x28, 0xc6, 0x31, 0x4e, 0x7a,
|
||||
0xcd, 0x08, 0x9e, 0x7d, 0xef, 0x17, 0xfc, 0x88, 0xd8, 0xa4, 0x6d, 0x44, 0x95, 0xd1, 0xb7,
|
||||
0xd4, 0x9b, 0xbe, 0x2a, 0x34, 0x64, 0x2b, 0xcf, 0x2e, 0xeb, 0x38, 0xce, 0x23, 0xe0, 0x3a,
|
||||
0x3f, 0xf7, 0x7b, 0x9f, 0x10, 0x53, 0xbc, 0x52, 0x67, 0x16, 0xe7, 0x80, 0x76, 0x04, 0xc4,
|
||||
0xb6, 0xc1, 0xc2, 0x7f, 0x9a, 0xda, 0xd5, 0x39, 0x42, 0x14, 0x9d, 0xb1, 0x0f, 0x35, 0xd6,
|
||||
0xcc,
|
||||
},
|
||||
{
|
||||
0xb9, 0xda, 0x38, 0x0c, 0xa2, 0x9c, 0x09, 0x1f, 0x06, 0xb1, 0xb6, 0xfd, 0x1a, 0x69, 0x23,
|
||||
0x30, 0xc4, 0xde, 0x01, 0xd1, 0xf4, 0x58, 0x29, 0x37, 0x1c, 0x7d, 0xd5, 0xbf, 0xff, 0xbd,
|
||||
0xc8, 0xc9, 0xcf, 0x65, 0xbe, 0x7b, 0x78, 0x97, 0x98, 0x67, 0x08, 0xb3, 0x26, 0x57, 0xf7,
|
||||
0xfa, 0x40, 0xad, 0x8e, 0x75, 0xa6, 0x7c, 0xdb, 0x91, 0x8b, 0x51, 0x99, 0xd4, 0x17, 0x7a,
|
||||
0x90, 0x8d, 0xce, 0x63, 0xcb, 0x4e, 0xa0, 0xab, 0x18, 0x3a, 0x5b, 0x50, 0x7f, 0x21, 0x74,
|
||||
0xc1, 0xbb, 0xb8, 0xb7, 0xba, 0x0b, 0x35, 0x95, 0x31, 0x59, 0x9a, 0x4d, 0x04, 0x07, 0x1e,
|
||||
0x5a, 0x76, 0x13, 0xf3, 0x71, 0x83, 0xf0, 0x86, 0x03, 0xa8, 0x39, 0x42, 0xaa, 0x28, 0xe6,
|
||||
0xe4, 0xd8, 0x5d, 0xd3, 0xd0, 0x6e, 0x6f, 0x96, 0xfb, 0x5e, 0xbc, 0x56, 0xc2, 0x5f, 0x85,
|
||||
0x9b, 0xe7, 0xaf, 0xd2, 0x3b, 0x84, 0x6a, 0xa7, 0x53, 0xc5, 0x44, 0x49, 0xa5, 0xf9, 0x36,
|
||||
0x72, 0x3d, 0x2c, 0xd9, 0x1b, 0xa1, 0xf5, 0x4f, 0x93, 0x9d, 0x68, 0x47, 0x41, 0x16, 0xca,
|
||||
0x2a, 0x4c, 0xa3, 0x87, 0xd6, 0xe5, 0x19, 0x2e, 0x77, 0x15, 0x6d, 0x70, 0xc0, 0xdf, 0xb2,
|
||||
0x00, 0x46, 0xed, 0xc6, 0x6c, 0x43, 0x60, 0x92, 0x2d, 0xa9, 0x22, 0x45, 0x8f, 0x34, 0x55,
|
||||
0xae, 0xa4, 0x0a, 0x66, 0x32, 0xe0, 0xdc, 0x02, 0xac, 0xe8, 0x20, 0x8c, 0x89, 0x62, 0x4a,
|
||||
0xfe, 0xee, 0xc3, 0xe3, 0x3c, 0xf1, 0x79, 0x05, 0xe9, 0xf6, 0x27, 0x33, 0xcc, 0xf2, 0x9e,
|
||||
0x11, 0x81, 0x7e, 0x80, 0x10, 0x8a, 0x82, 0x9f, 0x48, 0x0d, 0xd7, 0xb4, 0xfc, 0x2f, 0xb5,
|
||||
0xc7, 0xdd, 0x88, 0x14, 0x6b, 0x2b, 0x54, 0xea, 0x1d, 0x94, 0x5c, 0xb0, 0xef, 0x12, 0x24,
|
||||
0xcd, 0xeb, 0xe1, 0xe2, 0x64, 0x73, 0x3f, 0x0e, 0x52, 0x61, 0x25, 0x3e, 0xf8, 0x0f, 0x4b,
|
||||
0xec,
|
||||
},
|
||||
{
|
||||
0xc0, 0x00, 0x30, 0xf6, 0x02, 0x49, 0x3d, 0x10, 0x6e, 0x20, 0xc9, 0xa6, 0x2f, 0xfe, 0x2c,
|
||||
0x2b, 0x75, 0x2e, 0x45, 0x26, 0xab, 0x48, 0xa9, 0x80, 0xfc, 0x04, 0xcc, 0xd3, 0xb5, 0xba,
|
||||
0xa3, 0x38, 0x31, 0x7d, 0x01, 0xd9, 0xa7, 0x7b, 0x96, 0xb6, 0x63, 0x69, 0x4e, 0xf7, 0xde,
|
||||
0xe0, 0x78, 0xca, 0x50, 0xaa, 0x41, 0x91, 0x65, 0x88, 0xe4, 0x21, 0x85, 0xda, 0x3a, 0x27,
|
||||
0xbe, 0x1c, 0x3e, 0x42, 0x5e, 0x17, 0x52, 0x7f, 0x1f, 0x89, 0x24, 0x6f, 0x8f, 0x5c, 0x67,
|
||||
0x74, 0x0e, 0x12, 0x87, 0x8d, 0xe9, 0x34, 0xed, 0x73, 0xc4, 0xf8, 0x61, 0x5b, 0x05, 0xdf,
|
||||
0x59, 0x4c, 0x97, 0x79, 0x83, 0x18, 0xa4, 0x55, 0x95, 0xeb, 0xbd, 0x53, 0xf5, 0xf1, 0x57,
|
||||
0x66, 0x46, 0x9f, 0xb2, 0x81, 0x09, 0x51, 0x86, 0x22, 0x16, 0xdd, 0x23, 0x93, 0x76, 0x29,
|
||||
0xc2, 0xd7, 0x1d, 0xd4, 0xbf, 0x36, 0x3f, 0xea, 0x4b, 0x11, 0x32, 0xb9, 0x62, 0x54, 0x60,
|
||||
0xd6, 0x6d, 0x43, 0x9a, 0x0d, 0x92, 0x9c, 0xb0, 0xef, 0x58, 0x6c, 0x9d, 0x77, 0x2d, 0x70,
|
||||
0xfa, 0xf3, 0xb3, 0x0b, 0xe2, 0x40, 0x7e, 0xf4, 0x8a, 0xe5, 0x8c, 0x3c, 0x56, 0x71, 0xd1,
|
||||
0x64, 0xe1, 0x82, 0x0a, 0xcb, 0x13, 0x15, 0x90, 0xec, 0x03, 0x99, 0xaf, 0x14, 0x5d, 0x0f,
|
||||
0x33, 0x4a, 0x94, 0xa5, 0xa8, 0x35, 0x1b, 0xe3, 0x6a, 0xc6, 0x28, 0xff, 0x4d, 0xe7, 0x25,
|
||||
0x84, 0xac, 0x08, 0xae, 0xc5, 0xa2, 0x2a, 0xb8, 0x37, 0x0c, 0x7a, 0xa0, 0xc3, 0xce, 0xad,
|
||||
0x06, 0x1a, 0x9e, 0x8b, 0xfb, 0xd5, 0xf0, 0xc1, 0x1e, 0xd0, 0xb4, 0x9b, 0xb1, 0x44, 0xf2,
|
||||
0x47, 0xc7, 0x68, 0xcf, 0x72, 0xbb, 0x4f, 0x5a, 0xf9, 0xdc, 0x6b, 0xdb, 0xd2, 0xe8, 0x7c,
|
||||
0xc8, 0xee, 0x98, 0xa1, 0xe6, 0xd8, 0x39, 0x07, 0x5f, 0xfd, 0x8e, 0x19, 0xb7, 0x3b, 0xbc,
|
||||
0xcd,
|
||||
},
|
||||
{
|
||||
0x7c, 0xe3, 0x81, 0x73, 0xb2, 0x11, 0xbf, 0x6f, 0x20, 0x98, 0xfe, 0x75, 0x96, 0xef, 0x6c,
|
||||
0xda, 0x50, 0xe1, 0x09, 0x72, 0x54, 0x45, 0xba, 0x34, 0x80, 0x5b, 0xed, 0x3e, 0x53, 0x2c,
|
||||
0x87, 0xa4, 0x57, 0xf3, 0x33, 0x3f, 0x3c, 0xb7, 0x67, 0xb4, 0xa3, 0x25, 0x60, 0x4f, 0x07,
|
||||
0x6b, 0x1b, 0x47, 0x15, 0x0f, 0xe4, 0x0a, 0xea, 0xd1, 0x32, 0x78, 0x36, 0x49, 0x8d, 0x4b,
|
||||
0xd2, 0xbc, 0xa5, 0xdc, 0x1d, 0x0d, 0x4d, 0xcd, 0x9a, 0x82, 0x5f, 0xfc, 0x94, 0x65, 0xbe,
|
||||
0xe2, 0xf4, 0xc9, 0x1e, 0x44, 0xcb, 0x9e, 0x0c, 0x64, 0x71, 0x26, 0x63, 0xb3, 0x14, 0xe8,
|
||||
0x40, 0x70, 0x8a, 0x0e, 0x19, 0x42, 0x6d, 0xac, 0x88, 0x10, 0x5c, 0xdf, 0x41, 0xa9, 0xad,
|
||||
0xe5, 0xfb, 0x74, 0xcc, 0xd5, 0x06, 0x8e, 0x59, 0x86, 0xce, 0x1f, 0x3d, 0x76, 0xe0, 0x8f,
|
||||
0xb9, 0x77, 0x27, 0x7b, 0xa6, 0xd8, 0x29, 0xd3, 0xec, 0xb8, 0x13, 0xf7, 0xfa, 0xc3, 0x51,
|
||||
0x6a, 0xde, 0x4a, 0x5a, 0xeb, 0xc2, 0x8b, 0x23, 0x48, 0x92, 0xcf, 0x62, 0xa8, 0x99, 0xf8,
|
||||
0xd0, 0x2e, 0x85, 0x61, 0x43, 0xc8, 0xbd, 0xf0, 0x05, 0x93, 0xca, 0x4e, 0xf1, 0x7d, 0x30,
|
||||
0xfd, 0xc4, 0x69, 0x66, 0x2f, 0x08, 0xb1, 0x52, 0xf9, 0x21, 0xe6, 0x7a, 0x2b, 0xdd, 0x39,
|
||||
0x84, 0xff, 0xc0, 0x91, 0xd6, 0x37, 0xd4, 0x7f, 0x2d, 0x9b, 0x5d, 0xa1, 0x3b, 0x6e, 0xb5,
|
||||
0xc5, 0x46, 0x04, 0xf5, 0x90, 0xee, 0x7e, 0x83, 0x1c, 0x03, 0x56, 0xb6, 0xaa, 0x00, 0x17,
|
||||
0x01, 0x35, 0x55, 0x79, 0x0b, 0x12, 0xbb, 0x1a, 0x31, 0xe7, 0x02, 0x28, 0x16, 0xc1, 0xf6,
|
||||
0xa2, 0xdb, 0x18, 0x9c, 0x89, 0x68, 0x38, 0x97, 0xab, 0xc7, 0x2a, 0xd7, 0x3a, 0xf2, 0xc6,
|
||||
0x24, 0x4c, 0xb0, 0x58, 0xa0, 0x22, 0x5e, 0x9d, 0xd9, 0xa7, 0xe9, 0xae, 0xaf, 0x8c, 0x95,
|
||||
0x9f,
|
||||
},
|
||||
{
|
||||
0x28, 0xb7, 0x20, 0xd7, 0xb0, 0x30, 0xc3, 0x09, 0x19, 0xc0, 0x67, 0xd6, 0x00, 0x3c, 0x7e,
|
||||
0xe7, 0xe9, 0xf4, 0x08, 0x5a, 0xf8, 0xb8, 0x2e, 0x05, 0xa6, 0x25, 0x9e, 0x5c, 0xd8, 0x15,
|
||||
0x0d, 0xe1, 0xf6, 0x11, 0x54, 0x6b, 0xcd, 0x21, 0x46, 0x66, 0x5e, 0x84, 0xad, 0x06, 0x38,
|
||||
0x29, 0x44, 0xc5, 0xa2, 0xce, 0xf1, 0xaa, 0xc1, 0x40, 0x71, 0x86, 0xb5, 0xef, 0xfc, 0x36,
|
||||
0xa8, 0xcb, 0x0a, 0x48, 0x27, 0x45, 0x64, 0xa3, 0xaf, 0x8c, 0xb2, 0xc6, 0x9f, 0x07, 0x89,
|
||||
0xdc, 0x17, 0xd3, 0x49, 0x79, 0xfb, 0xfe, 0x1d, 0xd0, 0xb9, 0x88, 0x43, 0x52, 0xbc, 0x01,
|
||||
0x78, 0x2b, 0x7d, 0x94, 0xc7, 0x0e, 0xde, 0xa5, 0xd5, 0x9b, 0xcc, 0xf7, 0x61, 0x7a, 0xc2,
|
||||
0x74, 0x81, 0x39, 0x03, 0xab, 0x96, 0xa0, 0x37, 0xbd, 0x2d, 0x72, 0x75, 0x3f, 0xc9, 0xd4,
|
||||
0x8e, 0x6f, 0xf9, 0x8d, 0xed, 0x62, 0xdb, 0x1c, 0xdf, 0x04, 0xac, 0x1b, 0x6c, 0x14, 0x4b,
|
||||
0x63, 0xf0, 0xbf, 0xb4, 0x82, 0xec, 0x7b, 0x1a, 0x59, 0x92, 0xd2, 0x10, 0x60, 0xb6, 0x3d,
|
||||
0x5f, 0xe6, 0x80, 0x6e, 0x70, 0xc4, 0xf2, 0x35, 0xd9, 0x7c, 0xee, 0xe5, 0x41, 0xa4, 0x5b,
|
||||
0x50, 0xdd, 0xbb, 0x4c, 0xf3, 0x1f, 0x9d, 0x5d, 0x57, 0x55, 0x51, 0x97, 0xe3, 0x58, 0x42,
|
||||
0x4d, 0x9c, 0x73, 0xba, 0xc8, 0x77, 0x31, 0x69, 0x26, 0xae, 0xea, 0x8a, 0xda, 0x22, 0xb3,
|
||||
0x87, 0x56, 0xfa, 0x93, 0x0b, 0x34, 0x16, 0x33, 0xe8, 0xe4, 0x53, 0xbe, 0xa9, 0xb1, 0x3a,
|
||||
0x3e, 0xf5, 0x90, 0x6a, 0xcf, 0x3b, 0x12, 0xfd, 0x8f, 0x9a, 0xa7, 0x47, 0x91, 0x99, 0xeb,
|
||||
0x0f, 0x24, 0xff, 0x23, 0x18, 0x85, 0x4e, 0x7f, 0x0c, 0xe0, 0xa1, 0xe2, 0xd1, 0x2c, 0x2a,
|
||||
0x4a, 0x02, 0x4f, 0x1e, 0x95, 0x68, 0x8b, 0x98, 0x83, 0x6d, 0x76, 0xca, 0x65, 0x32, 0x13,
|
||||
0x2f,
|
||||
},
|
||||
{
|
||||
0xc3, 0x82, 0x9a, 0xa4, 0xba, 0x81, 0x60, 0x37, 0x34, 0x35, 0xfc, 0x80, 0xa8, 0x51, 0x65,
|
||||
0x67, 0xed, 0x30, 0x5f, 0x10, 0xd3, 0x4a, 0x27, 0x2f, 0x13, 0xb9, 0x2a, 0xd2, 0xcc, 0xe1,
|
||||
0xef, 0xae, 0xeb, 0xbe, 0xf4, 0xbd, 0xcf, 0x43, 0xb3, 0xc5, 0x88, 0x84, 0xb7, 0xdd, 0x39,
|
||||
0x40, 0xce, 0x48, 0x6d, 0x9b, 0x72, 0x61, 0x7e, 0xe7, 0xa1, 0x4e, 0x53, 0x2e, 0x77, 0x3b,
|
||||
0xe2, 0xc9, 0x36, 0x22, 0x1b, 0x6e, 0x73, 0xb1, 0x03, 0xb2, 0x4c, 0x87, 0xa9, 0xd4, 0x4d,
|
||||
0x0f, 0xd8, 0x15, 0x6c, 0xaa, 0x18, 0xf6, 0x49, 0x57, 0x5d, 0xfb, 0x7a, 0x14, 0x94, 0x63,
|
||||
0xa0, 0x11, 0xb0, 0x9e, 0xde, 0x05, 0x46, 0xc8, 0xee, 0x47, 0xdb, 0xdc, 0x24, 0x89, 0x9c,
|
||||
0x91, 0x97, 0x29, 0xe9, 0x7b, 0xc1, 0x07, 0x1e, 0xb8, 0xfd, 0xfe, 0xac, 0xc6, 0x62, 0x98,
|
||||
0x4f, 0xf1, 0x79, 0xe0, 0xe8, 0x6b, 0x78, 0x56, 0xb6, 0x8d, 0x04, 0x50, 0x86, 0xca, 0x6f,
|
||||
0x20, 0xe6, 0xea, 0xe5, 0x76, 0x17, 0x1c, 0x74, 0x7f, 0xbc, 0x0d, 0x2c, 0x85, 0xf7, 0x66,
|
||||
0x96, 0xe4, 0x8b, 0x75, 0x3f, 0x4b, 0xd9, 0x38, 0xaf, 0x7c, 0xda, 0x0b, 0x83, 0x2d, 0x31,
|
||||
0x32, 0xa2, 0xf5, 0x1d, 0x59, 0x41, 0x45, 0xbf, 0x3c, 0x1f, 0xf8, 0xf9, 0x8a, 0xd0, 0x16,
|
||||
0x25, 0x69, 0x12, 0x99, 0x9d, 0x21, 0x95, 0xab, 0x01, 0xa6, 0xd7, 0xb5, 0xc0, 0x7d, 0xff,
|
||||
0x58, 0x0e, 0x3a, 0x92, 0xd1, 0x55, 0xe3, 0x08, 0x9f, 0xd6, 0x3e, 0x52, 0x8e, 0xfa, 0xa3,
|
||||
0xc7, 0x02, 0xcd, 0xdf, 0x8f, 0x64, 0x19, 0x8c, 0xf3, 0xa7, 0x0c, 0x5e, 0x0a, 0x6a, 0x09,
|
||||
0xf0, 0x93, 0x5b, 0x42, 0xc2, 0x06, 0x23, 0xec, 0x71, 0xad, 0xb4, 0xcb, 0xbb, 0x70, 0x28,
|
||||
0xd5, 0x1a, 0x5c, 0x33, 0x68, 0x5a, 0x00, 0x44, 0x90, 0xa5, 0xc4, 0x26, 0x3d, 0x2b, 0xf2,
|
||||
0x54,
|
||||
},
|
||||
{
|
||||
0x96, 0xad, 0xda, 0x1f, 0xed, 0x33, 0xe1, 0x81, 0x69, 0x08, 0x0d, 0x0a, 0xdb, 0x35, 0x77,
|
||||
0x9a, 0x64, 0xd1, 0xfc, 0x78, 0xaa, 0x1b, 0xd0, 0x67, 0xa0, 0xdd, 0xfa, 0x6c, 0x63, 0x71,
|
||||
0x05, 0x84, 0x17, 0x6a, 0x89, 0x4f, 0x66, 0x7f, 0xc6, 0x50, 0x55, 0x92, 0x6f, 0xbd, 0xe7,
|
||||
0xd2, 0x40, 0x72, 0x8d, 0xbb, 0xec, 0x06, 0x42, 0x8a, 0xe4, 0x88, 0x9d, 0x7e, 0x7a, 0x82,
|
||||
0x27, 0x13, 0x41, 0x1a, 0xaf, 0xc8, 0xa4, 0x76, 0xb4, 0xc2, 0xfe, 0x6d, 0x1c, 0xd9, 0x61,
|
||||
0x30, 0xb3, 0x7c, 0xea, 0xf7, 0x29, 0x0f, 0xf2, 0x3b, 0x51, 0xc1, 0xde, 0x5f, 0xe5, 0x2a,
|
||||
0x2f, 0x99, 0x0b, 0x5d, 0xa3, 0x2b, 0x4a, 0xab, 0x95, 0xa5, 0xd3, 0x58, 0x56, 0xee, 0x28,
|
||||
0x31, 0x00, 0xcc, 0x15, 0x46, 0xca, 0xe6, 0x86, 0x38, 0x3c, 0x65, 0xf5, 0xe3, 0x9f, 0xd6,
|
||||
0x5b, 0x09, 0x49, 0x83, 0x70, 0x2d, 0x53, 0xa9, 0x7d, 0xe2, 0xc4, 0xac, 0x8e, 0x5e, 0xb8,
|
||||
0x25, 0xf4, 0xb9, 0x57, 0xf3, 0xf1, 0x68, 0x47, 0xb2, 0xa2, 0x59, 0x20, 0xce, 0x34, 0x79,
|
||||
0x5c, 0x90, 0x0e, 0x1e, 0xbe, 0xd5, 0x22, 0x23, 0xb1, 0xc9, 0x18, 0x62, 0x16, 0x2e, 0x91,
|
||||
0x3e, 0x07, 0x8f, 0xd8, 0x3f, 0x93, 0x3d, 0xd4, 0x9b, 0xdf, 0x85, 0x21, 0xfb, 0x11, 0x74,
|
||||
0x97, 0xc7, 0xd7, 0xdc, 0x4c, 0x19, 0x45, 0x98, 0xe9, 0x43, 0x02, 0x4b, 0xbc, 0xc3, 0x04,
|
||||
0x9c, 0x6b, 0xf0, 0x75, 0x52, 0xa7, 0x26, 0xf6, 0xc5, 0xba, 0xcf, 0xb0, 0xb7, 0xae, 0x5a,
|
||||
0xa1, 0xbf, 0x03, 0x8b, 0x80, 0x12, 0x6e, 0x0c, 0xeb, 0xf9, 0xc0, 0x44, 0x24, 0xef, 0x10,
|
||||
0xf8, 0xa8, 0x8c, 0xe8, 0x7b, 0xff, 0x9e, 0x2c, 0xcd, 0x60, 0x36, 0x87, 0xb5, 0x94, 0xa6,
|
||||
0x54, 0x73, 0x3a, 0x14, 0x4e, 0x01, 0x1d, 0xb6, 0xfd, 0x37, 0x48, 0x4d, 0x39, 0xcb, 0xe0,
|
||||
0x32,
|
||||
},
|
||||
}};
|
||||
|
||||
constexpr uint8_t kAnswers[7][6] = {
|
||||
{0xa8, 0x77, 0xa6, 0xe0, 0xf7, 0x43}, {0x5a, 0x35, 0x85, 0xe2, 0x72, 0x97},
|
||||
{0x8f, 0xb7, 0x1a, 0x62, 0x87, 0x38}, {0x0d, 0x67, 0xc7, 0xbe, 0x4f, 0x3e},
|
||||
{0x20, 0x76, 0x37, 0x8f, 0x68, 0xb7}, {0xa9, 0x26, 0x3f, 0x2b, 0x10, 0xe3},
|
||||
{0x30, 0x7e, 0x90, 0x0e, 0x85, 0x0a},
|
||||
};
|
||||
|
||||
using Rand = std::array<uint8_t, 10>;
|
||||
using Key = std::array<uint8_t, 6>;
|
||||
|
||||
Key KeyFor(const Rand& rand, size_t idx) {
|
||||
const uint8_t* ans = kAnswers[idx];
|
||||
Rand t{};
|
||||
for (size_t i = 0; i < t.size(); ++i) t[i] = kKeygenSbox[rand[i]];
|
||||
return {
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[0] ^ t[5], t[2]) - t[9]) ^ t[4]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[1] ^ t[1], t[0]) - t[5]) ^ t[7]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[2] ^ t[6], t[8]) - t[2]) ^ t[0]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[3] ^ t[4], t[7]) - t[3]) ^ t[2]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[4] ^ t[1], t[6]) - t[3]) ^ t[4]),
|
||||
static_cast<uint8_t>((std::rotr<uint8_t>(ans[5] ^ t[7], t[8]) - t[5]) ^ t[9]),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
std::optional<Tables> FromKey(const std::array<uint8_t, 16>& keyData) {
|
||||
using namespace Detail;
|
||||
Rand rand{};
|
||||
std::copy_n(keyData.begin(), rand.size(), rand.rbegin());
|
||||
Key key{};
|
||||
std::copy_n(keyData.begin() + rand.size(), key.size(), key.rbegin());
|
||||
// The key was derived from "rand" and an index the extension is not told.
|
||||
for (size_t idx = 0; idx < 7; ++idx) {
|
||||
if (KeyFor(rand, idx) != key) continue;
|
||||
const SBox& a = kSboxes[idx];
|
||||
const SBox& b = kSboxes[idx + 1];
|
||||
Tables t;
|
||||
t.ft = {static_cast<uint8_t>(a[key[4]] ^ b[rand[3]]), static_cast<uint8_t>(a[key[2]] ^ b[rand[5]]),
|
||||
static_cast<uint8_t>(a[key[5]] ^ b[rand[7]]), static_cast<uint8_t>(a[key[0]] ^ b[rand[2]]),
|
||||
static_cast<uint8_t>(a[key[1]] ^ b[rand[4]]), static_cast<uint8_t>(a[key[3]] ^ b[rand[9]]),
|
||||
static_cast<uint8_t>(a[rand[0]] ^ b[rand[6]]), static_cast<uint8_t>(a[rand[1]] ^ b[rand[8]])};
|
||||
t.sb = {static_cast<uint8_t>(a[key[0]] ^ b[rand[1]]), static_cast<uint8_t>(a[key[5]] ^ b[rand[4]]),
|
||||
static_cast<uint8_t>(a[key[3]] ^ b[rand[0]]), static_cast<uint8_t>(a[key[2]] ^ b[rand[9]]),
|
||||
static_cast<uint8_t>(a[key[4]] ^ b[rand[7]]), static_cast<uint8_t>(a[key[1]] ^ b[rand[8]]),
|
||||
static_cast<uint8_t>(a[rand[3]] ^ b[rand[5]]), static_cast<uint8_t>(a[rand[2]] ^ b[rand[6]])};
|
||||
return t;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Tables::Encrypt(uint8_t* data, uint32_t address, size_t length) const {
|
||||
for (size_t i = 0; i < length; ++i, ++address) {
|
||||
data[i] = static_cast<uint8_t>((data[i] - ft[address % 8]) ^ sb[address % 8]);
|
||||
}
|
||||
}
|
||||
|
||||
void Tables::Decrypt(uint8_t* data, uint32_t address, size_t length) const {
|
||||
for (size_t i = 0; i < length; ++i, ++address) {
|
||||
data[i] = static_cast<uint8_t>((data[i] ^ sb[address % 8]) + ft[address % 8]);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Wiimote::ExtCrypto
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "wiimote_extension.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Wiimote {
|
||||
namespace ExtensionDetail {
|
||||
|
||||
constexpr uint8_t kReportStatus = 0x20;
|
||||
constexpr uint8_t kReportReadReply = 0x21;
|
||||
constexpr uint8_t kReportWriteMemory = 0x16;
|
||||
constexpr uint8_t kReportReadMemory = 0x17;
|
||||
constexpr uint32_t kExtensionRegisters = 0xA40000;
|
||||
constexpr uint8_t kRegisterCalibration = 0x20;
|
||||
constexpr uint8_t kRegisterKey = 0x40;
|
||||
constexpr uint8_t kRegisterEncryption = 0xF0;
|
||||
constexpr uint8_t kRegisterIdentifier = 0xFA;
|
||||
constexpr uint8_t kEncryptionOn = 0xAA;
|
||||
constexpr uint8_t kEncryptionOff = 0x55;
|
||||
|
||||
struct ExtensionSpan {
|
||||
size_t offset;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
// Where the extension bytes sit in each data report.
|
||||
std::optional<ExtensionSpan> SpanFor(uint8_t id) {
|
||||
switch (id) {
|
||||
case 0x32: return ExtensionSpan{3, 8};
|
||||
case 0x34: return ExtensionSpan{3, 19};
|
||||
case 0x35: return ExtensionSpan{6, 16};
|
||||
case 0x36: return ExtensionSpan{13, 9};
|
||||
case 0x37: return ExtensionSpan{16, 6};
|
||||
case 0x3D: return ExtensionSpan{1, 21};
|
||||
default: return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool InExtensionSpace(uint32_t address) {
|
||||
return (address & 0xFF0000) == kExtensionRegisters;
|
||||
}
|
||||
|
||||
} // namespace ExtensionDetail
|
||||
|
||||
void ExtensionState::Reset() {
|
||||
m_type = Type::Unknown;
|
||||
m_format = 0;
|
||||
m_encrypted = false;
|
||||
m_tables.reset();
|
||||
m_haveCalibration = false;
|
||||
}
|
||||
|
||||
void ExtensionState::ObserveOutput(const uint8_t* report, size_t size) {
|
||||
using namespace ExtensionDetail;
|
||||
if (size < 6) return;
|
||||
const bool registerSpace = (report[1] & 0x04) != 0;
|
||||
const uint32_t address = (report[2] << 16) | (report[3] << 8) | report[4];
|
||||
if (report[0] == kReportReadMemory) {
|
||||
m_readAddress = registerSpace ? address : 0;
|
||||
return;
|
||||
}
|
||||
if (!registerSpace || report[0] != kReportWriteMemory || !InExtensionSpace(address)) return;
|
||||
const size_t length = std::min<size_t>(report[5], size - 6);
|
||||
bool keyChanged = false;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const size_t reg = (address & 0xFF) + i;
|
||||
const uint8_t value = report[6 + i];
|
||||
if (reg >= kRegisterKey && reg < kRegisterKey + m_key.size()) {
|
||||
m_key[reg - kRegisterKey] = value;
|
||||
keyChanged = true;
|
||||
} else if (reg == kRegisterEncryption) {
|
||||
if (value == kEncryptionOn) m_encrypted = true;
|
||||
if (value == kEncryptionOff) Reset();
|
||||
}
|
||||
}
|
||||
if (keyChanged) {
|
||||
m_tables = ExtCrypto::FromKey(m_key);
|
||||
}
|
||||
}
|
||||
|
||||
void ExtensionState::ObserveInput(const uint8_t* report, size_t size) {
|
||||
using namespace ExtensionDetail;
|
||||
if (size >= 4 && report[0] == kReportStatus) {
|
||||
if ((report[3] & 0x02) == 0) Reset();
|
||||
return;
|
||||
}
|
||||
if (size < 7 || report[0] != kReportReadReply || (report[3] & 0x0F) != 0 || !InExtensionSpace(m_readAddress)) {
|
||||
return;
|
||||
}
|
||||
const size_t length = std::min<size_t>((report[3] >> 4) + 1, size - 6);
|
||||
const uint32_t first = ((report[4] << 8) | report[5]) & 0xFF;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const uint32_t reg = first + static_cast<uint32_t>(i);
|
||||
uint8_t value = report[6 + i];
|
||||
if (reg >= kRegisterIdentifier) {
|
||||
// The identifier is read before the game turns encryption on.
|
||||
if (reg == 0xFE) m_format = value;
|
||||
if (reg == 0xFF) m_type = value == 0x00 ? Type::Nunchuk : value == 0x01 ? Type::Classic : Type::Unknown;
|
||||
} else if (reg >= kRegisterCalibration && reg < kRegisterCalibration + m_calibration.size()) {
|
||||
if (m_encrypted) {
|
||||
if (!m_tables) continue;
|
||||
m_tables->Decrypt(&value, reg, 1);
|
||||
}
|
||||
m_calibration[reg - kRegisterCalibration] = value;
|
||||
m_haveCalibration = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ExtensionState::Calibration(size_t index, uint8_t fallback) const {
|
||||
return m_haveCalibration ? m_calibration[index] : fallback;
|
||||
}
|
||||
|
||||
void ExtensionState::Neutralize(std::vector<uint8_t>& report) const {
|
||||
using namespace ExtensionDetail;
|
||||
if (report.empty() || m_type == Type::Unknown || (m_encrypted && !m_tables)) return;
|
||||
const auto span = SpanFor(report[0]);
|
||||
if (!span || report.size() < span->offset + 6) return;
|
||||
uint8_t* data = report.data() + span->offset;
|
||||
std::array<uint8_t, 6> plain{};
|
||||
std::copy_n(data, plain.size(), plain.begin());
|
||||
if (m_encrypted) m_tables->Decrypt(plain.data(), 0, plain.size());
|
||||
|
||||
if (m_type == Type::Nunchuk) {
|
||||
plain[0] = Calibration(10, 0x80);
|
||||
plain[1] = Calibration(13, 0x80);
|
||||
plain[5] |= 0x03; // C and Z are active low
|
||||
} else if (m_format == 0x01) {
|
||||
const uint8_t lx = Calibration(2, 0x80) >> 2;
|
||||
const uint8_t ly = Calibration(5, 0x80) >> 2;
|
||||
const uint8_t rx = Calibration(8, 0x80) >> 3;
|
||||
const uint8_t ry = Calibration(11, 0x80) >> 3;
|
||||
plain[0] = static_cast<uint8_t>(((rx >> 3) & 0x03) << 6 | (lx & 0x3F));
|
||||
plain[1] = static_cast<uint8_t>(((rx >> 1) & 0x03) << 6 | (ly & 0x3F));
|
||||
plain[2] = static_cast<uint8_t>((rx & 0x01) << 7 | (ry & 0x1F));
|
||||
plain[3] = 0x00;
|
||||
plain[4] = 0xFF; // buttons are active low
|
||||
plain[5] = 0xFF;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_encrypted) m_tables->Encrypt(plain.data(), 0, plain.size());
|
||||
std::copy(plain.begin(), plain.end(), data);
|
||||
}
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,828 @@
|
||||
#include "wiimote_host.h"
|
||||
|
||||
#include "input_bindings.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "wiimote_extension.h"
|
||||
#include "wiimote_platform.h"
|
||||
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
#include <SDL3/SDL_hints.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
extern "C" bool PAD_HLE_RumbleEnabled();
|
||||
|
||||
namespace Wiimote {
|
||||
namespace HostDetail {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Report = std::vector<uint8_t>;
|
||||
|
||||
constexpr uint16_t kNintendoVendor = 0x057E;
|
||||
constexpr uint16_t kProductRemote = 0x0306;
|
||||
constexpr uint16_t kProductRemotePlus = 0x0330;
|
||||
|
||||
constexpr auto kScanInterval = std::chrono::milliseconds(500);
|
||||
constexpr auto kEnumerateInterval = std::chrono::seconds(1);
|
||||
constexpr auto kProbeCooldown = std::chrono::seconds(3);
|
||||
constexpr auto kKeepAlive = std::chrono::seconds(1);
|
||||
constexpr auto kBlinkInterval = std::chrono::milliseconds(500);
|
||||
constexpr auto kPairingWindow = std::chrono::seconds(60);
|
||||
constexpr auto kGuestSearchTail = std::chrono::seconds(10);
|
||||
constexpr auto kAutoPairPause = std::chrono::seconds(4);
|
||||
constexpr int kReadTimeoutMs = 200;
|
||||
constexpr size_t kInputQueueLimit = 32;
|
||||
constexpr uint16_t kButtonMask = 0x9F1F;
|
||||
constexpr uint8_t kReportSpeakerData = 0x18;
|
||||
|
||||
enum class Phase : uint8_t {
|
||||
Waiting, // connected to the PC, offered to the game
|
||||
Linked, // the game's HID channels are open
|
||||
Asleep, // the game let it go and it could not be switched off
|
||||
PoweringOff,
|
||||
};
|
||||
|
||||
// Input report sizes including the id: Windows always reads the longest report.
|
||||
size_t ReportSize(uint8_t id, size_t read) {
|
||||
size_t size = 0;
|
||||
switch (id) {
|
||||
case 0x20: size = 7; break;
|
||||
case 0x21: size = 22; break;
|
||||
case 0x22: size = 5; break;
|
||||
case 0x30: size = 3; break;
|
||||
case 0x31: size = 6; break;
|
||||
case 0x32: size = 11; break;
|
||||
case 0x33: size = 18; break;
|
||||
case 0x34: case 0x35: case 0x36: case 0x37: case 0x3D: case 0x3E: case 0x3F: size = 22; break;
|
||||
default: return 0;
|
||||
}
|
||||
return size <= read ? size : 0;
|
||||
}
|
||||
|
||||
uint16_t Buttons(const Report& report) {
|
||||
if (report.size() < 3 || report[0] < 0x20 || report[0] == 0x3D) return 0;
|
||||
return static_cast<uint16_t>(((report[1] << 8) | report[2]) & kButtonMask);
|
||||
}
|
||||
|
||||
// The game's own battery bars (WPAD's status report thresholds).
|
||||
int BatteryBars(uint8_t level) {
|
||||
if (level < 0x03) return 0;
|
||||
if (level < 0x33) return 1;
|
||||
if (level < 0x44) return 2;
|
||||
if (level < 0x55) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
struct Remote {
|
||||
std::string path;
|
||||
std::string address;
|
||||
std::string name;
|
||||
bool plus = false;
|
||||
SDL_hid_device* device = nullptr;
|
||||
std::thread reader;
|
||||
std::thread writer;
|
||||
std::atomic<bool> alive{true};
|
||||
std::atomic<Phase> phase{Phase::Waiting};
|
||||
std::atomic<bool> wake{true};
|
||||
std::atomic<int> battery{-1};
|
||||
std::atomic<int> extension{-1}; // -1 none, else the extension id byte
|
||||
std::atomic<bool> extensionAttached{false};
|
||||
// Rumble bit of the game's last report, before the vibration setting.
|
||||
std::atomic<bool> gameRumble{false};
|
||||
|
||||
std::mutex writeMutex;
|
||||
std::condition_variable writeCv;
|
||||
std::deque<Report> writes;
|
||||
std::mutex inputMutex;
|
||||
std::deque<Report> input;
|
||||
// Emulation thread only.
|
||||
ExtensionState extensionState;
|
||||
};
|
||||
|
||||
class Manager final : public Bluetooth::RemoteHost {
|
||||
public:
|
||||
void Start();
|
||||
void Stop();
|
||||
|
||||
bool IsAttached(size_t slot) const override;
|
||||
bool ConsumeWake(size_t slot) override;
|
||||
bool PopInputReport(size_t slot, std::vector<uint8_t>& report) override;
|
||||
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override;
|
||||
void OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) override;
|
||||
void OnGuestDisconnect(size_t slot) override;
|
||||
void OnGuestInquiry(bool active) override;
|
||||
const char* DeviceName(size_t slot) const override;
|
||||
|
||||
SlotInfo Slot(size_t slot);
|
||||
PairingInfo Pairing();
|
||||
void StartPairing();
|
||||
void Disconnect(size_t slot);
|
||||
size_t Waiting();
|
||||
|
||||
private:
|
||||
std::shared_ptr<Remote> At(size_t slot) const;
|
||||
void ScanLoop();
|
||||
void DiscoveryLoop();
|
||||
void Enumerate();
|
||||
void Reap();
|
||||
void AssignWaiting();
|
||||
void Open(const SDL_hid_device_info& info);
|
||||
void SetPhase(const std::shared_ptr<Remote>& remote, Phase phase);
|
||||
void PowerOff(const std::shared_ptr<Remote>& remote);
|
||||
static void ReadLoop(std::shared_ptr<Remote> remote);
|
||||
static void WriteLoop(std::shared_ptr<Remote> remote);
|
||||
static void Queue(Remote& remote, Report report);
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::array<std::shared_ptr<Remote>, Bluetooth::kRemoteSlots> m_slots;
|
||||
std::array<Bluetooth::LinkState, Bluetooth::kRemoteSlots> m_links{};
|
||||
std::vector<std::shared_ptr<Remote>> m_waiting;
|
||||
std::map<std::string, size_t> m_lastSlot;
|
||||
std::map<std::string, Clock::time_point> m_cooldown;
|
||||
std::set<std::string> m_refusedLogged;
|
||||
// Remotes the OS knows but that were off when last seen, by path.
|
||||
std::map<std::string, std::string> m_dormant;
|
||||
Clock::time_point m_lastEnumerate{};
|
||||
uint32_t m_deviceChanges = 0;
|
||||
|
||||
std::atomic<bool> m_running{false};
|
||||
std::thread m_scanner;
|
||||
std::mutex m_scanMutex;
|
||||
std::condition_variable m_scanCv;
|
||||
|
||||
std::thread m_discovery;
|
||||
std::mutex m_discoveryMutex;
|
||||
std::condition_variable m_discoveryCv;
|
||||
std::atomic<bool> m_cancelDiscovery{false};
|
||||
std::atomic<bool> m_searching{false};
|
||||
std::atomic<bool> m_guestSearching{false};
|
||||
Clock::time_point m_guestSearchUntil{};
|
||||
Clock::time_point m_pairingUntil{};
|
||||
std::string m_pairingStatus;
|
||||
};
|
||||
|
||||
// Never destroyed: its threads may outlive static destruction at exit.
|
||||
Manager& TheManager() {
|
||||
static Manager* manager = new Manager();
|
||||
return *manager;
|
||||
}
|
||||
|
||||
std::shared_ptr<Remote> Manager::At(size_t slot) const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return slot < m_slots.size() ? m_slots[slot] : nullptr;
|
||||
}
|
||||
|
||||
bool Manager::IsAttached(size_t slot) const {
|
||||
const auto remote = At(slot);
|
||||
if (!remote || !remote->alive) return false;
|
||||
const Phase phase = remote->phase;
|
||||
return phase == Phase::Waiting || phase == Phase::Linked;
|
||||
}
|
||||
|
||||
bool Manager::ConsumeWake(size_t slot) {
|
||||
const auto remote = At(slot);
|
||||
return remote && remote->phase == Phase::Waiting && remote->wake.exchange(false);
|
||||
}
|
||||
|
||||
bool Manager::PopInputReport(size_t slot, std::vector<uint8_t>& report) {
|
||||
const auto remote = At(slot);
|
||||
if (!remote) return false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote->inputMutex);
|
||||
if (remote->input.empty()) return false;
|
||||
report = std::move(remote->input.front());
|
||||
remote->input.pop_front();
|
||||
}
|
||||
remote->extensionState.ObserveInput(report.data(), report.size());
|
||||
if (InputBindings::InputBlocked()) {
|
||||
// The overlay owns input: the game sees the controller at rest.
|
||||
if (Buttons(report) != 0) {
|
||||
report[1] &= static_cast<uint8_t>(~(kButtonMask >> 8));
|
||||
report[2] &= static_cast<uint8_t>(~kButtonMask);
|
||||
}
|
||||
remote->extensionState.Neutralize(report);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Manager::Queue(Remote& remote, Report report) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote.writeMutex);
|
||||
remote.writes.push_back(std::move(report));
|
||||
}
|
||||
remote.writeCv.notify_one();
|
||||
}
|
||||
|
||||
void Manager::SendOutputReport(size_t slot, const uint8_t* report, size_t size) {
|
||||
const auto remote = At(slot);
|
||||
if (!remote || remote->phase != Phase::Linked || size == 0) return;
|
||||
remote->extensionState.ObserveOutput(report, size);
|
||||
const bool rumble = size >= 2 && (report[1] & 0x01) != 0;
|
||||
const bool rumbleChanged = size >= 2 && remote->gameRumble.exchange(rumble) != rumble;
|
||||
// PC Bluetooth cannot feed the speaker smoothly enough to sound right, but
|
||||
// WPAD may carry a rumble change only on speaker data.
|
||||
if (report[0] == kReportSpeakerData) {
|
||||
if (rumbleChanged) Queue(*remote, {0x10, static_cast<uint8_t>(rumble ? 0x01 : 0x00)});
|
||||
return;
|
||||
}
|
||||
Queue(*remote, Report(report, report + size));
|
||||
}
|
||||
|
||||
void Manager::OnLinkStateChanged(size_t slot, Bluetooth::LinkState state) {
|
||||
std::shared_ptr<Remote> remote;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_links[slot] = state;
|
||||
remote = m_slots[slot];
|
||||
}
|
||||
if (!remote) return;
|
||||
if (state == Bluetooth::LinkState::Ready) {
|
||||
if (remote->phase == Phase::PoweringOff) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote->inputMutex);
|
||||
remote->input.clear();
|
||||
}
|
||||
SetPhase(remote, Phase::Linked);
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (slot + 1) << " connected to the game" << std::endl;
|
||||
} else if (state == Bluetooth::LinkState::Down && remote->phase == Phase::Linked) {
|
||||
// Lost without the game asking (e.g. the controller was reset): offer it again.
|
||||
SetPhase(remote, Phase::Waiting);
|
||||
remote->wake = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::OnGuestDisconnect(size_t slot) {
|
||||
const auto remote = At(slot);
|
||||
if (!remote) return;
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (slot + 1) << " disconnected by the game" << std::endl;
|
||||
PowerOff(remote);
|
||||
}
|
||||
|
||||
void Manager::PowerOff(const std::shared_ptr<Remote>& remote) {
|
||||
remote->wake = false;
|
||||
if (Platform::Query().powerOff && !remote->address.empty()) {
|
||||
SetPhase(remote, Phase::PoweringOff);
|
||||
std::thread([remote] {
|
||||
const bool requested = Platform::PowerOff(remote->address);
|
||||
// The HID device goes away once the remote is off; if it does not,
|
||||
// keep it asleep so a button press brings it back.
|
||||
const auto deadline = Clock::now() + std::chrono::seconds(5);
|
||||
while (requested && remote->alive && Clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
if (remote->alive && remote->phase == Phase::PoweringOff) {
|
||||
remote->phase = Phase::Asleep;
|
||||
remote->writeCv.notify_one();
|
||||
}
|
||||
}).detach();
|
||||
} else {
|
||||
SetPhase(remote, Phase::Asleep);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::SetPhase(const std::shared_ptr<Remote>& remote, Phase phase) {
|
||||
remote->phase = phase;
|
||||
remote->gameRumble = false;
|
||||
if (phase != Phase::Linked) {
|
||||
std::lock_guard<std::mutex> lock(remote->writeMutex);
|
||||
remote->writes.clear();
|
||||
}
|
||||
remote->writeCv.notify_one();
|
||||
}
|
||||
|
||||
void Manager::OnGuestInquiry(bool active) {
|
||||
if (active) {
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
m_guestSearching = true;
|
||||
} else {
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
m_guestSearching = false;
|
||||
m_guestSearchUntil = Clock::now() + kGuestSearchTail;
|
||||
}
|
||||
m_discoveryCv.notify_all();
|
||||
}
|
||||
|
||||
const char* Manager::DeviceName(size_t slot) const {
|
||||
const auto remote = At(slot);
|
||||
return remote && remote->plus ? "Nintendo RVL-CNT-01-TR" : "Nintendo RVL-CNT-01";
|
||||
}
|
||||
|
||||
void Manager::Start() {
|
||||
if (m_running.exchange(true)) return;
|
||||
if (SDL_hid_init() != 0) {
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remotes: HID access unavailable: " << SDL_GetError() << std::endl;
|
||||
m_running = false;
|
||||
return;
|
||||
}
|
||||
m_scanner = std::thread([this] { ScanLoop(); });
|
||||
if (Platform::Query().pairing) {
|
||||
m_discovery = std::thread([this] { DiscoveryLoop(); });
|
||||
}
|
||||
const std::string notice = Platform::Notice();
|
||||
if (!notice.empty()) {
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remotes: " << notice << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Stop() {
|
||||
if (!m_running.exchange(false)) return;
|
||||
m_cancelDiscovery = true;
|
||||
m_scanCv.notify_all();
|
||||
m_discoveryCv.notify_all();
|
||||
if (m_scanner.joinable()) m_scanner.join();
|
||||
// An inquiry in progress cannot be interrupted; let it finish on its own.
|
||||
if (m_discovery.joinable()) m_discovery.detach();
|
||||
|
||||
std::vector<std::shared_ptr<Remote>> remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto& remote : m_slots) {
|
||||
if (remote) remotes.push_back(remote);
|
||||
remote.reset();
|
||||
}
|
||||
remotes.insert(remotes.end(), m_waiting.begin(), m_waiting.end());
|
||||
m_waiting.clear();
|
||||
}
|
||||
for (const auto& remote : remotes) {
|
||||
remote->alive = false;
|
||||
remote->writeCv.notify_all();
|
||||
}
|
||||
// A console switches its remotes off when it shuts down.
|
||||
for (const auto& remote : remotes) {
|
||||
if (remote->writer.joinable()) remote->writer.join();
|
||||
if (remote->reader.joinable()) remote->reader.join();
|
||||
if (remote->device != nullptr) {
|
||||
const uint8_t rumbleOff[] = {0x10, 0x00};
|
||||
const uint8_t ledsOff[] = {0x11, 0x00};
|
||||
SDL_hid_write(remote->device, rumbleOff, sizeof(rumbleOff));
|
||||
SDL_hid_write(remote->device, ledsOff, sizeof(ledsOff));
|
||||
SDL_hid_close(remote->device);
|
||||
remote->device = nullptr;
|
||||
}
|
||||
if (Platform::Query().powerOff && !remote->address.empty()) {
|
||||
Platform::PowerOff(remote->address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::ScanLoop() {
|
||||
while (m_running) {
|
||||
Reap();
|
||||
Enumerate();
|
||||
AssignWaiting();
|
||||
std::unique_lock<std::mutex> lock(m_scanMutex);
|
||||
m_scanCv.wait_for(lock, kScanInterval, [this] { return !m_running; });
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Reap() {
|
||||
std::vector<std::shared_ptr<Remote>> dead;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto& remote : m_slots) {
|
||||
if (remote && !remote->alive) {
|
||||
dead.push_back(remote);
|
||||
remote.reset();
|
||||
}
|
||||
}
|
||||
for (auto it = m_waiting.begin(); it != m_waiting.end();) {
|
||||
if (!(*it)->alive) {
|
||||
dead.push_back(*it);
|
||||
it = m_waiting.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& remote : dead) {
|
||||
remote->writeCv.notify_all();
|
||||
if (remote->writer.joinable()) remote->writer.join();
|
||||
if (remote->reader.joinable()) remote->reader.join();
|
||||
if (remote->device != nullptr) SDL_hid_close(remote->device);
|
||||
remote->device = nullptr;
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << (remote->address.empty() ? remote->path : remote->address)
|
||||
<< " disconnected from the PC" << std::endl;
|
||||
m_cooldown[remote->path] = Clock::now() + std::chrono::seconds(1);
|
||||
if (!remote->address.empty()) m_dormant[remote->path] = remote->address;
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Enumerate() {
|
||||
const Uint32 changes = SDL_hid_device_change_count();
|
||||
const auto now = Clock::now();
|
||||
bool fullSlots = false;
|
||||
bool anyRemote = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
fullSlots = std::all_of(m_slots.begin(), m_slots.end(), [](const auto& remote) { return remote != nullptr; });
|
||||
anyRemote = !m_waiting.empty() ||
|
||||
std::any_of(m_slots.begin(), m_slots.end(), [](const auto& remote) { return remote != nullptr; });
|
||||
}
|
||||
bool pairing = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
pairing = m_guestSearching || now < m_guestSearchUntil || now < m_pairingUntil;
|
||||
}
|
||||
if (fullSlots) return;
|
||||
if (changes == m_deviceChanges) {
|
||||
if (now - m_lastEnumerate < kEnumerateInterval) return;
|
||||
// Once a remote is connected, skip HID enumeration unless pairing or a known remote came back.
|
||||
if (anyRemote && !pairing &&
|
||||
std::none_of(m_dormant.begin(), m_dormant.end(),
|
||||
[](const auto& entry) { return Platform::MaybeConnected(entry.second); })) {
|
||||
m_lastEnumerate = now;
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_deviceChanges = changes;
|
||||
m_lastEnumerate = now;
|
||||
|
||||
std::set<std::string> open;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (const auto& remote : m_slots) {
|
||||
if (remote) open.insert(remote->path);
|
||||
}
|
||||
for (const auto& remote : m_waiting) open.insert(remote->path);
|
||||
}
|
||||
std::set<std::string> listed;
|
||||
SDL_hid_device_info* list = SDL_hid_enumerate(kNintendoVendor, 0);
|
||||
for (const SDL_hid_device_info* info = list; info != nullptr; info = info->next) {
|
||||
if (info->path == nullptr || (info->product_id != kProductRemote && info->product_id != kProductRemotePlus)) {
|
||||
continue;
|
||||
}
|
||||
listed.insert(info->path);
|
||||
if (open.count(info->path) != 0) continue;
|
||||
const auto cooldown = m_cooldown.find(info->path);
|
||||
if (cooldown != m_cooldown.end() && now < cooldown->second) continue;
|
||||
open.insert(info->path);
|
||||
Open(*info);
|
||||
}
|
||||
SDL_hid_free_enumeration(list);
|
||||
for (auto it = m_dormant.begin(); it != m_dormant.end();) {
|
||||
it = listed.count(it->first) != 0 ? std::next(it) : m_dormant.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::Open(const SDL_hid_device_info& info) {
|
||||
const std::string path = info.path;
|
||||
const std::string address = Platform::AddressFor(info);
|
||||
const std::string name = Platform::NameFor(address, info);
|
||||
// Wii U Pro Controllers share the Wii Remote Plus id; balance boards are not used by the game.
|
||||
if (name.find("-UC") != std::string::npos || name.find("RVL-WBC") != std::string::npos) {
|
||||
m_cooldown[path] = Clock::time_point::max();
|
||||
return;
|
||||
}
|
||||
if (!address.empty() && !Platform::MaybeConnected(address)) {
|
||||
// Known to the OS but switched off: check again soon, a button press
|
||||
// reconnects it.
|
||||
m_cooldown[path] = Clock::now() + kScanInterval;
|
||||
m_dormant[path] = address;
|
||||
return;
|
||||
}
|
||||
if (!Platform::Usable(info)) {
|
||||
if (m_refusedLogged.insert(path).second) {
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << " is held by the system driver: " << Platform::Notice()
|
||||
<< std::endl;
|
||||
}
|
||||
m_cooldown[path] = Clock::now() + std::chrono::seconds(10);
|
||||
return;
|
||||
}
|
||||
SDL_hid_device* device = SDL_hid_open_path(path.c_str());
|
||||
if (device == nullptr) {
|
||||
m_cooldown[path] = Clock::now() + kProbeCooldown;
|
||||
return;
|
||||
}
|
||||
// A remote the OS remembers but that is switched off still has a HID device;
|
||||
// only a write tells.
|
||||
const uint8_t status[] = {0x15, 0x00};
|
||||
if (SDL_hid_write(device, status, sizeof(status)) < 0) {
|
||||
SDL_hid_close(device);
|
||||
m_cooldown[path] = Clock::now() + kProbeCooldown;
|
||||
if (!address.empty()) m_dormant[path] = address;
|
||||
return;
|
||||
}
|
||||
m_dormant.erase(path);
|
||||
|
||||
auto remote = std::make_shared<Remote>();
|
||||
remote->path = path;
|
||||
remote->address = address;
|
||||
remote->name = name;
|
||||
remote->plus = info.product_id == kProductRemotePlus;
|
||||
remote->device = device;
|
||||
remote->reader = std::thread(ReadLoop, remote);
|
||||
remote->writer = std::thread(WriteLoop, remote);
|
||||
RT_LOG(RT_TAG_HLE) << (remote->plus ? "Wii Remote Plus " : "Wii Remote ")
|
||||
<< (address.empty() ? path : address) << " connected to the PC" << std::endl;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_waiting.push_back(remote);
|
||||
}
|
||||
AssignWaiting();
|
||||
}
|
||||
|
||||
void Manager::AssignWaiting() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_waiting.begin(); it != m_waiting.end();) {
|
||||
const std::shared_ptr<Remote>& remote = *it;
|
||||
const std::string key = remote->address.empty() ? remote->path : remote->address;
|
||||
size_t slot = Bluetooth::kRemoteSlots;
|
||||
const auto last = m_lastSlot.find(key);
|
||||
// A slot is free once the game has seen its previous remote go.
|
||||
const auto free = [this](size_t i) { return !m_slots[i] && m_links[i] == Bluetooth::LinkState::Down; };
|
||||
if (last != m_lastSlot.end() && free(last->second)) {
|
||||
slot = last->second;
|
||||
} else {
|
||||
for (size_t i = 0; i < m_slots.size(); ++i) {
|
||||
if (free(i)) {
|
||||
slot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (slot == Bluetooth::kRemoteSlots) return;
|
||||
m_slots[slot] = remote;
|
||||
m_lastSlot[key] = slot;
|
||||
it = m_waiting.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::ReadLoop(std::shared_ptr<Remote> remote) {
|
||||
std::array<uint8_t, 32> buffer{};
|
||||
while (remote->alive) {
|
||||
const int read = SDL_hid_read_timeout(remote->device, buffer.data(), buffer.size(), kReadTimeoutMs);
|
||||
if (read < 0) {
|
||||
remote->alive = false;
|
||||
remote->writeCv.notify_all();
|
||||
break;
|
||||
}
|
||||
if (read == 0) continue;
|
||||
const size_t size = ReportSize(buffer[0], static_cast<size_t>(read));
|
||||
if (size == 0) continue;
|
||||
Report report(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(size));
|
||||
|
||||
if (report[0] == 0x20) {
|
||||
remote->battery = BatteryBars(report[6]);
|
||||
const bool attached = (report[3] & 0x02) != 0;
|
||||
remote->extensionAttached = attached;
|
||||
if (!attached) remote->extension = -1;
|
||||
} else if (report[0] == 0x21 && (report[3] & 0x0F) == 0 && report[4] == 0x00 && report[5] == 0xFA &&
|
||||
(report[3] >> 4) >= 5) {
|
||||
// The game reading the extension id at 0xA400FA (before encryption).
|
||||
remote->extension = report[11];
|
||||
}
|
||||
|
||||
const Phase phase = remote->phase;
|
||||
if (phase == Phase::Linked) {
|
||||
std::lock_guard<std::mutex> lock(remote->inputMutex);
|
||||
if (remote->input.size() >= kInputQueueLimit) {
|
||||
auto stale = std::find_if(remote->input.begin(), remote->input.end(),
|
||||
[](const Report& r) { return r[0] >= 0x30; });
|
||||
remote->input.erase(stale != remote->input.end() ? stale : remote->input.begin());
|
||||
}
|
||||
remote->input.push_back(std::move(report));
|
||||
} else if (Buttons(report) != 0) {
|
||||
if (phase == Phase::Asleep) {
|
||||
remote->phase = Phase::Waiting;
|
||||
remote->writeCv.notify_one();
|
||||
}
|
||||
if (phase == Phase::Asleep || phase == Phase::Waiting) {
|
||||
remote->wake = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::WriteLoop(std::shared_ptr<Remote> remote) {
|
||||
auto lastWrite = Clock::now();
|
||||
auto lastBlink = Clock::time_point{};
|
||||
bool blinkOn = false;
|
||||
bool appliedRumble = false;
|
||||
Phase lastPhase = Phase::Linked; // forces the quiet setup on the first pass
|
||||
const auto write = [&](const Report& report) {
|
||||
if (SDL_hid_write(remote->device, report.data(), report.size()) < 0) {
|
||||
remote->alive = false;
|
||||
return false;
|
||||
}
|
||||
lastWrite = Clock::now();
|
||||
return true;
|
||||
};
|
||||
while (remote->alive) {
|
||||
std::deque<Report> batch;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(remote->writeMutex);
|
||||
remote->writeCv.wait_for(lock, std::chrono::milliseconds(100),
|
||||
[&] { return !remote->alive || !remote->writes.empty(); });
|
||||
batch.swap(remote->writes);
|
||||
}
|
||||
const bool rumbleAllowed = PAD_HLE_RumbleEnabled();
|
||||
const Phase phase = remote->phase;
|
||||
// Queued just before the game let go of the remote.
|
||||
if (phase != Phase::Linked) batch.clear();
|
||||
for (Report& report : batch) {
|
||||
if (report.size() >= 2 && !rumbleAllowed) report[1] &= 0xFE;
|
||||
if (!write(report)) break;
|
||||
if (report.size() >= 2) appliedRumble = (report[1] & 0x01) != 0;
|
||||
}
|
||||
if (!remote->alive) break;
|
||||
|
||||
const auto now = Clock::now();
|
||||
if (phase != lastPhase && phase != Phase::Linked && phase != Phase::PoweringOff) {
|
||||
// Stop streaming and rumbling while the game does not own the remote.
|
||||
if (!write({0x12, 0x00, 0x30}) || !write({0x11, 0x00})) break;
|
||||
appliedRumble = false;
|
||||
blinkOn = false;
|
||||
lastBlink = now;
|
||||
}
|
||||
lastPhase = phase;
|
||||
if (phase == Phase::Waiting && now - lastBlink >= kBlinkInterval) {
|
||||
// Like a remote looking for its console.
|
||||
blinkOn = !blinkOn;
|
||||
lastBlink = now;
|
||||
if (!write({0x11, static_cast<uint8_t>(blinkOn ? 0xF0 : 0x00)})) break;
|
||||
} else if (phase == Phase::Linked) {
|
||||
const bool wanted = remote->gameRumble && rumbleAllowed;
|
||||
if (wanted != appliedRumble || now - lastWrite >= kKeepAlive) {
|
||||
// Also how Windows notices a remote that went away.
|
||||
if (!write({0x10, static_cast<uint8_t>(wanted ? 0x01 : 0x00)})) break;
|
||||
appliedRumble = wanted;
|
||||
}
|
||||
} else if (phase == Phase::Asleep && now - lastWrite >= kKeepAlive) {
|
||||
if (!write({0x10, 0x00})) break;
|
||||
}
|
||||
}
|
||||
remote->writeCv.notify_all();
|
||||
}
|
||||
|
||||
void Manager::DiscoveryLoop() {
|
||||
while (m_running) {
|
||||
bool search = false;
|
||||
bool automatic = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(m_discoveryMutex);
|
||||
const auto now = Clock::now();
|
||||
bool anyRemote = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> slotLock(m_mutex);
|
||||
for (const auto& remote : m_slots) anyRemote |= remote != nullptr;
|
||||
anyRemote |= !m_waiting.empty();
|
||||
}
|
||||
const bool requested = m_guestSearching || now < m_guestSearchUntil || now < m_pairingUntil;
|
||||
automatic = !requested && !anyRemote && RuntimeConfigFile::WiiAutoPairEnabled();
|
||||
search = requested || automatic;
|
||||
if (!search) {
|
||||
m_searching = false;
|
||||
m_discoveryCv.wait_for(lock, std::chrono::seconds(1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
m_searching = true;
|
||||
std::string status;
|
||||
const int connected = Platform::DiscoverAndPair(m_cancelDiscovery, status);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
if (!status.empty()) m_pairingStatus = status;
|
||||
if (connected > 0) {
|
||||
m_scanCv.notify_all();
|
||||
}
|
||||
}
|
||||
if (automatic && connected == 0) {
|
||||
std::unique_lock<std::mutex> lock(m_discoveryMutex);
|
||||
m_discoveryCv.wait_for(lock, kAutoPairPause, [this] {
|
||||
return !m_running || m_guestSearching || Clock::now() < m_pairingUntil;
|
||||
});
|
||||
}
|
||||
}
|
||||
m_searching = false;
|
||||
}
|
||||
|
||||
SlotInfo Manager::Slot(size_t slot) {
|
||||
SlotInfo info;
|
||||
std::shared_ptr<Remote> remote;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (slot >= m_slots.size()) return info;
|
||||
remote = m_slots[slot];
|
||||
info.link = m_links[slot];
|
||||
}
|
||||
if (!remote || !remote->alive || remote->phase == Phase::PoweringOff) {
|
||||
info.link = Bluetooth::LinkState::Down;
|
||||
return info;
|
||||
}
|
||||
info.attached = true;
|
||||
info.asleep = remote->phase == Phase::Asleep;
|
||||
info.model = remote->plus ? "Wii Remote Plus" : "Wii Remote";
|
||||
if (remote->extensionAttached) {
|
||||
switch (remote->extension.load()) {
|
||||
case 0x00: info.extension = "Nunchuk"; break;
|
||||
case 0x01: info.extension = "Classic Controller"; break;
|
||||
case 0x03: info.extension = "Classic Controller Pro"; break;
|
||||
default: info.extension = "Extension"; break;
|
||||
}
|
||||
}
|
||||
info.battery = remote->battery;
|
||||
return info;
|
||||
}
|
||||
|
||||
PairingInfo Manager::Pairing() {
|
||||
PairingInfo info;
|
||||
info.supported = Platform::Query().pairing && m_running;
|
||||
info.searching = m_searching;
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
info.status = m_pairingStatus;
|
||||
return info;
|
||||
}
|
||||
|
||||
void Manager::StartPairing() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_discoveryMutex);
|
||||
m_pairingUntil = Clock::now() + kPairingWindow;
|
||||
m_pairingStatus = "Press the red SYNC button (or 1 and 2) on the Wii Remote.";
|
||||
}
|
||||
m_discoveryCv.notify_all();
|
||||
}
|
||||
|
||||
void Manager::Disconnect(size_t slot) {
|
||||
if (const auto remote = At(slot)) {
|
||||
PowerOff(remote);
|
||||
}
|
||||
}
|
||||
|
||||
size_t Manager::Waiting() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_waiting.size();
|
||||
}
|
||||
|
||||
bool g_enabled = false;
|
||||
|
||||
} // namespace HostDetail
|
||||
|
||||
using namespace HostDetail;
|
||||
|
||||
void ConfigureSdlHints(bool enabled) {
|
||||
g_enabled = enabled;
|
||||
// The remotes belong to the game's Bluetooth stack: SDL must not open them
|
||||
// as gamepads (its own Wii driver, or the Linux kernel driver's evdev nodes).
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, enabled ? "0" : "1");
|
||||
if (enabled) {
|
||||
std::string ignored = "0x057e/0x0306,0x057e/0x0330";
|
||||
if (const char* existing = SDL_GetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES); existing && *existing) {
|
||||
ignored = std::string(existing) + "," + ignored;
|
||||
}
|
||||
SDL_SetHint(SDL_HINT_GAMECONTROLLER_IGNORE_DEVICES, ignored.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
if (g_enabled) {
|
||||
TheManager().Start();
|
||||
}
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
TheManager().Stop();
|
||||
}
|
||||
|
||||
Bluetooth::RemoteHost& Host() {
|
||||
return TheManager();
|
||||
}
|
||||
|
||||
SlotInfo Slot(size_t slot) {
|
||||
return TheManager().Slot(slot);
|
||||
}
|
||||
|
||||
PairingInfo Pairing() {
|
||||
return TheManager().Pairing();
|
||||
}
|
||||
|
||||
void StartPairing() {
|
||||
TheManager().StartPairing();
|
||||
}
|
||||
|
||||
void Disconnect(size_t slot) {
|
||||
TheManager().Disconnect(slot);
|
||||
}
|
||||
|
||||
std::string PlatformNotice() {
|
||||
return g_enabled ? Platform::Notice() : std::string();
|
||||
}
|
||||
|
||||
size_t WaitingRemotes() {
|
||||
return TheManager().Waiting();
|
||||
}
|
||||
|
||||
} // namespace Wiimote
|
||||
@@ -0,0 +1,496 @@
|
||||
#if !defined(_WIN32)
|
||||
|
||||
#include "wiimote_platform.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <filesystem>
|
||||
#endif
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
namespace Detail {
|
||||
|
||||
std::string Narrow(const wchar_t* text) {
|
||||
std::string out;
|
||||
if (text != nullptr) {
|
||||
for (const wchar_t* c = text; *c != L'\0'; ++c) out += static_cast<char>(*c < 128 ? *c : '?');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string HexDigits(const std::string& text) {
|
||||
std::string hex;
|
||||
for (const char c : text) {
|
||||
if (std::isxdigit(static_cast<unsigned char>(c))) hex += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
std::mutex g_noticeMutex;
|
||||
std::string g_notice;
|
||||
|
||||
void SetNotice(const std::string& notice) {
|
||||
std::lock_guard<std::mutex> lock(g_noticeMutex);
|
||||
g_notice = notice;
|
||||
}
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
// BlueZ over the system bus. libdbus-1 is loaded at run time so a system
|
||||
// without it still runs (pairing then falls back to the desktop's tools).
|
||||
extern "C" {
|
||||
struct BzConnection;
|
||||
struct BzMessage;
|
||||
struct BzError {
|
||||
const char* name;
|
||||
const char* message;
|
||||
unsigned int dummy1 : 1;
|
||||
unsigned int dummy2 : 1;
|
||||
unsigned int dummy3 : 1;
|
||||
unsigned int dummy4 : 1;
|
||||
unsigned int dummy5 : 1;
|
||||
void* padding1;
|
||||
};
|
||||
// Mirrors DBusMessageIter.
|
||||
struct BzIter {
|
||||
void* dummy1;
|
||||
void* dummy2;
|
||||
uint32_t dummy3;
|
||||
int dummy4;
|
||||
int dummy5;
|
||||
int dummy6;
|
||||
int dummy7;
|
||||
int dummy8;
|
||||
int dummy9;
|
||||
int dummy10;
|
||||
int dummy11;
|
||||
int pad1;
|
||||
int pad2;
|
||||
void* pad3;
|
||||
};
|
||||
}
|
||||
|
||||
constexpr int kBusSystem = 1;
|
||||
constexpr int kTypeInvalid = 0;
|
||||
constexpr int kTypeString = 's';
|
||||
constexpr int kTypeObjectPath = 'o';
|
||||
constexpr int kTypeBoolean = 'b';
|
||||
constexpr int kTypeInt16 = 'n';
|
||||
constexpr int kTypeArray = 'a';
|
||||
constexpr int kTypeVariant = 'v';
|
||||
constexpr int kTypeDictEntry = 'e';
|
||||
constexpr int kShortTimeoutMs = 2000;
|
||||
constexpr int kPairTimeoutMs = 30000;
|
||||
constexpr auto kInquiryTime = std::chrono::milliseconds(3840);
|
||||
|
||||
struct Device {
|
||||
std::string path;
|
||||
std::string address;
|
||||
std::string name;
|
||||
bool paired = false;
|
||||
bool connected = false;
|
||||
bool trusted = false;
|
||||
bool discovered = false; // has an RSSI: answered the current inquiry
|
||||
};
|
||||
|
||||
class BlueZ {
|
||||
public:
|
||||
bool Load() {
|
||||
if (m_loaded) return m_conn != nullptr;
|
||||
m_loaded = true;
|
||||
m_handle = dlopen("libdbus-1.so.3", RTLD_NOW | RTLD_LOCAL);
|
||||
if (m_handle == nullptr) return false;
|
||||
const bool ok = Bind(bus_get_private, "dbus_bus_get_private") &&
|
||||
Bind(connection_set_exit_on_disconnect, "dbus_connection_set_exit_on_disconnect") &&
|
||||
Bind(error_init, "dbus_error_init") && Bind(error_free, "dbus_error_free") &&
|
||||
Bind(message_new_method_call, "dbus_message_new_method_call") &&
|
||||
Bind(message_append_args, "dbus_message_append_args") &&
|
||||
Bind(send_with_reply_and_block, "dbus_connection_send_with_reply_and_block") &&
|
||||
Bind(message_unref, "dbus_message_unref") &&
|
||||
Bind(message_iter_init, "dbus_message_iter_init") &&
|
||||
Bind(message_iter_init_append, "dbus_message_iter_init_append") &&
|
||||
Bind(message_iter_open_container, "dbus_message_iter_open_container") &&
|
||||
Bind(message_iter_close_container, "dbus_message_iter_close_container") &&
|
||||
Bind(message_iter_append_basic, "dbus_message_iter_append_basic") &&
|
||||
Bind(message_iter_next, "dbus_message_iter_next") &&
|
||||
Bind(message_iter_recurse, "dbus_message_iter_recurse") &&
|
||||
Bind(message_iter_get_arg_type, "dbus_message_iter_get_arg_type") &&
|
||||
Bind(message_iter_get_basic, "dbus_message_iter_get_basic");
|
||||
if (!ok) return false;
|
||||
BzError error;
|
||||
error_init(&error);
|
||||
m_conn = bus_get_private(kBusSystem, &error);
|
||||
error_free(&error);
|
||||
if (m_conn != nullptr) connection_set_exit_on_disconnect(m_conn, 0);
|
||||
return m_conn != nullptr;
|
||||
}
|
||||
|
||||
// org.freedesktop.DBus.ObjectManager.GetManagedObjects on org.bluez.
|
||||
bool Objects(std::string& adapter, std::vector<Device>& devices) {
|
||||
BzMessage* reply = Call("/", "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", kShortTimeoutMs);
|
||||
if (reply == nullptr) return false;
|
||||
BzIter root;
|
||||
BzIter objects;
|
||||
if (message_iter_init(reply, &root) && message_iter_get_arg_type(&root) == kTypeArray) {
|
||||
message_iter_recurse(&root, &objects);
|
||||
while (message_iter_get_arg_type(&objects) == kTypeDictEntry) {
|
||||
BzIter entry;
|
||||
message_iter_recurse(&objects, &entry);
|
||||
const char* path = nullptr;
|
||||
message_iter_get_basic(&entry, &path);
|
||||
message_iter_next(&entry);
|
||||
ReadObject(path != nullptr ? path : "", entry, adapter, devices);
|
||||
message_iter_next(&objects);
|
||||
}
|
||||
}
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Simple(const std::string& path, const char* interface, const char* method, int timeoutMs,
|
||||
std::string* error = nullptr) {
|
||||
BzMessage* reply = Call(path.c_str(), interface, method, timeoutMs, nullptr, error);
|
||||
if (reply == nullptr) return false;
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RemoveDevice(const std::string& adapter, const std::string& device) {
|
||||
const char* path = device.c_str();
|
||||
BzMessage* reply = Call(adapter.c_str(), "org.bluez.Adapter1", "RemoveDevice", kShortTimeoutMs,
|
||||
[&](BzMessage* message) {
|
||||
return message_append_args(message, kTypeObjectPath, &path, kTypeInvalid) != 0;
|
||||
});
|
||||
if (reply == nullptr) return false;
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SetTrusted(const std::string& device) {
|
||||
BzMessage* reply = Call(device.c_str(), "org.freedesktop.DBus.Properties", "Set", kShortTimeoutMs,
|
||||
[&](BzMessage* message) {
|
||||
const char* interface = "org.bluez.Device1";
|
||||
const char* property = "Trusted";
|
||||
if (!message_append_args(message, kTypeString, &interface, kTypeString, &property,
|
||||
kTypeInvalid)) {
|
||||
return false;
|
||||
}
|
||||
BzIter args;
|
||||
BzIter variant;
|
||||
message_iter_init_append(message, &args);
|
||||
if (!message_iter_open_container(&args, kTypeVariant, "b", &variant)) return false;
|
||||
const uint32_t value = 1;
|
||||
message_iter_append_basic(&variant, kTypeBoolean, &value);
|
||||
return message_iter_close_container(&args, &variant) != 0;
|
||||
});
|
||||
if (reply == nullptr) return false;
|
||||
message_unref(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Fn>
|
||||
bool Bind(Fn& fn, const char* symbol) {
|
||||
fn = reinterpret_cast<Fn>(dlsym(m_handle, symbol));
|
||||
return fn != nullptr;
|
||||
}
|
||||
|
||||
template <typename Append = std::nullptr_t>
|
||||
BzMessage* Call(const char* path, const char* interface, const char* method, int timeoutMs,
|
||||
Append append = nullptr, std::string* errorText = nullptr) {
|
||||
BzMessage* message = message_new_method_call("org.bluez", path, interface, method);
|
||||
if (message == nullptr) return nullptr;
|
||||
if constexpr (!std::is_same_v<Append, std::nullptr_t>) {
|
||||
if (!append(message)) {
|
||||
message_unref(message);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
BzError error;
|
||||
error_init(&error);
|
||||
BzMessage* reply = send_with_reply_and_block(m_conn, message, timeoutMs, &error);
|
||||
message_unref(message);
|
||||
if (reply == nullptr && errorText != nullptr && error.name != nullptr) {
|
||||
*errorText = error.name;
|
||||
}
|
||||
error_free(&error);
|
||||
return reply;
|
||||
}
|
||||
|
||||
// One object's a{sa{sv}} interface map.
|
||||
void ReadObject(const std::string& path, BzIter& interfaces, std::string& adapter, std::vector<Device>& devices) {
|
||||
if (message_iter_get_arg_type(&interfaces) != kTypeArray) return;
|
||||
BzIter list;
|
||||
message_iter_recurse(&interfaces, &list);
|
||||
while (message_iter_get_arg_type(&list) == kTypeDictEntry) {
|
||||
BzIter entry;
|
||||
message_iter_recurse(&list, &entry);
|
||||
const char* interface = nullptr;
|
||||
message_iter_get_basic(&entry, &interface);
|
||||
message_iter_next(&entry);
|
||||
const std::string name = interface != nullptr ? interface : "";
|
||||
if (name == "org.bluez.Adapter1" && adapter.empty()) {
|
||||
adapter = path;
|
||||
} else if (name == "org.bluez.Device1") {
|
||||
Device device;
|
||||
device.path = path;
|
||||
ReadDevice(entry, device);
|
||||
devices.push_back(device);
|
||||
}
|
||||
message_iter_next(&list);
|
||||
}
|
||||
}
|
||||
|
||||
void ReadDevice(BzIter& properties, Device& device) {
|
||||
if (message_iter_get_arg_type(&properties) != kTypeArray) return;
|
||||
BzIter list;
|
||||
message_iter_recurse(&properties, &list);
|
||||
while (message_iter_get_arg_type(&list) == kTypeDictEntry) {
|
||||
BzIter entry;
|
||||
message_iter_recurse(&list, &entry);
|
||||
const char* key = nullptr;
|
||||
message_iter_get_basic(&entry, &key);
|
||||
message_iter_next(&entry);
|
||||
BzIter value;
|
||||
message_iter_recurse(&entry, &value);
|
||||
const std::string name = key != nullptr ? key : "";
|
||||
const int type = message_iter_get_arg_type(&value);
|
||||
if (type == kTypeString && (name == "Address" || name == "Name")) {
|
||||
const char* text = nullptr;
|
||||
message_iter_get_basic(&value, &text);
|
||||
(name == "Address" ? device.address : device.name) = text != nullptr ? text : "";
|
||||
} else if (type == kTypeBoolean) {
|
||||
uint32_t flag = 0;
|
||||
message_iter_get_basic(&value, &flag);
|
||||
if (name == "Paired") device.paired = flag != 0;
|
||||
if (name == "Connected") device.connected = flag != 0;
|
||||
if (name == "Trusted") device.trusted = flag != 0;
|
||||
} else if (type == kTypeInt16 && name == "RSSI") {
|
||||
device.discovered = true;
|
||||
}
|
||||
message_iter_next(&list);
|
||||
}
|
||||
}
|
||||
|
||||
bool m_loaded = false;
|
||||
void* m_handle = nullptr;
|
||||
BzConnection* m_conn = nullptr;
|
||||
BzConnection* (*bus_get_private)(int, BzError*) = nullptr;
|
||||
void (*connection_set_exit_on_disconnect)(BzConnection*, unsigned int) = nullptr;
|
||||
void (*error_init)(BzError*) = nullptr;
|
||||
void (*error_free)(BzError*) = nullptr;
|
||||
BzMessage* (*message_new_method_call)(const char*, const char*, const char*, const char*) = nullptr;
|
||||
unsigned int (*message_append_args)(BzMessage*, int, ...) = nullptr;
|
||||
BzMessage* (*send_with_reply_and_block)(BzConnection*, BzMessage*, int, BzError*) = nullptr;
|
||||
void (*message_unref)(BzMessage*) = nullptr;
|
||||
unsigned int (*message_iter_init)(BzMessage*, BzIter*) = nullptr;
|
||||
void (*message_iter_init_append)(BzMessage*, BzIter*) = nullptr;
|
||||
unsigned int (*message_iter_open_container)(BzIter*, int, const char*, BzIter*) = nullptr;
|
||||
unsigned int (*message_iter_close_container)(BzIter*, BzIter*) = nullptr;
|
||||
unsigned int (*message_iter_append_basic)(BzIter*, int, const void*) = nullptr;
|
||||
unsigned int (*message_iter_next)(BzIter*) = nullptr;
|
||||
void (*message_iter_recurse)(BzIter*, BzIter*) = nullptr;
|
||||
int (*message_iter_get_arg_type)(BzIter*) = nullptr;
|
||||
void (*message_iter_get_basic)(BzIter*, void*) = nullptr;
|
||||
};
|
||||
|
||||
// Every call happens on the discovery thread or the power-off worker.
|
||||
std::timed_mutex g_busMutex;
|
||||
|
||||
BlueZ* Bus() {
|
||||
static BlueZ bus;
|
||||
static const bool ok = bus.Load();
|
||||
return ok ? &bus : nullptr;
|
||||
}
|
||||
|
||||
// A remote that was just paired needs a few seconds to connect; until then it
|
||||
// must not be taken for a stale pairing.
|
||||
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> g_recent;
|
||||
|
||||
bool RecentlyPaired(const std::string& path) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
g_recent.erase(std::remove_if(g_recent.begin(), g_recent.end(),
|
||||
[&](const auto& entry) { return now - entry.second > std::chrono::seconds(30); }),
|
||||
g_recent.end());
|
||||
return std::any_of(g_recent.begin(), g_recent.end(), [&](const auto& entry) { return entry.first == path; });
|
||||
}
|
||||
|
||||
bool IsRemoteName(const std::string& name) {
|
||||
return name.rfind("Nintendo RVL-CNT-01", 0) == 0 && name.find("-UC") == std::string::npos;
|
||||
}
|
||||
|
||||
std::string DriverOf(const std::string& hidrawPath) {
|
||||
const std::filesystem::path node(hidrawPath);
|
||||
std::error_code error;
|
||||
const auto driver =
|
||||
std::filesystem::read_symlink(std::filesystem::path("/sys/class/hidraw") / node.filename() / "device" / "driver",
|
||||
error);
|
||||
return error ? std::string() : driver.filename().string();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
using namespace Detail;
|
||||
|
||||
Capabilities Query() {
|
||||
#if defined(__linux__)
|
||||
// Cached: adapters rarely change and this is asked often.
|
||||
static std::mutex mutex;
|
||||
static Capabilities cached;
|
||||
static std::chrono::steady_clock::time_point checked{};
|
||||
std::lock_guard<std::mutex> cacheLock(mutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (checked == std::chrono::steady_clock::time_point{} || now - checked > std::chrono::seconds(10)) {
|
||||
// A search holds the bus for seconds; keep the cached answer rather than wait.
|
||||
std::unique_lock<std::timed_mutex> lock(g_busMutex, std::try_to_lock);
|
||||
if (!lock.owns_lock()) return cached;
|
||||
checked = now;
|
||||
std::string adapter;
|
||||
std::vector<Device> devices;
|
||||
BlueZ* bus = Bus();
|
||||
cached.pairing = cached.powerOff = bus != nullptr && bus->Objects(adapter, devices) && !adapter.empty();
|
||||
}
|
||||
return cached;
|
||||
#else
|
||||
return {};
|
||||
#endif
|
||||
}
|
||||
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status) {
|
||||
#if defined(__linux__)
|
||||
std::lock_guard<std::timed_mutex> lock(g_busMutex);
|
||||
BlueZ* bus = Bus();
|
||||
std::string adapter;
|
||||
std::vector<Device> devices;
|
||||
if (bus == nullptr || !bus->Objects(adapter, devices) || adapter.empty()) {
|
||||
status = "No Bluetooth adapter found.";
|
||||
return 0;
|
||||
}
|
||||
bus->Simple(adapter, "org.bluez.Adapter1", "StartDiscovery", kShortTimeoutMs);
|
||||
int connected = 0;
|
||||
const auto deadline = std::chrono::steady_clock::now() + kInquiryTime;
|
||||
std::vector<std::string> handled;
|
||||
while (!cancel && std::chrono::steady_clock::now() < deadline) {
|
||||
devices.clear();
|
||||
bus->Objects(adapter, devices);
|
||||
for (const Device& device : devices) {
|
||||
if (!IsRemoteName(device.name) || device.connected || !device.discovered) continue;
|
||||
if (std::find(handled.begin(), handled.end(), device.path) != handled.end() || RecentlyPaired(device.path)) {
|
||||
continue;
|
||||
}
|
||||
if (device.paired) {
|
||||
// In pairing mode while paired: the remote forgot this PC.
|
||||
bus->RemoveDevice(adapter, device.path);
|
||||
continue;
|
||||
}
|
||||
handled.push_back(device.path);
|
||||
// BlueZ's wiimote plugin answers the PIN (SYNC or 1+2).
|
||||
std::string error;
|
||||
if (bus->Simple(device.path, "org.bluez.Device1", "Pair", kPairTimeoutMs, &error)) {
|
||||
bus->SetTrusted(device.path);
|
||||
g_recent.emplace_back(device.path, std::chrono::steady_clock::now());
|
||||
if (bus->Simple(device.path, "org.bluez.Device1", "Connect", kPairTimeoutMs, &error)) {
|
||||
++connected;
|
||||
status = "Wii Remote paired. It reconnects with any button from now on.";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << device.address << " paired" << std::endl;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
status = "Could not pair the Wii Remote. Press its red SYNC button and try again.";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << device.address << ": pairing failed (" << error << ")" << std::endl;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||
}
|
||||
bus->Simple(adapter, "org.bluez.Adapter1", "StopDiscovery", kShortTimeoutMs);
|
||||
return connected;
|
||||
#else
|
||||
(void)cancel;
|
||||
status = "Pair the Wii Remote in the system's Bluetooth settings.";
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PowerOff(const std::string& address) {
|
||||
#if defined(__linux__)
|
||||
std::unique_lock<std::timed_mutex> lock(g_busMutex, std::chrono::milliseconds(kShortTimeoutMs));
|
||||
if (!lock.owns_lock()) return false;
|
||||
BlueZ* bus = Bus();
|
||||
std::string adapter;
|
||||
std::vector<Device> devices;
|
||||
if (bus == nullptr || !bus->Objects(adapter, devices)) return false;
|
||||
for (const Device& device : devices) {
|
||||
if (HexDigits(device.address) == address) {
|
||||
return bus->Simple(device.path, "org.bluez.Device1", "Disconnect", kShortTimeoutMs);
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)address;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string AddressFor(const SDL_hid_device_info& info) {
|
||||
const std::string hex = HexDigits(Narrow(info.serial_number));
|
||||
return hex.size() == 12 && hex != "000000000000" ? hex : std::string();
|
||||
}
|
||||
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info) {
|
||||
(void)address;
|
||||
return Narrow(info.product_string);
|
||||
}
|
||||
|
||||
bool MaybeConnected(const std::string& address) {
|
||||
(void)address;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Usable(const SDL_hid_device_info& info) {
|
||||
#if defined(__linux__)
|
||||
if (info.path == nullptr) return false;
|
||||
if (DriverOf(info.path) == "wiimote") {
|
||||
SetNotice("The Linux hid-wiimote driver holds the Wii Remote, so the game cannot use it. Run "
|
||||
"\"sudo modprobe -r hid_wiimote\" and add \"blacklist hid_wiimote\" to /etc/modprobe.d/wiimote.conf.");
|
||||
return false;
|
||||
}
|
||||
if (access(info.path, R_OK | W_OK) != 0) {
|
||||
SetNotice("No permission to open the Wii Remote. Add the udev rule "
|
||||
"KERNEL==\"hidraw*\", KERNELS==\"0005:057E:*\", MODE=\"0666\" and reconnect it.");
|
||||
return false;
|
||||
}
|
||||
SetNotice({});
|
||||
#else
|
||||
(void)info;
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Notice() {
|
||||
std::lock_guard<std::mutex> lock(g_noticeMutex);
|
||||
#if defined(__APPLE__)
|
||||
if (g_notice.empty()) return "Pair Wii Remotes in System Settings > Bluetooth, then press a button on them.";
|
||||
#endif
|
||||
return g_notice;
|
||||
}
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,300 @@
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include "wiimote_platform.h"
|
||||
|
||||
#include "runtime_log.h"
|
||||
|
||||
#include <SDL3/SDL_hidapi.h>
|
||||
|
||||
#include <windows.h>
|
||||
#include <bluetoothapis.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Wiimote::Platform {
|
||||
namespace Detail {
|
||||
|
||||
// bthioctl.h IOCTL_BTH_DISCONNECT_DEVICE: CTL_CODE(FILE_DEVICE_BLUETOOTH, 0x03, METHOD_BUFFERED, FILE_ANY_ACCESS).
|
||||
constexpr DWORD kIoctlBthDisconnectDevice = 0x0041000C;
|
||||
const GUID kHidServiceClass = {0x00001124, 0x0000, 0x1000, {0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB}};
|
||||
// Each inquiry lasts this many 1.28 s units.
|
||||
constexpr UCHAR kInquiryUnits = 3;
|
||||
|
||||
struct Radio {
|
||||
HANDLE handle = nullptr;
|
||||
BLUETOOTH_RADIO_INFO info{};
|
||||
};
|
||||
|
||||
class Radios {
|
||||
public:
|
||||
Radios() {
|
||||
BLUETOOTH_FIND_RADIO_PARAMS params{sizeof(params)};
|
||||
HANDLE radio = nullptr;
|
||||
HBLUETOOTH_RADIO_FIND find = BluetoothFindFirstRadio(¶ms, &radio);
|
||||
if (find == nullptr) return;
|
||||
do {
|
||||
Radio entry;
|
||||
entry.handle = radio;
|
||||
entry.info.dwSize = sizeof(entry.info);
|
||||
if (BluetoothGetRadioInfo(radio, &entry.info) == ERROR_SUCCESS) {
|
||||
m_radios.push_back(entry);
|
||||
} else {
|
||||
CloseHandle(radio);
|
||||
}
|
||||
} while (BluetoothFindNextRadio(find, &radio));
|
||||
BluetoothFindRadioClose(find);
|
||||
}
|
||||
~Radios() {
|
||||
for (const Radio& radio : m_radios) CloseHandle(radio.handle);
|
||||
}
|
||||
Radios(const Radios&) = delete;
|
||||
Radios& operator=(const Radios&) = delete;
|
||||
const std::vector<Radio>& All() const { return m_radios; }
|
||||
|
||||
private:
|
||||
std::vector<Radio> m_radios;
|
||||
};
|
||||
|
||||
bool IsRemoteName(const WCHAR* name) {
|
||||
const std::wstring text = name;
|
||||
return text.rfind(L"Nintendo RVL-CNT-01", 0) == 0 && text.find(L"-UC") == std::wstring::npos;
|
||||
}
|
||||
|
||||
std::string Hex(const BLUETOOTH_ADDRESS& address) {
|
||||
char text[13];
|
||||
std::snprintf(text, sizeof(text), "%012llx", static_cast<unsigned long long>(address.ullLong & 0xFFFFFFFFFFFFull));
|
||||
return text;
|
||||
}
|
||||
|
||||
bool ParseHex(const std::string& text, BLUETOOTH_ADDRESS& address) {
|
||||
if (text.size() != 12) return false;
|
||||
address.ullLong = std::stoull(text, nullptr, 16);
|
||||
return true;
|
||||
}
|
||||
|
||||
ULONGLONG FileTimeOf(const SYSTEMTIME& time) {
|
||||
FILETIME file{};
|
||||
if (!SystemTimeToFileTime(&time, &file)) return 0;
|
||||
return (static_cast<ULONGLONG>(file.dwHighDateTime) << 32) | file.dwLowDateTime;
|
||||
}
|
||||
|
||||
ULONGLONG Now() {
|
||||
SYSTEMTIME now{};
|
||||
GetSystemTime(&now);
|
||||
return FileTimeOf(now);
|
||||
}
|
||||
|
||||
// The remote takes the host's address as PIN after its SYNC button, its own
|
||||
// address after 1+2; both as raw bytes, least significant first.
|
||||
DWORD Authenticate(const Radio& radio, BLUETOOTH_DEVICE_INFO& device, const BLUETOOTH_ADDRESS& pinAddress) {
|
||||
WCHAR pin[6];
|
||||
for (int i = 0; i < 6; ++i) pin[i] = pinAddress.rgBytes[i];
|
||||
return BluetoothAuthenticateDevice(nullptr, radio.handle, &device, pin, 6);
|
||||
}
|
||||
|
||||
// A remote that was just paired needs a few seconds to connect its HID service;
|
||||
// until then it must not be taken for a stale pairing.
|
||||
constexpr auto kPairingSettleTime = std::chrono::seconds(30);
|
||||
std::mutex g_recentMutex;
|
||||
std::vector<std::pair<std::string, std::chrono::steady_clock::time_point>> g_recent;
|
||||
|
||||
bool RecentlyPaired(const std::string& address) {
|
||||
std::lock_guard<std::mutex> lock(g_recentMutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
g_recent.erase(std::remove_if(g_recent.begin(), g_recent.end(),
|
||||
[&](const auto& entry) { return now - entry.second > kPairingSettleTime; }),
|
||||
g_recent.end());
|
||||
return std::any_of(g_recent.begin(), g_recent.end(), [&](const auto& entry) { return entry.first == address; });
|
||||
}
|
||||
|
||||
void MarkPaired(const std::string& address) {
|
||||
std::lock_guard<std::mutex> lock(g_recentMutex);
|
||||
g_recent.emplace_back(address, std::chrono::steady_clock::now());
|
||||
}
|
||||
|
||||
std::vector<BLUETOOTH_DEVICE_INFO> Devices(const Radio& radio, bool inquiry) {
|
||||
std::vector<BLUETOOTH_DEVICE_INFO> devices;
|
||||
BLUETOOTH_DEVICE_SEARCH_PARAMS params{sizeof(params)};
|
||||
params.fReturnAuthenticated = TRUE;
|
||||
params.fReturnRemembered = TRUE;
|
||||
params.fReturnUnknown = TRUE;
|
||||
params.fReturnConnected = TRUE;
|
||||
params.fIssueInquiry = inquiry ? TRUE : FALSE;
|
||||
params.cTimeoutMultiplier = inquiry ? kInquiryUnits : 0;
|
||||
params.hRadio = radio.handle;
|
||||
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
|
||||
HBLUETOOTH_DEVICE_FIND find = BluetoothFindFirstDevice(¶ms, &device);
|
||||
if (find == nullptr) return devices;
|
||||
do {
|
||||
devices.push_back(device);
|
||||
device = BLUETOOTH_DEVICE_INFO{sizeof(device)};
|
||||
} while (BluetoothFindNextDevice(find, &device));
|
||||
BluetoothFindDeviceClose(find);
|
||||
return devices;
|
||||
}
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
using namespace Detail;
|
||||
|
||||
Capabilities Query() {
|
||||
// Cached: radios rarely change and this is asked often.
|
||||
static std::mutex mutex;
|
||||
static Capabilities cached;
|
||||
static std::chrono::steady_clock::time_point checked{};
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (checked == std::chrono::steady_clock::time_point{} || now - checked > std::chrono::seconds(10)) {
|
||||
checked = now;
|
||||
const Radios radios;
|
||||
cached.pairing = cached.powerOff = !radios.All().empty();
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
int DiscoverAndPair(const std::atomic<bool>& cancel, std::string& status) {
|
||||
const Radios radios;
|
||||
if (radios.All().empty()) {
|
||||
status = "No Bluetooth adapter found.";
|
||||
return 0;
|
||||
}
|
||||
int connected = 0;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
// A remote Windows remembers without a bond can never reconnect.
|
||||
for (BLUETOOTH_DEVICE_INFO& device : Devices(radio, false)) {
|
||||
if (IsRemoteName(device.szName) && device.fRemembered && !device.fConnected && !device.fAuthenticated) {
|
||||
BluetoothRemoveDevice(&device.Address);
|
||||
}
|
||||
}
|
||||
if (cancel) return connected;
|
||||
const ULONGLONG started = Now();
|
||||
std::vector<ULONGLONG> seen;
|
||||
for (BLUETOOTH_DEVICE_INFO& found : Devices(radio, true)) {
|
||||
if (cancel) return connected;
|
||||
if (!IsRemoteName(found.szName) || std::find(seen.begin(), seen.end(), found.Address.ullLong) != seen.end()) {
|
||||
continue;
|
||||
}
|
||||
seen.push_back(found.Address.ullLong);
|
||||
const std::string address = Hex(found.Address);
|
||||
if (RecentlyPaired(address)) continue;
|
||||
// The inquiry results can be seconds old: act on the current state.
|
||||
BLUETOOTH_DEVICE_INFO device = found;
|
||||
BluetoothGetDeviceInfo(radio.handle, &device);
|
||||
if (device.fConnected) continue;
|
||||
// Only a remote in pairing mode answers an inquiry; remembered ones
|
||||
// that did not answer keep their old timestamp.
|
||||
const bool answered = !device.fRemembered || FileTimeOf(found.stLastSeen) >= started;
|
||||
if (!answered) continue;
|
||||
if (device.fRemembered) {
|
||||
// Paired before, but in pairing mode now: it forgot this PC.
|
||||
BluetoothRemoveDevice(&device.Address);
|
||||
device.fAuthenticated = FALSE;
|
||||
device.fRemembered = FALSE;
|
||||
}
|
||||
DWORD result = Authenticate(radio, device, radio.info.address);
|
||||
const bool permanent = result == ERROR_SUCCESS;
|
||||
if (!permanent) {
|
||||
result = Authenticate(radio, device, device.Address);
|
||||
}
|
||||
if (result == ERROR_SUCCESS) {
|
||||
// Windows only keeps the pairing once the services were read.
|
||||
DWORD count = 0;
|
||||
BluetoothEnumerateInstalledServices(radio.handle, &device, &count, nullptr);
|
||||
}
|
||||
const DWORD service =
|
||||
BluetoothSetServiceState(radio.handle, &device, const_cast<GUID*>(&kHidServiceClass), BLUETOOTH_SERVICE_ENABLE);
|
||||
if (result == ERROR_SUCCESS || service == ERROR_SUCCESS) {
|
||||
++connected;
|
||||
MarkPaired(address);
|
||||
status = permanent ? "Wii Remote paired. It reconnects with any button from now on."
|
||||
: "Wii Remote connected for this session (press SYNC to pair it for good).";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << (permanent ? " paired (SYNC)" : " connected (1+2)")
|
||||
<< std::endl;
|
||||
} else {
|
||||
status = "Could not pair the Wii Remote. Press its red SYNC button and try again.";
|
||||
RT_LOG(RT_TAG_HLE) << "Wii Remote " << address << ": pairing failed (" << result << ", " << service
|
||||
<< ")" << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
|
||||
bool PowerOff(const std::string& address) {
|
||||
BLUETOOTH_ADDRESS target{};
|
||||
if (!ParseHex(address, target)) return false;
|
||||
const Radios radios;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
ULONGLONG raw = target.ullLong;
|
||||
DWORD returned = 0;
|
||||
if (DeviceIoControl(radio.handle, kIoctlBthDisconnectDevice, &raw, sizeof(raw), nullptr, 0, &returned,
|
||||
nullptr)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string AddressFor(const SDL_hid_device_info& info) {
|
||||
// Bluetooth HID devices report the remote's address as their serial number.
|
||||
std::string hex;
|
||||
if (info.serial_number != nullptr) {
|
||||
for (const wchar_t* c = info.serial_number; *c != L'\0'; ++c) {
|
||||
if (*c < 128 && std::isxdigit(static_cast<int>(*c))) hex += static_cast<char>(std::tolower(*c));
|
||||
}
|
||||
}
|
||||
return hex.size() == 12 && hex != "000000000000" ? hex : std::string();
|
||||
}
|
||||
|
||||
std::string NameFor(const std::string& address, const SDL_hid_device_info& info) {
|
||||
BLUETOOTH_ADDRESS target{};
|
||||
if (ParseHex(address, target)) {
|
||||
const Radios radios;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
|
||||
device.Address = target;
|
||||
if (BluetoothGetDeviceInfo(radio.handle, &device) == ERROR_SUCCESS && device.szName[0] != L'\0') {
|
||||
std::string name;
|
||||
for (const WCHAR* c = device.szName; *c != L'\0'; ++c) name += static_cast<char>(*c < 128 ? *c : '?');
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::string name;
|
||||
if (info.product_string != nullptr) {
|
||||
for (const wchar_t* c = info.product_string; *c != L'\0'; ++c) name += static_cast<char>(*c < 128 ? *c : '?');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
bool MaybeConnected(const std::string& address) {
|
||||
BLUETOOTH_ADDRESS target{};
|
||||
if (!ParseHex(address, target)) return true;
|
||||
const Radios radios;
|
||||
for (const Radio& radio : radios.All()) {
|
||||
BLUETOOTH_DEVICE_INFO device{sizeof(device)};
|
||||
device.Address = target;
|
||||
if (BluetoothGetDeviceInfo(radio.handle, &device) == ERROR_SUCCESS) {
|
||||
return device.fConnected != FALSE;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Usable(const SDL_hid_device_info&) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Notice() {
|
||||
return Query().pairing ? std::string() : std::string("No Bluetooth adapter found.");
|
||||
}
|
||||
|
||||
} // namespace Wiimote::Platform
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,368 @@
|
||||
#include "bluetooth/bt_controller.h"
|
||||
#include "bluetooth/bt_sdp.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
using Bluetooth::BdAddr;
|
||||
using Bluetooth::Controller;
|
||||
using Bluetooth::LinkState;
|
||||
|
||||
int g_failures = 0;
|
||||
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
std::fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
struct FakeHost : Bluetooth::RemoteHost {
|
||||
std::array<bool, Bluetooth::kRemoteSlots> attached{};
|
||||
std::array<bool, Bluetooth::kRemoteSlots> wake{};
|
||||
std::array<std::deque<Bytes>, Bluetooth::kRemoteSlots> input;
|
||||
std::vector<std::pair<size_t, Bytes>> output;
|
||||
std::array<LinkState, Bluetooth::kRemoteSlots> states{};
|
||||
std::vector<size_t> guestDisconnects;
|
||||
bool inquiry = false;
|
||||
|
||||
bool IsAttached(size_t slot) const override { return attached[slot]; }
|
||||
bool ConsumeWake(size_t slot) override {
|
||||
const bool w = wake[slot];
|
||||
wake[slot] = false;
|
||||
return w;
|
||||
}
|
||||
bool PopInputReport(size_t slot, Bytes& report) override {
|
||||
if (input[slot].empty()) return false;
|
||||
report = input[slot].front();
|
||||
input[slot].pop_front();
|
||||
return true;
|
||||
}
|
||||
void SendOutputReport(size_t slot, const uint8_t* report, size_t size) override {
|
||||
output.emplace_back(slot, Bytes(report, report + size));
|
||||
}
|
||||
void OnLinkStateChanged(size_t slot, LinkState state) override { states[slot] = state; }
|
||||
void OnGuestDisconnect(size_t slot) override { guestDisconnects.push_back(slot); }
|
||||
void OnGuestInquiry(bool active) override { inquiry = active; }
|
||||
const char* DeviceName(size_t) const override { return "Nintendo RVL-CNT-01"; }
|
||||
};
|
||||
|
||||
uint16_t Le16(const Bytes& b, size_t i) {
|
||||
return static_cast<uint16_t>(b[i] | (b[i + 1] << 8));
|
||||
}
|
||||
|
||||
void PutLe16(Bytes& b, uint16_t v) {
|
||||
b.push_back(static_cast<uint8_t>(v));
|
||||
b.push_back(static_cast<uint8_t>(v >> 8));
|
||||
}
|
||||
|
||||
Bytes Command(uint16_t opcode, Bytes params) {
|
||||
Bytes packet;
|
||||
PutLe16(packet, opcode);
|
||||
packet.push_back(static_cast<uint8_t>(params.size()));
|
||||
packet.insert(packet.end(), params.begin(), params.end());
|
||||
return packet;
|
||||
}
|
||||
|
||||
struct Guest {
|
||||
Controller& controller;
|
||||
uint64_t now = 1000;
|
||||
|
||||
std::vector<Bytes> Run(uint16_t opcode, Bytes params = {}) {
|
||||
const Bytes packet = Command(opcode, std::move(params));
|
||||
controller.ExecuteCommand(packet.data(), packet.size(), now);
|
||||
return Events();
|
||||
}
|
||||
std::vector<Bytes> Events() {
|
||||
std::vector<Bytes> events;
|
||||
Bytes event;
|
||||
while (controller.PopEvent(event)) events.push_back(event);
|
||||
return events;
|
||||
}
|
||||
std::vector<Bytes> Acl() {
|
||||
std::vector<Bytes> packets;
|
||||
Bytes packet;
|
||||
while (controller.PopAcl(packet)) packets.push_back(packet);
|
||||
return packets;
|
||||
}
|
||||
void Tick() {
|
||||
now += 1000;
|
||||
controller.Tick(now);
|
||||
}
|
||||
void SendL2cap(uint16_t handle, uint16_t cid, const Bytes& payload) {
|
||||
Bytes packet;
|
||||
PutLe16(packet, static_cast<uint16_t>(handle | 0x2000));
|
||||
PutLe16(packet, static_cast<uint16_t>(payload.size() + 4));
|
||||
PutLe16(packet, static_cast<uint16_t>(payload.size()));
|
||||
PutLe16(packet, cid);
|
||||
packet.insert(packet.end(), payload.begin(), payload.end());
|
||||
controller.ReceiveAcl(packet.data(), packet.size(), now);
|
||||
}
|
||||
void Signal(uint16_t handle, uint8_t code, uint8_t ident, const Bytes& data) {
|
||||
Bytes payload = {code, ident};
|
||||
PutLe16(payload, static_cast<uint16_t>(data.size()));
|
||||
payload.insert(payload.end(), data.begin(), data.end());
|
||||
SendL2cap(handle, 0x0001, payload);
|
||||
}
|
||||
};
|
||||
|
||||
bool IsCommandComplete(const Bytes& e, uint16_t opcode) {
|
||||
return e.size() >= 6 && e[0] == 0x0E && Le16(e, 3) == opcode && e[5] == 0x00;
|
||||
}
|
||||
|
||||
bool IsCommandStatusOk(const Bytes& e, uint16_t opcode) {
|
||||
return e.size() == 6 && e[0] == 0x0F && e[2] == 0x00 && Le16(e, 4) == opcode;
|
||||
}
|
||||
|
||||
// Splits a signalling ACL packet from the remote into (code, ident, data).
|
||||
struct Signal {
|
||||
uint8_t code = 0;
|
||||
uint8_t ident = 0;
|
||||
Bytes data;
|
||||
};
|
||||
|
||||
std::vector<Signal> Signals(const std::vector<Bytes>& acl) {
|
||||
std::vector<Signal> out;
|
||||
for (const Bytes& packet : acl) {
|
||||
if (packet.size() < 8 || Le16(packet, 6) != 0x0001) continue;
|
||||
size_t i = 8;
|
||||
while (i + 4 <= packet.size()) {
|
||||
Signal s;
|
||||
s.code = packet[i];
|
||||
s.ident = packet[i + 1];
|
||||
const uint16_t length = Le16(packet, i + 2);
|
||||
s.data.assign(packet.begin() + static_cast<std::ptrdiff_t>(i + 4),
|
||||
packet.begin() + static_cast<std::ptrdiff_t>(i + 4 + length));
|
||||
out.push_back(s);
|
||||
i += 4 + length;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Opens one HID channel the remote requested and completes both configurations.
|
||||
uint16_t AcceptRemoteChannel(Guest& guest, uint16_t handle, uint16_t psm, uint16_t guestCid) {
|
||||
guest.Events();
|
||||
const auto requests = Signals(guest.Acl());
|
||||
CHECK(requests.size() == 1);
|
||||
if (requests.size() != 1) return 0;
|
||||
CHECK(requests[0].code == 0x02 && Le16(requests[0].data, 0) == psm);
|
||||
const uint16_t remoteCid = Le16(requests[0].data, 2);
|
||||
Bytes rsp;
|
||||
PutLe16(rsp, guestCid);
|
||||
PutLe16(rsp, remoteCid);
|
||||
PutLe16(rsp, 0);
|
||||
PutLe16(rsp, 0);
|
||||
guest.Signal(handle, 0x03, requests[0].ident, rsp);
|
||||
const auto config = Signals(guest.Acl());
|
||||
CHECK(config.size() == 1 && config[0].code == 0x04 && Le16(config[0].data, 0) == guestCid);
|
||||
Bytes cfg;
|
||||
PutLe16(cfg, remoteCid);
|
||||
PutLe16(cfg, 0);
|
||||
cfg.insert(cfg.end(), {0x01, 0x02, 0xA0, 0x02});
|
||||
guest.Signal(handle, 0x04, 0x40, cfg);
|
||||
const auto cfgRsp = Signals(guest.Acl());
|
||||
CHECK(cfgRsp.size() == 1 && cfgRsp[0].code == 0x05 && cfgRsp[0].ident == 0x40 &&
|
||||
Le16(cfgRsp[0].data, 0) == guestCid && Le16(cfgRsp[0].data, 4) == 0);
|
||||
Bytes ourCfgRsp;
|
||||
PutLe16(ourCfgRsp, remoteCid);
|
||||
PutLe16(ourCfgRsp, 0);
|
||||
PutLe16(ourCfgRsp, 0);
|
||||
if (!config.empty()) guest.Signal(handle, 0x05, config[0].ident, ourCfgRsp);
|
||||
return remoteCid;
|
||||
}
|
||||
|
||||
void TestBootAndConnection() {
|
||||
FakeHost host;
|
||||
Controller controller(host);
|
||||
Guest guest{controller};
|
||||
controller.Open(guest.now);
|
||||
|
||||
auto e = guest.Run(0x0C03);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C03));
|
||||
e = guest.Run(0x1005);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1005) && Le16(e[0], 6) == 339 && Le16(e[0], 9) == 10);
|
||||
e = guest.Run(0x1009);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1009) && e[0][11] == 0xFF);
|
||||
e = guest.Run(0x1001);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1001) && Le16(e[0], 7) == 0x40A7);
|
||||
e = guest.Run(0x1003);
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x1003));
|
||||
e = guest.Run(0x0C0D, {0, 0, 0, 0, 0, 0, 1});
|
||||
CHECK(e.size() == 2 && e[0][0] == 0x15 && e[0][2] == 4 && IsCommandComplete(e[1], 0x0C0D) &&
|
||||
Le16(e[1], 8) == 4);
|
||||
Bytes slot0Key;
|
||||
if (!e.empty() && e[0].size() >= 25) slot0Key.assign(e[0].begin() + 9, e[0].begin() + 25);
|
||||
e = guest.Run(0xFC4F, {0x00});
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0xFC4F));
|
||||
|
||||
// Attached remote, page scan off: nothing yet.
|
||||
host.attached[0] = true;
|
||||
host.wake[0] = true;
|
||||
guest.Tick();
|
||||
CHECK(guest.Events().empty());
|
||||
|
||||
e = guest.Run(0x0C1A, {0x02});
|
||||
CHECK(e.size() == 1 && IsCommandComplete(e[0], 0x0C1A));
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x04 && e[0][7] == 0x00);
|
||||
const BdAddr addr = Controller::RemoteAddress(0);
|
||||
|
||||
Bytes accept(addr.begin(), addr.end());
|
||||
accept.push_back(0x00);
|
||||
e = guest.Run(0x0409, accept);
|
||||
CHECK(e.size() == 3 && IsCommandStatusOk(e[0], 0x0409) && e[1][0] == 0x12 && e[2][0] == 0x03 &&
|
||||
e[2][2] == 0x00 && Le16(e[2], 3) == 0x0100);
|
||||
CHECK(host.states[0] == LinkState::Connecting);
|
||||
|
||||
// Authentication with the stored key.
|
||||
e = guest.Run(0x0411, {0x00, 0x01});
|
||||
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0411) && e[1][0] == 0x17);
|
||||
Bytes reply(addr.begin(), addr.end());
|
||||
reply.insert(reply.end(), slot0Key.begin(), slot0Key.end());
|
||||
e = guest.Run(0x040B, reply);
|
||||
CHECK(e.size() == 2 && IsCommandComplete(e[0], 0x040B) && e[1][0] == 0x06 && e[1][2] == 0x00);
|
||||
|
||||
guest.Tick();
|
||||
const uint16_t controlCid = AcceptRemoteChannel(guest, 0x0100, 0x0011, 0x0041);
|
||||
CHECK(host.states[0] == LinkState::Connecting);
|
||||
guest.Tick();
|
||||
const uint16_t interruptCid = AcceptRemoteChannel(guest, 0x0100, 0x0013, 0x0042);
|
||||
CHECK(controlCid != 0 && interruptCid != 0);
|
||||
CHECK(host.states[0] == LinkState::Ready);
|
||||
|
||||
// Input passthrough.
|
||||
host.input[0].push_back({0x30, 0x00, 0x08});
|
||||
guest.Tick();
|
||||
guest.Events();
|
||||
auto acl = guest.Acl();
|
||||
CHECK(acl.size() == 1);
|
||||
if (acl.size() == 1) {
|
||||
const Bytes& p = acl[0];
|
||||
CHECK(Le16(p, 0) == 0x2100 && Le16(p, 6) == 0x0042 && p[8] == 0xA1 && p[9] == 0x30 && p[11] == 0x08);
|
||||
}
|
||||
|
||||
// Output passthrough on the interrupt channel, and a SET_REPORT on control.
|
||||
guest.SendL2cap(0x0100, interruptCid, {0xA2, 0x11, 0x10});
|
||||
guest.SendL2cap(0x0100, controlCid, {0x52, 0x15, 0x00});
|
||||
CHECK(host.output.size() == 2 && host.output[0].second == Bytes({0x11, 0x10}) &&
|
||||
host.output[1].second == Bytes({0x15, 0x00}));
|
||||
acl = guest.Acl();
|
||||
CHECK(acl.size() == 1 && acl[0].size() == 9 && acl[0][8] == 0x00);
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x13 && e[0][2] == 1 && Le16(e[0], 3) == 0x0100 && Le16(e[0], 5) == 2);
|
||||
|
||||
// The game drops the remote: a real one powers off.
|
||||
e = guest.Run(0x0406, {0x00, 0x01, 0x13});
|
||||
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0406) && e[1][0] == 0x05 && e[1][5] == 0x16);
|
||||
CHECK(host.guestDisconnects.size() == 1 && host.states[0] == LinkState::Down);
|
||||
guest.Tick();
|
||||
CHECK(guest.Events().empty());
|
||||
}
|
||||
|
||||
void TestInquiryAndPaging() {
|
||||
FakeHost host;
|
||||
Controller controller(host);
|
||||
Guest guest{controller};
|
||||
controller.Open(guest.now);
|
||||
guest.Run(0x0C45, {0x01});
|
||||
host.attached[1] = true;
|
||||
host.wake[1] = true;
|
||||
guest.Tick(); // consumes the wake; page scan is off
|
||||
auto e = guest.Run(0x0401, {0x00, 0x8B, 0x9E, 0x03, 0x01});
|
||||
CHECK(e.size() == 1 && IsCommandStatusOk(e[0], 0x0401) && host.inquiry);
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x22 && e[0][8] == 0x01);
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x01);
|
||||
CHECK(!host.inquiry);
|
||||
Bytes create;
|
||||
const BdAddr addr = Controller::RemoteAddress(1);
|
||||
create.insert(create.end(), addr.begin(), addr.end());
|
||||
create.insert(create.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
|
||||
e = guest.Run(0x0405, create);
|
||||
CHECK(e.size() == 2 && IsCommandStatusOk(e[0], 0x0405) && e[1][0] == 0x03 && e[1][2] == 0x00 &&
|
||||
Le16(e[1], 3) == 0x0101);
|
||||
|
||||
// Paging a remote that is not there times out.
|
||||
const BdAddr missing = Controller::RemoteAddress(2);
|
||||
Bytes create2(missing.begin(), missing.end());
|
||||
create2.insert(create2.end(), {0x18, 0xCC, 0x01, 0x00, 0x00, 0x00, 0x00});
|
||||
e = guest.Run(0x0405, create2);
|
||||
CHECK(e.size() == 1);
|
||||
guest.now += 6'000'000;
|
||||
guest.Tick();
|
||||
e = guest.Events();
|
||||
CHECK(e.size() == 1 && e[0][0] == 0x03 && e[0][2] == 0x04);
|
||||
|
||||
// The guest opens SDP itself on a paged remote.
|
||||
Bytes sdpReq;
|
||||
PutLe16(sdpReq, 0x0001);
|
||||
PutLe16(sdpReq, 0x0050);
|
||||
guest.Signal(0x0101, 0x02, 0x07, sdpReq);
|
||||
const auto rsp = Signals(guest.Acl());
|
||||
CHECK(rsp.size() == 2 && rsp[0].code == 0x03 && Le16(rsp[0].data, 4) == 0 && rsp[1].code == 0x04);
|
||||
}
|
||||
|
||||
void TestSdp() {
|
||||
// ServiceAttributeRequest for the whole HID record.
|
||||
const Bytes request = {0x04, 0x00, 0x01, 0x00, 0x0E, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
|
||||
const Bytes response = Bluetooth::Sdp::HandleRequest(request.data(), request.size(), 4096);
|
||||
CHECK(response.size() > 10 && response[0] == 0x05);
|
||||
if (response.size() > 10) {
|
||||
const uint16_t count = static_cast<uint16_t>((response[5] << 8) | response[6]);
|
||||
CHECK(count == 0x01CC + 3 && response[7] == 0x36 && response[8] == 0x01 && response[9] == 0xCC);
|
||||
CHECK(response.back() == 0x00);
|
||||
}
|
||||
CHECK(Bluetooth::Sdp::WiiRemoteHidDescriptor().size() == 0xD9);
|
||||
|
||||
// Continuation with a small MTU reassembles to the same list.
|
||||
Bytes assembled;
|
||||
Bytes cont = {0x00};
|
||||
for (int guard = 0; guard < 64; ++guard) {
|
||||
Bytes req = {0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40,
|
||||
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF};
|
||||
req.insert(req.end(), cont.begin(), cont.end());
|
||||
req[4] = static_cast<uint8_t>(req.size() - 5);
|
||||
const Bytes part = Bluetooth::Sdp::HandleRequest(req.data(), req.size(), 48);
|
||||
CHECK(part.size() <= 48 && part[0] == 0x05);
|
||||
const uint16_t count = static_cast<uint16_t>((part[5] << 8) | part[6]);
|
||||
assembled.insert(assembled.end(), part.begin() + 7, part.begin() + 7 + count);
|
||||
cont.assign(part.begin() + 7 + count, part.end());
|
||||
if (cont.size() == 1 && cont[0] == 0) break;
|
||||
}
|
||||
CHECK(assembled.size() == 0x01CC + 3);
|
||||
|
||||
// Device ID record via ServiceSearchAttribute.
|
||||
const Bytes search = {0x06, 0x00, 0x03, 0x00, 0x0F, 0x35, 0x03, 0x19, 0x12, 0x00, 0xFF, 0xFF,
|
||||
0x35, 0x05, 0x0A, 0x00, 0x00, 0xFF, 0xFF, 0x00};
|
||||
const Bytes found = Bluetooth::Sdp::HandleRequest(search.data(), search.size(), 4096);
|
||||
CHECK(found.size() > 12 && found[0] == 0x07 && found[7] == 0x35 && found[9] == 0x36 && found[10] == 0x00 &&
|
||||
found[11] == 0x5A);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestBootAndConnection();
|
||||
TestInquiryAndPaging();
|
||||
TestSdp();
|
||||
if (g_failures != 0) {
|
||||
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::printf("bluetooth controller tests passed\n");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -93,6 +93,13 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(8, isFloat: true)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
else if (TryGetKnownEffectiveAddress(load.Address, knownConstants, out var knownLoadAddress) &&
|
||||
FlatWriteNeedsPolicy(knownLoadAddress))
|
||||
{
|
||||
// The flat view maps the MMIO window no-access, so a hardware register read would fault
|
||||
// without knowing its width. The checked read reaches the runtime's register HLE.
|
||||
sb.AppendLine($"{pad}{dest} = {CheckedMmioLoadHelper(load.SizeBytes)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{pad}{dest} = {GuestLoadHelper(load.SizeBytes, isFloat: false)}({RemapAddress(load.Address, types, localConstants, linkedAddressRemap)});");
|
||||
|
||||
@@ -84,6 +84,14 @@ public sealed partial class CxxLinearCodeGenerator
|
||||
private static string GuestLoadHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatReadHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
private static string CheckedMmioLoadHelper(int sizeBytes) => Bits(sizeBytes) switch
|
||||
{
|
||||
8 => "Memory::Read8",
|
||||
16 => "Memory::Read16",
|
||||
64 => "Memory::Read64",
|
||||
_ => "Memory::Read32"
|
||||
};
|
||||
|
||||
private static string GuestStoreHelper(int sizeBytes, bool isFloat) =>
|
||||
FlatWriteHelper(Bits(sizeBytes), isFloat);
|
||||
|
||||
|
||||
@@ -480,6 +480,41 @@ public class CodeGenTests
|
||||
Assert.Contains("MemoryInline::FlatWrite8((ctx->gpr[6] + -32768), static_cast<uint32_t>(ctx->gpr[3]));", code, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeGenerator_ReadsKnownMmioAddressesThroughCheckedHelper()
|
||||
{
|
||||
var function = new IrFunction(
|
||||
"known_mmio_load",
|
||||
"entry",
|
||||
new[]
|
||||
{
|
||||
new IrBasicBlock("entry", new IrInstruction[]
|
||||
{
|
||||
new IrAssign("r4", IrValue.Imm(unchecked((int)0xCD000000))),
|
||||
new IrLoad("r0", new IrAddress("r4", 0xC0), 4),
|
||||
new IrLoad("r5", new IrAddress("r3", 8), 4),
|
||||
new IrReturn(null)
|
||||
})
|
||||
});
|
||||
|
||||
var types = new RepresentationEnvironment(new Dictionary<string, ValueRepresentation>
|
||||
{
|
||||
["r0"] = ValueRepresentation.UInt32,
|
||||
["r3"] = ValueRepresentation.UInt32,
|
||||
["r4"] = ValueRepresentation.UInt32,
|
||||
["r5"] = ValueRepresentation.UInt32
|
||||
});
|
||||
var signature = new FunctionAbiClassification(
|
||||
"known_mmio_load",
|
||||
ValueRepresentation.Void);
|
||||
|
||||
var ssa = new SsaTransformer().Convert(function);
|
||||
var code = new CxxLinearCodeGenerator().Emit(0x80008020, ssa, signature, types);
|
||||
|
||||
Assert.Contains("= Memory::Read32(", code, StringComparison.Ordinal);
|
||||
Assert.Contains("= MemoryInline::FlatRead32((ctx->gpr[3] + 8));", code, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeGenerator_ReusesFctiwzStackLowWordForMatchingLoad()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user