8 changed files with 1786 additions and 3 deletions
-
26src/common/input.h
-
5src/input_common/CMakeLists.txt
-
615src/input_common/drivers/joycon.cpp
-
107src/input_common/drivers/joycon.h
-
382src/input_common/helpers/joycon_driver.cpp
-
146src/input_common/helpers/joycon_driver.h
-
494src/input_common/helpers/joycon_protocol/joycon_types.h
-
14src/input_common/main.cpp
@ -0,0 +1,615 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <fmt/format.h>
|
|||
|
|||
#include "common/param_package.h"
|
|||
#include "common/settings.h"
|
|||
#include "common/thread.h"
|
|||
#include "input_common/drivers/joycon.h"
|
|||
#include "input_common/helpers/joycon_driver.h"
|
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
|||
|
|||
namespace InputCommon { |
|||
|
|||
Joycons::Joycons(const std::string& input_engine_) : InputEngine(input_engine_) { |
|||
LOG_INFO(Input, "Joycon driver Initialization started"); |
|||
const int init_res = SDL_hid_init(); |
|||
if (init_res == 0) { |
|||
Setup(); |
|||
} else { |
|||
LOG_ERROR(Input, "Hidapi could not be initialized. failed with error = {}", init_res); |
|||
} |
|||
} |
|||
|
|||
Joycons::~Joycons() { |
|||
Reset(); |
|||
} |
|||
|
|||
void Joycons::Reset() { |
|||
scan_thread = {}; |
|||
for (const auto& device : left_joycons) { |
|||
if (!device) { |
|||
continue; |
|||
} |
|||
device->Stop(); |
|||
} |
|||
for (const auto& device : right_joycons) { |
|||
if (!device) { |
|||
continue; |
|||
} |
|||
device->Stop(); |
|||
} |
|||
for (const auto& device : pro_joycons) { |
|||
if (!device) { |
|||
continue; |
|||
} |
|||
device->Stop(); |
|||
} |
|||
SDL_hid_exit(); |
|||
} |
|||
|
|||
void Joycons::Setup() { |
|||
u32 port = 0; |
|||
for (auto& device : left_joycons) { |
|||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Left)); |
|||
device = std::make_shared<Joycon::JoyconDriver>(port++); |
|||
} |
|||
for (auto& device : right_joycons) { |
|||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Right)); |
|||
device = std::make_shared<Joycon::JoyconDriver>(port++); |
|||
} |
|||
for (auto& device : pro_joycons) { |
|||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Pro)); |
|||
device = std::make_shared<Joycon::JoyconDriver>(port++); |
|||
} |
|||
|
|||
if (!scan_thread_running) { |
|||
scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); }); |
|||
} |
|||
} |
|||
|
|||
void Joycons::ScanThread(std::stop_token stop_token) { |
|||
constexpr u16 nintendo_vendor_id = 0x057e; |
|||
Common::SetCurrentThreadName("yuzu:input:JoyconScanThread"); |
|||
scan_thread_running = true; |
|||
while (!stop_token.stop_requested()) { |
|||
SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0); |
|||
SDL_hid_device_info* cur_dev = devs; |
|||
|
|||
while (cur_dev) { |
|||
if (IsDeviceNew(cur_dev)) { |
|||
LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id, |
|||
cur_dev->product_id); |
|||
RegisterNewDevice(cur_dev); |
|||
} |
|||
cur_dev = cur_dev->next; |
|||
} |
|||
|
|||
std::this_thread::sleep_for(std::chrono::seconds(5)); |
|||
} |
|||
scan_thread_running = false; |
|||
} |
|||
|
|||
bool Joycons::IsDeviceNew(SDL_hid_device_info* device_info) const { |
|||
Joycon::ControllerType type{}; |
|||
Joycon::SerialNumber serial_number{}; |
|||
|
|||
const auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type); |
|||
if (result != Joycon::DriverResult::Success) { |
|||
return false; |
|||
} |
|||
|
|||
const auto result2 = Joycon::JoyconDriver::GetSerialNumber(device_info, serial_number); |
|||
if (result2 != Joycon::DriverResult::Success) { |
|||
return false; |
|||
} |
|||
|
|||
auto is_handle_identical = [&](std::shared_ptr<Joycon::JoyconDriver> device) { |
|||
if (!device) { |
|||
return false; |
|||
} |
|||
if (!device->IsConnected()) { |
|||
return false; |
|||
} |
|||
if (device->GetHandleSerialNumber() != serial_number) { |
|||
return false; |
|||
} |
|||
return true; |
|||
}; |
|||
|
|||
// Check if device already exist
|
|||
switch (type) { |
|||
case Joycon::ControllerType::Left: |
|||
for (const auto& device : left_joycons) { |
|||
if (is_handle_identical(device)) { |
|||
return false; |
|||
} |
|||
} |
|||
break; |
|||
case Joycon::ControllerType::Right: |
|||
for (const auto& device : right_joycons) { |
|||
if (is_handle_identical(device)) { |
|||
return false; |
|||
} |
|||
} |
|||
break; |
|||
case Joycon::ControllerType::Pro: |
|||
case Joycon::ControllerType::Grip: |
|||
for (const auto& device : pro_joycons) { |
|||
if (is_handle_identical(device)) { |
|||
return false; |
|||
} |
|||
} |
|||
break; |
|||
default: |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
void Joycons::RegisterNewDevice(SDL_hid_device_info* device_info) { |
|||
Joycon::ControllerType type{}; |
|||
auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type); |
|||
auto handle = GetNextFreeHandle(type); |
|||
if (handle == nullptr) { |
|||
LOG_WARNING(Input, "No free handles available"); |
|||
return; |
|||
} |
|||
if (result == Joycon::DriverResult::Success) { |
|||
result = handle->RequestDeviceAccess(device_info); |
|||
} |
|||
if (result == Joycon::DriverResult::Success) { |
|||
LOG_WARNING(Input, "Initialize device"); |
|||
|
|||
std::function<void(Joycon::Battery)> on_battery_data; |
|||
std::function<void(Joycon::Color)> on_button_data; |
|||
std::function<void(int, f32)> on_stick_data; |
|||
std::function<void(int, std::array<u8, 6>)> on_motion_data; |
|||
std::function<void(s16)> on_ring_data; |
|||
std::function<void(const std::vector<u8>&)> on_amiibo_data; |
|||
|
|||
const std::size_t port = handle->GetDevicePort(); |
|||
handle->on_battery_data = { |
|||
[this, port, type](Joycon::Battery value) { OnBatteryUpdate(port, type, value); }}; |
|||
handle->on_color_data = { |
|||
[this, port, type](Joycon::Color value) { OnColorUpdate(port, type, value); }}; |
|||
handle->on_button_data = { |
|||
[this, port, type](int id, bool value) { OnButtonUpdate(port, type, id, value); }}; |
|||
handle->on_stick_data = { |
|||
[this, port, type](int id, f32 value) { OnStickUpdate(port, type, id, value); }}; |
|||
handle->on_motion_data = {[this, port, type](int id, Joycon::MotionData value) { |
|||
OnMotionUpdate(port, type, id, value); |
|||
}}; |
|||
handle->on_ring_data = {[this](f32 ring_data) { OnRingConUpdate(ring_data); }}; |
|||
handle->on_amiibo_data = {[this, port](const std::vector<u8>& amiibo_data) { |
|||
OnAmiiboUpdate(port, amiibo_data); |
|||
}}; |
|||
handle->InitializeDevice(); |
|||
} |
|||
} |
|||
|
|||
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle( |
|||
Joycon::ControllerType type) const { |
|||
|
|||
if (type == Joycon::ControllerType::Left) { |
|||
for (const auto& device : left_joycons) { |
|||
if (!device->IsConnected()) { |
|||
return device; |
|||
} |
|||
} |
|||
} |
|||
if (type == Joycon::ControllerType::Right) { |
|||
for (const auto& device : right_joycons) { |
|||
if (!device->IsConnected()) { |
|||
return device; |
|||
} |
|||
} |
|||
} |
|||
if (type == Joycon::ControllerType::Pro || type == Joycon::ControllerType::Grip) { |
|||
for (const auto& device : pro_joycons) { |
|||
if (!device->IsConnected()) { |
|||
return device; |
|||
} |
|||
} |
|||
} |
|||
return nullptr; |
|||
} |
|||
|
|||
bool Joycons::IsVibrationEnabled(const PadIdentifier& identifier) { |
|||
const auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return false; |
|||
} |
|||
return handle->IsVibrationEnabled(); |
|||
} |
|||
|
|||
Common::Input::VibrationError Joycons::SetVibration( |
|||
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) { |
|||
const Joycon::VibrationValue native_vibration{ |
|||
.low_amplitude = vibration.low_amplitude, |
|||
.low_frequency = vibration.low_frequency, |
|||
.high_amplitude = vibration.high_amplitude, |
|||
.high_frequency = vibration.high_amplitude, |
|||
}; |
|||
auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return Common::Input::VibrationError::InvalidHandle; |
|||
} |
|||
|
|||
handle->SetVibration(native_vibration); |
|||
return Common::Input::VibrationError::None; |
|||
} |
|||
|
|||
void Joycons::SetLeds(const PadIdentifier& identifier, const Common::Input::LedStatus& led_status) { |
|||
auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return; |
|||
} |
|||
int led_config = led_status.led_1 ? 1 : 0; |
|||
led_config += led_status.led_2 ? 2 : 0; |
|||
led_config += led_status.led_3 ? 4 : 0; |
|||
led_config += led_status.led_4 ? 8 : 0; |
|||
|
|||
const auto result = handle->SetLedConfig(static_cast<u8>(led_config)); |
|||
if (result != Joycon::DriverResult::Success) { |
|||
LOG_ERROR(Input, "Failed to set led config"); |
|||
} |
|||
} |
|||
|
|||
Common::Input::CameraError Joycons::SetCameraFormat(const PadIdentifier& identifier_, |
|||
Common::Input::CameraFormat camera_format) { |
|||
return Common::Input::CameraError::NotSupported; |
|||
}; |
|||
|
|||
Common::Input::NfcState Joycons::SupportsNfc(const PadIdentifier& identifier_) const { |
|||
return Common::Input::NfcState::Success; |
|||
}; |
|||
|
|||
Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier_, |
|||
const std::vector<u8>& data) { |
|||
return Common::Input::NfcState::NotSupported; |
|||
}; |
|||
|
|||
Common::Input::PollingError Joycons::SetPollingMode(const PadIdentifier& identifier, |
|||
const Common::Input::PollingMode polling_mode) { |
|||
auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
LOG_ERROR(Input, "Invalid handle {}", identifier.port); |
|||
return Common::Input::PollingError::InvalidHandle; |
|||
} |
|||
|
|||
switch (polling_mode) { |
|||
case Common::Input::PollingMode::NFC: |
|||
handle->SetNfcMode(); |
|||
break; |
|||
case Common::Input::PollingMode::Active: |
|||
handle->SetActiveMode(); |
|||
break; |
|||
case Common::Input::PollingMode::Pasive: |
|||
handle->SetPasiveMode(); |
|||
break; |
|||
case Common::Input::PollingMode::Ring: |
|||
handle->SetRingConMode(); |
|||
break; |
|||
default: |
|||
return Common::Input::PollingError::NotSupported; |
|||
} |
|||
|
|||
return Common::Input::PollingError::None; |
|||
} |
|||
|
|||
void Joycons::OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, |
|||
Joycon::Battery value) { |
|||
const auto identifier = GetIdentifier(port, type); |
|||
if (value.charging != 0) { |
|||
SetBattery(identifier, Common::Input::BatteryLevel::Charging); |
|||
return; |
|||
} |
|||
|
|||
Common::Input::BatteryLevel battery{value.status.Value()}; |
|||
switch (value.status) { |
|||
case 0: |
|||
battery = Common::Input::BatteryLevel::Empty; |
|||
break; |
|||
case 1: |
|||
battery = Common::Input::BatteryLevel::Critical; |
|||
break; |
|||
case 2: |
|||
battery = Common::Input::BatteryLevel::Low; |
|||
break; |
|||
case 3: |
|||
battery = Common::Input::BatteryLevel::Medium; |
|||
break; |
|||
case 4: |
|||
default: |
|||
battery = Common::Input::BatteryLevel::Full; |
|||
break; |
|||
} |
|||
SetBattery(identifier, battery); |
|||
} |
|||
|
|||
void Joycons::OnColorUpdate(std::size_t port, Joycon::ControllerType type, |
|||
const Joycon::Color& value) {} |
|||
|
|||
void Joycons::OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value) { |
|||
const auto identifier = GetIdentifier(port, type); |
|||
SetButton(identifier, id, value); |
|||
} |
|||
|
|||
void Joycons::OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value) { |
|||
const auto identifier = GetIdentifier(port, type); |
|||
SetAxis(identifier, id, value); |
|||
} |
|||
|
|||
void Joycons::OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id, |
|||
const Joycon::MotionData& value) { |
|||
const auto identifier = GetIdentifier(port, type); |
|||
BasicMotion motion_data{ |
|||
.gyro_x = value.gyro_x, |
|||
.gyro_y = value.gyro_y, |
|||
.gyro_z = value.gyro_z, |
|||
.accel_x = value.accel_x, |
|||
.accel_y = value.accel_y, |
|||
.accel_z = value.accel_z, |
|||
.delta_timestamp = 15000, |
|||
}; |
|||
SetMotion(identifier, id, motion_data); |
|||
} |
|||
|
|||
void Joycons::OnRingConUpdate(f32 ring_data) { |
|||
// To simplify ring detection it will always be mapped to an empty identifier for all
|
|||
// controllers
|
|||
constexpr PadIdentifier identifier = { |
|||
.guid = Common::UUID{}, |
|||
.port = 0, |
|||
.pad = 0, |
|||
}; |
|||
SetAxis(identifier, 100, ring_data); |
|||
} |
|||
|
|||
void Joycons::OnAmiiboUpdate(std::size_t port, const std::vector<u8>& amiibo_data) { |
|||
const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right); |
|||
SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, amiibo_data}); |
|||
} |
|||
|
|||
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetHandle(PadIdentifier identifier) const { |
|||
auto is_handle_active = [&](std::shared_ptr<Joycon::JoyconDriver> device) { |
|||
if (!device) { |
|||
return false; |
|||
} |
|||
if (!device->IsConnected()) { |
|||
return false; |
|||
} |
|||
if (device->GetDevicePort() == identifier.port) { |
|||
return true; |
|||
} |
|||
return false; |
|||
}; |
|||
const auto type = static_cast<Joycon::ControllerType>(identifier.pad); |
|||
if (type == Joycon::ControllerType::Left) { |
|||
for (const auto& device : left_joycons) { |
|||
if (is_handle_active(device)) { |
|||
return device; |
|||
} |
|||
} |
|||
} |
|||
if (type == Joycon::ControllerType::Right) { |
|||
for (const auto& device : right_joycons) { |
|||
if (is_handle_active(device)) { |
|||
return device; |
|||
} |
|||
} |
|||
} |
|||
if (type == Joycon::ControllerType::Pro || type == Joycon::ControllerType::Grip) { |
|||
for (const auto& device : pro_joycons) { |
|||
if (is_handle_active(device)) { |
|||
return device; |
|||
} |
|||
} |
|||
} |
|||
return nullptr; |
|||
} |
|||
|
|||
PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const { |
|||
return { |
|||
.guid = Common::UUID{Common::InvalidUUID}, |
|||
.port = port, |
|||
.pad = static_cast<std::size_t>(type), |
|||
}; |
|||
} |
|||
|
|||
std::vector<Common::ParamPackage> Joycons::GetInputDevices() const { |
|||
std::vector<Common::ParamPackage> devices{}; |
|||
|
|||
auto add_entry = [&](std::shared_ptr<Joycon::JoyconDriver> device) { |
|||
if (!device) { |
|||
return; |
|||
} |
|||
if (!device->IsConnected()) { |
|||
return; |
|||
} |
|||
std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()), |
|||
device->GetDevicePort()); |
|||
devices.emplace_back(Common::ParamPackage{ |
|||
{"engine", GetEngineName()}, |
|||
{"display", std::move(name)}, |
|||
{"port", std::to_string(device->GetDevicePort())}, |
|||
{"pad", std::to_string(static_cast<std::size_t>(device->GetHandleDeviceType()))}, |
|||
}); |
|||
}; |
|||
|
|||
for (const auto& controller : left_joycons) { |
|||
add_entry(controller); |
|||
} |
|||
for (const auto& controller : right_joycons) { |
|||
add_entry(controller); |
|||
} |
|||
for (const auto& controller : pro_joycons) { |
|||
add_entry(controller); |
|||
} |
|||
|
|||
return devices; |
|||
} |
|||
|
|||
ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) { |
|||
static constexpr std::array<std::pair<Settings::NativeButton::Values, Joycon::PadButton>, 20> |
|||
switch_to_joycon_button = { |
|||
std::pair{Settings::NativeButton::A, Joycon::PadButton::A}, |
|||
{Settings::NativeButton::B, Joycon::PadButton::B}, |
|||
{Settings::NativeButton::X, Joycon::PadButton::X}, |
|||
{Settings::NativeButton::Y, Joycon::PadButton::Y}, |
|||
{Settings::NativeButton::DLeft, Joycon::PadButton::Left}, |
|||
{Settings::NativeButton::DUp, Joycon::PadButton::Up}, |
|||
{Settings::NativeButton::DRight, Joycon::PadButton::Right}, |
|||
{Settings::NativeButton::DDown, Joycon::PadButton::Down}, |
|||
{Settings::NativeButton::SL, Joycon::PadButton::LeftSL}, |
|||
{Settings::NativeButton::SR, Joycon::PadButton::LeftSR}, |
|||
{Settings::NativeButton::L, Joycon::PadButton::L}, |
|||
{Settings::NativeButton::R, Joycon::PadButton::R}, |
|||
{Settings::NativeButton::ZL, Joycon::PadButton::ZL}, |
|||
{Settings::NativeButton::ZR, Joycon::PadButton::ZR}, |
|||
{Settings::NativeButton::Plus, Joycon::PadButton::Plus}, |
|||
{Settings::NativeButton::Minus, Joycon::PadButton::Minus}, |
|||
{Settings::NativeButton::Home, Joycon::PadButton::Home}, |
|||
{Settings::NativeButton::Screenshot, Joycon::PadButton::Capture}, |
|||
{Settings::NativeButton::LStick, Joycon::PadButton::StickL}, |
|||
{Settings::NativeButton::RStick, Joycon::PadButton::StickR}, |
|||
}; |
|||
|
|||
if (!params.Has("port")) { |
|||
return {}; |
|||
} |
|||
|
|||
ButtonMapping mapping{}; |
|||
for (const auto& [switch_button, joycon_button] : switch_to_joycon_button) { |
|||
Common::ParamPackage button_params{}; |
|||
button_params.Set("engine", GetEngineName()); |
|||
button_params.Set("port", params.Get("port", 0)); |
|||
button_params.Set("button", static_cast<int>(joycon_button)); |
|||
mapping.insert_or_assign(switch_button, std::move(button_params)); |
|||
} |
|||
|
|||
return mapping; |
|||
} |
|||
|
|||
AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) { |
|||
if (!params.Has("port")) { |
|||
return {}; |
|||
} |
|||
|
|||
AnalogMapping mapping = {}; |
|||
Common::ParamPackage left_analog_params; |
|||
left_analog_params.Set("engine", GetEngineName()); |
|||
left_analog_params.Set("port", params.Get("port", 0)); |
|||
left_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::LeftStickX)); |
|||
left_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::LeftStickY)); |
|||
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params)); |
|||
Common::ParamPackage right_analog_params; |
|||
right_analog_params.Set("engine", GetEngineName()); |
|||
right_analog_params.Set("port", params.Get("port", 0)); |
|||
right_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::RightStickX)); |
|||
right_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::RightStickY)); |
|||
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params)); |
|||
return mapping; |
|||
} |
|||
|
|||
MotionMapping Joycons::GetMotionMappingForDevice(const Common::ParamPackage& params) { |
|||
if (!params.Has("port")) { |
|||
return {}; |
|||
} |
|||
|
|||
MotionMapping mapping = {}; |
|||
Common::ParamPackage left_motion_params; |
|||
left_motion_params.Set("engine", GetEngineName()); |
|||
left_motion_params.Set("port", params.Get("port", 0)); |
|||
left_motion_params.Set("motion", 0); |
|||
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params)); |
|||
Common::ParamPackage right_Motion_params; |
|||
right_Motion_params.Set("engine", GetEngineName()); |
|||
right_Motion_params.Set("port", params.Get("port", 0)); |
|||
right_Motion_params.Set("motion", 1); |
|||
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_Motion_params)); |
|||
return mapping; |
|||
} |
|||
|
|||
Common::Input::ButtonNames Joycons::GetUIButtonName(const Common::ParamPackage& params) const { |
|||
const auto button = static_cast<Joycon::PadButton>(params.Get("button", 0)); |
|||
switch (button) { |
|||
case Joycon::PadButton::Left: |
|||
return Common::Input::ButtonNames::ButtonLeft; |
|||
case Joycon::PadButton::Right: |
|||
return Common::Input::ButtonNames::ButtonRight; |
|||
case Joycon::PadButton::Down: |
|||
return Common::Input::ButtonNames::ButtonDown; |
|||
case Joycon::PadButton::Up: |
|||
return Common::Input::ButtonNames::ButtonUp; |
|||
case Joycon::PadButton::LeftSL: |
|||
case Joycon::PadButton::RightSL: |
|||
return Common::Input::ButtonNames::TriggerSL; |
|||
case Joycon::PadButton::LeftSR: |
|||
case Joycon::PadButton::RightSR: |
|||
return Common::Input::ButtonNames::TriggerSR; |
|||
case Joycon::PadButton::L: |
|||
return Common::Input::ButtonNames::TriggerL; |
|||
case Joycon::PadButton::R: |
|||
return Common::Input::ButtonNames::TriggerR; |
|||
case Joycon::PadButton::ZL: |
|||
return Common::Input::ButtonNames::TriggerZL; |
|||
case Joycon::PadButton::ZR: |
|||
return Common::Input::ButtonNames::TriggerZR; |
|||
case Joycon::PadButton::A: |
|||
return Common::Input::ButtonNames::ButtonA; |
|||
case Joycon::PadButton::B: |
|||
return Common::Input::ButtonNames::ButtonB; |
|||
case Joycon::PadButton::X: |
|||
return Common::Input::ButtonNames::ButtonX; |
|||
case Joycon::PadButton::Y: |
|||
return Common::Input::ButtonNames::ButtonY; |
|||
case Joycon::PadButton::Plus: |
|||
return Common::Input::ButtonNames::ButtonPlus; |
|||
case Joycon::PadButton::Minus: |
|||
return Common::Input::ButtonNames::ButtonMinus; |
|||
case Joycon::PadButton::Home: |
|||
return Common::Input::ButtonNames::ButtonHome; |
|||
case Joycon::PadButton::Capture: |
|||
return Common::Input::ButtonNames::ButtonCapture; |
|||
case Joycon::PadButton::StickL: |
|||
return Common::Input::ButtonNames::ButtonStickL; |
|||
case Joycon::PadButton::StickR: |
|||
return Common::Input::ButtonNames::ButtonStickR; |
|||
default: |
|||
return Common::Input::ButtonNames::Undefined; |
|||
} |
|||
} |
|||
|
|||
Common::Input::ButtonNames Joycons::GetUIName(const Common::ParamPackage& params) const { |
|||
if (params.Has("button")) { |
|||
return GetUIButtonName(params); |
|||
} |
|||
if (params.Has("axis")) { |
|||
return Common::Input::ButtonNames::Value; |
|||
} |
|||
if (params.Has("motion")) { |
|||
return Common::Input::ButtonNames::Engine; |
|||
} |
|||
|
|||
return Common::Input::ButtonNames::Invalid; |
|||
} |
|||
|
|||
std::string Joycons::JoyconName(Joycon::ControllerType type) const { |
|||
switch (type) { |
|||
case Joycon::ControllerType::Left: |
|||
return "Left Joycon"; |
|||
case Joycon::ControllerType::Right: |
|||
return "Right Joycon"; |
|||
case Joycon::ControllerType::Pro: |
|||
return "Pro Controller"; |
|||
case Joycon::ControllerType::Grip: |
|||
return "Grip Controller"; |
|||
default: |
|||
return "Unknow Joycon"; |
|||
} |
|||
} |
|||
} // namespace InputCommon
|
|||
@ -0,0 +1,107 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <array> |
|||
#include <span> |
|||
#include <thread> |
|||
#include <SDL_hidapi.h> |
|||
|
|||
#include "input_common/input_engine.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
using SerialNumber = std::array<u8, 15>; |
|||
struct Battery; |
|||
struct Color; |
|||
struct MotionData; |
|||
enum class ControllerType; |
|||
enum class DriverResult; |
|||
class JoyconDriver; |
|||
} // namespace InputCommon::Joycon |
|||
|
|||
namespace InputCommon { |
|||
|
|||
class Joycons final : public InputCommon::InputEngine { |
|||
public: |
|||
explicit Joycons(const std::string& input_engine_); |
|||
|
|||
~Joycons(); |
|||
|
|||
bool IsVibrationEnabled(const PadIdentifier& identifier) override; |
|||
Common::Input::VibrationError SetVibration( |
|||
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; |
|||
|
|||
void SetLeds(const PadIdentifier& identifier, |
|||
const Common::Input::LedStatus& led_status) override; |
|||
|
|||
Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_, |
|||
Common::Input::CameraFormat camera_format) override; |
|||
|
|||
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override; |
|||
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier_, |
|||
const std::vector<u8>& data) override; |
|||
|
|||
Common::Input::PollingError SetPollingMode( |
|||
const PadIdentifier& identifier, const Common::Input::PollingMode polling_mode) override; |
|||
|
|||
/// Used for automapping features |
|||
std::vector<Common::ParamPackage> GetInputDevices() const override; |
|||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override; |
|||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override; |
|||
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override; |
|||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override; |
|||
|
|||
private: |
|||
static constexpr std::size_t MaxSupportedControllers = 8; |
|||
|
|||
/// For shutting down, clear all data, join all threads, release usb devices |
|||
void Reset(); |
|||
|
|||
/// Registers controllers, clears all data and starts the scan thread |
|||
void Setup(); |
|||
|
|||
/// Actively searchs for new devices |
|||
void ScanThread(std::stop_token stop_token); |
|||
|
|||
/// Returns true if device is valid and not registered |
|||
bool IsDeviceNew(SDL_hid_device_info* device_info) const; |
|||
|
|||
/// Tries to connect to the new device |
|||
void RegisterNewDevice(SDL_hid_device_info* device_info); |
|||
|
|||
/// Returns the next free handle |
|||
std::shared_ptr<Joycon::JoyconDriver> GetNextFreeHandle(Joycon::ControllerType type) const; |
|||
|
|||
void OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, Joycon::Battery value); |
|||
void OnColorUpdate(std::size_t port, Joycon::ControllerType type, const Joycon::Color& value); |
|||
void OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value); |
|||
void OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value); |
|||
void OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id, |
|||
const Joycon::MotionData& value); |
|||
void OnRingConUpdate(f32 ring_data); |
|||
void OnAmiiboUpdate(std::size_t port, const std::vector<u8>& amiibo_data); |
|||
|
|||
/// Returns a JoyconHandle corresponding to a PadIdentifier |
|||
std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const; |
|||
|
|||
/// Returns a PadIdentifier corresponding to the port number |
|||
PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const; |
|||
|
|||
std::string JoyconName(std::size_t port) const; |
|||
|
|||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const; |
|||
|
|||
/// Returns the name of the device in text format |
|||
std::string JoyconName(Joycon::ControllerType type) const; |
|||
|
|||
std::jthread scan_thread; |
|||
bool scan_thread_running{}; |
|||
|
|||
// Joycon types are split by type to ease supporting dualjoycon configurations |
|||
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> left_joycons{}; |
|||
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> right_joycons{}; |
|||
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> pro_joycons{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon |
|||
@ -0,0 +1,382 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "common/logging/log.h"
|
|||
#include "common/swap.h"
|
|||
#include "common/thread.h"
|
|||
#include "input_common/helpers/joycon_driver.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
JoyconDriver::JoyconDriver(std::size_t port_) : port{port_} { |
|||
hidapi_handle = std::make_shared<JoyconHandle>(); |
|||
} |
|||
|
|||
JoyconDriver::~JoyconDriver() { |
|||
Stop(); |
|||
} |
|||
|
|||
void JoyconDriver::Stop() { |
|||
is_connected = false; |
|||
input_thread = {}; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::RequestDeviceAccess(SDL_hid_device_info* device_info) { |
|||
std::scoped_lock lock{mutex}; |
|||
|
|||
handle_device_type = ControllerType::None; |
|||
GetDeviceType(device_info, handle_device_type); |
|||
if (handle_device_type == ControllerType::None) { |
|||
return DriverResult::UnsupportedControllerType; |
|||
} |
|||
|
|||
hidapi_handle->handle = |
|||
SDL_hid_open(device_info->vendor_id, device_info->product_id, device_info->serial_number); |
|||
std::memcpy(&handle_serial_number, device_info->serial_number, 15); |
|||
if (!hidapi_handle->handle) { |
|||
LOG_ERROR(Input, "Yuzu can't gain access to this device: ID {:04X}:{:04X}.", |
|||
device_info->vendor_id, device_info->product_id); |
|||
return DriverResult::HandleInUse; |
|||
} |
|||
SDL_hid_set_nonblocking(hidapi_handle->handle, 1); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::InitializeDevice() { |
|||
if (!hidapi_handle->handle) { |
|||
return DriverResult::InvalidHandle; |
|||
} |
|||
std::scoped_lock lock{mutex}; |
|||
disable_input_thread = true; |
|||
|
|||
// Reset Counters
|
|||
error_counter = 0; |
|||
hidapi_handle->packet_counter = 0; |
|||
|
|||
// Set HW default configuration
|
|||
vibration_enabled = true; |
|||
motion_enabled = true; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
gyro_sensitivity = Joycon::GyroSensitivity::DPS2000; |
|||
gyro_performance = Joycon::GyroPerformance::HZ833; |
|||
accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8; |
|||
accelerometer_performance = Joycon::AccelerometerPerformance::HZ100; |
|||
|
|||
// Initialize HW Protocols
|
|||
|
|||
// Get fixed joycon info
|
|||
supported_features = GetSupportedFeatures(); |
|||
|
|||
// Get Calibration data
|
|||
|
|||
// Set led status
|
|||
|
|||
// Apply HW configuration
|
|||
SetPollingMode(); |
|||
|
|||
// Start pooling for data
|
|||
is_connected = true; |
|||
if (!input_thread_running) { |
|||
input_thread = |
|||
std::jthread([this](std::stop_token stop_token) { InputThread(stop_token); }); |
|||
} |
|||
|
|||
disable_input_thread = false; |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
void JoyconDriver::InputThread(std::stop_token stop_token) { |
|||
LOG_INFO(Input, "JC Adapter input thread started"); |
|||
Common::SetCurrentThreadName("JoyconInput"); |
|||
input_thread_running = true; |
|||
|
|||
// Max update rate is 5ms, ensure we are always able to read a bit faster
|
|||
constexpr int ThreadDelay = 2; |
|||
std::vector<u8> buffer(MaxBufferSize); |
|||
|
|||
while (!stop_token.stop_requested()) { |
|||
int status = 0; |
|||
|
|||
if (!IsInputThreadValid()) { |
|||
input_thread.request_stop(); |
|||
continue; |
|||
} |
|||
|
|||
// By disabling the input thread we can ensure custom commands will succeed as no package is
|
|||
// skipped
|
|||
if (!disable_input_thread) { |
|||
status = SDL_hid_read_timeout(hidapi_handle->handle, buffer.data(), buffer.size(), |
|||
ThreadDelay); |
|||
} else { |
|||
std::this_thread::sleep_for(std::chrono::milliseconds(ThreadDelay)); |
|||
} |
|||
|
|||
if (IsPayloadCorrect(status, buffer)) { |
|||
OnNewData(buffer); |
|||
} |
|||
|
|||
std::this_thread::yield(); |
|||
} |
|||
|
|||
is_connected = false; |
|||
input_thread_running = false; |
|||
LOG_INFO(Input, "JC Adapter input thread stopped"); |
|||
} |
|||
|
|||
void JoyconDriver::OnNewData(std::span<u8> buffer) { |
|||
const auto report_mode = static_cast<InputReport>(buffer[0]); |
|||
|
|||
switch (report_mode) { |
|||
case InputReport::STANDARD_FULL_60HZ: |
|||
ReadActiveMode(buffer); |
|||
break; |
|||
case InputReport::NFC_IR_MODE_60HZ: |
|||
ReadNfcIRMode(buffer); |
|||
break; |
|||
case InputReport::SIMPLE_HID_MODE: |
|||
ReadPassiveMode(buffer); |
|||
break; |
|||
default: |
|||
LOG_ERROR(Input, "Report mode not Implemented {}", report_mode); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void JoyconDriver::SetPollingMode() { |
|||
disable_input_thread = true; |
|||
disable_input_thread = false; |
|||
} |
|||
|
|||
JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() { |
|||
SupportedFeatures features{ |
|||
.passive = true, |
|||
.motion = true, |
|||
.vibration = true, |
|||
}; |
|||
|
|||
if (device_type == ControllerType::Right) { |
|||
features.nfc = true; |
|||
features.irs = true; |
|||
features.hidbus = true; |
|||
} |
|||
|
|||
if (device_type == ControllerType::Pro) { |
|||
features.nfc = true; |
|||
} |
|||
return features; |
|||
} |
|||
|
|||
void JoyconDriver::ReadActiveMode(std::span<u8> buffer) { |
|||
InputReportActive data{}; |
|||
memcpy(&data, buffer.data(), sizeof(InputReportActive)); |
|||
|
|||
// Packages can be a litte bit inconsistent. Average the delta time to provide a smoother motion
|
|||
// experience
|
|||
const auto now = std::chrono::steady_clock::now(); |
|||
const auto new_delta_time = |
|||
std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count(); |
|||
delta_time = static_cast<u64>((delta_time * 0.8f) + (new_delta_time * 0.2)); |
|||
last_update = now; |
|||
|
|||
switch (device_type) { |
|||
case Joycon::ControllerType::Left: |
|||
break; |
|||
case Joycon::ControllerType::Right: |
|||
break; |
|||
case Joycon::ControllerType::Pro: |
|||
break; |
|||
case Joycon::ControllerType::Grip: |
|||
case Joycon::ControllerType::Dual: |
|||
case Joycon::ControllerType::None: |
|||
break; |
|||
} |
|||
|
|||
on_battery_data(data.battery_status); |
|||
on_color_data(color); |
|||
} |
|||
|
|||
void JoyconDriver::ReadPassiveMode(std::span<u8> buffer) { |
|||
InputReportPassive data{}; |
|||
memcpy(&data, buffer.data(), sizeof(InputReportPassive)); |
|||
|
|||
switch (device_type) { |
|||
case Joycon::ControllerType::Left: |
|||
break; |
|||
case Joycon::ControllerType::Right: |
|||
break; |
|||
case Joycon::ControllerType::Pro: |
|||
break; |
|||
case Joycon::ControllerType::Grip: |
|||
case Joycon::ControllerType::Dual: |
|||
case Joycon::ControllerType::None: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void JoyconDriver::ReadNfcIRMode(std::span<u8> buffer) { |
|||
// This mode is compatible with the active mode
|
|||
ReadActiveMode(buffer); |
|||
|
|||
if (!nfc_enabled) { |
|||
return; |
|||
} |
|||
} |
|||
|
|||
bool JoyconDriver::IsInputThreadValid() const { |
|||
if (!is_connected) { |
|||
return false; |
|||
} |
|||
if (hidapi_handle->handle == nullptr) { |
|||
return false; |
|||
} |
|||
// Controller is not responding. Terminate connection
|
|||
if (error_counter > MaxErrorCount) { |
|||
return false; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
bool JoyconDriver::IsPayloadCorrect(int status, std::span<const u8> buffer) { |
|||
if (status <= -1) { |
|||
error_counter++; |
|||
return false; |
|||
} |
|||
// There's no new data
|
|||
if (status == 0) { |
|||
return false; |
|||
} |
|||
// No reply ever starts with zero
|
|||
if (buffer[0] == 0x00) { |
|||
error_counter++; |
|||
return false; |
|||
} |
|||
error_counter = 0; |
|||
return true; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetVibration(const VibrationValue& vibration) { |
|||
std::scoped_lock lock{mutex}; |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetLedConfig(u8 led_pattern) { |
|||
std::scoped_lock lock{mutex}; |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetPasiveMode() { |
|||
motion_enabled = false; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = true; |
|||
SetPollingMode(); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetActiveMode() { |
|||
motion_enabled = false; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
SetPollingMode(); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetNfcMode() { |
|||
motion_enabled = false; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = true; |
|||
passive_enabled = false; |
|||
SetPollingMode(); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetRingConMode() { |
|||
motion_enabled = true; |
|||
hidbus_enabled = true; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
SetPollingMode(); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
bool JoyconDriver::IsConnected() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return is_connected; |
|||
} |
|||
|
|||
bool JoyconDriver::IsVibrationEnabled() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return vibration_enabled; |
|||
} |
|||
|
|||
FirmwareVersion JoyconDriver::GetDeviceVersion() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return version; |
|||
} |
|||
|
|||
Color JoyconDriver::GetDeviceColor() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return color; |
|||
} |
|||
|
|||
std::size_t JoyconDriver::GetDevicePort() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return port; |
|||
} |
|||
|
|||
ControllerType JoyconDriver::GetDeviceType() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return handle_device_type; |
|||
} |
|||
|
|||
ControllerType JoyconDriver::GetHandleDeviceType() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return handle_device_type; |
|||
} |
|||
|
|||
SerialNumber JoyconDriver::GetSerialNumber() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return serial_number; |
|||
} |
|||
|
|||
SerialNumber JoyconDriver::GetHandleSerialNumber() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return handle_serial_number; |
|||
} |
|||
|
|||
Joycon::DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info, |
|||
ControllerType& controller_type) { |
|||
std::array<std::pair<u32, Joycon::ControllerType>, 4> supported_devices{ |
|||
std::pair<u32, Joycon::ControllerType>{0x2006, Joycon::ControllerType::Left}, |
|||
{0x2007, Joycon::ControllerType::Right}, |
|||
{0x2009, Joycon::ControllerType::Pro}, |
|||
{0x200E, Joycon::ControllerType::Grip}, |
|||
}; |
|||
constexpr u16 nintendo_vendor_id = 0x057e; |
|||
|
|||
controller_type = Joycon::ControllerType::None; |
|||
if (device_info->vendor_id != nintendo_vendor_id) { |
|||
return Joycon::DriverResult::UnsupportedControllerType; |
|||
} |
|||
|
|||
for (const auto& [product_id, type] : supported_devices) { |
|||
if (device_info->product_id == static_cast<u16>(product_id)) { |
|||
controller_type = type; |
|||
return Joycon::DriverResult::Success; |
|||
} |
|||
} |
|||
return Joycon::DriverResult::UnsupportedControllerType; |
|||
} |
|||
|
|||
Joycon::DriverResult JoyconDriver::GetSerialNumber(SDL_hid_device_info* device_info, |
|||
Joycon::SerialNumber& serial_number) { |
|||
if (device_info->serial_number == nullptr) { |
|||
return Joycon::DriverResult::Unknown; |
|||
} |
|||
std::memcpy(&serial_number, device_info->serial_number, 15); |
|||
return Joycon::DriverResult::Success; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,146 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <functional> |
|||
#include <mutex> |
|||
#include <span> |
|||
#include <thread> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
class JoyconDriver final { |
|||
public: |
|||
explicit JoyconDriver(std::size_t port_); |
|||
|
|||
~JoyconDriver(); |
|||
|
|||
DriverResult RequestDeviceAccess(SDL_hid_device_info* device_info); |
|||
DriverResult InitializeDevice(); |
|||
void Stop(); |
|||
|
|||
bool IsConnected() const; |
|||
bool IsVibrationEnabled() const; |
|||
|
|||
FirmwareVersion GetDeviceVersion() const; |
|||
Color GetDeviceColor() const; |
|||
std::size_t GetDevicePort() const; |
|||
ControllerType GetDeviceType() const; |
|||
ControllerType GetHandleDeviceType() const; |
|||
SerialNumber GetSerialNumber() const; |
|||
SerialNumber GetHandleSerialNumber() const; |
|||
|
|||
DriverResult SetVibration(const VibrationValue& vibration); |
|||
DriverResult SetLedConfig(u8 led_pattern); |
|||
DriverResult SetPasiveMode(); |
|||
DriverResult SetActiveMode(); |
|||
DriverResult SetNfcMode(); |
|||
DriverResult SetRingConMode(); |
|||
|
|||
// Returns device type from hidapi handle |
|||
static Joycon::DriverResult GetDeviceType(SDL_hid_device_info* device_info, |
|||
Joycon::ControllerType& controller_type); |
|||
|
|||
// Returns serial number from hidapi handle |
|||
static Joycon::DriverResult GetSerialNumber(SDL_hid_device_info* device_info, |
|||
Joycon::SerialNumber& serial_number); |
|||
|
|||
std::function<void(Battery)> on_battery_data; |
|||
std::function<void(Color)> on_color_data; |
|||
std::function<void(int, bool)> on_button_data; |
|||
std::function<void(int, f32)> on_stick_data; |
|||
std::function<void(int, MotionData)> on_motion_data; |
|||
std::function<void(f32)> on_ring_data; |
|||
std::function<void(const std::vector<u8>&)> on_amiibo_data; |
|||
|
|||
private: |
|||
struct SupportedFeatures { |
|||
bool passive{}; |
|||
bool hidbus{}; |
|||
bool irs{}; |
|||
bool motion{}; |
|||
bool nfc{}; |
|||
bool vibration{}; |
|||
}; |
|||
|
|||
/// Main thread, actively request new data from the handle |
|||
void InputThread(std::stop_token stop_token); |
|||
|
|||
/// Called everytime a valid package arrives |
|||
void OnNewData(std::span<u8> buffer); |
|||
|
|||
/// Updates device configuration to enable or disable features |
|||
void SetPollingMode(); |
|||
|
|||
/// Returns true if input thread is valid and doesn't need to be stopped |
|||
bool IsInputThreadValid() const; |
|||
|
|||
/// Returns true if the data should be interpreted. Otherwise the error counter is incremented |
|||
bool IsPayloadCorrect(int status, std::span<const u8> buffer); |
|||
|
|||
/// Returns a list of supported features that can be enabled on this device |
|||
SupportedFeatures GetSupportedFeatures(); |
|||
|
|||
/// Handles data from passive packages |
|||
void ReadPassiveMode(std::span<u8> buffer); |
|||
|
|||
/// Handles data from active packages |
|||
void ReadActiveMode(std::span<u8> buffer); |
|||
|
|||
/// Handles data from nfc or ir packages |
|||
void ReadNfcIRMode(std::span<u8> buffer); |
|||
|
|||
// Protocol Features |
|||
|
|||
// Connection status |
|||
bool is_connected{}; |
|||
u64 delta_time; |
|||
std::size_t error_counter{}; |
|||
std::shared_ptr<JoyconHandle> hidapi_handle = nullptr; |
|||
std::chrono::time_point<std::chrono::steady_clock> last_update; |
|||
|
|||
// External device status |
|||
bool starlink_connected{}; |
|||
bool ring_connected{}; |
|||
bool amiibo_detected{}; |
|||
|
|||
// Harware configuration |
|||
u8 leds{}; |
|||
ReportMode mode{}; |
|||
bool passive_enabled{}; // Low power mode, Ideal for multiple controllers at the same time |
|||
bool hidbus_enabled{}; // External device support |
|||
bool irs_enabled{}; // Infrared camera input |
|||
bool motion_enabled{}; // Enables motion input |
|||
bool nfc_enabled{}; // Enables Amiibo detection |
|||
bool vibration_enabled{}; // Allows vibrations |
|||
|
|||
// Calibration data |
|||
GyroSensitivity gyro_sensitivity{}; |
|||
GyroPerformance gyro_performance{}; |
|||
AccelerometerSensitivity accelerometer_sensitivity{}; |
|||
AccelerometerPerformance accelerometer_performance{}; |
|||
JoyStickCalibration left_stick_calibration{}; |
|||
JoyStickCalibration right_stick_calibration{}; |
|||
MotionCalibration motion_calibration{}; |
|||
|
|||
// Fixed joycon info |
|||
FirmwareVersion version{}; |
|||
Color color{}; |
|||
std::size_t port{}; |
|||
ControllerType device_type{}; // Device type reported by controller |
|||
ControllerType handle_device_type{}; // Device type reported by hidapi |
|||
SerialNumber serial_number{}; // Serial number reported by controller |
|||
SerialNumber handle_serial_number{}; // Serial number type reported by hidapi |
|||
SupportedFeatures supported_features{}; |
|||
|
|||
// Thread related |
|||
mutable std::mutex mutex; |
|||
std::jthread input_thread; |
|||
bool input_thread_running{}; |
|||
bool disable_input_thread{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,494 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse |
|||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c |
|||
// https://github.com/CTCaer/jc_toolkit |
|||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering |
|||
|
|||
#pragma once |
|||
|
|||
#include <array> |
|||
#include <functional> |
|||
#include <SDL_hidapi.h> |
|||
|
|||
#include "common/bit_field.h" |
|||
#include "common/common_funcs.h" |
|||
#include "common/common_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
constexpr u32 MaxErrorCount = 50; |
|||
constexpr u32 MaxBufferSize = 60; |
|||
constexpr u32 MaxResponseSize = 49; |
|||
constexpr u32 MaxSubCommandResponseSize = 64; |
|||
constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40}; |
|||
|
|||
using MacAddress = std::array<u8, 6>; |
|||
using SerialNumber = std::array<u8, 15>; |
|||
|
|||
enum class ControllerType { |
|||
None, |
|||
Left, |
|||
Right, |
|||
Pro, |
|||
Grip, |
|||
Dual, |
|||
}; |
|||
|
|||
enum class PadAxes { |
|||
LeftStickX, |
|||
LeftStickY, |
|||
RightStickX, |
|||
RightStickY, |
|||
Undefined, |
|||
}; |
|||
|
|||
enum class PadMotion { |
|||
LeftMotion, |
|||
RightMotion, |
|||
Undefined, |
|||
}; |
|||
|
|||
enum class PadButton : u32 { |
|||
Down = 0x000001, |
|||
Up = 0x000002, |
|||
Right = 0x000004, |
|||
Left = 0x000008, |
|||
LeftSR = 0x000010, |
|||
LeftSL = 0x000020, |
|||
L = 0x000040, |
|||
ZL = 0x000080, |
|||
Y = 0x000100, |
|||
X = 0x000200, |
|||
B = 0x000400, |
|||
A = 0x000800, |
|||
RightSR = 0x001000, |
|||
RightSL = 0x002000, |
|||
R = 0x004000, |
|||
ZR = 0x008000, |
|||
Minus = 0x010000, |
|||
Plus = 0x020000, |
|||
StickR = 0x040000, |
|||
StickL = 0x080000, |
|||
Home = 0x100000, |
|||
Capture = 0x200000, |
|||
}; |
|||
|
|||
enum class PasivePadButton : u32 { |
|||
Down_A = 0x0001, |
|||
Right_X = 0x0002, |
|||
Left_B = 0x0004, |
|||
Up_Y = 0x0008, |
|||
SL = 0x0010, |
|||
SR = 0x0020, |
|||
Minus = 0x0100, |
|||
Plus = 0x0200, |
|||
StickL = 0x0400, |
|||
StickR = 0x0800, |
|||
Home = 0x1000, |
|||
Capture = 0x2000, |
|||
L_R = 0x4000, |
|||
ZL_ZR = 0x8000, |
|||
}; |
|||
|
|||
enum class OutputReport : u8 { |
|||
RUMBLE_AND_SUBCMD = 0x01, |
|||
FW_UPDATE_PKT = 0x03, |
|||
RUMBLE_ONLY = 0x10, |
|||
MCU_DATA = 0x11, |
|||
USB_CMD = 0x80, |
|||
}; |
|||
|
|||
enum class InputReport : u8 { |
|||
SUBCMD_REPLY = 0x21, |
|||
STANDARD_FULL_60HZ = 0x30, |
|||
NFC_IR_MODE_60HZ = 0x31, |
|||
SIMPLE_HID_MODE = 0x3F, |
|||
INPUT_USB_RESPONSE = 0x81, |
|||
}; |
|||
|
|||
enum class FeatureReport : u8 { |
|||
Last_SUBCMD = 0x02, |
|||
OTA_GW_UPGRADE = 0x70, |
|||
SETUP_MEM_READ = 0x71, |
|||
MEM_READ = 0x72, |
|||
ERASE_MEM_SECTOR = 0x73, |
|||
MEM_WRITE = 0x74, |
|||
LAUNCH = 0x75, |
|||
}; |
|||
|
|||
enum class SubCommand : u8 { |
|||
STATE = 0x00, |
|||
MANUAL_BT_PAIRING = 0x01, |
|||
REQ_DEV_INFO = 0x02, |
|||
SET_REPORT_MODE = 0x03, |
|||
TRIGGERS_ELAPSED = 0x04, |
|||
GET_PAGE_LIST_STATE = 0x05, |
|||
SET_HCI_STATE = 0x06, |
|||
RESET_PAIRING_INFO = 0x07, |
|||
LOW_POWER_MODE = 0x08, |
|||
SPI_FLASH_READ = 0x10, |
|||
SPI_FLASH_WRITE = 0x11, |
|||
RESET_MCU = 0x20, |
|||
SET_MCU_CONFIG = 0x21, |
|||
SET_MCU_STATE = 0x22, |
|||
SET_PLAYER_LIGHTS = 0x30, |
|||
GET_PLAYER_LIGHTS = 0x31, |
|||
SET_HOME_LIGHT = 0x38, |
|||
ENABLE_IMU = 0x40, |
|||
SET_IMU_SENSITIVITY = 0x41, |
|||
WRITE_IMU_REG = 0x42, |
|||
READ_IMU_REG = 0x43, |
|||
ENABLE_VIBRATION = 0x48, |
|||
GET_REGULATED_VOLTAGE = 0x50, |
|||
SET_EXTERNAL_CONFIG = 0x58, |
|||
UNKNOWN_RINGCON = 0x59, |
|||
UNKNOWN_RINGCON2 = 0x5A, |
|||
UNKNOWN_RINGCON3 = 0x5C, |
|||
}; |
|||
|
|||
enum class UsbSubCommand : u8 { |
|||
CONN_STATUS = 0x01, |
|||
HADSHAKE = 0x02, |
|||
BAUDRATE_3M = 0x03, |
|||
NO_TIMEOUT = 0x04, |
|||
EN_TIMEOUT = 0x05, |
|||
RESET = 0x06, |
|||
PRE_HANDSHAKE = 0x91, |
|||
SEND_UART = 0x92, |
|||
}; |
|||
|
|||
enum class CalMagic : u8 { |
|||
USR_MAGIC_0 = 0xB2, |
|||
USR_MAGIC_1 = 0xA1, |
|||
USRR_MAGI_SIZE = 2, |
|||
}; |
|||
|
|||
enum class CalAddr { |
|||
SERIAL_NUMBER = 0X6000, |
|||
DEVICE_TYPE = 0X6012, |
|||
COLOR_EXIST = 0X601B, |
|||
FACT_LEFT_DATA = 0X603d, |
|||
FACT_RIGHT_DATA = 0X6046, |
|||
COLOR_DATA = 0X6050, |
|||
FACT_IMU_DATA = 0X6020, |
|||
USER_LEFT_MAGIC = 0X8010, |
|||
USER_LEFT_DATA = 0X8012, |
|||
USER_RIGHT_MAGIC = 0X801B, |
|||
USER_RIGHT_DATA = 0X801D, |
|||
USER_IMU_MAGIC = 0X8026, |
|||
USER_IMU_DATA = 0X8028, |
|||
}; |
|||
|
|||
enum class ReportMode : u8 { |
|||
ACTIVE_POLLING_NFC_IR_CAMERA_DATA = 0x00, |
|||
ACTIVE_POLLING_NFC_IR_CAMERA_CONFIGURATION = 0x01, |
|||
ACTIVE_POLLING_NFC_IR_CAMERA_DATA_CONFIGURATION = 0x02, |
|||
ACTIVE_POLLING_IR_CAMERA_DATA = 0x03, |
|||
MCU_UPDATE_STATE = 0x23, |
|||
STANDARD_FULL_60HZ = 0x30, |
|||
NFC_IR_MODE_60HZ = 0x31, |
|||
SIMPLE_HID_MODE = 0x3F, |
|||
}; |
|||
|
|||
enum class GyroSensitivity : u8 { |
|||
DPS250, |
|||
DPS500, |
|||
DPS1000, |
|||
DPS2000, // Default |
|||
}; |
|||
|
|||
enum class AccelerometerSensitivity : u8 { |
|||
G8, // Default |
|||
G4, |
|||
G2, |
|||
G16, |
|||
}; |
|||
|
|||
enum class GyroPerformance : u8 { |
|||
HZ833, |
|||
HZ208, // Default |
|||
}; |
|||
|
|||
enum class AccelerometerPerformance : u8 { |
|||
HZ200, |
|||
HZ100, // Default |
|||
}; |
|||
|
|||
enum class MCUCommand : u8 { |
|||
ConfigureMCU = 0x21, |
|||
ConfigureIR = 0x23, |
|||
}; |
|||
|
|||
enum class MCUSubCommand : u8 { |
|||
SetMCUMode = 0x0, |
|||
SetDeviceMode = 0x1, |
|||
ReadDeviceMode = 0x02, |
|||
WriteDeviceRegisters = 0x4, |
|||
}; |
|||
|
|||
enum class MCUMode : u8 { |
|||
Suspend = 0, |
|||
Standby = 1, |
|||
Ringcon = 3, |
|||
NFC = 4, |
|||
IR = 5, |
|||
MaybeFWUpdate = 6, |
|||
}; |
|||
|
|||
enum class MCURequest : u8 { |
|||
GetMCUStatus = 1, |
|||
GetNFCData = 2, |
|||
GetIRData = 3, |
|||
}; |
|||
|
|||
enum class MCUReport : u8 { |
|||
Empty = 0x00, |
|||
StateReport = 0x01, |
|||
IRData = 0x03, |
|||
BusyInitializing = 0x0b, |
|||
IRStatus = 0x13, |
|||
IRRegisters = 0x1b, |
|||
NFCState = 0x2a, |
|||
NFCReadData = 0x3a, |
|||
EmptyAwaitingCmd = 0xff, |
|||
}; |
|||
|
|||
enum class MCUPacketFlag : u8 { |
|||
MorePacketsRemaining = 0x00, |
|||
LastCommandPacket = 0x08, |
|||
}; |
|||
|
|||
enum class NFCReadCommand : u8 { |
|||
CancelAll = 0x00, |
|||
StartPolling = 0x01, |
|||
StopPolling = 0x02, |
|||
StartWaitingRecieve = 0x04, |
|||
Ntag = 0x06, |
|||
Mifare = 0x0F, |
|||
}; |
|||
|
|||
enum class NFCTagType : u8 { |
|||
AllTags = 0x00, |
|||
Ntag215 = 0x01, |
|||
}; |
|||
|
|||
enum class DriverResult { |
|||
Success, |
|||
WrongReply, |
|||
Timeout, |
|||
UnsupportedControllerType, |
|||
HandleInUse, |
|||
ErrorReadingData, |
|||
ErrorWritingData, |
|||
NoDeviceDetected, |
|||
InvalidHandle, |
|||
NotSupported, |
|||
Unknown, |
|||
}; |
|||
|
|||
struct MotionSensorCalibration { |
|||
s16 offset; |
|||
s16 scale; |
|||
}; |
|||
|
|||
struct MotionCalibration { |
|||
std::array<MotionSensorCalibration, 3> accelerometer; |
|||
std::array<MotionSensorCalibration, 3> gyro; |
|||
}; |
|||
|
|||
// Basic motion data containing data from the sensors and a timestamp in microseconds |
|||
struct MotionData { |
|||
float gyro_x{}; |
|||
float gyro_y{}; |
|||
float gyro_z{}; |
|||
float accel_x{}; |
|||
float accel_y{}; |
|||
float accel_z{}; |
|||
u64 delta_timestamp{}; |
|||
}; |
|||
|
|||
struct JoyStickAxisCalibration { |
|||
u16 max{1}; |
|||
u16 min{1}; |
|||
u16 center{0}; |
|||
}; |
|||
|
|||
struct JoyStickCalibration { |
|||
JoyStickAxisCalibration x; |
|||
JoyStickAxisCalibration y; |
|||
}; |
|||
|
|||
struct RingCalibration { |
|||
s16 default_value; |
|||
s16 max_value; |
|||
s16 min_value; |
|||
}; |
|||
|
|||
struct Color { |
|||
u32 body; |
|||
u32 buttons; |
|||
u32 left_grip; |
|||
u32 right_grip; |
|||
}; |
|||
|
|||
struct Battery { |
|||
union { |
|||
u8 raw{}; |
|||
|
|||
BitField<0, 4, u8> unknown; |
|||
BitField<4, 1, u8> charging; |
|||
BitField<5, 3, u8> status; |
|||
}; |
|||
}; |
|||
|
|||
struct VibrationValue { |
|||
f32 low_amplitude; |
|||
f32 low_frequency; |
|||
f32 high_amplitude; |
|||
f32 high_frequency; |
|||
}; |
|||
|
|||
struct JoyconHandle { |
|||
SDL_hid_device* handle = nullptr; |
|||
u8 packet_counter{}; |
|||
}; |
|||
|
|||
struct MCUConfig { |
|||
MCUCommand command; |
|||
MCUSubCommand sub_command; |
|||
MCUMode mode; |
|||
INSERT_PADDING_BYTES(0x22); |
|||
u8 crc; |
|||
}; |
|||
static_assert(sizeof(MCUConfig) == 0x26, "MCUConfig is an invalid size"); |
|||
|
|||
#pragma pack(push, 1) |
|||
struct InputReportPassive { |
|||
InputReport report_mode; |
|||
u16 button_input; |
|||
u8 stick_state; |
|||
std::array<u8, 10> unknown_data; |
|||
}; |
|||
static_assert(sizeof(InputReportPassive) == 0xE, "InputReportPassive is an invalid size"); |
|||
|
|||
struct InputReportActive { |
|||
InputReport report_mode; |
|||
u8 packet_id; |
|||
Battery battery_status; |
|||
std::array<u8, 3> button_input; |
|||
std::array<u8, 3> left_stick_state; |
|||
std::array<u8, 3> right_stick_state; |
|||
u8 vibration_code; |
|||
std::array<s16, 6 * 2> motion_input; |
|||
INSERT_PADDING_BYTES(0x2); |
|||
s16 ring_input; |
|||
}; |
|||
static_assert(sizeof(InputReportActive) == 0x29, "InputReportActive is an invalid size"); |
|||
|
|||
struct InputReportNfcIr { |
|||
InputReport report_mode; |
|||
u8 packet_id; |
|||
Battery battery_status; |
|||
std::array<u8, 3> button_input; |
|||
std::array<u8, 3> left_stick_state; |
|||
std::array<u8, 3> right_stick_state; |
|||
u8 vibration_code; |
|||
std::array<s16, 6 * 2> motion_input; |
|||
INSERT_PADDING_BYTES(0x4); |
|||
}; |
|||
static_assert(sizeof(InputReportNfcIr) == 0x29, "InputReportNfcIr is an invalid size"); |
|||
#pragma pack(pop) |
|||
|
|||
struct IMUCalibration { |
|||
std::array<s16, 3> accelerometer_offset; |
|||
std::array<s16, 3> accelerometer_scale; |
|||
std::array<s16, 3> gyroscope_offset; |
|||
std::array<s16, 3> gyroscope_scale; |
|||
}; |
|||
static_assert(sizeof(IMUCalibration) == 0x18, "IMUCalibration is an invalid size"); |
|||
|
|||
struct NFCReadBlock { |
|||
u8 start; |
|||
u8 end; |
|||
}; |
|||
static_assert(sizeof(NFCReadBlock) == 0x2, "NFCReadBlock is an invalid size"); |
|||
|
|||
struct NFCReadBlockCommand { |
|||
u8 block_count{}; |
|||
std::array<NFCReadBlock, 4> blocks{}; |
|||
}; |
|||
static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an invalid size"); |
|||
|
|||
struct NFCReadCommandData { |
|||
u8 unknown; |
|||
u8 uuid_length; |
|||
u8 unknown_2; |
|||
std::array<u8, 6> uid; |
|||
NFCTagType tag_type; |
|||
NFCReadBlockCommand read_block; |
|||
}; |
|||
static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size"); |
|||
|
|||
struct NFCPollingCommandData { |
|||
u8 enable_mifare; |
|||
u8 unknown_1; |
|||
u8 unknown_2; |
|||
u8 unknown_3; |
|||
u8 unknown_4; |
|||
}; |
|||
static_assert(sizeof(NFCPollingCommandData) == 0x05, "NFCPollingCommandData is an invalid size"); |
|||
|
|||
struct NFCRequestState { |
|||
MCUSubCommand sub_command; |
|||
NFCReadCommand command_argument; |
|||
u8 packet_id; |
|||
INSERT_PADDING_BYTES(0x1); |
|||
MCUPacketFlag packet_flag; |
|||
u8 data_length; |
|||
union { |
|||
std::array<u8, 0x1F> raw_data; |
|||
NFCReadCommandData nfc_read; |
|||
NFCPollingCommandData nfc_polling; |
|||
}; |
|||
u8 crc; |
|||
}; |
|||
static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size"); |
|||
|
|||
struct FirmwareVersion { |
|||
u8 major; |
|||
u8 minor; |
|||
}; |
|||
static_assert(sizeof(FirmwareVersion) == 0x2, "FirmwareVersion is an invalid size"); |
|||
|
|||
struct DeviceInfo { |
|||
FirmwareVersion firmware; |
|||
MacAddress mac_address; |
|||
}; |
|||
static_assert(sizeof(DeviceInfo) == 0x8, "DeviceInfo is an invalid size"); |
|||
|
|||
struct MotionStatus { |
|||
bool is_enabled; |
|||
u64 delta_time; |
|||
GyroSensitivity gyro_sensitivity; |
|||
AccelerometerSensitivity accelerometer_sensitivity; |
|||
}; |
|||
|
|||
struct RingStatus { |
|||
bool is_enabled; |
|||
s16 default_value; |
|||
s16 max_value; |
|||
s16 min_value; |
|||
}; |
|||
|
|||
struct JoyconCallbacks { |
|||
std::function<void(Battery)> on_battery_data; |
|||
std::function<void(Color)> on_color_data; |
|||
std::function<void(int, bool)> on_button_data; |
|||
std::function<void(int, f32)> on_stick_data; |
|||
std::function<void(int, const MotionData&)> on_motion_data; |
|||
std::function<void(f32)> on_ring_data; |
|||
std::function<void(const std::vector<u8>&)> on_amiibo_data; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue