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