Browse Source
Merge pull request #9492 from german77/joycon_release
Merge pull request #9492 from german77/joycon_release
Input_common: Implement custom joycon driver v2nce_cpp
committed by
GitHub
58 changed files with 5812 additions and 408 deletions
-
68src/common/input.h
-
1src/common/settings.h
-
298src/core/hid/emulated_controller.cpp
-
64src/core/hid/emulated_controller.h
-
46src/core/hid/emulated_devices.cpp
-
18src/core/hid/emulated_devices.h
-
12src/core/hid/input_converter.cpp
-
10src/core/hid/input_converter.h
-
18src/core/hle/service/hid/controllers/npad.cpp
-
24src/core/hle/service/hid/hidbus.cpp
-
8src/core/hle/service/hid/hidbus/ringcon.cpp
-
4src/core/hle/service/hid/hidbus/ringcon.h
-
18src/core/hle/service/hid/irs.cpp
-
7src/core/hle/service/nfc/nfc_device.cpp
-
7src/core/hle/service/nfp/nfp_device.cpp
-
21src/input_common/CMakeLists.txt
-
4src/input_common/drivers/camera.cpp
-
4src/input_common/drivers/camera.h
-
6src/input_common/drivers/gc_adapter.cpp
-
2src/input_common/drivers/gc_adapter.h
-
677src/input_common/drivers/joycon.cpp
-
111src/input_common/drivers/joycon.h
-
23src/input_common/drivers/sdl_driver.cpp
-
2src/input_common/drivers/sdl_driver.h
-
11src/input_common/drivers/virtual_amiibo.cpp
-
2src/input_common/drivers/virtual_amiibo.h
-
572src/input_common/helpers/joycon_driver.cpp
-
150src/input_common/helpers/joycon_driver.h
-
184src/input_common/helpers/joycon_protocol/calibration.cpp
-
64src/input_common/helpers/joycon_protocol/calibration.h
-
299src/input_common/helpers/joycon_protocol/common_protocol.cpp
-
173src/input_common/helpers/joycon_protocol/common_protocol.h
-
125src/input_common/helpers/joycon_protocol/generic_functions.cpp
-
108src/input_common/helpers/joycon_protocol/generic_functions.h
-
298src/input_common/helpers/joycon_protocol/irs.cpp
-
63src/input_common/helpers/joycon_protocol/irs.h
-
612src/input_common/helpers/joycon_protocol/joycon_types.h
-
400src/input_common/helpers/joycon_protocol/nfc.cpp
-
61src/input_common/helpers/joycon_protocol/nfc.h
-
341src/input_common/helpers/joycon_protocol/poller.cpp
-
81src/input_common/helpers/joycon_protocol/poller.h
-
117src/input_common/helpers/joycon_protocol/ringcon.cpp
-
38src/input_common/helpers/joycon_protocol/ringcon.h
-
299src/input_common/helpers/joycon_protocol/rumble.cpp
-
33src/input_common/helpers/joycon_protocol/rumble.h
-
37src/input_common/input_engine.cpp
-
25src/input_common/input_engine.h
-
78src/input_common/input_poller.cpp
-
11src/input_common/input_poller.h
-
14src/input_common/main.cpp
-
2src/yuzu/configuration/config.cpp
-
2src/yuzu/configuration/configure_input_advanced.cpp
-
22src/yuzu/configuration/configure_input_advanced.ui
-
20src/yuzu/configuration/configure_input_player.cpp
-
10src/yuzu/configuration/configure_input_player_widget.cpp
-
105src/yuzu/configuration/configure_ringcon.cpp
-
14src/yuzu/configuration/configure_ringcon.h
-
396src/yuzu/configuration/configure_ringcon.ui
@ -0,0 +1,677 @@ |
|||
// 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_) { |
|||
// Avoid conflicting with SDL driver
|
|||
if (!Settings::values.enable_joycon_driver) { |
|||
return; |
|||
} |
|||
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(); |
|||
} |
|||
SDL_hid_exit(); |
|||
} |
|||
|
|||
void Joycons::Setup() { |
|||
u32 port = 0; |
|||
PreSetController(GetIdentifier(0, Joycon::ControllerType::None)); |
|||
for (auto& device : left_joycons) { |
|||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Left)); |
|||
device = std::make_shared<Joycon::JoyconDriver>(port++); |
|||
} |
|||
port = 0; |
|||
for (auto& device : right_joycons) { |
|||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Right)); |
|||
device = std::make_shared<Joycon::JoyconDriver>(port++); |
|||
} |
|||
|
|||
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("JoyconScanThread"); |
|||
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; |
|||
} |
|||
|
|||
SDL_hid_free_enumeration(devs); |
|||
std::this_thread::sleep_for(std::chrono::seconds(5)); |
|||
} |
|||
} |
|||
|
|||
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 = [serial_number](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; |
|||
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"); |
|||
|
|||
const std::size_t port = handle->GetDevicePort(); |
|||
const Joycon::JoyconCallbacks callbacks{ |
|||
.on_battery_data = {[this, port, type](Joycon::Battery value) { |
|||
OnBatteryUpdate(port, type, value); |
|||
}}, |
|||
.on_color_data = {[this, port, type](Joycon::Color value) { |
|||
OnColorUpdate(port, type, value); |
|||
}}, |
|||
.on_button_data = {[this, port, type](int id, bool value) { |
|||
OnButtonUpdate(port, type, id, value); |
|||
}}, |
|||
.on_stick_data = {[this, port, type](int id, f32 value) { |
|||
OnStickUpdate(port, type, id, value); |
|||
}}, |
|||
.on_motion_data = {[this, port, type](int id, const Joycon::MotionData& value) { |
|||
OnMotionUpdate(port, type, id, value); |
|||
}}, |
|||
.on_ring_data = {[this](f32 ring_data) { OnRingConUpdate(ring_data); }}, |
|||
.on_amiibo_data = {[this, port](const std::vector<u8>& amiibo_data) { |
|||
OnAmiiboUpdate(port, amiibo_data); |
|||
}}, |
|||
.on_camera_data = {[this, port](const std::vector<u8>& camera_data, |
|||
Joycon::IrsResolution format) { |
|||
OnCameraUpdate(port, camera_data, format); |
|||
}}, |
|||
}; |
|||
|
|||
handle->InitializeDevice(); |
|||
handle->SetCallbacks(callbacks); |
|||
} |
|||
} |
|||
|
|||
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle( |
|||
Joycon::ControllerType type) const { |
|||
if (type == Joycon::ControllerType::Left) { |
|||
const auto unconnected_device = |
|||
std::ranges::find_if(left_joycons, [](auto& device) { return !device->IsConnected(); }); |
|||
if (unconnected_device != left_joycons.end()) { |
|||
return *unconnected_device; |
|||
} |
|||
} |
|||
if (type == Joycon::ControllerType::Right) { |
|||
const auto unconnected_device = std::ranges::find_if( |
|||
right_joycons, [](auto& device) { return !device->IsConnected(); }); |
|||
|
|||
if (unconnected_device != right_joycons.end()) { |
|||
return *unconnected_device; |
|||
} |
|||
} |
|||
return nullptr; |
|||
} |
|||
|
|||
bool Joycons::IsVibrationEnabled(const PadIdentifier& identifier) { |
|||
const auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return false; |
|||
} |
|||
return handle->IsVibrationEnabled(); |
|||
} |
|||
|
|||
Common::Input::DriverResult 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_frequency, |
|||
}; |
|||
auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return Common::Input::DriverResult::InvalidHandle; |
|||
} |
|||
|
|||
handle->SetVibration(native_vibration); |
|||
return Common::Input::DriverResult::Success; |
|||
} |
|||
|
|||
Common::Input::DriverResult Joycons::SetLeds(const PadIdentifier& identifier, |
|||
const Common::Input::LedStatus& led_status) { |
|||
auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return Common::Input::DriverResult::InvalidHandle; |
|||
} |
|||
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; |
|||
|
|||
return static_cast<Common::Input::DriverResult>( |
|||
handle->SetLedConfig(static_cast<u8>(led_config))); |
|||
} |
|||
|
|||
Common::Input::DriverResult Joycons::SetCameraFormat(const PadIdentifier& identifier, |
|||
Common::Input::CameraFormat camera_format) { |
|||
auto handle = GetHandle(identifier); |
|||
if (handle == nullptr) { |
|||
return Common::Input::DriverResult::InvalidHandle; |
|||
} |
|||
return static_cast<Common::Input::DriverResult>(handle->SetIrsConfig( |
|||
Joycon::IrsMode::ImageTransfer, static_cast<Joycon::IrsResolution>(camera_format))); |
|||
}; |
|||
|
|||
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::DriverResult 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::DriverResult::InvalidHandle; |
|||
} |
|||
|
|||
switch (polling_mode) { |
|||
case Common::Input::PollingMode::Active: |
|||
return static_cast<Common::Input::DriverResult>(handle->SetActiveMode()); |
|||
case Common::Input::PollingMode::Pasive: |
|||
return static_cast<Common::Input::DriverResult>(handle->SetPasiveMode()); |
|||
case Common::Input::PollingMode::IR: |
|||
return static_cast<Common::Input::DriverResult>(handle->SetIrMode()); |
|||
case Common::Input::PollingMode::NFC: |
|||
return static_cast<Common::Input::DriverResult>(handle->SetNfcMode()); |
|||
case Common::Input::PollingMode::Ring: |
|||
return static_cast<Common::Input::DriverResult>(handle->SetRingConMode()); |
|||
default: |
|||
return Common::Input::DriverResult::NotSupported; |
|||
} |
|||
} |
|||
|
|||
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{}; |
|||
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) { |
|||
const auto identifier = GetIdentifier(port, type); |
|||
Common::Input::BodyColorStatus color{ |
|||
.body = value.body, |
|||
.buttons = value.buttons, |
|||
.left_grip = value.left_grip, |
|||
.right_grip = value.right_grip, |
|||
}; |
|||
SetColor(identifier, color); |
|||
} |
|||
|
|||
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); |
|||
const auto nfc_state = amiibo_data.empty() ? Common::Input::NfcState::AmiiboRemoved |
|||
: Common::Input::NfcState::NewAmiibo; |
|||
SetNfc(identifier, {nfc_state, amiibo_data}); |
|||
} |
|||
|
|||
void Joycons::OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data, |
|||
Joycon::IrsResolution format) { |
|||
const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right); |
|||
SetCamera(identifier, {static_cast<Common::Input::CameraFormat>(format), camera_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) { |
|||
const auto matching_device = std::ranges::find_if( |
|||
left_joycons, [is_handle_active](auto& device) { return is_handle_active(device); }); |
|||
|
|||
if (matching_device != left_joycons.end()) { |
|||
return *matching_device; |
|||
} |
|||
} |
|||
|
|||
if (type == Joycon::ControllerType::Right) { |
|||
const auto matching_device = std::ranges::find_if( |
|||
right_joycons, [is_handle_active](auto& device) { return is_handle_active(device); }); |
|||
|
|||
if (matching_device != right_joycons.end()) { |
|||
return *matching_device; |
|||
} |
|||
} |
|||
|
|||
return nullptr; |
|||
} |
|||
|
|||
PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const { |
|||
const std::array<u8, 16> guid{0, 0, 0, 0, 0, 0, 0, 0, |
|||
0, 0, 0, 0, 0, 0, 0, static_cast<u8>(type)}; |
|||
return { |
|||
.guid = Common::UUID{guid}, |
|||
.port = port, |
|||
.pad = static_cast<std::size_t>(type), |
|||
}; |
|||
} |
|||
|
|||
Common::ParamPackage Joycons::GetParamPackage(std::size_t port, Joycon::ControllerType type) const { |
|||
const auto identifier = GetIdentifier(port, type); |
|||
return { |
|||
{"engine", GetEngineName()}, |
|||
{"guid", identifier.guid.RawString()}, |
|||
{"port", std::to_string(identifier.port)}, |
|||
{"pad", std::to_string(identifier.pad)}, |
|||
}; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
auto param = GetParamPackage(device->GetDevicePort(), device->GetHandleDeviceType()); |
|||
std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()), |
|||
device->GetDevicePort() + 1); |
|||
param.Set("display", std::move(name)); |
|||
devices.emplace_back(param); |
|||
}; |
|||
|
|||
for (const auto& controller : left_joycons) { |
|||
add_entry(controller); |
|||
} |
|||
for (const auto& controller : right_joycons) { |
|||
add_entry(controller); |
|||
} |
|||
|
|||
// List dual joycon pairs
|
|||
for (std::size_t i = 0; i < MaxSupportedControllers; i++) { |
|||
if (!left_joycons[i] || !right_joycons[i]) { |
|||
continue; |
|||
} |
|||
if (!left_joycons[i]->IsConnected() || !right_joycons[i]->IsConnected()) { |
|||
continue; |
|||
} |
|||
auto main_param = GetParamPackage(i, left_joycons[i]->GetHandleDeviceType()); |
|||
const auto second_param = GetParamPackage(i, right_joycons[i]->GetHandleDeviceType()); |
|||
const auto type = Joycon::ControllerType::Dual; |
|||
std::string name = fmt::format("{} {}", JoyconName(type), i + 1); |
|||
|
|||
main_param.Set("display", std::move(name)); |
|||
main_param.Set("guid2", second_param.Get("guid", "")); |
|||
main_param.Set("pad", std::to_string(static_cast<size_t>(type))); |
|||
devices.emplace_back(main_param); |
|||
} |
|||
|
|||
return devices; |
|||
} |
|||
|
|||
ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) { |
|||
static constexpr std::array<std::tuple<Settings::NativeButton::Values, Joycon::PadButton, bool>, |
|||
18> |
|||
switch_to_joycon_button = { |
|||
std::tuple{Settings::NativeButton::A, Joycon::PadButton::A, true}, |
|||
{Settings::NativeButton::B, Joycon::PadButton::B, true}, |
|||
{Settings::NativeButton::X, Joycon::PadButton::X, true}, |
|||
{Settings::NativeButton::Y, Joycon::PadButton::Y, true}, |
|||
{Settings::NativeButton::DLeft, Joycon::PadButton::Left, false}, |
|||
{Settings::NativeButton::DUp, Joycon::PadButton::Up, false}, |
|||
{Settings::NativeButton::DRight, Joycon::PadButton::Right, false}, |
|||
{Settings::NativeButton::DDown, Joycon::PadButton::Down, false}, |
|||
{Settings::NativeButton::L, Joycon::PadButton::L, false}, |
|||
{Settings::NativeButton::R, Joycon::PadButton::R, true}, |
|||
{Settings::NativeButton::ZL, Joycon::PadButton::ZL, false}, |
|||
{Settings::NativeButton::ZR, Joycon::PadButton::ZR, true}, |
|||
{Settings::NativeButton::Plus, Joycon::PadButton::Plus, true}, |
|||
{Settings::NativeButton::Minus, Joycon::PadButton::Minus, false}, |
|||
{Settings::NativeButton::Home, Joycon::PadButton::Home, true}, |
|||
{Settings::NativeButton::Screenshot, Joycon::PadButton::Capture, false}, |
|||
{Settings::NativeButton::LStick, Joycon::PadButton::StickL, false}, |
|||
{Settings::NativeButton::RStick, Joycon::PadButton::StickR, true}, |
|||
}; |
|||
|
|||
if (!params.Has("port")) { |
|||
return {}; |
|||
} |
|||
|
|||
ButtonMapping mapping{}; |
|||
for (const auto& [switch_button, joycon_button, side] : switch_to_joycon_button) { |
|||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0)); |
|||
auto pad = static_cast<Joycon::ControllerType>(params.Get("pad", 0)); |
|||
if (pad == Joycon::ControllerType::Dual) { |
|||
pad = side ? Joycon::ControllerType::Right : Joycon::ControllerType::Left; |
|||
} |
|||
|
|||
Common::ParamPackage button_params = GetParamPackage(port, pad); |
|||
button_params.Set("button", static_cast<int>(joycon_button)); |
|||
mapping.insert_or_assign(switch_button, std::move(button_params)); |
|||
} |
|||
|
|||
// Map SL and SR buttons for left joycons
|
|||
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Left)) { |
|||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0)); |
|||
Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Left); |
|||
|
|||
Common::ParamPackage sl_button_params = button_params; |
|||
Common::ParamPackage sr_button_params = button_params; |
|||
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSL)); |
|||
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSR)); |
|||
mapping.insert_or_assign(Settings::NativeButton::SL, std::move(sl_button_params)); |
|||
mapping.insert_or_assign(Settings::NativeButton::SR, std::move(sr_button_params)); |
|||
} |
|||
|
|||
// Map SL and SR buttons for right joycons
|
|||
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Right)) { |
|||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0)); |
|||
Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Right); |
|||
|
|||
Common::ParamPackage sl_button_params = button_params; |
|||
Common::ParamPackage sr_button_params = button_params; |
|||
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSL)); |
|||
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSR)); |
|||
mapping.insert_or_assign(Settings::NativeButton::SL, std::move(sl_button_params)); |
|||
mapping.insert_or_assign(Settings::NativeButton::SR, std::move(sr_button_params)); |
|||
} |
|||
|
|||
return mapping; |
|||
} |
|||
|
|||
AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) { |
|||
if (!params.Has("port")) { |
|||
return {}; |
|||
} |
|||
|
|||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0)); |
|||
auto pad_left = static_cast<Joycon::ControllerType>(params.Get("pad", 0)); |
|||
auto pad_right = pad_left; |
|||
if (pad_left == Joycon::ControllerType::Dual) { |
|||
pad_left = Joycon::ControllerType::Left; |
|||
pad_right = Joycon::ControllerType::Right; |
|||
} |
|||
|
|||
AnalogMapping mapping = {}; |
|||
Common::ParamPackage left_analog_params = GetParamPackage(port, pad_left); |
|||
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 = GetParamPackage(port, pad_right); |
|||
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 {}; |
|||
} |
|||
|
|||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0)); |
|||
auto pad_left = static_cast<Joycon::ControllerType>(params.Get("pad", 0)); |
|||
auto pad_right = pad_left; |
|||
if (pad_left == Joycon::ControllerType::Dual) { |
|||
pad_left = Joycon::ControllerType::Left; |
|||
pad_right = Joycon::ControllerType::Right; |
|||
} |
|||
|
|||
MotionMapping mapping = {}; |
|||
Common::ParamPackage left_motion_params = GetParamPackage(port, pad_left); |
|||
left_motion_params.Set("motion", 0); |
|||
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params)); |
|||
Common::ParamPackage right_Motion_params = GetParamPackage(port, pad_right); |
|||
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"; |
|||
case Joycon::ControllerType::Dual: |
|||
return "Dual Joycon"; |
|||
default: |
|||
return "Unknown Joycon"; |
|||
} |
|||
} |
|||
} // namespace InputCommon
|
|||
@ -0,0 +1,111 @@ |
|||
// 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; |
|||
enum class IrsResolution; |
|||
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::DriverResult SetVibration( |
|||
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; |
|||
|
|||
Common::Input::DriverResult SetLeds(const PadIdentifier& identifier, |
|||
const Common::Input::LedStatus& led_status) override; |
|||
|
|||
Common::Input::DriverResult 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::DriverResult 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); |
|||
void OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data, |
|||
Joycon::IrsResolution format); |
|||
|
|||
/// Returns a JoyconHandle corresponding to a PadIdentifier |
|||
std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const; |
|||
|
|||
/// Returns a PadIdentifier corresponding to the port number and joycon type |
|||
PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const; |
|||
|
|||
/// Returns a ParamPackage corresponding to the port number and joycon type |
|||
Common::ParamPackage GetParamPackage(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; |
|||
|
|||
// 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{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon |
|||
@ -0,0 +1,572 @@ |
|||
// 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"
|
|||
#include "input_common/helpers/joycon_protocol/calibration.h"
|
|||
#include "input_common/helpers/joycon_protocol/generic_functions.h"
|
|||
#include "input_common/helpers/joycon_protocol/irs.h"
|
|||
#include "input_common/helpers/joycon_protocol/nfc.h"
|
|||
#include "input_common/helpers/joycon_protocol/poller.h"
|
|||
#include "input_common/helpers/joycon_protocol/ringcon.h"
|
|||
#include "input_common/helpers/joycon_protocol/rumble.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; |
|||
|
|||
// Reset external device status
|
|||
starlink_connected = false; |
|||
ring_connected = false; |
|||
amiibo_detected = false; |
|||
|
|||
// Set HW default configuration
|
|||
vibration_enabled = true; |
|||
motion_enabled = true; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
irs_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
|
|||
calibration_protocol = std::make_unique<CalibrationProtocol>(hidapi_handle); |
|||
generic_protocol = std::make_unique<GenericProtocol>(hidapi_handle); |
|||
irs_protocol = std::make_unique<IrsProtocol>(hidapi_handle); |
|||
nfc_protocol = std::make_unique<NfcProtocol>(hidapi_handle); |
|||
ring_protocol = std::make_unique<RingConProtocol>(hidapi_handle); |
|||
rumble_protocol = std::make_unique<RumbleProtocol>(hidapi_handle); |
|||
|
|||
// Get fixed joycon info
|
|||
generic_protocol->GetVersionNumber(version); |
|||
generic_protocol->GetColor(color); |
|||
if (handle_device_type == ControllerType::Pro) { |
|||
// Some 3rd party controllers aren't pro controllers
|
|||
generic_protocol->GetControllerType(device_type); |
|||
} else { |
|||
device_type = handle_device_type; |
|||
} |
|||
generic_protocol->GetSerialNumber(serial_number); |
|||
supported_features = GetSupportedFeatures(); |
|||
|
|||
// Get Calibration data
|
|||
calibration_protocol->GetLeftJoyStickCalibration(left_stick_calibration); |
|||
calibration_protocol->GetRightJoyStickCalibration(right_stick_calibration); |
|||
calibration_protocol->GetImuCalibration(motion_calibration); |
|||
|
|||
// Set led status
|
|||
generic_protocol->SetLedBlinkPattern(static_cast<u8>(1 + port)); |
|||
|
|||
// Apply HW configuration
|
|||
SetPollingMode(); |
|||
|
|||
// Initialize joycon poller
|
|||
joycon_poller = std::make_unique<JoyconPoller>(device_type, left_stick_calibration, |
|||
right_stick_calibration, motion_calibration); |
|||
|
|||
// 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, "Joycon 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, "Joycon Adapter input thread stopped"); |
|||
} |
|||
|
|||
void JoyconDriver::OnNewData(std::span<u8> buffer) { |
|||
const auto report_mode = static_cast<InputReport>(buffer[0]); |
|||
|
|||
// Packages can be a litte bit inconsistent. Average the delta time to provide a smoother motion
|
|||
// experience
|
|||
switch (report_mode) { |
|||
case InputReport::STANDARD_FULL_60HZ: |
|||
case InputReport::NFC_IR_MODE_60HZ: |
|||
case InputReport::SIMPLE_HID_MODE: { |
|||
const auto now = std::chrono::steady_clock::now(); |
|||
const auto new_delta_time = static_cast<u64>( |
|||
std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count()); |
|||
delta_time = ((delta_time * 8) + (new_delta_time * 2)) / 10; |
|||
last_update = now; |
|||
joycon_poller->UpdateColor(color); |
|||
break; |
|||
} |
|||
default: |
|||
break; |
|||
} |
|||
|
|||
const MotionStatus motion_status{ |
|||
.is_enabled = motion_enabled, |
|||
.delta_time = delta_time, |
|||
.gyro_sensitivity = gyro_sensitivity, |
|||
.accelerometer_sensitivity = accelerometer_sensitivity, |
|||
}; |
|||
|
|||
// TODO: Remove this when calibration is properly loaded and not calculated
|
|||
if (ring_connected && report_mode == InputReport::STANDARD_FULL_60HZ) { |
|||
InputReportActive data{}; |
|||
memcpy(&data, buffer.data(), sizeof(InputReportActive)); |
|||
calibration_protocol->GetRingCalibration(ring_calibration, data.ring_input); |
|||
} |
|||
|
|||
const RingStatus ring_status{ |
|||
.is_enabled = ring_connected, |
|||
.default_value = ring_calibration.default_value, |
|||
.max_value = ring_calibration.max_value, |
|||
.min_value = ring_calibration.min_value, |
|||
}; |
|||
|
|||
if (irs_protocol->IsEnabled()) { |
|||
irs_protocol->RequestImage(buffer); |
|||
joycon_poller->UpdateCamera(irs_protocol->GetImage(), irs_protocol->GetIrsFormat()); |
|||
} |
|||
|
|||
if (nfc_protocol->IsEnabled()) { |
|||
if (amiibo_detected) { |
|||
if (!nfc_protocol->HasAmiibo()) { |
|||
joycon_poller->UpdateAmiibo({}); |
|||
amiibo_detected = false; |
|||
return; |
|||
} |
|||
} |
|||
|
|||
if (!amiibo_detected) { |
|||
std::vector<u8> data(0x21C); |
|||
const auto result = nfc_protocol->ScanAmiibo(data); |
|||
if (result == DriverResult::Success) { |
|||
joycon_poller->UpdateAmiibo(data); |
|||
amiibo_detected = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
switch (report_mode) { |
|||
case InputReport::STANDARD_FULL_60HZ: |
|||
joycon_poller->ReadActiveMode(buffer, motion_status, ring_status); |
|||
break; |
|||
case InputReport::NFC_IR_MODE_60HZ: |
|||
joycon_poller->ReadNfcIRMode(buffer, motion_status); |
|||
break; |
|||
case InputReport::SIMPLE_HID_MODE: |
|||
joycon_poller->ReadPassiveMode(buffer); |
|||
break; |
|||
case InputReport::SUBCMD_REPLY: |
|||
LOG_DEBUG(Input, "Unhandled command reply"); |
|||
break; |
|||
default: |
|||
LOG_ERROR(Input, "Report mode not Implemented {}", report_mode); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetPollingMode() { |
|||
disable_input_thread = true; |
|||
|
|||
rumble_protocol->EnableRumble(vibration_enabled && supported_features.vibration); |
|||
|
|||
if (motion_enabled && supported_features.motion) { |
|||
generic_protocol->EnableImu(true); |
|||
generic_protocol->SetImuConfig(gyro_sensitivity, gyro_performance, |
|||
accelerometer_sensitivity, accelerometer_performance); |
|||
} else { |
|||
generic_protocol->EnableImu(false); |
|||
} |
|||
|
|||
if (irs_protocol->IsEnabled()) { |
|||
irs_protocol->DisableIrs(); |
|||
} |
|||
|
|||
if (nfc_protocol->IsEnabled()) { |
|||
amiibo_detected = false; |
|||
nfc_protocol->DisableNfc(); |
|||
} |
|||
|
|||
if (ring_protocol->IsEnabled()) { |
|||
ring_connected = false; |
|||
ring_protocol->DisableRingCon(); |
|||
} |
|||
|
|||
if (irs_enabled && supported_features.irs) { |
|||
auto result = irs_protocol->EnableIrs(); |
|||
if (result == DriverResult::Success) { |
|||
disable_input_thread = false; |
|||
return result; |
|||
} |
|||
irs_protocol->DisableIrs(); |
|||
LOG_ERROR(Input, "Error enabling IRS"); |
|||
} |
|||
|
|||
if (nfc_enabled && supported_features.nfc) { |
|||
auto result = nfc_protocol->EnableNfc(); |
|||
if (result == DriverResult::Success) { |
|||
result = nfc_protocol->StartNFCPollingMode(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
disable_input_thread = false; |
|||
return result; |
|||
} |
|||
nfc_protocol->DisableNfc(); |
|||
LOG_ERROR(Input, "Error enabling NFC"); |
|||
} |
|||
|
|||
if (hidbus_enabled && supported_features.hidbus) { |
|||
auto result = ring_protocol->EnableRingCon(); |
|||
if (result == DriverResult::Success) { |
|||
result = ring_protocol->StartRingconPolling(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
ring_connected = true; |
|||
disable_input_thread = false; |
|||
return result; |
|||
} |
|||
ring_connected = false; |
|||
ring_protocol->DisableRingCon(); |
|||
LOG_ERROR(Input, "Error enabling Ringcon"); |
|||
} |
|||
|
|||
if (passive_enabled && supported_features.passive) { |
|||
const auto result = generic_protocol->EnablePassiveMode(); |
|||
if (result == DriverResult::Success) { |
|||
disable_input_thread = false; |
|||
return result; |
|||
} |
|||
LOG_ERROR(Input, "Error enabling passive mode"); |
|||
} |
|||
|
|||
// Default Mode
|
|||
const auto result = generic_protocol->EnableActiveMode(); |
|||
if (result != DriverResult::Success) { |
|||
LOG_ERROR(Input, "Error enabling active mode"); |
|||
} |
|||
|
|||
disable_input_thread = false; |
|||
return result; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
|
|||
bool JoyconDriver::IsInputThreadValid() const { |
|||
if (!is_connected.load()) { |
|||
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}; |
|||
if (disable_input_thread) { |
|||
return DriverResult::HandleInUse; |
|||
} |
|||
return rumble_protocol->SendVibration(vibration); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetLedConfig(u8 led_pattern) { |
|||
std::scoped_lock lock{mutex}; |
|||
if (disable_input_thread) { |
|||
return DriverResult::HandleInUse; |
|||
} |
|||
return generic_protocol->SetLedPattern(led_pattern); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetIrsConfig(IrsMode mode_, IrsResolution format_) { |
|||
std::scoped_lock lock{mutex}; |
|||
if (disable_input_thread) { |
|||
return DriverResult::HandleInUse; |
|||
} |
|||
disable_input_thread = true; |
|||
const auto result = irs_protocol->SetIrsConfig(mode_, format_); |
|||
disable_input_thread = false; |
|||
return result; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetPasiveMode() { |
|||
std::scoped_lock lock{mutex}; |
|||
motion_enabled = false; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = true; |
|||
irs_enabled = false; |
|||
return SetPollingMode(); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetActiveMode() { |
|||
if (is_ring_disabled_by_irs) { |
|||
is_ring_disabled_by_irs = false; |
|||
SetActiveMode(); |
|||
return SetRingConMode(); |
|||
} |
|||
|
|||
std::scoped_lock lock{mutex}; |
|||
motion_enabled = true; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
irs_enabled = false; |
|||
return SetPollingMode(); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetIrMode() { |
|||
std::scoped_lock lock{mutex}; |
|||
|
|||
if (!supported_features.irs) { |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
if (ring_connected) { |
|||
is_ring_disabled_by_irs = true; |
|||
} |
|||
|
|||
motion_enabled = false; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
irs_enabled = true; |
|||
return SetPollingMode(); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetNfcMode() { |
|||
std::scoped_lock lock{mutex}; |
|||
|
|||
if (!supported_features.nfc) { |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
motion_enabled = true; |
|||
hidbus_enabled = false; |
|||
nfc_enabled = true; |
|||
passive_enabled = false; |
|||
irs_enabled = false; |
|||
return SetPollingMode(); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::SetRingConMode() { |
|||
std::scoped_lock lock{mutex}; |
|||
|
|||
if (!supported_features.hidbus) { |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
motion_enabled = true; |
|||
hidbus_enabled = true; |
|||
nfc_enabled = false; |
|||
passive_enabled = false; |
|||
irs_enabled = false; |
|||
|
|||
const auto result = SetPollingMode(); |
|||
|
|||
if (!ring_connected) { |
|||
return DriverResult::NoDeviceDetected; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
bool JoyconDriver::IsConnected() const { |
|||
std::scoped_lock lock{mutex}; |
|||
return is_connected.load(); |
|||
} |
|||
|
|||
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 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; |
|||
} |
|||
|
|||
void JoyconDriver::SetCallbacks(const JoyconCallbacks& callbacks) { |
|||
joycon_poller->SetCallbacks(callbacks); |
|||
} |
|||
|
|||
DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info, |
|||
ControllerType& controller_type) { |
|||
static constexpr std::array<std::pair<u32, ControllerType>, 2> supported_devices{ |
|||
std::pair<u32, ControllerType>{0x2006, ControllerType::Left}, |
|||
{0x2007, ControllerType::Right}, |
|||
}; |
|||
constexpr u16 nintendo_vendor_id = 0x057e; |
|||
|
|||
controller_type = ControllerType::None; |
|||
if (device_info->vendor_id != nintendo_vendor_id) { |
|||
return 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; |
|||
} |
|||
|
|||
DriverResult JoyconDriver::GetSerialNumber(SDL_hid_device_info* device_info, |
|||
SerialNumber& serial_number) { |
|||
if (device_info->serial_number == nullptr) { |
|||
return DriverResult::Unknown; |
|||
} |
|||
std::memcpy(&serial_number, device_info->serial_number, 15); |
|||
return Joycon::DriverResult::Success; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,150 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project |
|||
// SPDX-License-Identifier: GPL-2.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <atomic> |
|||
#include <functional> |
|||
#include <mutex> |
|||
#include <span> |
|||
#include <thread> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
class CalibrationProtocol; |
|||
class GenericProtocol; |
|||
class IrsProtocol; |
|||
class NfcProtocol; |
|||
class JoyconPoller; |
|||
class RingConProtocol; |
|||
class RumbleProtocol; |
|||
|
|||
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 SetIrsConfig(IrsMode mode_, IrsResolution format_); |
|||
DriverResult SetPasiveMode(); |
|||
DriverResult SetActiveMode(); |
|||
DriverResult SetIrMode(); |
|||
DriverResult SetNfcMode(); |
|||
DriverResult SetRingConMode(); |
|||
|
|||
void SetCallbacks(const JoyconCallbacks& callbacks); |
|||
|
|||
// Returns device type from hidapi handle |
|||
static DriverResult GetDeviceType(SDL_hid_device_info* device_info, |
|||
ControllerType& controller_type); |
|||
|
|||
// Returns serial number from hidapi handle |
|||
static DriverResult GetSerialNumber(SDL_hid_device_info* device_info, |
|||
SerialNumber& serial_number); |
|||
|
|||
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 |
|||
DriverResult 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(); |
|||
|
|||
// Protocol Features |
|||
std::unique_ptr<CalibrationProtocol> calibration_protocol; |
|||
std::unique_ptr<GenericProtocol> generic_protocol; |
|||
std::unique_ptr<IrsProtocol> irs_protocol; |
|||
std::unique_ptr<NfcProtocol> nfc_protocol; |
|||
std::unique_ptr<JoyconPoller> joycon_poller; |
|||
std::unique_ptr<RingConProtocol> ring_protocol; |
|||
std::unique_ptr<RumbleProtocol> rumble_protocol; |
|||
|
|||
// Connection status |
|||
std::atomic<bool> is_connected{}; |
|||
u64 delta_time; |
|||
std::size_t error_counter{}; |
|||
std::shared_ptr<JoyconHandle> hidapi_handle; |
|||
std::chrono::time_point<std::chrono::steady_clock> last_update; |
|||
|
|||
// External device status |
|||
bool starlink_connected{}; |
|||
bool ring_connected{}; |
|||
bool amiibo_detected{}; |
|||
bool is_ring_disabled_by_irs{}; |
|||
|
|||
// 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{}; |
|||
RingCalibration ring_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,184 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <cstring>
|
|||
|
|||
#include "input_common/helpers/joycon_protocol/calibration.h"
|
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
CalibrationProtocol::CalibrationProtocol(std::shared_ptr<JoyconHandle> handle) |
|||
: JoyconCommonProtocol(std::move(handle)) {} |
|||
|
|||
DriverResult CalibrationProtocol::GetLeftJoyStickCalibration(JoyStickCalibration& calibration) { |
|||
ScopedSetBlocking sb(this); |
|||
std::vector<u8> buffer; |
|||
DriverResult result{DriverResult::Success}; |
|||
calibration = {}; |
|||
|
|||
result = ReadSPI(CalAddr::USER_LEFT_MAGIC, sizeof(u16), buffer); |
|||
|
|||
if (result == DriverResult::Success) { |
|||
const bool has_user_calibration = buffer[0] == 0xB2 && buffer[1] == 0xA1; |
|||
if (has_user_calibration) { |
|||
result = ReadSPI(CalAddr::USER_LEFT_DATA, 9, buffer); |
|||
} else { |
|||
result = ReadSPI(CalAddr::FACT_LEFT_DATA, 9, buffer); |
|||
} |
|||
} |
|||
|
|||
if (result == DriverResult::Success) { |
|||
calibration.x.max = static_cast<u16>(((buffer[1] & 0x0F) << 8) | buffer[0]); |
|||
calibration.y.max = static_cast<u16>((buffer[2] << 4) | (buffer[1] >> 4)); |
|||
calibration.x.center = static_cast<u16>(((buffer[4] & 0x0F) << 8) | buffer[3]); |
|||
calibration.y.center = static_cast<u16>((buffer[5] << 4) | (buffer[4] >> 4)); |
|||
calibration.x.min = static_cast<u16>(((buffer[7] & 0x0F) << 8) | buffer[6]); |
|||
calibration.y.min = static_cast<u16>((buffer[8] << 4) | (buffer[7] >> 4)); |
|||
} |
|||
|
|||
// Nintendo fix for drifting stick
|
|||
// result = ReadSPI(0x60, 0x86 ,buffer, 16);
|
|||
// calibration.deadzone = (u16)((buffer[4] << 8) & 0xF00 | buffer[3]);
|
|||
|
|||
// Set a valid default calibration if data is missing
|
|||
ValidateCalibration(calibration); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult CalibrationProtocol::GetRightJoyStickCalibration(JoyStickCalibration& calibration) { |
|||
ScopedSetBlocking sb(this); |
|||
std::vector<u8> buffer; |
|||
DriverResult result{DriverResult::Success}; |
|||
calibration = {}; |
|||
|
|||
result = ReadSPI(CalAddr::USER_RIGHT_MAGIC, sizeof(u16), buffer); |
|||
|
|||
if (result == DriverResult::Success) { |
|||
const bool has_user_calibration = buffer[0] == 0xB2 && buffer[1] == 0xA1; |
|||
if (has_user_calibration) { |
|||
result = ReadSPI(CalAddr::USER_RIGHT_DATA, 9, buffer); |
|||
} else { |
|||
result = ReadSPI(CalAddr::FACT_RIGHT_DATA, 9, buffer); |
|||
} |
|||
} |
|||
|
|||
if (result == DriverResult::Success) { |
|||
calibration.x.center = static_cast<u16>(((buffer[1] & 0x0F) << 8) | buffer[0]); |
|||
calibration.y.center = static_cast<u16>((buffer[2] << 4) | (buffer[1] >> 4)); |
|||
calibration.x.min = static_cast<u16>(((buffer[4] & 0x0F) << 8) | buffer[3]); |
|||
calibration.y.min = static_cast<u16>((buffer[5] << 4) | (buffer[4] >> 4)); |
|||
calibration.x.max = static_cast<u16>(((buffer[7] & 0x0F) << 8) | buffer[6]); |
|||
calibration.y.max = static_cast<u16>((buffer[8] << 4) | (buffer[7] >> 4)); |
|||
} |
|||
|
|||
// Nintendo fix for drifting stick
|
|||
// buffer = ReadSPI(0x60, 0x98 , 16);
|
|||
// joystick.deadzone = (u16)((buffer[4] << 8) & 0xF00 | buffer[3]);
|
|||
|
|||
// Set a valid default calibration if data is missing
|
|||
ValidateCalibration(calibration); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult CalibrationProtocol::GetImuCalibration(MotionCalibration& calibration) { |
|||
ScopedSetBlocking sb(this); |
|||
std::vector<u8> buffer; |
|||
DriverResult result{DriverResult::Success}; |
|||
calibration = {}; |
|||
|
|||
result = ReadSPI(CalAddr::USER_IMU_MAGIC, sizeof(u16), buffer); |
|||
|
|||
if (result == DriverResult::Success) { |
|||
const bool has_user_calibration = buffer[0] == 0xB2 && buffer[1] == 0xA1; |
|||
if (has_user_calibration) { |
|||
result = ReadSPI(CalAddr::USER_IMU_DATA, sizeof(IMUCalibration), buffer); |
|||
} else { |
|||
result = ReadSPI(CalAddr::FACT_IMU_DATA, sizeof(IMUCalibration), buffer); |
|||
} |
|||
} |
|||
|
|||
if (result == DriverResult::Success) { |
|||
IMUCalibration device_calibration{}; |
|||
memcpy(&device_calibration, buffer.data(), sizeof(IMUCalibration)); |
|||
calibration.accelerometer[0].offset = device_calibration.accelerometer_offset[0]; |
|||
calibration.accelerometer[1].offset = device_calibration.accelerometer_offset[1]; |
|||
calibration.accelerometer[2].offset = device_calibration.accelerometer_offset[2]; |
|||
|
|||
calibration.accelerometer[0].scale = device_calibration.accelerometer_scale[0]; |
|||
calibration.accelerometer[1].scale = device_calibration.accelerometer_scale[1]; |
|||
calibration.accelerometer[2].scale = device_calibration.accelerometer_scale[2]; |
|||
|
|||
calibration.gyro[0].offset = device_calibration.gyroscope_offset[0]; |
|||
calibration.gyro[1].offset = device_calibration.gyroscope_offset[1]; |
|||
calibration.gyro[2].offset = device_calibration.gyroscope_offset[2]; |
|||
|
|||
calibration.gyro[0].scale = device_calibration.gyroscope_scale[0]; |
|||
calibration.gyro[1].scale = device_calibration.gyroscope_scale[1]; |
|||
calibration.gyro[2].scale = device_calibration.gyroscope_scale[2]; |
|||
} |
|||
|
|||
ValidateCalibration(calibration); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult CalibrationProtocol::GetRingCalibration(RingCalibration& calibration, |
|||
s16 current_value) { |
|||
// TODO: Get default calibration form ring itself
|
|||
if (ring_data_max == 0 && ring_data_min == 0) { |
|||
ring_data_max = current_value + 800; |
|||
ring_data_min = current_value - 800; |
|||
ring_data_default = current_value; |
|||
} |
|||
ring_data_max = std::max(ring_data_max, current_value); |
|||
ring_data_min = std::min(ring_data_min, current_value); |
|||
calibration = { |
|||
.default_value = ring_data_default, |
|||
.max_value = ring_data_max, |
|||
.min_value = ring_data_min, |
|||
}; |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
void CalibrationProtocol::ValidateCalibration(JoyStickCalibration& calibration) { |
|||
constexpr u16 DefaultStickCenter{2048}; |
|||
constexpr u16 DefaultStickRange{1740}; |
|||
|
|||
if (calibration.x.center == 0xFFF || calibration.x.center == 0) { |
|||
calibration.x.center = DefaultStickCenter; |
|||
} |
|||
if (calibration.x.max == 0xFFF || calibration.x.max == 0) { |
|||
calibration.x.max = DefaultStickRange; |
|||
} |
|||
if (calibration.x.min == 0xFFF || calibration.x.min == 0) { |
|||
calibration.x.min = DefaultStickRange; |
|||
} |
|||
|
|||
if (calibration.y.center == 0xFFF || calibration.y.center == 0) { |
|||
calibration.y.center = DefaultStickCenter; |
|||
} |
|||
if (calibration.y.max == 0xFFF || calibration.y.max == 0) { |
|||
calibration.y.max = DefaultStickRange; |
|||
} |
|||
if (calibration.y.min == 0xFFF || calibration.y.min == 0) { |
|||
calibration.y.min = DefaultStickRange; |
|||
} |
|||
} |
|||
|
|||
void CalibrationProtocol::ValidateCalibration(MotionCalibration& calibration) { |
|||
for (auto& sensor : calibration.accelerometer) { |
|||
if (sensor.scale == 0) { |
|||
sensor.scale = 0x4000; |
|||
} |
|||
} |
|||
for (auto& sensor : calibration.gyro) { |
|||
if (sensor.scale == 0) { |
|||
sensor.scale = 0x3be7; |
|||
} |
|||
} |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,64 @@ |
|||
// 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 <vector> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/common_protocol.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
enum class DriverResult; |
|||
struct JoyStickCalibration; |
|||
struct IMUCalibration; |
|||
struct JoyconHandle; |
|||
} // namespace InputCommon::Joycon |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
/// Driver functions related to retrieving calibration data from the device |
|||
class CalibrationProtocol final : private JoyconCommonProtocol { |
|||
public: |
|||
explicit CalibrationProtocol(std::shared_ptr<JoyconHandle> handle); |
|||
|
|||
/** |
|||
* Sends a request to obtain the left stick calibration from memory |
|||
* @param is_factory_calibration if true factory values will be returned |
|||
* @returns JoyStickCalibration of the left joystick |
|||
*/ |
|||
DriverResult GetLeftJoyStickCalibration(JoyStickCalibration& calibration); |
|||
|
|||
/** |
|||
* Sends a request to obtain the right stick calibration from memory |
|||
* @param is_factory_calibration if true factory values will be returned |
|||
* @returns JoyStickCalibration of the right joystick |
|||
*/ |
|||
DriverResult GetRightJoyStickCalibration(JoyStickCalibration& calibration); |
|||
|
|||
/** |
|||
* Sends a request to obtain the motion calibration from memory |
|||
* @returns ImuCalibration of the motion sensor |
|||
*/ |
|||
DriverResult GetImuCalibration(MotionCalibration& calibration); |
|||
|
|||
/** |
|||
* Calculates on run time the proper calibration of the ring controller |
|||
* @returns RingCalibration of the ring sensor |
|||
*/ |
|||
DriverResult GetRingCalibration(RingCalibration& calibration, s16 current_value); |
|||
|
|||
private: |
|||
void ValidateCalibration(JoyStickCalibration& calibration); |
|||
void ValidateCalibration(MotionCalibration& calibration); |
|||
|
|||
s16 ring_data_max = 0; |
|||
s16 ring_data_default = 0; |
|||
s16 ring_data_min = 0; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,299 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
JoyconCommonProtocol::JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_) |
|||
: hidapi_handle{std::move(hidapi_handle_)} {} |
|||
|
|||
u8 JoyconCommonProtocol::GetCounter() { |
|||
hidapi_handle->packet_counter = (hidapi_handle->packet_counter + 1) & 0x0F; |
|||
return hidapi_handle->packet_counter; |
|||
} |
|||
|
|||
void JoyconCommonProtocol::SetBlocking() { |
|||
SDL_hid_set_nonblocking(hidapi_handle->handle, 0); |
|||
} |
|||
|
|||
void JoyconCommonProtocol::SetNonBlocking() { |
|||
SDL_hid_set_nonblocking(hidapi_handle->handle, 1); |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::GetDeviceType(ControllerType& controller_type) { |
|||
std::vector<u8> buffer; |
|||
const auto result = ReadSPI(CalAddr::DEVICE_TYPE, 1, buffer); |
|||
controller_type = ControllerType::None; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
controller_type = static_cast<ControllerType>(buffer[0]); |
|||
// Fallback to 3rd party pro controllers
|
|||
if (controller_type == ControllerType::None) { |
|||
controller_type = ControllerType::Pro; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::CheckDeviceAccess(SDL_hid_device_info* device_info) { |
|||
ControllerType controller_type{ControllerType::None}; |
|||
const auto result = GetDeviceType(controller_type); |
|||
if (result != DriverResult::Success || controller_type == ControllerType::None) { |
|||
return DriverResult::UnsupportedControllerType; |
|||
} |
|||
|
|||
hidapi_handle->handle = |
|||
SDL_hid_open(device_info->vendor_id, device_info->product_id, device_info->serial_number); |
|||
|
|||
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; |
|||
} |
|||
|
|||
SetNonBlocking(); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SetReportMode(ReportMode report_mode) { |
|||
const std::array<u8, 1> buffer{static_cast<u8>(report_mode)}; |
|||
return SendSubCommand(SubCommand::SET_REPORT_MODE, buffer); |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SendData(std::span<const u8> buffer) { |
|||
const auto result = SDL_hid_write(hidapi_handle->handle, buffer.data(), buffer.size()); |
|||
|
|||
if (result == -1) { |
|||
return DriverResult::ErrorWritingData; |
|||
} |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::GetSubCommandResponse(SubCommand sc, std::vector<u8>& output) { |
|||
constexpr int timeout_mili = 66; |
|||
constexpr int MaxTries = 15; |
|||
int tries = 0; |
|||
output.resize(MaxSubCommandResponseSize); |
|||
|
|||
do { |
|||
int result = SDL_hid_read_timeout(hidapi_handle->handle, output.data(), |
|||
MaxSubCommandResponseSize, timeout_mili); |
|||
|
|||
if (result < 1) { |
|||
LOG_ERROR(Input, "No response from joycon"); |
|||
} |
|||
if (tries++ > MaxTries) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} while (output[0] != 0x21 && output[14] != static_cast<u8>(sc)); |
|||
|
|||
if (output[0] != 0x21 && output[14] != static_cast<u8>(sc)) { |
|||
return DriverResult::WrongReply; |
|||
} |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const u8> buffer, |
|||
std::vector<u8>& output) { |
|||
std::vector<u8> local_buffer(MaxResponseSize); |
|||
|
|||
local_buffer[0] = static_cast<u8>(OutputReport::RUMBLE_AND_SUBCMD); |
|||
local_buffer[1] = GetCounter(); |
|||
local_buffer[10] = static_cast<u8>(sc); |
|||
for (std::size_t i = 0; i < buffer.size(); ++i) { |
|||
local_buffer[11 + i] = buffer[i]; |
|||
} |
|||
|
|||
auto result = SendData(local_buffer); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
result = GetSubCommandResponse(sc, output); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const u8> buffer) { |
|||
std::vector<u8> output; |
|||
return SendSubCommand(sc, buffer, output); |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SendMCUCommand(SubCommand sc, std::span<const u8> buffer) { |
|||
std::vector<u8> local_buffer(MaxResponseSize); |
|||
|
|||
local_buffer[0] = static_cast<u8>(OutputReport::MCU_DATA); |
|||
local_buffer[1] = GetCounter(); |
|||
local_buffer[10] = static_cast<u8>(sc); |
|||
for (std::size_t i = 0; i < buffer.size(); ++i) { |
|||
local_buffer[11 + i] = buffer[i]; |
|||
} |
|||
|
|||
return SendData(local_buffer); |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SendVibrationReport(std::span<const u8> buffer) { |
|||
std::vector<u8> local_buffer(MaxResponseSize); |
|||
|
|||
local_buffer[0] = static_cast<u8>(Joycon::OutputReport::RUMBLE_ONLY); |
|||
local_buffer[1] = GetCounter(); |
|||
|
|||
memcpy(local_buffer.data() + 2, buffer.data(), buffer.size()); |
|||
|
|||
return SendData(local_buffer); |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::ReadSPI(CalAddr addr, u8 size, std::vector<u8>& output) { |
|||
constexpr std::size_t MaxTries = 10; |
|||
std::size_t tries = 0; |
|||
std::array<u8, 5> buffer = {0x00, 0x00, 0x00, 0x00, size}; |
|||
std::vector<u8> local_buffer(size + 20); |
|||
|
|||
buffer[0] = static_cast<u8>(static_cast<u16>(addr) & 0x00FF); |
|||
buffer[1] = static_cast<u8>((static_cast<u16>(addr) & 0xFF00) >> 8); |
|||
do { |
|||
const auto result = SendSubCommand(SubCommand::SPI_FLASH_READ, buffer, local_buffer); |
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
if (tries++ > MaxTries) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} while (local_buffer[15] != buffer[0] || local_buffer[16] != buffer[1]); |
|||
|
|||
// Remove header from output
|
|||
output = std::vector<u8>(local_buffer.begin() + 20, local_buffer.begin() + 20 + size); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::EnableMCU(bool enable) { |
|||
const std::array<u8, 1> mcu_state{static_cast<u8>(enable ? 1 : 0)}; |
|||
const auto result = SendSubCommand(SubCommand::SET_MCU_STATE, mcu_state); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
LOG_ERROR(Input, "SendMCUData failed with error {}", result); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::ConfigureMCU(const MCUConfig& config) { |
|||
LOG_DEBUG(Input, "ConfigureMCU"); |
|||
std::array<u8, sizeof(MCUConfig)> config_buffer; |
|||
memcpy(config_buffer.data(), &config, sizeof(MCUConfig)); |
|||
config_buffer[37] = CalculateMCU_CRC8(config_buffer.data() + 1, 36); |
|||
|
|||
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, config_buffer); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
LOG_ERROR(Input, "Set MCU config failed with error {}", result); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::GetMCUDataResponse(ReportMode report_mode_, |
|||
std::vector<u8>& output) { |
|||
const int report_mode = static_cast<u8>(report_mode_); |
|||
constexpr int TimeoutMili = 200; |
|||
constexpr int MaxTries = 9; |
|||
int tries = 0; |
|||
output.resize(0x170); |
|||
|
|||
do { |
|||
int result = SDL_hid_read_timeout(hidapi_handle->handle, output.data(), 0x170, TimeoutMili); |
|||
|
|||
if (result < 1) { |
|||
LOG_ERROR(Input, "No response from joycon attempt {}", tries); |
|||
} |
|||
if (tries++ > MaxTries) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} while (output[0] != report_mode || output[49] == 0xFF); |
|||
|
|||
if (output[0] != report_mode || output[49] == 0xFF) { |
|||
return DriverResult::WrongReply; |
|||
} |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::SendMCUData(ReportMode report_mode, SubCommand sc, |
|||
std::span<const u8> buffer, |
|||
std::vector<u8>& output) { |
|||
std::vector<u8> local_buffer(MaxResponseSize); |
|||
|
|||
local_buffer[0] = static_cast<u8>(OutputReport::MCU_DATA); |
|||
local_buffer[1] = GetCounter(); |
|||
local_buffer[9] = static_cast<u8>(sc); |
|||
for (std::size_t i = 0; i < buffer.size(); ++i) { |
|||
local_buffer[10 + i] = buffer[i]; |
|||
} |
|||
|
|||
auto result = SendData(local_buffer); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
result = GetMCUDataResponse(report_mode, output); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult JoyconCommonProtocol::WaitSetMCUMode(ReportMode report_mode, MCUMode mode) { |
|||
std::vector<u8> output; |
|||
constexpr std::size_t MaxTries{8}; |
|||
std::size_t tries{}; |
|||
|
|||
do { |
|||
const std::vector<u8> mcu_data{static_cast<u8>(MCUMode::Standby)}; |
|||
const auto result = SendMCUData(report_mode, SubCommand::STATE, mcu_data, output); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
if (tries++ > MaxTries) { |
|||
return DriverResult::WrongReply; |
|||
} |
|||
} while (output[49] != 1 || output[56] != static_cast<u8>(mode)); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
// crc-8-ccitt / polynomial 0x07 look up table
|
|||
constexpr std::array<u8, 256> mcu_crc8_table = { |
|||
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D, |
|||
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, |
|||
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, |
|||
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD, |
|||
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, |
|||
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, |
|||
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A, |
|||
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, |
|||
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, |
|||
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4, |
|||
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, |
|||
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, |
|||
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63, |
|||
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, |
|||
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, |
|||
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3}; |
|||
|
|||
u8 JoyconCommonProtocol::CalculateMCU_CRC8(u8* buffer, u8 size) const { |
|||
u8 crc8 = 0x0; |
|||
|
|||
for (int i = 0; i < size; ++i) { |
|||
crc8 = mcu_crc8_table[static_cast<u8>(crc8 ^ buffer[i])]; |
|||
} |
|||
return crc8; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,173 @@ |
|||
// 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 <memory> |
|||
#include <span> |
|||
#include <vector> |
|||
|
|||
#include "common/common_types.h" |
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
/// Joycon driver functions that handle low level communication |
|||
class JoyconCommonProtocol { |
|||
public: |
|||
explicit JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_); |
|||
|
|||
/** |
|||
* Sets handle to blocking. In blocking mode, SDL_hid_read() will wait (block) until there is |
|||
* data to read before returning. |
|||
*/ |
|||
void SetBlocking(); |
|||
|
|||
/** |
|||
* Sets handle to non blocking. In non-blocking mode calls to SDL_hid_read() will return |
|||
* immediately with a value of 0 if there is no data to be read |
|||
*/ |
|||
void SetNonBlocking(); |
|||
|
|||
/** |
|||
* Sends a request to obtain the joycon type from device |
|||
* @returns controller type of the joycon |
|||
*/ |
|||
DriverResult GetDeviceType(ControllerType& controller_type); |
|||
|
|||
/** |
|||
* Verifies and sets the joycon_handle if device is valid |
|||
* @param device info from the driver |
|||
* @returns success if the device is valid |
|||
*/ |
|||
DriverResult CheckDeviceAccess(SDL_hid_device_info* device); |
|||
|
|||
/** |
|||
* Sends a request to set the polling mode of the joycon |
|||
* @param report_mode polling mode to be set |
|||
*/ |
|||
DriverResult SetReportMode(Joycon::ReportMode report_mode); |
|||
|
|||
/** |
|||
* Sends data to the joycon device |
|||
* @param buffer data to be send |
|||
*/ |
|||
DriverResult SendData(std::span<const u8> buffer); |
|||
|
|||
/** |
|||
* Waits for incoming data of the joycon device that matchs the subcommand |
|||
* @param sub_command type of data to be returned |
|||
* @returns a buffer containing the responce |
|||
*/ |
|||
DriverResult GetSubCommandResponse(SubCommand sub_command, std::vector<u8>& output); |
|||
|
|||
/** |
|||
* Sends a sub command to the device and waits for it's reply |
|||
* @param sc sub command to be send |
|||
* @param buffer data to be send |
|||
* @returns output buffer containing the responce |
|||
*/ |
|||
DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer, std::vector<u8>& output); |
|||
|
|||
/** |
|||
* Sends a sub command to the device and waits for it's reply and ignores the output |
|||
* @param sc sub command to be send |
|||
* @param buffer data to be send |
|||
*/ |
|||
DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer); |
|||
|
|||
/** |
|||
* Sends a mcu command to the device |
|||
* @param sc sub command to be send |
|||
* @param buffer data to be send |
|||
*/ |
|||
DriverResult SendMCUCommand(SubCommand sc, std::span<const u8> buffer); |
|||
|
|||
/** |
|||
* Sends vibration data to the joycon |
|||
* @param buffer data to be send |
|||
*/ |
|||
DriverResult SendVibrationReport(std::span<const u8> buffer); |
|||
|
|||
/** |
|||
* Reads the SPI memory stored on the joycon |
|||
* @param Initial address location |
|||
* @param size in bytes to be read |
|||
* @returns output buffer containing the responce |
|||
*/ |
|||
DriverResult ReadSPI(CalAddr addr, u8 size, std::vector<u8>& output); |
|||
|
|||
/** |
|||
* Enables MCU chip on the joycon |
|||
* @param enable if true the chip will be enabled |
|||
*/ |
|||
DriverResult EnableMCU(bool enable); |
|||
|
|||
/** |
|||
* Configures the MCU to the correspoinding mode |
|||
* @param MCUConfig configuration |
|||
*/ |
|||
DriverResult ConfigureMCU(const MCUConfig& config); |
|||
|
|||
/** |
|||
* Waits until there's MCU data available. On timeout returns error |
|||
* @param report mode of the expected reply |
|||
* @returns a buffer containing the responce |
|||
*/ |
|||
DriverResult GetMCUDataResponse(ReportMode report_mode_, std::vector<u8>& output); |
|||
|
|||
/** |
|||
* Sends data to the MCU chip and waits for it's reply |
|||
* @param report mode of the expected reply |
|||
* @param sub command to be send |
|||
* @param buffer data to be send |
|||
* @returns output buffer containing the responce |
|||
*/ |
|||
DriverResult SendMCUData(ReportMode report_mode, SubCommand sc, std::span<const u8> buffer, |
|||
std::vector<u8>& output); |
|||
|
|||
/** |
|||
* Wait's until the MCU chip is on the specified mode |
|||
* @param report mode of the expected reply |
|||
* @param MCUMode configuration |
|||
*/ |
|||
DriverResult WaitSetMCUMode(ReportMode report_mode, MCUMode mode); |
|||
|
|||
/** |
|||
* Calculates the checksum from the MCU data |
|||
* @param buffer containing the data to be send |
|||
* @param size of the buffer in bytes |
|||
* @returns byte with the correct checksum |
|||
*/ |
|||
u8 CalculateMCU_CRC8(u8* buffer, u8 size) const; |
|||
|
|||
private: |
|||
/** |
|||
* Increments and returns the packet counter of the handle |
|||
* @param joycon_handle device to send the data |
|||
* @returns packet counter value |
|||
*/ |
|||
u8 GetCounter(); |
|||
|
|||
std::shared_ptr<JoyconHandle> hidapi_handle; |
|||
}; |
|||
|
|||
class ScopedSetBlocking { |
|||
public: |
|||
explicit ScopedSetBlocking(JoyconCommonProtocol* self) : m_self{self} { |
|||
m_self->SetBlocking(); |
|||
} |
|||
|
|||
~ScopedSetBlocking() { |
|||
m_self->SetNonBlocking(); |
|||
} |
|||
|
|||
private: |
|||
JoyconCommonProtocol* m_self{}; |
|||
}; |
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,125 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/generic_functions.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
GenericProtocol::GenericProtocol(std::shared_ptr<JoyconHandle> handle) |
|||
: JoyconCommonProtocol(std::move(handle)) {} |
|||
|
|||
DriverResult GenericProtocol::EnablePassiveMode() { |
|||
ScopedSetBlocking sb(this); |
|||
return SetReportMode(ReportMode::SIMPLE_HID_MODE); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::EnableActiveMode() { |
|||
ScopedSetBlocking sb(this); |
|||
return SetReportMode(ReportMode::STANDARD_FULL_60HZ); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetDeviceInfo(DeviceInfo& device_info) { |
|||
ScopedSetBlocking sb(this); |
|||
std::vector<u8> output; |
|||
|
|||
const auto result = SendSubCommand(SubCommand::REQ_DEV_INFO, {}, output); |
|||
|
|||
device_info = {}; |
|||
if (result == DriverResult::Success) { |
|||
memcpy(&device_info, output.data(), sizeof(DeviceInfo)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetControllerType(ControllerType& controller_type) { |
|||
return GetDeviceType(controller_type); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::EnableImu(bool enable) { |
|||
ScopedSetBlocking sb(this); |
|||
const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)}; |
|||
return SendSubCommand(SubCommand::ENABLE_IMU, buffer); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec, |
|||
AccelerometerSensitivity asen, |
|||
AccelerometerPerformance afrec) { |
|||
ScopedSetBlocking sb(this); |
|||
const std::array<u8, 4> buffer{static_cast<u8>(gsen), static_cast<u8>(asen), |
|||
static_cast<u8>(gfrec), static_cast<u8>(afrec)}; |
|||
return SendSubCommand(SubCommand::SET_IMU_SENSITIVITY, buffer); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetBattery(u32& battery_level) { |
|||
// This function is meant to request the high resolution battery status
|
|||
battery_level = 0; |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetColor(Color& color) { |
|||
ScopedSetBlocking sb(this); |
|||
std::vector<u8> buffer; |
|||
const auto result = ReadSPI(CalAddr::COLOR_DATA, 12, buffer); |
|||
|
|||
color = {}; |
|||
if (result == DriverResult::Success) { |
|||
color.body = static_cast<u32>((buffer[0] << 16) | (buffer[1] << 8) | buffer[2]); |
|||
color.buttons = static_cast<u32>((buffer[3] << 16) | (buffer[4] << 8) | buffer[5]); |
|||
color.left_grip = static_cast<u32>((buffer[6] << 16) | (buffer[7] << 8) | buffer[8]); |
|||
color.right_grip = static_cast<u32>((buffer[9] << 16) | (buffer[10] << 8) | buffer[11]); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetSerialNumber(SerialNumber& serial_number) { |
|||
ScopedSetBlocking sb(this); |
|||
std::vector<u8> buffer; |
|||
const auto result = ReadSPI(CalAddr::SERIAL_NUMBER, 16, buffer); |
|||
|
|||
serial_number = {}; |
|||
if (result == DriverResult::Success) { |
|||
memcpy(serial_number.data(), buffer.data() + 1, sizeof(SerialNumber)); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetTemperature(u32& temperature) { |
|||
// Not all devices have temperature sensor
|
|||
temperature = 25; |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::GetVersionNumber(FirmwareVersion& version) { |
|||
DeviceInfo device_info{}; |
|||
|
|||
const auto result = GetDeviceInfo(device_info); |
|||
version = device_info.firmware; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::SetHomeLight() { |
|||
ScopedSetBlocking sb(this); |
|||
static constexpr std::array<u8, 3> buffer{0x0f, 0xf0, 0x00}; |
|||
return SendSubCommand(SubCommand::SET_HOME_LIGHT, buffer); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::SetLedBusy() { |
|||
return DriverResult::NotSupported; |
|||
} |
|||
|
|||
DriverResult GenericProtocol::SetLedPattern(u8 leds) { |
|||
ScopedSetBlocking sb(this); |
|||
const std::array<u8, 1> buffer{leds}; |
|||
return SendSubCommand(SubCommand::SET_PLAYER_LIGHTS, buffer); |
|||
} |
|||
|
|||
DriverResult GenericProtocol::SetLedBlinkPattern(u8 leds) { |
|||
return SetLedPattern(static_cast<u8>(leds << 4)); |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,108 @@ |
|||
// 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 "input_common/helpers/joycon_protocol/common_protocol.h" |
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
/// Joycon driver functions that easily implemented |
|||
class GenericProtocol final : private JoyconCommonProtocol { |
|||
public: |
|||
explicit GenericProtocol(std::shared_ptr<JoyconHandle> handle); |
|||
|
|||
/// Enables passive mode. This mode only sends button data on change. Sticks will return digital |
|||
/// data instead of analog. Motion will be disabled |
|||
DriverResult EnablePassiveMode(); |
|||
|
|||
/// Enables active mode. This mode will return the current status every 5-15ms |
|||
DriverResult EnableActiveMode(); |
|||
|
|||
/** |
|||
* Sends a request to obtain the joycon firmware and mac from handle |
|||
* @returns controller device info |
|||
*/ |
|||
DriverResult GetDeviceInfo(DeviceInfo& controller_type); |
|||
|
|||
/** |
|||
* Sends a request to obtain the joycon type from handle |
|||
* @returns controller type of the joycon |
|||
*/ |
|||
DriverResult GetControllerType(ControllerType& controller_type); |
|||
|
|||
/** |
|||
* Enables motion input |
|||
* @param enable if true motion data will be enabled |
|||
*/ |
|||
DriverResult EnableImu(bool enable); |
|||
|
|||
/** |
|||
* Configures the motion sensor with the specified parameters |
|||
* @param gsen gyroscope sensor sensitvity in degrees per second |
|||
* @param gfrec gyroscope sensor frequency in hertz |
|||
* @param asen accelerometer sensitivity in G force |
|||
* @param afrec accelerometer frequency in hertz |
|||
*/ |
|||
DriverResult SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec, |
|||
AccelerometerSensitivity asen, AccelerometerPerformance afrec); |
|||
|
|||
/** |
|||
* Request battery level from the device |
|||
* @returns battery level |
|||
*/ |
|||
DriverResult GetBattery(u32& battery_level); |
|||
|
|||
/** |
|||
* Request joycon colors from the device |
|||
* @returns colors of the body and buttons |
|||
*/ |
|||
DriverResult GetColor(Color& color); |
|||
|
|||
/** |
|||
* Request joycon serial number from the device |
|||
* @returns 16 byte serial number |
|||
*/ |
|||
DriverResult GetSerialNumber(SerialNumber& serial_number); |
|||
|
|||
/** |
|||
* Request joycon serial number from the device |
|||
* @returns 16 byte serial number |
|||
*/ |
|||
DriverResult GetTemperature(u32& temperature); |
|||
|
|||
/** |
|||
* Request joycon serial number from the device |
|||
* @returns 16 byte serial number |
|||
*/ |
|||
DriverResult GetVersionNumber(FirmwareVersion& version); |
|||
|
|||
/** |
|||
* Sets home led behaviour |
|||
*/ |
|||
DriverResult SetHomeLight(); |
|||
|
|||
/** |
|||
* Sets home led into a slow breathing state |
|||
*/ |
|||
DriverResult SetLedBusy(); |
|||
|
|||
/** |
|||
* Sets the 4 player leds on the joycon on a solid state |
|||
* @params bit flag containing the led state |
|||
*/ |
|||
DriverResult SetLedPattern(u8 leds); |
|||
|
|||
/** |
|||
* Sets the 4 player leds on the joycon on a blinking state |
|||
* @returns bit flag containing the led state |
|||
*/ |
|||
DriverResult SetLedBlinkPattern(u8 leds); |
|||
}; |
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,298 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <thread>
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/irs.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
IrsProtocol::IrsProtocol(std::shared_ptr<JoyconHandle> handle) |
|||
: JoyconCommonProtocol(std::move(handle)) {} |
|||
|
|||
DriverResult IrsProtocol::EnableIrs() { |
|||
LOG_INFO(Input, "Enable IRS"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = EnableMCU(true); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
const MCUConfig config{ |
|||
.command = MCUCommand::ConfigureMCU, |
|||
.sub_command = MCUSubCommand::SetMCUMode, |
|||
.mode = MCUMode::IR, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
result = ConfigureMCU(config); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::IR); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = ConfigureIrs(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WriteRegistersStep1(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WriteRegistersStep2(); |
|||
} |
|||
|
|||
is_enabled = true; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult IrsProtocol::DisableIrs() { |
|||
LOG_DEBUG(Input, "Disable IRS"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = EnableMCU(false); |
|||
} |
|||
|
|||
is_enabled = false; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult IrsProtocol::SetIrsConfig(IrsMode mode, IrsResolution format) { |
|||
irs_mode = mode; |
|||
switch (format) { |
|||
case IrsResolution::Size320x240: |
|||
resolution_code = IrsResolutionCode::Size320x240; |
|||
fragments = IrsFragments::Size320x240; |
|||
resolution = IrsResolution::Size320x240; |
|||
break; |
|||
case IrsResolution::Size160x120: |
|||
resolution_code = IrsResolutionCode::Size160x120; |
|||
fragments = IrsFragments::Size160x120; |
|||
resolution = IrsResolution::Size160x120; |
|||
break; |
|||
case IrsResolution::Size80x60: |
|||
resolution_code = IrsResolutionCode::Size80x60; |
|||
fragments = IrsFragments::Size80x60; |
|||
resolution = IrsResolution::Size80x60; |
|||
break; |
|||
case IrsResolution::Size20x15: |
|||
resolution_code = IrsResolutionCode::Size20x15; |
|||
fragments = IrsFragments::Size20x15; |
|||
resolution = IrsResolution::Size20x15; |
|||
break; |
|||
case IrsResolution::Size40x30: |
|||
default: |
|||
resolution_code = IrsResolutionCode::Size40x30; |
|||
fragments = IrsFragments::Size40x30; |
|||
resolution = IrsResolution::Size40x30; |
|||
break; |
|||
} |
|||
|
|||
// Restart feature
|
|||
if (is_enabled) { |
|||
DisableIrs(); |
|||
return EnableIrs(); |
|||
} |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult IrsProtocol::RequestImage(std::span<u8> buffer) { |
|||
const u8 next_packet_fragment = |
|||
static_cast<u8>((packet_fragment + 1) % (static_cast<u8>(fragments) + 1)); |
|||
|
|||
if (buffer[0] == 0x31 && buffer[49] == 0x03) { |
|||
u8 new_packet_fragment = buffer[52]; |
|||
if (new_packet_fragment == next_packet_fragment) { |
|||
packet_fragment = next_packet_fragment; |
|||
memcpy(buf_image.data() + (300 * packet_fragment), buffer.data() + 59, 300); |
|||
|
|||
return RequestFrame(packet_fragment); |
|||
} |
|||
|
|||
if (new_packet_fragment == packet_fragment) { |
|||
return RequestFrame(packet_fragment); |
|||
} |
|||
|
|||
return ResendFrame(next_packet_fragment); |
|||
} |
|||
|
|||
return RequestFrame(packet_fragment); |
|||
} |
|||
|
|||
DriverResult IrsProtocol::ConfigureIrs() { |
|||
LOG_DEBUG(Input, "Configure IRS"); |
|||
constexpr std::size_t max_tries = 28; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
const IrsConfigure irs_configuration{ |
|||
.command = MCUCommand::ConfigureIR, |
|||
.sub_command = MCUSubCommand::SetDeviceMode, |
|||
.irs_mode = IrsMode::ImageTransfer, |
|||
.number_of_fragments = fragments, |
|||
.mcu_major_version = 0x0500, |
|||
.mcu_minor_version = 0x1800, |
|||
.crc = {}, |
|||
}; |
|||
buf_image.resize((static_cast<u8>(fragments) + 1) * 300); |
|||
|
|||
std::array<u8, sizeof(IrsConfigure)> request_data{}; |
|||
memcpy(request_data.data(), &irs_configuration, sizeof(IrsConfigure)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
do { |
|||
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
if (tries++ >= max_tries) { |
|||
return DriverResult::WrongReply; |
|||
} |
|||
} while (output[15] != 0x0b); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult IrsProtocol::WriteRegistersStep1() { |
|||
LOG_DEBUG(Input, "WriteRegistersStep1"); |
|||
DriverResult result{DriverResult::Success}; |
|||
constexpr std::size_t max_tries = 28; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
const IrsWriteRegisters irs_registers{ |
|||
.command = MCUCommand::ConfigureIR, |
|||
.sub_command = MCUSubCommand::WriteDeviceRegisters, |
|||
.number_of_registers = 0x9, |
|||
.registers = |
|||
{ |
|||
IrsRegister{IrRegistersAddress::Resolution, static_cast<u8>(resolution_code)}, |
|||
{IrRegistersAddress::ExposureLSB, static_cast<u8>(exposure & 0xff)}, |
|||
{IrRegistersAddress::ExposureMSB, static_cast<u8>(exposure >> 8)}, |
|||
{IrRegistersAddress::ExposureTime, 0x00}, |
|||
{IrRegistersAddress::Leds, static_cast<u8>(leds)}, |
|||
{IrRegistersAddress::DigitalGainLSB, static_cast<u8>((digital_gain & 0x0f) << 4)}, |
|||
{IrRegistersAddress::DigitalGainMSB, static_cast<u8>((digital_gain & 0xf0) >> 4)}, |
|||
{IrRegistersAddress::LedFilter, static_cast<u8>(led_filter)}, |
|||
{IrRegistersAddress::WhitePixelThreshold, 0xc8}, |
|||
}, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
std::array<u8, sizeof(IrsWriteRegisters)> request_data{}; |
|||
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
|
|||
std::array<u8, 38> mcu_request{0x02}; |
|||
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36); |
|||
mcu_request[37] = 0xFF; |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
do { |
|||
result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output); |
|||
|
|||
// First time we need to set the report mode
|
|||
if (result == DriverResult::Success && tries == 0) { |
|||
result = SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request); |
|||
} |
|||
if (result == DriverResult::Success && tries == 0) { |
|||
GetSubCommandResponse(SubCommand::SET_MCU_CONFIG, output); |
|||
} |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
if (tries++ >= max_tries) { |
|||
return DriverResult::WrongReply; |
|||
} |
|||
} while (!(output[15] == 0x13 && output[17] == 0x07) && output[15] != 0x23); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult IrsProtocol::WriteRegistersStep2() { |
|||
LOG_DEBUG(Input, "WriteRegistersStep2"); |
|||
constexpr std::size_t max_tries = 28; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
const IrsWriteRegisters irs_registers{ |
|||
.command = MCUCommand::ConfigureIR, |
|||
.sub_command = MCUSubCommand::WriteDeviceRegisters, |
|||
.number_of_registers = 0x8, |
|||
.registers = |
|||
{ |
|||
IrsRegister{IrRegistersAddress::LedIntensitiyMSB, |
|||
static_cast<u8>(led_intensity >> 8)}, |
|||
{IrRegistersAddress::LedIntensitiyLSB, static_cast<u8>(led_intensity & 0xff)}, |
|||
{IrRegistersAddress::ImageFlip, static_cast<u8>(image_flip)}, |
|||
{IrRegistersAddress::DenoiseSmoothing, static_cast<u8>((denoise >> 16) & 0xff)}, |
|||
{IrRegistersAddress::DenoiseEdge, static_cast<u8>((denoise >> 8) & 0xff)}, |
|||
{IrRegistersAddress::DenoiseColor, static_cast<u8>(denoise & 0xff)}, |
|||
{IrRegistersAddress::UpdateTime, 0x2d}, |
|||
{IrRegistersAddress::FinalizeConfig, 0x01}, |
|||
}, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
std::array<u8, sizeof(IrsWriteRegisters)> request_data{}; |
|||
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
do { |
|||
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
if (tries++ >= max_tries) { |
|||
return DriverResult::WrongReply; |
|||
} |
|||
} while (output[15] != 0x13 && output[15] != 0x23); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult IrsProtocol::RequestFrame(u8 frame) { |
|||
std::array<u8, 38> mcu_request{}; |
|||
mcu_request[3] = frame; |
|||
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36); |
|||
mcu_request[37] = 0xFF; |
|||
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request); |
|||
} |
|||
|
|||
DriverResult IrsProtocol::ResendFrame(u8 frame) { |
|||
std::array<u8, 38> mcu_request{}; |
|||
mcu_request[1] = 0x1; |
|||
mcu_request[2] = frame; |
|||
mcu_request[3] = 0x0; |
|||
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36); |
|||
mcu_request[37] = 0xFF; |
|||
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request); |
|||
} |
|||
|
|||
std::vector<u8> IrsProtocol::GetImage() const { |
|||
return buf_image; |
|||
} |
|||
|
|||
IrsResolution IrsProtocol::GetIrsFormat() const { |
|||
return resolution; |
|||
} |
|||
|
|||
bool IrsProtocol::IsEnabled() const { |
|||
return is_enabled; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,63 @@ |
|||
// 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 <vector> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/common_protocol.h" |
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
class IrsProtocol final : private JoyconCommonProtocol { |
|||
public: |
|||
explicit IrsProtocol(std::shared_ptr<JoyconHandle> handle); |
|||
|
|||
DriverResult EnableIrs(); |
|||
|
|||
DriverResult DisableIrs(); |
|||
|
|||
DriverResult SetIrsConfig(IrsMode mode, IrsResolution format); |
|||
|
|||
DriverResult RequestImage(std::span<u8> buffer); |
|||
|
|||
std::vector<u8> GetImage() const; |
|||
|
|||
IrsResolution GetIrsFormat() const; |
|||
|
|||
bool IsEnabled() const; |
|||
|
|||
private: |
|||
DriverResult ConfigureIrs(); |
|||
|
|||
DriverResult WriteRegistersStep1(); |
|||
DriverResult WriteRegistersStep2(); |
|||
|
|||
DriverResult RequestFrame(u8 frame); |
|||
DriverResult ResendFrame(u8 frame); |
|||
|
|||
IrsMode irs_mode{IrsMode::ImageTransfer}; |
|||
IrsResolution resolution{IrsResolution::Size40x30}; |
|||
IrsResolutionCode resolution_code{IrsResolutionCode::Size40x30}; |
|||
IrsFragments fragments{IrsFragments::Size40x30}; |
|||
IrLeds leds{IrLeds::BrightAndDim}; |
|||
IrExLedFilter led_filter{IrExLedFilter::Enabled}; |
|||
IrImageFlip image_flip{IrImageFlip::Normal}; |
|||
u8 digital_gain{0x01}; |
|||
u16 exposure{0x2490}; |
|||
u16 led_intensity{0x0f10}; |
|||
u32 denoise{0x012344}; |
|||
|
|||
u8 packet_fragment{}; |
|||
std::vector<u8> buf_image; // 8bpp greyscale image. |
|||
|
|||
bool is_enabled{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,612 @@ |
|||
// 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 = 368; |
|||
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 NFCPages { |
|||
Block0 = 0, |
|||
Block45 = 45, |
|||
Block135 = 135, |
|||
Block231 = 231, |
|||
}; |
|||
|
|||
enum class NFCStatus : u8 { |
|||
LastPackage = 0x04, |
|||
TagLost = 0x07, |
|||
}; |
|||
|
|||
enum class IrsMode : u8 { |
|||
None = 0x02, |
|||
Moment = 0x03, |
|||
Dpd = 0x04, |
|||
Clustering = 0x06, |
|||
ImageTransfer = 0x07, |
|||
Silhouette = 0x08, |
|||
TeraImage = 0x09, |
|||
SilhouetteTeraImage = 0x0A, |
|||
}; |
|||
|
|||
enum class IrsResolution { |
|||
Size320x240, |
|||
Size160x120, |
|||
Size80x60, |
|||
Size40x30, |
|||
Size20x15, |
|||
None, |
|||
}; |
|||
|
|||
enum class IrsResolutionCode : u8 { |
|||
Size320x240 = 0x00, // Full pixel array |
|||
Size160x120 = 0x50, // Sensor Binning [2 X 2] |
|||
Size80x60 = 0x64, // Sensor Binning [4 x 2] and Skipping [1 x 2] |
|||
Size40x30 = 0x69, // Sensor Binning [4 x 2] and Skipping [2 x 4] |
|||
Size20x15 = 0x6A, // Sensor Binning [4 x 2] and Skipping [4 x 4] |
|||
}; |
|||
|
|||
// Size of image divided by 300 |
|||
enum class IrsFragments : u8 { |
|||
Size20x15 = 0x00, |
|||
Size40x30 = 0x03, |
|||
Size80x60 = 0x0f, |
|||
Size160x120 = 0x3f, |
|||
Size320x240 = 0xFF, |
|||
}; |
|||
|
|||
enum class IrLeds : u8 { |
|||
BrightAndDim = 0x00, |
|||
Bright = 0x20, |
|||
Dim = 0x10, |
|||
None = 0x30, |
|||
}; |
|||
|
|||
enum class IrExLedFilter : u8 { |
|||
Disabled = 0x00, |
|||
Enabled = 0x03, |
|||
}; |
|||
|
|||
enum class IrImageFlip : u8 { |
|||
Normal = 0x00, |
|||
Inverted = 0x02, |
|||
}; |
|||
|
|||
enum class IrRegistersAddress : u16 { |
|||
UpdateTime = 0x0400, |
|||
FinalizeConfig = 0x0700, |
|||
LedFilter = 0x0e00, |
|||
Leds = 0x1000, |
|||
LedIntensitiyMSB = 0x1100, |
|||
LedIntensitiyLSB = 0x1200, |
|||
ImageFlip = 0x2d00, |
|||
Resolution = 0x2e00, |
|||
DigitalGainLSB = 0x2e01, |
|||
DigitalGainMSB = 0x2f01, |
|||
ExposureLSB = 0x3001, |
|||
ExposureMSB = 0x3101, |
|||
ExposureTime = 0x3201, |
|||
WhitePixelThreshold = 0x4301, |
|||
DenoiseSmoothing = 0x6701, |
|||
DenoiseEdge = 0x6801, |
|||
DenoiseColor = 0x6901, |
|||
}; |
|||
|
|||
enum class DriverResult { |
|||
Success, |
|||
WrongReply, |
|||
Timeout, |
|||
UnsupportedControllerType, |
|||
HandleInUse, |
|||
ErrorReadingData, |
|||
ErrorWritingData, |
|||
NoDeviceDetected, |
|||
InvalidHandle, |
|||
NotSupported, |
|||
Disabled, |
|||
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 IrsConfigure { |
|||
MCUCommand command; |
|||
MCUSubCommand sub_command; |
|||
IrsMode irs_mode; |
|||
IrsFragments number_of_fragments; |
|||
u16 mcu_major_version; |
|||
u16 mcu_minor_version; |
|||
INSERT_PADDING_BYTES(0x1D); |
|||
u8 crc; |
|||
}; |
|||
static_assert(sizeof(IrsConfigure) == 0x26, "IrsConfigure is an invalid size"); |
|||
|
|||
#pragma pack(push, 1) |
|||
struct IrsRegister { |
|||
IrRegistersAddress address; |
|||
u8 value; |
|||
}; |
|||
static_assert(sizeof(IrsRegister) == 0x3, "IrsRegister is an invalid size"); |
|||
|
|||
struct IrsWriteRegisters { |
|||
MCUCommand command; |
|||
MCUSubCommand sub_command; |
|||
u8 number_of_registers; |
|||
std::array<IrsRegister, 9> registers; |
|||
INSERT_PADDING_BYTES(0x7); |
|||
u8 crc; |
|||
}; |
|||
static_assert(sizeof(IrsWriteRegisters) == 0x26, "IrsWriteRegisters is an invalid size"); |
|||
#pragma pack(pop) |
|||
|
|||
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; |
|||
std::function<void(const std::vector<u8>&, IrsResolution)> on_camera_data; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,400 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <thread>
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/nfc.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
NfcProtocol::NfcProtocol(std::shared_ptr<JoyconHandle> handle) |
|||
: JoyconCommonProtocol(std::move(handle)) {} |
|||
|
|||
DriverResult NfcProtocol::EnableNfc() { |
|||
LOG_INFO(Input, "Enable NFC"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = EnableMCU(true); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
const MCUConfig config{ |
|||
.command = MCUCommand::ConfigureMCU, |
|||
.sub_command = MCUSubCommand::SetMCUMode, |
|||
.mode = MCUMode::NFC, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
result = ConfigureMCU(config); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::DisableNfc() { |
|||
LOG_DEBUG(Input, "Disable NFC"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = EnableMCU(false); |
|||
} |
|||
|
|||
is_enabled = false; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::StartNFCPollingMode() { |
|||
LOG_DEBUG(Input, "Start NFC pooling Mode"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
TagFoundData tag_data{}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WaitUntilNfcIsReady(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
is_enabled = true; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::ScanAmiibo(std::vector<u8>& data) { |
|||
LOG_DEBUG(Input, "Start NFC pooling Mode"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
TagFoundData tag_data{}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = StartPolling(tag_data); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = ReadTag(tag_data); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = WaitUntilNfcIsReady(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = StartPolling(tag_data); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = GetAmiiboData(data); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
bool NfcProtocol::HasAmiibo() { |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
TagFoundData tag_data{}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = StartPolling(tag_data); |
|||
} |
|||
|
|||
return result == DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::WaitUntilNfcIsReady() { |
|||
constexpr std::size_t timeout_limit = 10; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
do { |
|||
auto result = SendStartWaitingRecieveRequest(output); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
if (tries++ > timeout_limit) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} while (output[49] != 0x2a || (output[51] << 8) + output[50] != 0x0500 || output[55] != 0x31 || |
|||
output[56] != 0x00); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::StartPolling(TagFoundData& data) { |
|||
LOG_DEBUG(Input, "Start Polling for tag"); |
|||
constexpr std::size_t timeout_limit = 7; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
do { |
|||
const auto result = SendStartPollingRequest(output); |
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
if (tries++ > timeout_limit) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} while (output[49] != 0x2a || (output[51] << 8) + output[50] != 0x0500 || output[56] != 0x09); |
|||
|
|||
data.type = output[62]; |
|||
data.uuid.resize(output[64]); |
|||
memcpy(data.uuid.data(), output.data() + 65, data.uuid.size()); |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::ReadTag(const TagFoundData& data) { |
|||
constexpr std::size_t timeout_limit = 10; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
std::string uuid_string; |
|||
for (auto& content : data.uuid) { |
|||
uuid_string += fmt::format(" {:02x}", content); |
|||
} |
|||
|
|||
LOG_INFO(Input, "Tag detected, type={}, uuid={}", data.type, uuid_string); |
|||
|
|||
tries = 0; |
|||
NFCPages ntag_pages = NFCPages::Block0; |
|||
// Read Tag data
|
|||
while (true) { |
|||
auto result = SendReadAmiiboRequest(output, ntag_pages); |
|||
const auto mcu_report = static_cast<MCUReport>(output[49]); |
|||
const auto nfc_status = static_cast<NFCStatus>(output[56]); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
if ((mcu_report == MCUReport::NFCReadData || mcu_report == MCUReport::NFCState) && |
|||
nfc_status == NFCStatus::TagLost) { |
|||
return DriverResult::ErrorReadingData; |
|||
} |
|||
|
|||
if (mcu_report == MCUReport::NFCReadData && output[51] == 0x07 && output[52] == 0x01) { |
|||
if (data.type != 2) { |
|||
continue; |
|||
} |
|||
switch (output[74]) { |
|||
case 0: |
|||
ntag_pages = NFCPages::Block135; |
|||
break; |
|||
case 3: |
|||
ntag_pages = NFCPages::Block45; |
|||
break; |
|||
case 4: |
|||
ntag_pages = NFCPages::Block231; |
|||
break; |
|||
default: |
|||
return DriverResult::ErrorReadingData; |
|||
} |
|||
continue; |
|||
} |
|||
|
|||
if (mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) { |
|||
// finished
|
|||
SendStopPollingRequest(output); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
// Ignore other state reports
|
|||
if (mcu_report == MCUReport::NFCState) { |
|||
continue; |
|||
} |
|||
|
|||
if (tries++ > timeout_limit) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::GetAmiiboData(std::vector<u8>& ntag_data) { |
|||
constexpr std::size_t timeout_limit = 10; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
|
|||
NFCPages ntag_pages = NFCPages::Block135; |
|||
std::size_t ntag_buffer_pos = 0; |
|||
// Read Tag data
|
|||
while (true) { |
|||
auto result = SendReadAmiiboRequest(output, ntag_pages); |
|||
const auto mcu_report = static_cast<MCUReport>(output[49]); |
|||
const auto nfc_status = static_cast<NFCStatus>(output[56]); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
if ((mcu_report == MCUReport::NFCReadData || mcu_report == MCUReport::NFCState) && |
|||
nfc_status == NFCStatus::TagLost) { |
|||
return DriverResult::ErrorReadingData; |
|||
} |
|||
|
|||
if (mcu_report == MCUReport::NFCReadData && output[51] == 0x07) { |
|||
std::size_t payload_size = (output[54] << 8 | output[55]) & 0x7FF; |
|||
if (output[52] == 0x01) { |
|||
memcpy(ntag_data.data() + ntag_buffer_pos, output.data() + 116, payload_size - 60); |
|||
ntag_buffer_pos += payload_size - 60; |
|||
} else { |
|||
memcpy(ntag_data.data() + ntag_buffer_pos, output.data() + 56, payload_size); |
|||
} |
|||
continue; |
|||
} |
|||
|
|||
if (mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) { |
|||
LOG_INFO(Input, "Finished reading amiibo"); |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
// Ignore other state reports
|
|||
if (mcu_report == MCUReport::NFCState) { |
|||
continue; |
|||
} |
|||
|
|||
if (tries++ > timeout_limit) { |
|||
return DriverResult::Timeout; |
|||
} |
|||
} |
|||
|
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult NfcProtocol::SendStartPollingRequest(std::vector<u8>& output) { |
|||
NFCRequestState request{ |
|||
.sub_command = MCUSubCommand::ReadDeviceMode, |
|||
.command_argument = NFCReadCommand::StartPolling, |
|||
.packet_id = 0x0, |
|||
.packet_flag = MCUPacketFlag::LastCommandPacket, |
|||
.data_length = sizeof(NFCPollingCommandData), |
|||
.nfc_polling = |
|||
{ |
|||
.enable_mifare = 0x01, |
|||
.unknown_1 = 0x00, |
|||
.unknown_2 = 0x00, |
|||
.unknown_3 = 0x2c, |
|||
.unknown_4 = 0x01, |
|||
}, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
std::array<u8, sizeof(NFCRequestState)> request_data{}; |
|||
memcpy(request_data.data(), &request, sizeof(NFCRequestState)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output); |
|||
} |
|||
|
|||
DriverResult NfcProtocol::SendStopPollingRequest(std::vector<u8>& output) { |
|||
NFCRequestState request{ |
|||
.sub_command = MCUSubCommand::ReadDeviceMode, |
|||
.command_argument = NFCReadCommand::StopPolling, |
|||
.packet_id = 0x0, |
|||
.packet_flag = MCUPacketFlag::LastCommandPacket, |
|||
.data_length = 0, |
|||
.raw_data = {}, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
std::array<u8, sizeof(NFCRequestState)> request_data{}; |
|||
memcpy(request_data.data(), &request, sizeof(NFCRequestState)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output); |
|||
} |
|||
|
|||
DriverResult NfcProtocol::SendStartWaitingRecieveRequest(std::vector<u8>& output) { |
|||
NFCRequestState request{ |
|||
.sub_command = MCUSubCommand::ReadDeviceMode, |
|||
.command_argument = NFCReadCommand::StartWaitingRecieve, |
|||
.packet_id = 0x0, |
|||
.packet_flag = MCUPacketFlag::LastCommandPacket, |
|||
.data_length = 0, |
|||
.raw_data = {}, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
std::vector<u8> request_data(sizeof(NFCRequestState)); |
|||
memcpy(request_data.data(), &request, sizeof(NFCRequestState)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output); |
|||
} |
|||
|
|||
DriverResult NfcProtocol::SendReadAmiiboRequest(std::vector<u8>& output, NFCPages ntag_pages) { |
|||
NFCRequestState request{ |
|||
.sub_command = MCUSubCommand::ReadDeviceMode, |
|||
.command_argument = NFCReadCommand::Ntag, |
|||
.packet_id = 0x0, |
|||
.packet_flag = MCUPacketFlag::LastCommandPacket, |
|||
.data_length = sizeof(NFCReadCommandData), |
|||
.nfc_read = |
|||
{ |
|||
.unknown = 0xd0, |
|||
.uuid_length = 0x07, |
|||
.unknown_2 = 0x00, |
|||
.uid = {}, |
|||
.tag_type = NFCTagType::AllTags, |
|||
.read_block = GetReadBlockCommand(ntag_pages), |
|||
}, |
|||
.crc = {}, |
|||
}; |
|||
|
|||
std::array<u8, sizeof(NFCRequestState)> request_data{}; |
|||
memcpy(request_data.data(), &request, sizeof(NFCRequestState)); |
|||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); |
|||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output); |
|||
} |
|||
|
|||
NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(NFCPages pages) const { |
|||
switch (pages) { |
|||
case NFCPages::Block0: |
|||
return { |
|||
.block_count = 1, |
|||
}; |
|||
case NFCPages::Block45: |
|||
return { |
|||
.block_count = 1, |
|||
.blocks = |
|||
{ |
|||
NFCReadBlock{0x00, 0x2C}, |
|||
}, |
|||
}; |
|||
case NFCPages::Block135: |
|||
return { |
|||
.block_count = 3, |
|||
.blocks = |
|||
{ |
|||
NFCReadBlock{0x00, 0x3b}, |
|||
{0x3c, 0x77}, |
|||
{0x78, 0x86}, |
|||
}, |
|||
}; |
|||
case NFCPages::Block231: |
|||
return { |
|||
.block_count = 4, |
|||
.blocks = |
|||
{ |
|||
NFCReadBlock{0x00, 0x3b}, |
|||
{0x3c, 0x77}, |
|||
{0x78, 0x83}, |
|||
{0xb4, 0xe6}, |
|||
}, |
|||
}; |
|||
default: |
|||
return {}; |
|||
}; |
|||
} |
|||
|
|||
bool NfcProtocol::IsEnabled() const { |
|||
return is_enabled; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,61 @@ |
|||
// 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 <vector> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/common_protocol.h" |
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
class NfcProtocol final : private JoyconCommonProtocol { |
|||
public: |
|||
explicit NfcProtocol(std::shared_ptr<JoyconHandle> handle); |
|||
|
|||
DriverResult EnableNfc(); |
|||
|
|||
DriverResult DisableNfc(); |
|||
|
|||
DriverResult StartNFCPollingMode(); |
|||
|
|||
DriverResult ScanAmiibo(std::vector<u8>& data); |
|||
|
|||
bool HasAmiibo(); |
|||
|
|||
bool IsEnabled() const; |
|||
|
|||
private: |
|||
struct TagFoundData { |
|||
u8 type; |
|||
std::vector<u8> uuid; |
|||
}; |
|||
|
|||
DriverResult WaitUntilNfcIsReady(); |
|||
|
|||
DriverResult StartPolling(TagFoundData& data); |
|||
|
|||
DriverResult ReadTag(const TagFoundData& data); |
|||
|
|||
DriverResult GetAmiiboData(std::vector<u8>& data); |
|||
|
|||
DriverResult SendStartPollingRequest(std::vector<u8>& output); |
|||
|
|||
DriverResult SendStopPollingRequest(std::vector<u8>& output); |
|||
|
|||
DriverResult SendStartWaitingRecieveRequest(std::vector<u8>& output); |
|||
|
|||
DriverResult SendReadAmiiboRequest(std::vector<u8>& output, NFCPages ntag_pages); |
|||
|
|||
NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const; |
|||
|
|||
bool is_enabled{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,341 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/poller.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
JoyconPoller::JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_, |
|||
JoyStickCalibration right_stick_calibration_, |
|||
MotionCalibration motion_calibration_) |
|||
: device_type{device_type_}, left_stick_calibration{left_stick_calibration_}, |
|||
right_stick_calibration{right_stick_calibration_}, motion_calibration{motion_calibration_} {} |
|||
|
|||
void JoyconPoller::SetCallbacks(const Joycon::JoyconCallbacks& callbacks_) { |
|||
callbacks = std::move(callbacks_); |
|||
} |
|||
|
|||
void JoyconPoller::ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status, |
|||
const RingStatus& ring_status) { |
|||
InputReportActive data{}; |
|||
memcpy(&data, buffer.data(), sizeof(InputReportActive)); |
|||
|
|||
switch (device_type) { |
|||
case Joycon::ControllerType::Left: |
|||
UpdateActiveLeftPadInput(data, motion_status); |
|||
break; |
|||
case Joycon::ControllerType::Right: |
|||
UpdateActiveRightPadInput(data, motion_status); |
|||
break; |
|||
case Joycon::ControllerType::Pro: |
|||
UpdateActiveProPadInput(data, motion_status); |
|||
break; |
|||
case Joycon::ControllerType::Grip: |
|||
case Joycon::ControllerType::Dual: |
|||
case Joycon::ControllerType::None: |
|||
break; |
|||
} |
|||
|
|||
if (ring_status.is_enabled) { |
|||
UpdateRing(data.ring_input, ring_status); |
|||
} |
|||
|
|||
callbacks.on_battery_data(data.battery_status); |
|||
} |
|||
|
|||
void JoyconPoller::ReadPassiveMode(std::span<u8> buffer) { |
|||
InputReportPassive data{}; |
|||
memcpy(&data, buffer.data(), sizeof(InputReportPassive)); |
|||
|
|||
switch (device_type) { |
|||
case Joycon::ControllerType::Left: |
|||
UpdatePasiveLeftPadInput(data); |
|||
break; |
|||
case Joycon::ControllerType::Right: |
|||
UpdatePasiveRightPadInput(data); |
|||
break; |
|||
case Joycon::ControllerType::Pro: |
|||
UpdatePasiveProPadInput(data); |
|||
break; |
|||
case Joycon::ControllerType::Grip: |
|||
case Joycon::ControllerType::Dual: |
|||
case Joycon::ControllerType::None: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
void JoyconPoller::ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status) { |
|||
// This mode is compatible with the active mode
|
|||
ReadActiveMode(buffer, motion_status, {}); |
|||
} |
|||
|
|||
void JoyconPoller::UpdateColor(const Color& color) { |
|||
callbacks.on_color_data(color); |
|||
} |
|||
|
|||
void JoyconPoller::UpdateAmiibo(const std::vector<u8>& amiibo_data) { |
|||
callbacks.on_amiibo_data(amiibo_data); |
|||
} |
|||
|
|||
void JoyconPoller::UpdateCamera(const std::vector<u8>& camera_data, IrsResolution format) { |
|||
callbacks.on_camera_data(camera_data, format); |
|||
} |
|||
|
|||
void JoyconPoller::UpdateRing(s16 value, const RingStatus& ring_status) { |
|||
float normalized_value = static_cast<float>(value - ring_status.default_value); |
|||
if (normalized_value > 0) { |
|||
normalized_value = normalized_value / |
|||
static_cast<float>(ring_status.max_value - ring_status.default_value); |
|||
} |
|||
if (normalized_value < 0) { |
|||
normalized_value = normalized_value / |
|||
static_cast<float>(ring_status.default_value - ring_status.min_value); |
|||
} |
|||
callbacks.on_ring_data(normalized_value); |
|||
} |
|||
|
|||
void JoyconPoller::UpdateActiveLeftPadInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status) { |
|||
static constexpr std::array<Joycon::PadButton, 11> left_buttons{ |
|||
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right, |
|||
Joycon::PadButton::Left, Joycon::PadButton::LeftSL, Joycon::PadButton::LeftSR, |
|||
Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus, |
|||
Joycon::PadButton::Capture, Joycon::PadButton::StickL, |
|||
}; |
|||
|
|||
const u32 raw_button = |
|||
static_cast<u32>(input.button_input[2] | ((input.button_input[1] & 0b00101001) << 16)); |
|||
for (std::size_t i = 0; i < left_buttons.size(); ++i) { |
|||
const bool button_status = (raw_button & static_cast<u32>(left_buttons[i])) != 0; |
|||
const int button = static_cast<int>(left_buttons[i]); |
|||
callbacks.on_button_data(button, button_status); |
|||
} |
|||
|
|||
const u16 raw_left_axis_x = |
|||
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8)); |
|||
const u16 raw_left_axis_y = |
|||
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4)); |
|||
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x); |
|||
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y); |
|||
|
|||
if (motion_status.is_enabled) { |
|||
auto left_motion = GetMotionInput(input, motion_status); |
|||
// Rotate motion axis to the correct direction
|
|||
left_motion.accel_y = -left_motion.accel_y; |
|||
left_motion.accel_z = -left_motion.accel_z; |
|||
left_motion.gyro_x = -left_motion.gyro_x; |
|||
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), left_motion); |
|||
} |
|||
} |
|||
|
|||
void JoyconPoller::UpdateActiveRightPadInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status) { |
|||
static constexpr std::array<Joycon::PadButton, 11> right_buttons{ |
|||
Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B, |
|||
Joycon::PadButton::A, Joycon::PadButton::RightSL, Joycon::PadButton::RightSR, |
|||
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus, |
|||
Joycon::PadButton::Home, Joycon::PadButton::StickR, |
|||
}; |
|||
|
|||
const u32 raw_button = |
|||
static_cast<u32>((input.button_input[0] << 8) | (input.button_input[1] << 16)); |
|||
for (std::size_t i = 0; i < right_buttons.size(); ++i) { |
|||
const bool button_status = (raw_button & static_cast<u32>(right_buttons[i])) != 0; |
|||
const int button = static_cast<int>(right_buttons[i]); |
|||
callbacks.on_button_data(button, button_status); |
|||
} |
|||
|
|||
const u16 raw_right_axis_x = |
|||
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8)); |
|||
const u16 raw_right_axis_y = |
|||
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4)); |
|||
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x); |
|||
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y); |
|||
|
|||
if (motion_status.is_enabled) { |
|||
auto right_motion = GetMotionInput(input, motion_status); |
|||
// Rotate motion axis to the correct direction
|
|||
right_motion.accel_x = -right_motion.accel_x; |
|||
right_motion.accel_y = -right_motion.accel_y; |
|||
right_motion.gyro_z = -right_motion.gyro_z; |
|||
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), right_motion); |
|||
} |
|||
} |
|||
|
|||
void JoyconPoller::UpdateActiveProPadInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status) { |
|||
static constexpr std::array<Joycon::PadButton, 18> pro_buttons{ |
|||
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right, |
|||
Joycon::PadButton::Left, Joycon::PadButton::L, Joycon::PadButton::ZL, |
|||
Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::Y, |
|||
Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A, |
|||
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus, |
|||
Joycon::PadButton::Home, Joycon::PadButton::StickL, Joycon::PadButton::StickR, |
|||
}; |
|||
|
|||
const u32 raw_button = static_cast<u32>(input.button_input[2] | (input.button_input[0] << 8) | |
|||
(input.button_input[1] << 16)); |
|||
for (std::size_t i = 0; i < pro_buttons.size(); ++i) { |
|||
const bool button_status = (raw_button & static_cast<u32>(pro_buttons[i])) != 0; |
|||
const int button = static_cast<int>(pro_buttons[i]); |
|||
callbacks.on_button_data(button, button_status); |
|||
} |
|||
|
|||
const u16 raw_left_axis_x = |
|||
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8)); |
|||
const u16 raw_left_axis_y = |
|||
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4)); |
|||
const u16 raw_right_axis_x = |
|||
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8)); |
|||
const u16 raw_right_axis_y = |
|||
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4)); |
|||
|
|||
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x); |
|||
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y); |
|||
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x); |
|||
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x); |
|||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y); |
|||
|
|||
if (motion_status.is_enabled) { |
|||
auto pro_motion = GetMotionInput(input, motion_status); |
|||
pro_motion.gyro_x = -pro_motion.gyro_x; |
|||
pro_motion.accel_y = -pro_motion.accel_y; |
|||
pro_motion.accel_z = -pro_motion.accel_z; |
|||
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), pro_motion); |
|||
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), pro_motion); |
|||
} |
|||
} |
|||
|
|||
void JoyconPoller::UpdatePasiveLeftPadInput(const InputReportPassive& input) { |
|||
static constexpr std::array<Joycon::PasivePadButton, 11> left_buttons{ |
|||
Joycon::PasivePadButton::Down_A, Joycon::PasivePadButton::Right_X, |
|||
Joycon::PasivePadButton::Left_B, Joycon::PasivePadButton::Up_Y, |
|||
Joycon::PasivePadButton::SL, Joycon::PasivePadButton::SR, |
|||
Joycon::PasivePadButton::L_R, Joycon::PasivePadButton::ZL_ZR, |
|||
Joycon::PasivePadButton::Minus, Joycon::PasivePadButton::Capture, |
|||
Joycon::PasivePadButton::StickL, |
|||
}; |
|||
|
|||
for (auto left_button : left_buttons) { |
|||
const bool button_status = (input.button_input & static_cast<u32>(left_button)) != 0; |
|||
const int button = static_cast<int>(left_button); |
|||
callbacks.on_button_data(button, button_status); |
|||
} |
|||
} |
|||
|
|||
void JoyconPoller::UpdatePasiveRightPadInput(const InputReportPassive& input) { |
|||
static constexpr std::array<Joycon::PasivePadButton, 11> right_buttons{ |
|||
Joycon::PasivePadButton::Down_A, Joycon::PasivePadButton::Right_X, |
|||
Joycon::PasivePadButton::Left_B, Joycon::PasivePadButton::Up_Y, |
|||
Joycon::PasivePadButton::SL, Joycon::PasivePadButton::SR, |
|||
Joycon::PasivePadButton::L_R, Joycon::PasivePadButton::ZL_ZR, |
|||
Joycon::PasivePadButton::Plus, Joycon::PasivePadButton::Home, |
|||
Joycon::PasivePadButton::StickR, |
|||
}; |
|||
|
|||
for (auto right_button : right_buttons) { |
|||
const bool button_status = (input.button_input & static_cast<u32>(right_button)) != 0; |
|||
const int button = static_cast<int>(right_button); |
|||
callbacks.on_button_data(button, button_status); |
|||
} |
|||
} |
|||
|
|||
void JoyconPoller::UpdatePasiveProPadInput(const InputReportPassive& input) { |
|||
static constexpr std::array<Joycon::PasivePadButton, 14> pro_buttons{ |
|||
Joycon::PasivePadButton::Down_A, Joycon::PasivePadButton::Right_X, |
|||
Joycon::PasivePadButton::Left_B, Joycon::PasivePadButton::Up_Y, |
|||
Joycon::PasivePadButton::SL, Joycon::PasivePadButton::SR, |
|||
Joycon::PasivePadButton::L_R, Joycon::PasivePadButton::ZL_ZR, |
|||
Joycon::PasivePadButton::Minus, Joycon::PasivePadButton::Plus, |
|||
Joycon::PasivePadButton::Capture, Joycon::PasivePadButton::Home, |
|||
Joycon::PasivePadButton::StickL, Joycon::PasivePadButton::StickR, |
|||
}; |
|||
|
|||
for (auto pro_button : pro_buttons) { |
|||
const bool button_status = (input.button_input & static_cast<u32>(pro_button)) != 0; |
|||
const int button = static_cast<int>(pro_button); |
|||
callbacks.on_button_data(button, button_status); |
|||
} |
|||
} |
|||
|
|||
f32 JoyconPoller::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const { |
|||
const f32 value = static_cast<f32>(raw_value - calibration.center); |
|||
if (value > 0.0f) { |
|||
return value / calibration.max; |
|||
} |
|||
return value / calibration.min; |
|||
} |
|||
|
|||
f32 JoyconPoller::GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal, |
|||
AccelerometerSensitivity sensitivity) const { |
|||
const f32 value = raw * (1.0f / (cal.scale - cal.offset)) * 4; |
|||
switch (sensitivity) { |
|||
case Joycon::AccelerometerSensitivity::G2: |
|||
return value / 4.0f; |
|||
case Joycon::AccelerometerSensitivity::G4: |
|||
return value / 2.0f; |
|||
case Joycon::AccelerometerSensitivity::G8: |
|||
return value; |
|||
case Joycon::AccelerometerSensitivity::G16: |
|||
return value * 2.0f; |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
f32 JoyconPoller::GetGyroValue(s16 raw, const MotionSensorCalibration& cal, |
|||
GyroSensitivity sensitivity) const { |
|||
const f32 value = (raw - cal.offset) * (936.0f / (cal.scale - cal.offset)) / 360.0f; |
|||
switch (sensitivity) { |
|||
case Joycon::GyroSensitivity::DPS250: |
|||
return value / 8.0f; |
|||
case Joycon::GyroSensitivity::DPS500: |
|||
return value / 4.0f; |
|||
case Joycon::GyroSensitivity::DPS1000: |
|||
return value / 2.0f; |
|||
case Joycon::GyroSensitivity::DPS2000: |
|||
return value; |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
s16 JoyconPoller::GetRawIMUValues(std::size_t sensor, size_t axis, |
|||
const InputReportActive& input) const { |
|||
return input.motion_input[(sensor * 3) + axis]; |
|||
} |
|||
|
|||
MotionData JoyconPoller::GetMotionInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status) const { |
|||
MotionData motion{}; |
|||
const auto& accel_cal = motion_calibration.accelerometer; |
|||
const auto& gyro_cal = motion_calibration.gyro; |
|||
const s16 raw_accel_x = input.motion_input[1]; |
|||
const s16 raw_accel_y = input.motion_input[0]; |
|||
const s16 raw_accel_z = input.motion_input[2]; |
|||
const s16 raw_gyro_x = input.motion_input[4]; |
|||
const s16 raw_gyro_y = input.motion_input[3]; |
|||
const s16 raw_gyro_z = input.motion_input[5]; |
|||
|
|||
motion.delta_timestamp = motion_status.delta_time; |
|||
motion.accel_x = |
|||
GetAccelerometerValue(raw_accel_x, accel_cal[1], motion_status.accelerometer_sensitivity); |
|||
motion.accel_y = |
|||
GetAccelerometerValue(raw_accel_y, accel_cal[0], motion_status.accelerometer_sensitivity); |
|||
motion.accel_z = |
|||
GetAccelerometerValue(raw_accel_z, accel_cal[2], motion_status.accelerometer_sensitivity); |
|||
motion.gyro_x = GetGyroValue(raw_gyro_x, gyro_cal[1], motion_status.gyro_sensitivity); |
|||
motion.gyro_y = GetGyroValue(raw_gyro_y, gyro_cal[0], motion_status.gyro_sensitivity); |
|||
motion.gyro_z = GetGyroValue(raw_gyro_z, gyro_cal[2], motion_status.gyro_sensitivity); |
|||
|
|||
// TODO(German77): Return all three samples data
|
|||
return motion; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,81 @@ |
|||
// 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 <functional> |
|||
#include <span> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
// Handles input packages and triggers the corresponding input events |
|||
class JoyconPoller { |
|||
public: |
|||
JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_, |
|||
JoyStickCalibration right_stick_calibration_, |
|||
MotionCalibration motion_calibration_); |
|||
|
|||
void SetCallbacks(const Joycon::JoyconCallbacks& callbacks_); |
|||
|
|||
/// Handles data from passive packages |
|||
void ReadPassiveMode(std::span<u8> buffer); |
|||
|
|||
/// Handles data from active packages |
|||
void ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status, |
|||
const RingStatus& ring_status); |
|||
|
|||
/// Handles data from nfc or ir packages |
|||
void ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status); |
|||
|
|||
void UpdateColor(const Color& color); |
|||
void UpdateRing(s16 value, const RingStatus& ring_status); |
|||
void UpdateAmiibo(const std::vector<u8>& amiibo_data); |
|||
void UpdateCamera(const std::vector<u8>& amiibo_data, IrsResolution format); |
|||
|
|||
private: |
|||
void UpdateActiveLeftPadInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status); |
|||
void UpdateActiveRightPadInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status); |
|||
void UpdateActiveProPadInput(const InputReportActive& input, const MotionStatus& motion_status); |
|||
|
|||
void UpdatePasiveLeftPadInput(const InputReportPassive& buffer); |
|||
void UpdatePasiveRightPadInput(const InputReportPassive& buffer); |
|||
void UpdatePasiveProPadInput(const InputReportPassive& buffer); |
|||
|
|||
/// Returns a calibrated joystick axis from raw axis data |
|||
f32 GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const; |
|||
|
|||
/// Returns a calibrated accelerometer axis from raw motion data |
|||
f32 GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal, |
|||
AccelerometerSensitivity sensitivity) const; |
|||
|
|||
/// Returns a calibrated gyro axis from raw motion data |
|||
f32 GetGyroValue(s16 raw_value, const MotionSensorCalibration& cal, |
|||
GyroSensitivity sensitivity) const; |
|||
|
|||
/// Returns a raw motion value from a buffer |
|||
s16 GetRawIMUValues(size_t sensor, size_t axis, const InputReportActive& input) const; |
|||
|
|||
/// Returns motion data from a buffer |
|||
MotionData GetMotionInput(const InputReportActive& input, |
|||
const MotionStatus& motion_status) const; |
|||
|
|||
ControllerType device_type{}; |
|||
|
|||
// Device calibration |
|||
JoyStickCalibration left_stick_calibration{}; |
|||
JoyStickCalibration right_stick_calibration{}; |
|||
MotionCalibration motion_calibration{}; |
|||
|
|||
Joycon::JoyconCallbacks callbacks{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,117 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/ringcon.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
RingConProtocol::RingConProtocol(std::shared_ptr<JoyconHandle> handle) |
|||
: JoyconCommonProtocol(std::move(handle)) {} |
|||
|
|||
DriverResult RingConProtocol::EnableRingCon() { |
|||
LOG_DEBUG(Input, "Enable Ringcon"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = SetReportMode(ReportMode::STANDARD_FULL_60HZ); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
result = EnableMCU(true); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
const MCUConfig config{ |
|||
.command = MCUCommand::ConfigureMCU, |
|||
.sub_command = MCUSubCommand::SetDeviceMode, |
|||
.mode = MCUMode::Standby, |
|||
.crc = {}, |
|||
}; |
|||
result = ConfigureMCU(config); |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult RingConProtocol::DisableRingCon() { |
|||
LOG_DEBUG(Input, "Disable RingCon"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = EnableMCU(false); |
|||
} |
|||
|
|||
is_enabled = false; |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult RingConProtocol::StartRingconPolling() { |
|||
LOG_DEBUG(Input, "Enable Ringcon"); |
|||
ScopedSetBlocking sb(this); |
|||
DriverResult result{DriverResult::Success}; |
|||
bool is_connected = false; |
|||
|
|||
if (result == DriverResult::Success) { |
|||
result = IsRingConnected(is_connected); |
|||
} |
|||
if (result == DriverResult::Success && is_connected) { |
|||
LOG_INFO(Input, "Ringcon detected"); |
|||
result = ConfigureRing(); |
|||
} |
|||
if (result == DriverResult::Success) { |
|||
is_enabled = true; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
DriverResult RingConProtocol::IsRingConnected(bool& is_connected) { |
|||
LOG_DEBUG(Input, "IsRingConnected"); |
|||
constexpr std::size_t max_tries = 28; |
|||
constexpr u8 ring_controller_id = 0x20; |
|||
std::vector<u8> output; |
|||
std::size_t tries = 0; |
|||
is_connected = false; |
|||
|
|||
do { |
|||
std::array<u8, 1> empty_data{}; |
|||
const auto result = SendSubCommand(SubCommand::UNKNOWN_RINGCON, empty_data, output); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
if (tries++ >= max_tries) { |
|||
return DriverResult::NoDeviceDetected; |
|||
} |
|||
} while (output[16] != ring_controller_id); |
|||
|
|||
is_connected = true; |
|||
return DriverResult::Success; |
|||
} |
|||
|
|||
DriverResult RingConProtocol::ConfigureRing() { |
|||
LOG_DEBUG(Input, "ConfigureRing"); |
|||
|
|||
static constexpr std::array<u8, 37> ring_config{ |
|||
0x06, 0x03, 0x25, 0x06, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x16, 0xED, 0x34, 0x36, |
|||
0x00, 0x00, 0x00, 0x0A, 0x64, 0x0B, 0xE6, 0xA9, 0x22, 0x00, 0x00, 0x04, 0x00, |
|||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xA8, 0xE1, 0x34, 0x36}; |
|||
|
|||
const DriverResult result = SendSubCommand(SubCommand::UNKNOWN_RINGCON3, ring_config); |
|||
|
|||
if (result != DriverResult::Success) { |
|||
return result; |
|||
} |
|||
|
|||
static constexpr std::array<u8, 4> ringcon_data{0x04, 0x01, 0x01, 0x02}; |
|||
return SendSubCommand(SubCommand::UNKNOWN_RINGCON2, ringcon_data); |
|||
} |
|||
|
|||
bool RingConProtocol::IsEnabled() const { |
|||
return is_enabled; |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,38 @@ |
|||
// 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 <vector> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/common_protocol.h" |
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
class RingConProtocol final : private JoyconCommonProtocol { |
|||
public: |
|||
explicit RingConProtocol(std::shared_ptr<JoyconHandle> handle); |
|||
|
|||
DriverResult EnableRingCon(); |
|||
|
|||
DriverResult DisableRingCon(); |
|||
|
|||
DriverResult StartRingconPolling(); |
|||
|
|||
bool IsEnabled() const; |
|||
|
|||
private: |
|||
DriverResult IsRingConnected(bool& is_connected); |
|||
|
|||
DriverResult ConfigureRing(); |
|||
|
|||
bool is_enabled{}; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
@ -0,0 +1,299 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|||
|
|||
#include <algorithm>
|
|||
#include <cmath>
|
|||
|
|||
#include "common/logging/log.h"
|
|||
#include "input_common/helpers/joycon_protocol/rumble.h"
|
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
RumbleProtocol::RumbleProtocol(std::shared_ptr<JoyconHandle> handle) |
|||
: JoyconCommonProtocol(std::move(handle)) {} |
|||
|
|||
DriverResult RumbleProtocol::EnableRumble(bool is_enabled) { |
|||
LOG_DEBUG(Input, "Enable Rumble"); |
|||
ScopedSetBlocking sb(this); |
|||
const std::array<u8, 1> buffer{static_cast<u8>(is_enabled ? 1 : 0)}; |
|||
return SendSubCommand(SubCommand::ENABLE_VIBRATION, buffer); |
|||
} |
|||
|
|||
DriverResult RumbleProtocol::SendVibration(const VibrationValue& vibration) { |
|||
std::array<u8, sizeof(DefaultVibrationBuffer)> buffer{}; |
|||
|
|||
if (vibration.high_amplitude <= 0.0f && vibration.low_amplitude <= 0.0f) { |
|||
return SendVibrationReport(DefaultVibrationBuffer); |
|||
} |
|||
|
|||
// Protect joycons from damage from strong vibrations
|
|||
const f32 clamp_amplitude = |
|||
1.0f / std::max(1.0f, vibration.high_amplitude + vibration.low_amplitude); |
|||
|
|||
const u16 encoded_high_frequency = EncodeHighFrequency(vibration.high_frequency); |
|||
const u8 encoded_high_amplitude = |
|||
EncodeHighAmplitude(vibration.high_amplitude * clamp_amplitude); |
|||
const u8 encoded_low_frequency = EncodeLowFrequency(vibration.low_frequency); |
|||
const u16 encoded_low_amplitude = EncodeLowAmplitude(vibration.low_amplitude * clamp_amplitude); |
|||
|
|||
buffer[0] = static_cast<u8>(encoded_high_frequency & 0xFF); |
|||
buffer[1] = static_cast<u8>(encoded_high_amplitude | ((encoded_high_frequency >> 8) & 0x01)); |
|||
buffer[2] = static_cast<u8>(encoded_low_frequency | ((encoded_low_amplitude >> 8) & 0x80)); |
|||
buffer[3] = static_cast<u8>(encoded_low_amplitude & 0xFF); |
|||
|
|||
// Duplicate rumble for now
|
|||
buffer[4] = buffer[0]; |
|||
buffer[5] = buffer[1]; |
|||
buffer[6] = buffer[2]; |
|||
buffer[7] = buffer[3]; |
|||
|
|||
return SendVibrationReport(buffer); |
|||
} |
|||
|
|||
u16 RumbleProtocol::EncodeHighFrequency(f32 frequency) const { |
|||
const u8 new_frequency = |
|||
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f)); |
|||
return static_cast<u16>((new_frequency - 0x60) * 4); |
|||
} |
|||
|
|||
u8 RumbleProtocol::EncodeLowFrequency(f32 frequency) const { |
|||
const u8 new_frequency = |
|||
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f)); |
|||
return static_cast<u8>(new_frequency - 0x40); |
|||
} |
|||
|
|||
u8 RumbleProtocol::EncodeHighAmplitude(f32 amplitude) const { |
|||
// More information about these values can be found here:
|
|||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
|
|||
|
|||
static constexpr std::array<std::pair<f32, int>, 101> high_fequency_amplitude{ |
|||
std::pair<f32, int>{0.0f, 0x0}, |
|||
{0.01f, 0x2}, |
|||
{0.012f, 0x4}, |
|||
{0.014f, 0x6}, |
|||
{0.017f, 0x8}, |
|||
{0.02f, 0x0a}, |
|||
{0.024f, 0x0c}, |
|||
{0.028f, 0x0e}, |
|||
{0.033f, 0x10}, |
|||
{0.04f, 0x12}, |
|||
{0.047f, 0x14}, |
|||
{0.056f, 0x16}, |
|||
{0.067f, 0x18}, |
|||
{0.08f, 0x1a}, |
|||
{0.095f, 0x1c}, |
|||
{0.112f, 0x1e}, |
|||
{0.117f, 0x20}, |
|||
{0.123f, 0x22}, |
|||
{0.128f, 0x24}, |
|||
{0.134f, 0x26}, |
|||
{0.14f, 0x28}, |
|||
{0.146f, 0x2a}, |
|||
{0.152f, 0x2c}, |
|||
{0.159f, 0x2e}, |
|||
{0.166f, 0x30}, |
|||
{0.173f, 0x32}, |
|||
{0.181f, 0x34}, |
|||
{0.189f, 0x36}, |
|||
{0.198f, 0x38}, |
|||
{0.206f, 0x3a}, |
|||
{0.215f, 0x3c}, |
|||
{0.225f, 0x3e}, |
|||
{0.23f, 0x40}, |
|||
{0.235f, 0x42}, |
|||
{0.24f, 0x44}, |
|||
{0.245f, 0x46}, |
|||
{0.251f, 0x48}, |
|||
{0.256f, 0x4a}, |
|||
{0.262f, 0x4c}, |
|||
{0.268f, 0x4e}, |
|||
{0.273f, 0x50}, |
|||
{0.279f, 0x52}, |
|||
{0.286f, 0x54}, |
|||
{0.292f, 0x56}, |
|||
{0.298f, 0x58}, |
|||
{0.305f, 0x5a}, |
|||
{0.311f, 0x5c}, |
|||
{0.318f, 0x5e}, |
|||
{0.325f, 0x60}, |
|||
{0.332f, 0x62}, |
|||
{0.34f, 0x64}, |
|||
{0.347f, 0x66}, |
|||
{0.355f, 0x68}, |
|||
{0.362f, 0x6a}, |
|||
{0.37f, 0x6c}, |
|||
{0.378f, 0x6e}, |
|||
{0.387f, 0x70}, |
|||
{0.395f, 0x72}, |
|||
{0.404f, 0x74}, |
|||
{0.413f, 0x76}, |
|||
{0.422f, 0x78}, |
|||
{0.431f, 0x7a}, |
|||
{0.44f, 0x7c}, |
|||
{0.45f, 0x7e}, |
|||
{0.46f, 0x80}, |
|||
{0.47f, 0x82}, |
|||
{0.48f, 0x84}, |
|||
{0.491f, 0x86}, |
|||
{0.501f, 0x88}, |
|||
{0.512f, 0x8a}, |
|||
{0.524f, 0x8c}, |
|||
{0.535f, 0x8e}, |
|||
{0.547f, 0x90}, |
|||
{0.559f, 0x92}, |
|||
{0.571f, 0x94}, |
|||
{0.584f, 0x96}, |
|||
{0.596f, 0x98}, |
|||
{0.609f, 0x9a}, |
|||
{0.623f, 0x9c}, |
|||
{0.636f, 0x9e}, |
|||
{0.65f, 0xa0}, |
|||
{0.665f, 0xa2}, |
|||
{0.679f, 0xa4}, |
|||
{0.694f, 0xa6}, |
|||
{0.709f, 0xa8}, |
|||
{0.725f, 0xaa}, |
|||
{0.741f, 0xac}, |
|||
{0.757f, 0xae}, |
|||
{0.773f, 0xb0}, |
|||
{0.79f, 0xb2}, |
|||
{0.808f, 0xb4}, |
|||
{0.825f, 0xb6}, |
|||
{0.843f, 0xb8}, |
|||
{0.862f, 0xba}, |
|||
{0.881f, 0xbc}, |
|||
{0.9f, 0xbe}, |
|||
{0.92f, 0xc0}, |
|||
{0.94f, 0xc2}, |
|||
{0.96f, 0xc4}, |
|||
{0.981f, 0xc6}, |
|||
{1.003f, 0xc8}, |
|||
}; |
|||
|
|||
for (const auto& [amplitude_value, code] : high_fequency_amplitude) { |
|||
if (amplitude <= amplitude_value) { |
|||
return static_cast<u8>(code); |
|||
} |
|||
} |
|||
|
|||
return static_cast<u8>(high_fequency_amplitude[high_fequency_amplitude.size() - 1].second); |
|||
} |
|||
|
|||
u16 RumbleProtocol::EncodeLowAmplitude(f32 amplitude) const { |
|||
// More information about these values can be found here:
|
|||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
|
|||
|
|||
static constexpr std::array<std::pair<f32, int>, 101> high_fequency_amplitude{ |
|||
std::pair<f32, int>{0.0f, 0x0040}, |
|||
{0.01f, 0x8040}, |
|||
{0.012f, 0x0041}, |
|||
{0.014f, 0x8041}, |
|||
{0.017f, 0x0042}, |
|||
{0.02f, 0x8042}, |
|||
{0.024f, 0x0043}, |
|||
{0.028f, 0x8043}, |
|||
{0.033f, 0x0044}, |
|||
{0.04f, 0x8044}, |
|||
{0.047f, 0x0045}, |
|||
{0.056f, 0x8045}, |
|||
{0.067f, 0x0046}, |
|||
{0.08f, 0x8046}, |
|||
{0.095f, 0x0047}, |
|||
{0.112f, 0x8047}, |
|||
{0.117f, 0x0048}, |
|||
{0.123f, 0x8048}, |
|||
{0.128f, 0x0049}, |
|||
{0.134f, 0x8049}, |
|||
{0.14f, 0x004a}, |
|||
{0.146f, 0x804a}, |
|||
{0.152f, 0x004b}, |
|||
{0.159f, 0x804b}, |
|||
{0.166f, 0x004c}, |
|||
{0.173f, 0x804c}, |
|||
{0.181f, 0x004d}, |
|||
{0.189f, 0x804d}, |
|||
{0.198f, 0x004e}, |
|||
{0.206f, 0x804e}, |
|||
{0.215f, 0x004f}, |
|||
{0.225f, 0x804f}, |
|||
{0.23f, 0x0050}, |
|||
{0.235f, 0x8050}, |
|||
{0.24f, 0x0051}, |
|||
{0.245f, 0x8051}, |
|||
{0.251f, 0x0052}, |
|||
{0.256f, 0x8052}, |
|||
{0.262f, 0x0053}, |
|||
{0.268f, 0x8053}, |
|||
{0.273f, 0x0054}, |
|||
{0.279f, 0x8054}, |
|||
{0.286f, 0x0055}, |
|||
{0.292f, 0x8055}, |
|||
{0.298f, 0x0056}, |
|||
{0.305f, 0x8056}, |
|||
{0.311f, 0x0057}, |
|||
{0.318f, 0x8057}, |
|||
{0.325f, 0x0058}, |
|||
{0.332f, 0x8058}, |
|||
{0.34f, 0x0059}, |
|||
{0.347f, 0x8059}, |
|||
{0.355f, 0x005a}, |
|||
{0.362f, 0x805a}, |
|||
{0.37f, 0x005b}, |
|||
{0.378f, 0x805b}, |
|||
{0.387f, 0x005c}, |
|||
{0.395f, 0x805c}, |
|||
{0.404f, 0x005d}, |
|||
{0.413f, 0x805d}, |
|||
{0.422f, 0x005e}, |
|||
{0.431f, 0x805e}, |
|||
{0.44f, 0x005f}, |
|||
{0.45f, 0x805f}, |
|||
{0.46f, 0x0060}, |
|||
{0.47f, 0x8060}, |
|||
{0.48f, 0x0061}, |
|||
{0.491f, 0x8061}, |
|||
{0.501f, 0x0062}, |
|||
{0.512f, 0x8062}, |
|||
{0.524f, 0x0063}, |
|||
{0.535f, 0x8063}, |
|||
{0.547f, 0x0064}, |
|||
{0.559f, 0x8064}, |
|||
{0.571f, 0x0065}, |
|||
{0.584f, 0x8065}, |
|||
{0.596f, 0x0066}, |
|||
{0.609f, 0x8066}, |
|||
{0.623f, 0x0067}, |
|||
{0.636f, 0x8067}, |
|||
{0.65f, 0x0068}, |
|||
{0.665f, 0x8068}, |
|||
{0.679f, 0x0069}, |
|||
{0.694f, 0x8069}, |
|||
{0.709f, 0x006a}, |
|||
{0.725f, 0x806a}, |
|||
{0.741f, 0x006b}, |
|||
{0.757f, 0x806b}, |
|||
{0.773f, 0x006c}, |
|||
{0.79f, 0x806c}, |
|||
{0.808f, 0x006d}, |
|||
{0.825f, 0x806d}, |
|||
{0.843f, 0x006e}, |
|||
{0.862f, 0x806e}, |
|||
{0.881f, 0x006f}, |
|||
{0.9f, 0x806f}, |
|||
{0.92f, 0x0070}, |
|||
{0.94f, 0x8070}, |
|||
{0.96f, 0x0071}, |
|||
{0.981f, 0x8071}, |
|||
{1.003f, 0x0072}, |
|||
}; |
|||
|
|||
for (const auto& [amplitude_value, code] : high_fequency_amplitude) { |
|||
if (amplitude <= amplitude_value) { |
|||
return static_cast<u16>(code); |
|||
} |
|||
} |
|||
|
|||
return static_cast<u16>(high_fequency_amplitude[high_fequency_amplitude.size() - 1].second); |
|||
} |
|||
|
|||
} // namespace InputCommon::Joycon
|
|||
@ -0,0 +1,33 @@ |
|||
// 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 <vector> |
|||
|
|||
#include "input_common/helpers/joycon_protocol/common_protocol.h" |
|||
#include "input_common/helpers/joycon_protocol/joycon_types.h" |
|||
|
|||
namespace InputCommon::Joycon { |
|||
|
|||
class RumbleProtocol final : private JoyconCommonProtocol { |
|||
public: |
|||
explicit RumbleProtocol(std::shared_ptr<JoyconHandle> handle); |
|||
|
|||
DriverResult EnableRumble(bool is_enabled); |
|||
|
|||
DriverResult SendVibration(const VibrationValue& vibration); |
|||
|
|||
private: |
|||
u16 EncodeHighFrequency(f32 frequency) const; |
|||
u8 EncodeLowFrequency(f32 frequency) const; |
|||
u8 EncodeHighAmplitude(f32 amplitude) const; |
|||
u16 EncodeLowAmplitude(f32 amplitude) const; |
|||
}; |
|||
|
|||
} // namespace InputCommon::Joycon |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue