diff --git a/src/common/input.h b/src/common/input.h index d27b1d7728..d61cd7ca88 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 { @@ -62,21 +64,22 @@ enum class CameraFormat { None, }; -// Vibration reply from the controller -enum class VibrationError { - None, +// Different results that can happen from a device request +enum class DriverResult { + Success, + WrongReply, + Timeout, + UnsupportedControllerType, + HandleInUse, + ErrorReadingData, + ErrorWritingData, + NoDeviceDetected, + InvalidHandle, NotSupported, Disabled, Unknown, }; -// Polling mode reply from the controller -enum class PollingError { - None, - NotSupported, - Unknown, -}; - // Nfc reply from the controller enum class NfcState { Success, @@ -90,13 +93,6 @@ enum class NfcState { Unknown, }; -// Ir camera reply from the controller -enum class CameraError { - None, - NotSupported, - Unknown, -}; - // Hint for amplification curve to be used enum class VibrationAmplificationType { Linear, @@ -190,6 +186,8 @@ struct TouchStatus { struct BodyColorStatus { u32 body{}; u32 buttons{}; + u32 left_grip{}; + u32 right_grip{}; }; // HD rumble data @@ -228,17 +226,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 @@ -316,22 +328,24 @@ class OutputDevice { public: virtual ~OutputDevice() = default; - virtual void SetLED([[maybe_unused]] const LedStatus& led_status) {} + virtual DriverResult SetLED([[maybe_unused]] const LedStatus& led_status) { + return DriverResult::NotSupported; + } - virtual VibrationError SetVibration([[maybe_unused]] const VibrationStatus& vibration_status) { - return VibrationError::NotSupported; + virtual DriverResult SetVibration([[maybe_unused]] const VibrationStatus& vibration_status) { + return DriverResult::NotSupported; } virtual bool IsVibrationEnabled() { return false; } - virtual PollingError SetPollingMode([[maybe_unused]] PollingMode polling_mode) { - return PollingError::NotSupported; + virtual DriverResult SetPollingMode([[maybe_unused]] PollingMode polling_mode) { + return DriverResult::NotSupported; } - virtual CameraError SetCameraFormat([[maybe_unused]] CameraFormat camera_format) { - return CameraError::NotSupported; + virtual DriverResult SetCameraFormat([[maybe_unused]] CameraFormat camera_format) { + return DriverResult::NotSupported; } virtual NfcState SupportsNfc() const { diff --git a/src/common/settings.h b/src/common/settings.h index 80b2eeabcd..4b4da4da29 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -483,6 +483,7 @@ struct Values { Setting enable_raw_input{false, "enable_raw_input"}; Setting controller_navigation{true, "controller_navigation"}; + Setting enable_joycon_driver{true, "enable_joycon_driver"}; SwitchableSetting vibration_enabled{true, "vibration_enabled"}; SwitchableSetting enable_accurate_vibrations{false, "enable_accurate_vibrations"}; diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp index a959c9db90..6e9812e6ec 100644 --- a/src/core/hid/emulated_controller.cpp +++ b/src/core/hid/emulated_controller.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include #include "common/polyfill_ranges.h" #include "common/thread.h" @@ -94,6 +95,7 @@ void EmulatedController::ReloadFromSettings() { motion_params[index] = Common::ParamPackage(player.motions[index]); } + controller.color_values = {}; controller.colors_state.fullkey = { .body = GetNpadColor(player.body_color_left), .button = GetNpadColor(player.button_color_left), @@ -107,6 +109,8 @@ void EmulatedController::ReloadFromSettings() { .button = GetNpadColor(player.button_color_right), }; + ring_params[0] = Common::ParamPackage(Settings::values.ringcon_analogs); + // Other or debug controller should always be a pro controller if (npad_id_type != NpadIdType::Other) { SetNpadStyleIndex(MapSettingsTypeToNPad(player.controller_type)); @@ -133,18 +137,28 @@ void EmulatedController::LoadDevices() { trigger_params[LeftIndex] = button_params[Settings::NativeButton::ZL]; trigger_params[RightIndex] = button_params[Settings::NativeButton::ZR]; + color_params[LeftIndex] = left_joycon; + color_params[RightIndex] = right_joycon; + color_params[LeftIndex].Set("color", true); + color_params[RightIndex].Set("color", true); + battery_params[LeftIndex] = left_joycon; battery_params[RightIndex] = right_joycon; battery_params[LeftIndex].Set("battery", true); battery_params[RightIndex].Set("battery", true); - camera_params = Common::ParamPackage{"engine:camera,camera:1"}; - nfc_params = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; + camera_params[0] = right_joycon; + camera_params[0].Set("camera", true); + camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"}; + ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"}; + nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; + nfc_params[1] = right_joycon; + nfc_params[1].Set("nfc", true); output_params[LeftIndex] = left_joycon; output_params[RightIndex] = right_joycon; - output_params[2] = camera_params; - output_params[3] = nfc_params; + output_params[2] = camera_params[1]; + output_params[3] = nfc_params[0]; output_params[LeftIndex].Set("output", true); output_params[RightIndex].Set("output", true); output_params[2].Set("output", true); @@ -160,8 +174,11 @@ void EmulatedController::LoadDevices() { Common::Input::CreateInputDevice); std::ranges::transform(battery_params, battery_devices.begin(), Common::Input::CreateInputDevice); - camera_devices = Common::Input::CreateInputDevice(camera_params); - nfc_devices = Common::Input::CreateInputDevice(nfc_params); + std::ranges::transform(color_params, color_devices.begin(), Common::Input::CreateInputDevice); + std::ranges::transform(camera_params, camera_devices.begin(), Common::Input::CreateInputDevice); + std::ranges::transform(ring_params, ring_analog_devices.begin(), + Common::Input::CreateInputDevice); + std::ranges::transform(nfc_params, nfc_devices.begin(), Common::Input::CreateInputDevice); std::ranges::transform(output_params, output_devices.begin(), Common::Input::CreateOutputDevice); @@ -323,6 +340,19 @@ void EmulatedController::ReloadInput() { battery_devices[index]->ForceUpdate(); } + for (std::size_t index = 0; index < color_devices.size(); ++index) { + if (!color_devices[index]) { + continue; + } + color_devices[index]->SetCallback({ + .on_change = + [this, index](const Common::Input::CallbackStatus& callback) { + SetColors(callback, index); + }, + }); + color_devices[index]->ForceUpdate(); + } + for (std::size_t index = 0; index < motion_devices.size(); ++index) { if (!motion_devices[index]) { continue; @@ -336,22 +366,37 @@ void EmulatedController::ReloadInput() { motion_devices[index]->ForceUpdate(); } - if (camera_devices) { - camera_devices->SetCallback({ + for (std::size_t index = 0; index < camera_devices.size(); ++index) { + if (!camera_devices[index]) { + continue; + } + camera_devices[index]->SetCallback({ .on_change = [this](const Common::Input::CallbackStatus& callback) { SetCamera(callback); }, }); - camera_devices->ForceUpdate(); + camera_devices[index]->ForceUpdate(); } - if (nfc_devices) { - if (npad_id_type == NpadIdType::Handheld || npad_id_type == NpadIdType::Player1) { - nfc_devices->SetCallback({ - .on_change = - [this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); }, - }); - nfc_devices->ForceUpdate(); + for (std::size_t index = 0; index < ring_analog_devices.size(); ++index) { + if (!ring_analog_devices[index]) { + continue; } + ring_analog_devices[index]->SetCallback({ + .on_change = + [this](const Common::Input::CallbackStatus& callback) { SetRingAnalog(callback); }, + }); + ring_analog_devices[index]->ForceUpdate(); + } + + for (std::size_t index = 0; index < nfc_devices.size(); ++index) { + if (!nfc_devices[index]) { + continue; + } + nfc_devices[index]->SetCallback({ + .on_change = + [this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); }, + }); + nfc_devices[index]->ForceUpdate(); } // Register TAS devices. No need to force update @@ -421,6 +466,9 @@ void EmulatedController::UnloadInput() { for (auto& battery : battery_devices) { battery.reset(); } + for (auto& color : color_devices) { + color.reset(); + } for (auto& output : output_devices) { output.reset(); } @@ -436,8 +484,15 @@ void EmulatedController::UnloadInput() { for (auto& stick : virtual_stick_devices) { stick.reset(); } - camera_devices.reset(); - nfc_devices.reset(); + for (auto& camera : camera_devices) { + camera.reset(); + } + for (auto& ring : ring_analog_devices) { + ring.reset(); + } + for (auto& nfc : nfc_devices) { + nfc.reset(); + } } void EmulatedController::EnableConfiguration() { @@ -449,6 +504,11 @@ void EmulatedController::EnableConfiguration() { void EmulatedController::DisableConfiguration() { is_configuring = false; + // Get Joycon colors before turning on the controller + for (const auto& color_device : color_devices) { + color_device->ForceUpdate(); + } + // Apply temporary npad type to the real controller if (tmp_npad_type != npad_type) { if (is_connected) { @@ -502,6 +562,9 @@ void EmulatedController::SaveCurrentConfig() { for (std::size_t index = 0; index < player.motions.size(); ++index) { player.motions[index] = motion_params[index].Serialize(); } + if (npad_id_type == NpadIdType::Player1) { + Settings::values.ringcon_analogs = ring_params[0].Serialize(); + } } void EmulatedController::RestoreConfig() { @@ -773,17 +836,21 @@ void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback, if (index >= controller.stick_values.size()) { return; } - std::unique_lock lock{mutex}; + auto trigger_guard = + SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Stick, !is_configuring); }); + std::scoped_lock lock{mutex}; const auto stick_value = TransformToStick(callback); // Only read stick values that have the same uuid or are over the threshold to avoid flapping if (controller.stick_values[index].uuid != uuid) { const bool is_tas = uuid == TAS_UUID; if (is_tas && stick_value.x.value == 0 && stick_value.y.value == 0) { + trigger_guard.Cancel(); return; } if (!is_tas && !stick_value.down && !stick_value.up && !stick_value.left && !stick_value.right) { + trigger_guard.Cancel(); return; } } @@ -794,8 +861,6 @@ void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback, if (is_configuring) { controller.analog_stick_state.left = {}; controller.analog_stick_state.right = {}; - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Stick, false); return; } @@ -827,9 +892,6 @@ void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback, controller.npad_button_state.stick_r_down.Assign(controller.stick_values[index].down); break; } - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Stick, true); } void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callback, @@ -837,7 +899,9 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac if (index >= controller.trigger_values.size()) { return; } - std::unique_lock lock{mutex}; + auto trigger_guard = + SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Trigger, !is_configuring); }); + std::scoped_lock lock{mutex}; const auto trigger_value = TransformToTrigger(callback); // Only read trigger values that have the same uuid or are pressed once @@ -853,13 +917,12 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac if (is_configuring) { controller.gc_trigger_state.left = 0; controller.gc_trigger_state.right = 0; - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Trigger, false); return; } // Only GC controllers have analog triggers if (npad_type != NpadStyleIndex::GameCube) { + trigger_guard.Cancel(); return; } @@ -876,9 +939,6 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac controller.npad_button_state.zr.Assign(trigger.pressed.value); break; } - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Trigger, true); } void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback, @@ -886,7 +946,8 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback if (index >= controller.motion_values.size()) { return; } - std::unique_lock lock{mutex}; + SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Motion, !is_configuring); }); + std::scoped_lock lock{mutex}; auto& raw_status = controller.motion_values[index].raw_status; auto& emulated = controller.motion_values[index].emulated; @@ -907,8 +968,6 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback force_update_motion = raw_status.force_update; if (is_configuring) { - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Motion, false); return; } @@ -918,9 +977,56 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback motion.rotation = emulated.GetRotations(); motion.orientation = emulated.GetOrientation(); motion.is_at_rest = !emulated.IsMoving(motion_sensitivity); +} - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Motion, true); +void EmulatedController::SetColors(const Common::Input::CallbackStatus& callback, + std::size_t index) { + if (index >= controller.color_values.size()) { + return; + } + auto trigger_guard = + SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Color, !is_configuring); }); + std::scoped_lock lock{mutex}; + controller.color_values[index] = TransformToColor(callback); + + if (is_configuring) { + return; + } + + if (controller.color_values[index].body == 0) { + trigger_guard.Cancel(); + return; + } + + controller.colors_state.fullkey = { + .body = GetNpadColor(controller.color_values[index].body), + .button = GetNpadColor(controller.color_values[index].buttons), + }; + if (npad_type == NpadStyleIndex::ProController) { + controller.colors_state.left = { + .body = GetNpadColor(controller.color_values[index].left_grip), + .button = GetNpadColor(controller.color_values[index].buttons), + }; + controller.colors_state.right = { + .body = GetNpadColor(controller.color_values[index].right_grip), + .button = GetNpadColor(controller.color_values[index].buttons), + }; + } else { + switch (index) { + case LeftIndex: + controller.colors_state.left = { + .body = GetNpadColor(controller.color_values[index].body), + .button = GetNpadColor(controller.color_values[index].buttons), + }; + break; + case RightIndex: + controller.colors_state.right = { + .body = GetNpadColor(controller.color_values[index].body), + .button = GetNpadColor(controller.color_values[index].buttons), + }; + break; + } + } } void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback, @@ -928,12 +1034,11 @@ void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callbac if (index >= controller.battery_values.size()) { return; } - std::unique_lock lock{mutex}; + SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Battery, !is_configuring); }); + std::scoped_lock lock{mutex}; controller.battery_values[index] = TransformToBattery(callback); if (is_configuring) { - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Battery, false); return; } @@ -989,18 +1094,14 @@ void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callbac }; break; } - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Battery, true); } void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback) { - std::unique_lock lock{mutex}; + SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::IrSensor, !is_configuring); }); + std::scoped_lock lock{mutex}; controller.camera_values = TransformToCamera(callback); if (is_configuring) { - lock.unlock(); - TriggerOnChange(ControllerTriggerType::IrSensor, false); return; } @@ -1008,18 +1109,28 @@ void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback controller.camera_state.format = static_cast(controller.camera_values.format); controller.camera_state.data = controller.camera_values.data; +} - lock.unlock(); - TriggerOnChange(ControllerTriggerType::IrSensor, true); +void EmulatedController::SetRingAnalog(const Common::Input::CallbackStatus& callback) { + SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::RingController, !is_configuring); }); + std::scoped_lock lock{mutex}; + const auto force_value = TransformToStick(callback); + + controller.ring_analog_value = force_value.x; + + if (is_configuring) { + return; + } + + controller.ring_analog_state.force = force_value.x.value; } void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) { - std::unique_lock lock{mutex}; + SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Nfc, !is_configuring); }); + std::scoped_lock lock{mutex}; controller.nfc_values = TransformToNfc(callback); if (is_configuring) { - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Nfc, false); return; } @@ -1027,9 +1138,6 @@ void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) { controller.nfc_values.state, controller.nfc_values.data, }; - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Nfc, true); } bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue vibration) { @@ -1061,7 +1169,7 @@ bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue v .type = type, }; return output_devices[device_index]->SetVibration(status) == - Common::Input::VibrationError::None; + Common::Input::DriverResult::Success; } bool EmulatedController::IsVibrationEnabled(std::size_t device_index) { @@ -1083,16 +1191,32 @@ bool EmulatedController::IsVibrationEnabled(std::size_t device_index) { return output_devices[device_index]->IsVibrationEnabled(); } -bool EmulatedController::SetPollingMode(Common::Input::PollingMode polling_mode) { - LOG_INFO(Service_HID, "Set polling mode {}", polling_mode); - auto& output_device = output_devices[static_cast(DeviceIndex::Right)]; +Common::Input::DriverResult EmulatedController::SetPollingMode( + EmulatedDeviceIndex device_index, Common::Input::PollingMode polling_mode) { + LOG_INFO(Service_HID, "Set polling mode {}, device_index={}", polling_mode, device_index); + + auto& left_output_device = output_devices[static_cast(DeviceIndex::Left)]; + auto& right_output_device = output_devices[static_cast(DeviceIndex::Right)]; auto& nfc_output_device = output_devices[3]; - const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode); - const auto mapped_nfc_result = output_device->SetPollingMode(polling_mode); + if (device_index == EmulatedDeviceIndex::LeftIndex) { + return left_output_device->SetPollingMode(polling_mode); + } - return virtual_nfc_result == Common::Input::PollingError::None || - mapped_nfc_result == Common::Input::PollingError::None; + if (device_index == EmulatedDeviceIndex::RightIndex) { + const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode); + const auto mapped_nfc_result = right_output_device->SetPollingMode(polling_mode); + + if (virtual_nfc_result == Common::Input::DriverResult::Success) { + return virtual_nfc_result; + } + return mapped_nfc_result; + } + + left_output_device->SetPollingMode(polling_mode); + right_output_device->SetPollingMode(polling_mode); + nfc_output_device->SetPollingMode(polling_mode); + return Common::Input::DriverResult::Success; } bool EmulatedController::SetCameraFormat( @@ -1103,13 +1227,22 @@ bool EmulatedController::SetCameraFormat( auto& camera_output_device = output_devices[2]; if (right_output_device->SetCameraFormat(static_cast( - camera_format)) == Common::Input::CameraError::None) { + camera_format)) == Common::Input::DriverResult::Success) { return true; } // Fallback to Qt camera if native device doesn't have support return camera_output_device->SetCameraFormat(static_cast( - camera_format)) == Common::Input::CameraError::None; + camera_format)) == Common::Input::DriverResult::Success; +} + +Common::ParamPackage EmulatedController::GetRingParam() const { + return ring_params[0]; +} + +void EmulatedController::SetRingParam(Common::ParamPackage param) { + ring_params[0] = std::move(param); + ReloadInput(); } bool EmulatedController::HasNfc() const { @@ -1263,39 +1396,35 @@ void EmulatedController::Connect(bool use_temporary_value) { return; } - std::unique_lock lock{mutex}; + auto trigger_guard = + SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Connected, !is_configuring); }); + std::scoped_lock lock{mutex}; if (is_configuring) { tmp_is_connected = true; - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Connected, false); return; } if (is_connected) { + trigger_guard.Cancel(); return; } is_connected = true; - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Connected, true); } void EmulatedController::Disconnect() { - std::unique_lock lock{mutex}; + auto trigger_guard = + SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Disconnected, !is_configuring); }); + std::scoped_lock lock{mutex}; if (is_configuring) { tmp_is_connected = false; - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Disconnected, false); return; } if (!is_connected) { + trigger_guard.Cancel(); return; } is_connected = false; - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Disconnected, true); } bool EmulatedController::IsConnected(bool get_temporary_value) const { @@ -1320,19 +1449,21 @@ NpadStyleIndex EmulatedController::GetNpadStyleIndex(bool get_temporary_value) c } void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) { - std::unique_lock lock{mutex}; + auto trigger_guard = + SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Type, !is_configuring); }); + std::scoped_lock lock{mutex}; if (is_configuring) { if (tmp_npad_type == npad_type_) { + trigger_guard.Cancel(); return; } tmp_npad_type = npad_type_; - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Type, false); return; } if (npad_type == npad_type_) { + trigger_guard.Cancel(); return; } if (is_connected) { @@ -1340,9 +1471,6 @@ void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) { NpadIdTypeToIndex(npad_id_type)); } npad_type = npad_type_; - - lock.unlock(); - TriggerOnChange(ControllerTriggerType::Type, true); } LedPattern EmulatedController::GetLedPattern() const { @@ -1403,6 +1531,10 @@ CameraValues EmulatedController::GetCameraValues() const { return controller.camera_values; } +RingAnalogValue EmulatedController::GetRingSensorValues() const { + return controller.ring_analog_value; +} + HomeButtonState EmulatedController::GetHomeButtons() const { std::scoped_lock lock{mutex}; if (is_configuring) { @@ -1436,7 +1568,7 @@ DebugPadButton EmulatedController::GetDebugPadButtons() const { } AnalogSticks EmulatedController::GetSticks() const { - std::unique_lock lock{mutex}; + std::scoped_lock lock{mutex}; if (is_configuring) { return {}; @@ -1486,6 +1618,10 @@ const CameraState& EmulatedController::GetCamera() const { return controller.camera_state; } +RingSensorForce EmulatedController::GetRingSensorForce() const { + return controller.ring_analog_state; +} + const NfcState& EmulatedController::GetNfc() const { std::scoped_lock lock{mutex}; return controller.nfc_state; diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h index a398543a64..3ac77b2b5e 100644 --- a/src/core/hid/emulated_controller.h +++ b/src/core/hid/emulated_controller.h @@ -35,19 +35,27 @@ using ControllerMotionDevices = std::array, Settings::NativeMotion::NumMotions>; using TriggerDevices = std::array, Settings::NativeTrigger::NumTriggers>; +using ColorDevices = + std::array, max_emulated_controllers>; using BatteryDevices = std::array, max_emulated_controllers>; -using CameraDevices = std::unique_ptr; -using NfcDevices = std::unique_ptr; +using CameraDevices = + std::array, max_emulated_controllers>; +using RingAnalogDevices = + std::array, max_emulated_controllers>; +using NfcDevices = + std::array, max_emulated_controllers>; using OutputDevices = std::array, output_devices_size>; using ButtonParams = std::array; using StickParams = std::array; using ControllerMotionParams = std::array; using TriggerParams = std::array; +using ColorParams = std::array; using BatteryParams = std::array; -using CameraParams = Common::ParamPackage; -using NfcParams = Common::ParamPackage; +using CameraParams = std::array; +using RingAnalogParams = std::array; +using NfcParams = std::array; using OutputParams = std::array; using ButtonValues = std::array; @@ -58,6 +66,7 @@ using ControllerMotionValues = std::array; using BatteryValues = std::array; using CameraValues = Common::Input::CameraStatus; +using RingAnalogValue = Common::Input::AnalogStatus; using NfcValues = Common::Input::NfcStatus; using VibrationValues = std::array; @@ -84,6 +93,10 @@ struct CameraState { std::size_t sample{}; }; +struct RingSensorForce { + f32 force; +}; + struct NfcState { Common::Input::NfcState state{}; std::vector data{}; @@ -116,6 +129,7 @@ struct ControllerStatus { BatteryValues battery_values{}; VibrationValues vibration_values{}; CameraValues camera_values{}; + RingAnalogValue ring_analog_value{}; NfcValues nfc_values{}; // Data for HID serices @@ -129,6 +143,7 @@ struct ControllerStatus { ControllerColors colors_state{}; BatteryLevelState battery_state{}; CameraState camera_state{}; + RingSensorForce ring_analog_state{}; NfcState nfc_state{}; }; @@ -141,6 +156,7 @@ enum class ControllerTriggerType { Battery, Vibration, IrSensor, + RingController, Nfc, Connected, Disconnected, @@ -294,6 +310,9 @@ public: /// Returns the latest camera status from the controller with parameters CameraValues GetCameraValues() const; + /// Returns the latest status of analog input from the ring sensor with parameters + RingAnalogValue GetRingSensorValues() const; + /// Returns the latest status of button input for the hid::HomeButton service HomeButtonState GetHomeButtons() const; @@ -324,6 +343,9 @@ public: /// Returns the latest camera status from the controller const CameraState& GetCamera() const; + /// Returns the latest ringcon force sensor value + RingSensorForce GetRingSensorForce() const; + /// Returns the latest ntag status from the controller const NfcState& GetNfc() const; @@ -341,10 +363,12 @@ public: /** * Sets the desired data to be polled from a controller + * @param device_index index of the controller to set the polling mode * @param polling_mode type of input desired buttons, gyro, nfc, ir, etc. - * @return true if SetPollingMode was successfull + * @return driver result from this command */ - bool SetPollingMode(Common::Input::PollingMode polling_mode); + Common::Input::DriverResult SetPollingMode(EmulatedDeviceIndex device_index, + Common::Input::PollingMode polling_mode); /** * Sets the desired camera format to be polled from a controller @@ -353,6 +377,15 @@ public: */ bool SetCameraFormat(Core::IrSensor::ImageTransferProcessorFormat camera_format); + // Returns the current mapped ring device + Common::ParamPackage GetRingParam() const; + + /** + * Updates the current mapped ring device + * @param param ParamPackage with ring sensor data to be mapped + */ + void SetRingParam(Common::ParamPackage param); + /// Returns true if the device has nfc support bool HasNfc() const; @@ -432,10 +465,17 @@ private: */ void SetMotion(const Common::Input::CallbackStatus& callback, std::size_t index); + /** + * Updates the color status of the controller + * @param callback A CallbackStatus containing the color status + * @param index color ID of the to be updated + */ + void SetColors(const Common::Input::CallbackStatus& callback, std::size_t index); + /** * Updates the battery status of the controller * @param callback A CallbackStatus containing the battery status - * @param index Button ID of the to be updated + * @param index battery ID of the to be updated */ void SetBattery(const Common::Input::CallbackStatus& callback, std::size_t index); @@ -445,6 +485,12 @@ private: */ void SetCamera(const Common::Input::CallbackStatus& callback); + /** + * Updates the ring analog sensor status of the ring controller + * @param callback A CallbackStatus containing the force status + */ + void SetRingAnalog(const Common::Input::CallbackStatus& callback); + /** * Updates the nfc status of the controller * @param callback A CallbackStatus containing the nfc status @@ -484,7 +530,9 @@ private: ControllerMotionParams motion_params; TriggerParams trigger_params; BatteryParams battery_params; + ColorParams color_params; CameraParams camera_params; + RingAnalogParams ring_params; NfcParams nfc_params; OutputParams output_params; @@ -493,7 +541,9 @@ private: ControllerMotionDevices motion_devices; TriggerDevices trigger_devices; BatteryDevices battery_devices; + ColorDevices color_devices; CameraDevices camera_devices; + RingAnalogDevices ring_analog_devices; NfcDevices nfc_devices; OutputDevices output_devices; diff --git a/src/core/hid/emulated_devices.cpp b/src/core/hid/emulated_devices.cpp index e421828d25..836f32c0f8 100644 --- a/src/core/hid/emulated_devices.cpp +++ b/src/core/hid/emulated_devices.cpp @@ -14,7 +14,6 @@ EmulatedDevices::EmulatedDevices() = default; EmulatedDevices::~EmulatedDevices() = default; void EmulatedDevices::ReloadFromSettings() { - ring_params = Common::ParamPackage(Settings::values.ringcon_analogs); ReloadInput(); } @@ -66,8 +65,6 @@ void EmulatedDevices::ReloadInput() { key_index++; } - ring_analog_device = Common::Input::CreateInputDevice(ring_params); - for (std::size_t index = 0; index < mouse_button_devices.size(); ++index) { if (!mouse_button_devices[index]) { continue; @@ -122,13 +119,6 @@ void EmulatedDevices::ReloadInput() { }, }); } - - if (ring_analog_device) { - ring_analog_device->SetCallback({ - .on_change = - [this](const Common::Input::CallbackStatus& callback) { SetRingAnalog(callback); }, - }); - } } void EmulatedDevices::UnloadInput() { @@ -145,7 +135,6 @@ void EmulatedDevices::UnloadInput() { for (auto& button : keyboard_modifier_devices) { button.reset(); } - ring_analog_device.reset(); } void EmulatedDevices::EnableConfiguration() { @@ -165,7 +154,6 @@ void EmulatedDevices::SaveCurrentConfig() { if (!is_configuring) { return; } - Settings::values.ringcon_analogs = ring_params.Serialize(); } void EmulatedDevices::RestoreConfig() { @@ -175,15 +163,6 @@ void EmulatedDevices::RestoreConfig() { ReloadFromSettings(); } -Common::ParamPackage EmulatedDevices::GetRingParam() const { - return ring_params; -} - -void EmulatedDevices::SetRingParam(Common::ParamPackage param) { - ring_params = std::move(param); - ReloadInput(); -} - void EmulatedDevices::SetKeyboardButton(const Common::Input::CallbackStatus& callback, std::size_t index) { if (index >= device_status.keyboard_values.size()) { @@ -430,23 +409,6 @@ void EmulatedDevices::SetMouseStick(const Common::Input::CallbackStatus& callbac TriggerOnChange(DeviceTriggerType::Mouse); } -void EmulatedDevices::SetRingAnalog(const Common::Input::CallbackStatus& callback) { - std::lock_guard lock{mutex}; - const auto force_value = TransformToStick(callback); - - device_status.ring_analog_value = force_value.x; - - if (is_configuring) { - device_status.ring_analog_value = {}; - TriggerOnChange(DeviceTriggerType::RingController); - return; - } - - device_status.ring_analog_state.force = force_value.x.value; - - TriggerOnChange(DeviceTriggerType::RingController); -} - KeyboardValues EmulatedDevices::GetKeyboardValues() const { std::scoped_lock lock{mutex}; return device_status.keyboard_values; @@ -462,10 +424,6 @@ MouseButtonValues EmulatedDevices::GetMouseButtonsValues() const { return device_status.mouse_button_values; } -RingAnalogValue EmulatedDevices::GetRingSensorValues() const { - return device_status.ring_analog_value; -} - KeyboardKey EmulatedDevices::GetKeyboard() const { std::scoped_lock lock{mutex}; return device_status.keyboard_state; @@ -491,10 +449,6 @@ AnalogStickState EmulatedDevices::GetMouseWheel() const { return device_status.mouse_wheel_state; } -RingSensorForce EmulatedDevices::GetRingSensorForce() const { - return device_status.ring_analog_state; -} - void EmulatedDevices::TriggerOnChange(DeviceTriggerType type) { std::scoped_lock lock{callback_mutex}; for (const auto& poller_pair : callback_list) { diff --git a/src/core/hid/emulated_devices.h b/src/core/hid/emulated_devices.h index 4cdbf9dc64..76f9150df4 100644 --- a/src/core/hid/emulated_devices.h +++ b/src/core/hid/emulated_devices.h @@ -26,11 +26,9 @@ using MouseButtonDevices = std::array, Settings::NativeMouseWheel::NumMouseWheels>; using MouseStickDevice = std::unique_ptr; -using RingAnalogDevice = std::unique_ptr; using MouseButtonParams = std::array; -using RingAnalogParams = Common::ParamPackage; using KeyboardValues = std::array; @@ -41,17 +39,12 @@ using MouseButtonValues = using MouseAnalogValues = std::array; using MouseStickValue = Common::Input::TouchStatus; -using RingAnalogValue = Common::Input::AnalogStatus; struct MousePosition { f32 x; f32 y; }; -struct RingSensorForce { - f32 force; -}; - struct DeviceStatus { // Data from input_common KeyboardValues keyboard_values{}; @@ -59,7 +52,6 @@ struct DeviceStatus { MouseButtonValues mouse_button_values{}; MouseAnalogValues mouse_analog_values{}; MouseStickValue mouse_stick_value{}; - RingAnalogValue ring_analog_value{}; // Data for HID serices KeyboardKey keyboard_state{}; @@ -67,7 +59,6 @@ struct DeviceStatus { MouseButton mouse_button_state{}; MousePosition mouse_position_state{}; AnalogStickState mouse_wheel_state{}; - RingSensorForce ring_analog_state{}; }; enum class DeviceTriggerType { @@ -138,9 +129,6 @@ public: /// Returns the latest status of button input from the mouse with parameters MouseButtonValues GetMouseButtonsValues() const; - /// Returns the latest status of analog input from the ring sensor with parameters - RingAnalogValue GetRingSensorValues() const; - /// Returns the latest status of button input from the keyboard KeyboardKey GetKeyboard() const; @@ -156,9 +144,6 @@ public: /// Returns the latest mouse wheel change AnalogStickState GetMouseWheel() const; - /// Returns the latest ringcon force sensor value - RingSensorForce GetRingSensorForce() const; - /** * Adds a callback to the list of events * @param update_callback InterfaceUpdateCallback that will be triggered @@ -224,14 +209,11 @@ private: bool is_configuring{false}; - RingAnalogParams ring_params; - KeyboardDevices keyboard_devices; KeyboardModifierDevices keyboard_modifier_devices; MouseButtonDevices mouse_button_devices; MouseAnalogDevices mouse_analog_devices; MouseStickDevice mouse_stick_device; - RingAnalogDevice ring_analog_device; mutable std::mutex mutex; mutable std::mutex callback_mutex; diff --git a/src/core/hid/input_converter.cpp b/src/core/hid/input_converter.cpp index 5026928753..3f7b8c0904 100644 --- a/src/core/hid/input_converter.cpp +++ b/src/core/hid/input_converter.cpp @@ -304,6 +304,18 @@ Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& cal return nfc; } +Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback) { + switch (callback.type) { + case Common::Input::InputType::Color: + return callback.color_status; + break; + default: + LOG_ERROR(Input, "Conversion from type {} to color not implemented", callback.type); + return {}; + break; + } +} + void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) { const auto& properties = analog.properties; float& raw_value = analog.raw_value; diff --git a/src/core/hid/input_converter.h b/src/core/hid/input_converter.h index b7eb6e660c..c51c03e57e 100644 --- a/src/core/hid/input_converter.h +++ b/src/core/hid/input_converter.h @@ -88,10 +88,18 @@ Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatu * Converts raw input data into a valid nfc status. * * @param callback Supported callbacks: Nfc. - * @return A valid CameraObject object. + * @return A valid data tag vector. */ Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& callback); +/** + * Converts raw input data into a valid color status. + * + * @param callback Supported callbacks: Color. + * @return A valid Color object. + */ +Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback); + /** * Converts raw analog data into a valid analog value * @param analog An analog object containing raw data and properties diff --git a/src/core/hle/service/hid/controllers/npad.cpp b/src/core/hle/service/hid/controllers/npad.cpp index 2f871de310..5713f12888 100644 --- a/src/core/hle/service/hid/controllers/npad.cpp +++ b/src/core/hle/service/hid/controllers/npad.cpp @@ -272,6 +272,8 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) { } break; case Core::HID::NpadStyleIndex::JoyconLeft: + shared_memory->fullkey_color.attribute = ColorAttribute::Ok; + shared_memory->fullkey_color.fullkey = body_colors.left; shared_memory->joycon_color.attribute = ColorAttribute::Ok; shared_memory->joycon_color.left = body_colors.left; shared_memory->battery_level_dual = battery_level.left.battery_level; @@ -285,6 +287,8 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) { shared_memory->sixaxis_left_properties.is_newly_assigned.Assign(1); break; case Core::HID::NpadStyleIndex::JoyconRight: + shared_memory->fullkey_color.attribute = ColorAttribute::Ok; + shared_memory->fullkey_color.fullkey = body_colors.right; shared_memory->joycon_color.attribute = ColorAttribute::Ok; shared_memory->joycon_color.right = body_colors.right; shared_memory->battery_level_right = battery_level.right.battery_level; @@ -332,6 +336,20 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) { controller.is_connected = true; controller.device->Connect(); + controller.device->SetLedPattern(); + if (controller_type == Core::HID::NpadStyleIndex::JoyconDual) { + if (controller.is_dual_left_connected) { + controller.device->SetPollingMode(Core::HID::EmulatedDeviceIndex::LeftIndex, + Common::Input::PollingMode::Active); + } + if (controller.is_dual_right_connected) { + controller.device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); + } + } else { + controller.device->SetPollingMode(Core::HID::EmulatedDeviceIndex::AllDevices, + Common::Input::PollingMode::Active); + } SignalStyleSetChangedEvent(npad_id); WriteEmptyEntry(controller.shared_memory); } diff --git a/src/core/hle/service/hid/hidbus.cpp b/src/core/hle/service/hid/hidbus.cpp index e5e50845f8..17252a84ae 100644 --- a/src/core/hle/service/hid/hidbus.cpp +++ b/src/core/hle/service/hid/hidbus.cpp @@ -297,13 +297,13 @@ void HidBus::EnableExternalDevice(Kernel::HLERequestContext& ctx) { const auto parameters{rp.PopRaw()}; - LOG_INFO(Service_HID, - "called, enable={}, abstracted_pad_id={}, bus_type={}, internal_index={}, " - "player_number={}, is_valid={}, inval={}, applet_resource_user_id{}", - parameters.enable, parameters.bus_handle.abstracted_pad_id, - parameters.bus_handle.bus_type, parameters.bus_handle.internal_index, - parameters.bus_handle.player_number, parameters.bus_handle.is_valid, parameters.inval, - parameters.applet_resource_user_id); + LOG_DEBUG(Service_HID, + "called, enable={}, abstracted_pad_id={}, bus_type={}, internal_index={}, " + "player_number={}, is_valid={}, inval={}, applet_resource_user_id{}", + parameters.enable, parameters.bus_handle.abstracted_pad_id, + parameters.bus_handle.bus_type, parameters.bus_handle.internal_index, + parameters.bus_handle.player_number, parameters.bus_handle.is_valid, parameters.inval, + parameters.applet_resource_user_id); const auto device_index = GetDeviceIndexFromHandle(parameters.bus_handle); @@ -326,11 +326,11 @@ void HidBus::GetExternalDeviceId(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp{ctx}; const auto bus_handle_{rp.PopRaw()}; - LOG_INFO(Service_HID, - "called, abstracted_pad_id={}, bus_type={}, internal_index={}, player_number={}, " - "is_valid={}", - bus_handle_.abstracted_pad_id, bus_handle_.bus_type, bus_handle_.internal_index, - bus_handle_.player_number, bus_handle_.is_valid); + LOG_DEBUG(Service_HID, + "called, abstracted_pad_id={}, bus_type={}, internal_index={}, player_number={}, " + "is_valid={}", + bus_handle_.abstracted_pad_id, bus_handle_.bus_type, bus_handle_.internal_index, + bus_handle_.player_number, bus_handle_.is_valid); const auto device_index = GetDeviceIndexFromHandle(bus_handle_); diff --git a/src/core/hle/service/hid/hidbus/ringcon.cpp b/src/core/hle/service/hid/hidbus/ringcon.cpp index 57f1a2a26b..78ed470144 100644 --- a/src/core/hle/service/hid/hidbus/ringcon.cpp +++ b/src/core/hle/service/hid/hidbus/ringcon.cpp @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include "core/hid/emulated_devices.h" +#include "core/hid/emulated_controller.h" #include "core/hid/hid_core.h" #include "core/hle/kernel/k_event.h" #include "core/hle/kernel/k_readable_event.h" @@ -12,16 +12,20 @@ namespace Service::HID { RingController::RingController(Core::HID::HIDCore& hid_core_, KernelHelpers::ServiceContext& service_context_) : HidbusBase(service_context_) { - input = hid_core_.GetEmulatedDevices(); + input = hid_core_.GetEmulatedController(Core::HID::NpadIdType::Player1); } RingController::~RingController() = default; void RingController::OnInit() { + input->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Ring); return; } void RingController::OnRelease() { + input->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); return; }; diff --git a/src/core/hle/service/hid/hidbus/ringcon.h b/src/core/hle/service/hid/hidbus/ringcon.h index b37df50ac8..845ce85a5b 100644 --- a/src/core/hle/service/hid/hidbus/ringcon.h +++ b/src/core/hle/service/hid/hidbus/ringcon.h @@ -9,7 +9,7 @@ #include "core/hle/service/hid/hidbus/hidbus_base.h" namespace Core::HID { -class EmulatedDevices; +class EmulatedController; } // namespace Core::HID namespace Service::HID { @@ -248,6 +248,6 @@ private: .zero = {.value = idle_value, .crc = 225}, }; - Core::HID::EmulatedDevices* input; + Core::HID::EmulatedController* input; }; } // namespace Service::HID diff --git a/src/core/hle/service/hid/irs.cpp b/src/core/hle/service/hid/irs.cpp index 6a3453457f..52f402c568 100644 --- a/src/core/hle/service/hid/irs.cpp +++ b/src/core/hle/service/hid/irs.cpp @@ -108,6 +108,8 @@ void IRS::StopImageProcessor(Kernel::HLERequestContext& ctx) { auto result = IsIrCameraHandleValid(parameters.camera_handle); if (result.IsSuccess()) { // TODO: Stop Image processor + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); result = ResultSuccess; } @@ -139,6 +141,8 @@ void IRS::RunMomentProcessor(Kernel::HLERequestContext& ctx) { MakeProcessor(parameters.camera_handle, device); auto& image_transfer_processor = GetProcessor(parameters.camera_handle); image_transfer_processor.SetConfig(parameters.processor_config); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -170,6 +174,8 @@ void IRS::RunClusteringProcessor(Kernel::HLERequestContext& ctx) { auto& image_transfer_processor = GetProcessor(parameters.camera_handle); image_transfer_processor.SetConfig(parameters.processor_config); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -219,6 +225,8 @@ void IRS::RunImageTransferProcessor(Kernel::HLERequestContext& ctx) { GetProcessor(parameters.camera_handle); image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetTransferMemoryPointer(transfer_memory); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -294,6 +302,8 @@ void IRS::RunTeraPluginProcessor(Kernel::HLERequestContext& ctx) { auto& image_transfer_processor = GetProcessor(parameters.camera_handle); image_transfer_processor.SetConfig(parameters.processor_config); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -343,6 +353,8 @@ void IRS::RunPointingProcessor(Kernel::HLERequestContext& ctx) { MakeProcessor(camera_handle, device); auto& image_transfer_processor = GetProcessor(camera_handle); image_transfer_processor.SetConfig(processor_config); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -453,6 +465,8 @@ void IRS::RunImageTransferExProcessor(Kernel::HLERequestContext& ctx) { GetProcessor(parameters.camera_handle); image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetTransferMemoryPointer(transfer_memory); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -479,6 +493,8 @@ void IRS::RunIrLedProcessor(Kernel::HLERequestContext& ctx) { MakeProcessor(camera_handle, device); auto& image_transfer_processor = GetProcessor(camera_handle); image_transfer_processor.SetConfig(processor_config); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::IR); } IPC::ResponseBuilder rb{ctx, 2}; @@ -504,6 +520,8 @@ void IRS::StopImageProcessorAsync(Kernel::HLERequestContext& ctx) { auto result = IsIrCameraHandleValid(parameters.camera_handle); if (result.IsSuccess()) { // TODO: Stop image processor async + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); result = ResultSuccess; } diff --git a/src/core/hle/service/nfc/nfc_device.cpp b/src/core/hle/service/nfc/nfc_device.cpp index 78578f7238..9a3234e8c5 100644 --- a/src/core/hle/service/nfc/nfc_device.cpp +++ b/src/core/hle/service/nfc/nfc_device.cpp @@ -130,7 +130,9 @@ Result NfcDevice::StartDetection(NFP::TagProtocol allowed_protocol) { return WrongDeviceState; } - if (!npad_device->SetPollingMode(Common::Input::PollingMode::NFC)) { + if (npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::NFC) != + Common::Input::DriverResult::Success) { LOG_ERROR(Service_NFC, "Nfc not supported"); return NfcDisabled; } @@ -141,7 +143,8 @@ Result NfcDevice::StartDetection(NFP::TagProtocol allowed_protocol) { } Result NfcDevice::StopDetection() { - npad_device->SetPollingMode(Common::Input::PollingMode::Active); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); if (device_state == NFP::DeviceState::Initialized) { return ResultSuccess; diff --git a/src/core/hle/service/nfp/nfp_device.cpp b/src/core/hle/service/nfp/nfp_device.cpp index c860fd1a10..e67a76f55a 100644 --- a/src/core/hle/service/nfp/nfp_device.cpp +++ b/src/core/hle/service/nfp/nfp_device.cpp @@ -152,7 +152,9 @@ Result NfpDevice::StartDetection(TagProtocol allowed_protocol) { return WrongDeviceState; } - if (!npad_device->SetPollingMode(Common::Input::PollingMode::NFC)) { + if (npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::NFC) != + Common::Input::DriverResult::Success) { LOG_ERROR(Service_NFP, "Nfc not supported"); return NfcDisabled; } @@ -163,7 +165,8 @@ Result NfpDevice::StartDetection(TagProtocol allowed_protocol) { } Result NfpDevice::StopDetection() { - npad_device->SetPollingMode(Common::Input::PollingMode::Active); + npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); if (device_state == DeviceState::Initialized) { return ResultSuccess; diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt index cef2c4d52f..e3b627e4ff 100644 --- a/src/input_common/CMakeLists.txt +++ b/src/input_common/CMakeLists.txt @@ -51,8 +51,29 @@ 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/calibration.cpp + helpers/joycon_protocol/calibration.h + helpers/joycon_protocol/common_protocol.cpp + helpers/joycon_protocol/common_protocol.h + helpers/joycon_protocol/generic_functions.cpp + helpers/joycon_protocol/generic_functions.h + helpers/joycon_protocol/joycon_types.h + helpers/joycon_protocol/irs.cpp + helpers/joycon_protocol/irs.h + helpers/joycon_protocol/nfc.cpp + helpers/joycon_protocol/nfc.h + helpers/joycon_protocol/poller.cpp + helpers/joycon_protocol/poller.h + helpers/joycon_protocol/ringcon.cpp + helpers/joycon_protocol/ringcon.h + helpers/joycon_protocol/rumble.cpp + helpers/joycon_protocol/rumble.h ) target_link_libraries(input_common PRIVATE SDL2::SDL2) target_compile_definitions(input_common PRIVATE HAVE_SDL2) diff --git a/src/input_common/drivers/camera.cpp b/src/input_common/drivers/camera.cpp index fad9177dcb..04970f635d 100644 --- a/src/input_common/drivers/camera.cpp +++ b/src/input_common/drivers/camera.cpp @@ -72,11 +72,11 @@ std::size_t Camera::getImageHeight() const { } } -Common::Input::CameraError Camera::SetCameraFormat( +Common::Input::DriverResult Camera::SetCameraFormat( [[maybe_unused]] const PadIdentifier& identifier_, const Common::Input::CameraFormat camera_format) { status.format = camera_format; - return Common::Input::CameraError::None; + return Common::Input::DriverResult::Success; } } // namespace InputCommon diff --git a/src/input_common/drivers/camera.h b/src/input_common/drivers/camera.h index ead3e0fdee..24b27e3258 100644 --- a/src/input_common/drivers/camera.h +++ b/src/input_common/drivers/camera.h @@ -22,8 +22,8 @@ public: std::size_t getImageWidth() const; std::size_t getImageHeight() const; - Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_, - Common::Input::CameraFormat camera_format) override; + Common::Input::DriverResult SetCameraFormat(const PadIdentifier& identifier_, + Common::Input::CameraFormat camera_format) override; private: Common::Input::CameraStatus status{}; diff --git a/src/input_common/drivers/gc_adapter.cpp b/src/input_common/drivers/gc_adapter.cpp index 826fa21097..ecb3e9dc29 100644 --- a/src/input_common/drivers/gc_adapter.cpp +++ b/src/input_common/drivers/gc_adapter.cpp @@ -324,7 +324,7 @@ bool GCAdapter::GetGCEndpoint(libusb_device* device) { return true; } -Common::Input::VibrationError GCAdapter::SetVibration( +Common::Input::DriverResult GCAdapter::SetVibration( const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) { const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f; const auto processed_amplitude = @@ -333,9 +333,9 @@ Common::Input::VibrationError GCAdapter::SetVibration( pads[identifier.port].rumble_amplitude = processed_amplitude; if (!rumble_enabled) { - return Common::Input::VibrationError::Disabled; + return Common::Input::DriverResult::Disabled; } - return Common::Input::VibrationError::None; + return Common::Input::DriverResult::Success; } bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) { diff --git a/src/input_common/drivers/gc_adapter.h b/src/input_common/drivers/gc_adapter.h index b5270fd0bb..3c2eb376dc 100644 --- a/src/input_common/drivers/gc_adapter.h +++ b/src/input_common/drivers/gc_adapter.h @@ -25,7 +25,7 @@ public: explicit GCAdapter(std::string input_engine_); ~GCAdapter() override; - Common::Input::VibrationError SetVibration( + Common::Input::DriverResult SetVibration( const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; bool IsVibrationEnabled(const PadIdentifier& identifier) override; diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp new file mode 100644 index 0000000000..7122093c62 --- /dev/null +++ b/src/input_common/drivers/joycon.cpp @@ -0,0 +1,677 @@ +// 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_) { + // 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(port++); + } + port = 0; + for (auto& device : right_joycons) { + PreSetController(GetIdentifier(port, Joycon::ControllerType::Right)); + device = std::make_shared(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 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& amiibo_data) { + OnAmiiboUpdate(port, amiibo_data); + }}, + .on_camera_data = {[this, port](const std::vector& camera_data, + Joycon::IrsResolution format) { + OnCameraUpdate(port, camera_data, format); + }}, + }; + + handle->InitializeDevice(); + handle->SetCallbacks(callbacks); + } +} + +std::shared_ptr 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( + handle->SetLedConfig(static_cast(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(handle->SetIrsConfig( + Joycon::IrsMode::ImageTransfer, static_cast(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& 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(handle->SetActiveMode()); + case Common::Input::PollingMode::Pasive: + return static_cast(handle->SetPasiveMode()); + case Common::Input::PollingMode::IR: + return static_cast(handle->SetIrMode()); + case Common::Input::PollingMode::NFC: + return static_cast(handle->SetNfcMode()); + case Common::Input::PollingMode::Ring: + return static_cast(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& 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& camera_data, + Joycon::IrsResolution format) { + const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right); + SetCamera(identifier, {static_cast(format), camera_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) { + 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 guid{0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, static_cast(type)}; + return { + .guid = Common::UUID{guid}, + .port = port, + .pad = static_cast(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 Joycons::GetInputDevices() const { + std::vector devices{}; + + auto add_entry = [&](std::shared_ptr 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(type))); + devices.emplace_back(main_param); + } + + return devices; +} + +ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) { + static constexpr std::array, + 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(params.Get("port", 0)); + auto pad = static_cast(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(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(Joycon::ControllerType::Left)) { + const std::size_t port = static_cast(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(Joycon::PadButton::LeftSL)); + sr_button_params.Set("button", static_cast(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(Joycon::ControllerType::Right)) { + const std::size_t port = static_cast(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(Joycon::PadButton::RightSL)); + sr_button_params.Set("button", static_cast(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(params.Get("port", 0)); + auto pad_left = static_cast(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(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 = GetParamPackage(port, pad_right); + 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 {}; + } + + const std::size_t port = static_cast(params.Get("port", 0)); + auto pad_left = static_cast(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(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 diff --git a/src/input_common/drivers/joycon.h b/src/input_common/drivers/joycon.h new file mode 100644 index 0000000000..316d383d88 --- /dev/null +++ b/src/input_common/drivers/joycon.h @@ -0,0 +1,111 @@ +// 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; +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& data) override; + + Common::Input::DriverResult 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); + void OnCameraUpdate(std::size_t port, const std::vector& camera_data, + Joycon::IrsResolution format); + + /// Returns a JoyconHandle corresponding to a PadIdentifier + std::shared_ptr 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, MaxSupportedControllers> left_joycons{}; + std::array, MaxSupportedControllers> right_joycons{}; +}; + +} // namespace InputCommon diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp index 9835d99d28..d975eb8152 100644 --- a/src/input_common/drivers/sdl_driver.cpp +++ b/src/input_common/drivers/sdl_driver.cpp @@ -334,6 +334,15 @@ void SDLDriver::InitJoystick(int joystick_index) { const auto guid = GetGUID(sdl_joystick); + if (Settings::values.enable_joycon_driver) { + if (guid.uuid[5] == 0x05 && guid.uuid[4] == 0x7e && + (guid.uuid[8] == 0x06 || guid.uuid[8] == 0x07)) { + LOG_WARNING(Input, "Preferring joycon driver for device index {}", joystick_index); + SDL_JoystickClose(sdl_joystick); + return; + } + } + std::scoped_lock lock{joystick_map_mutex}; if (joystick_map.find(guid) == joystick_map.end()) { auto joystick = std::make_shared(guid, 0, sdl_joystick, sdl_gamecontroller); @@ -456,9 +465,13 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); - // Use hidapi driver for joycons. This will allow joycons to be detected as a GameController and - // not a generic one - SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); + // Disable hidapi drivers for switch controllers when the custom joycon driver is enabled + if (Settings::values.enable_joycon_driver) { + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "0"); + } else { + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); + } + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1"); // Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native // driver on Linux. @@ -548,7 +561,7 @@ std::vector SDLDriver::GetInputDevices() const { return devices; } -Common::Input::VibrationError SDLDriver::SetVibration( +Common::Input::DriverResult SDLDriver::SetVibration( const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) { const auto joystick = GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast(identifier.port)); @@ -582,7 +595,7 @@ Common::Input::VibrationError SDLDriver::SetVibration( .vibration = new_vibration, }); - return Common::Input::VibrationError::None; + return Common::Input::DriverResult::Success; } bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) { diff --git a/src/input_common/drivers/sdl_driver.h b/src/input_common/drivers/sdl_driver.h index 366bcc4965..ffde169b35 100644 --- a/src/input_common/drivers/sdl_driver.h +++ b/src/input_common/drivers/sdl_driver.h @@ -63,7 +63,7 @@ public: bool IsStickInverted(const Common::ParamPackage& params) override; - Common::Input::VibrationError SetVibration( + Common::Input::DriverResult SetVibration( const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; bool IsVibrationEnabled(const PadIdentifier& identifier) override; diff --git a/src/input_common/drivers/virtual_amiibo.cpp b/src/input_common/drivers/virtual_amiibo.cpp index 63ffaca67b..4a0268a4dc 100644 --- a/src/input_common/drivers/virtual_amiibo.cpp +++ b/src/input_common/drivers/virtual_amiibo.cpp @@ -22,22 +22,23 @@ VirtualAmiibo::VirtualAmiibo(std::string input_engine_) : InputEngine(std::move( VirtualAmiibo::~VirtualAmiibo() = default; -Common::Input::PollingError VirtualAmiibo::SetPollingMode( +Common::Input::DriverResult VirtualAmiibo::SetPollingMode( [[maybe_unused]] const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) { polling_mode = polling_mode_; - if (polling_mode == Common::Input::PollingMode::NFC) { + switch (polling_mode) { + case Common::Input::PollingMode::NFC: if (state == State::Initialized) { state = State::WaitingForAmiibo; } - } else { + return Common::Input::DriverResult::Success; + default: if (state == State::AmiiboIsOpen) { CloseAmiibo(); } + return Common::Input::DriverResult::NotSupported; } - - return Common::Input::PollingError::None; } Common::Input::NfcState VirtualAmiibo::SupportsNfc( diff --git a/src/input_common/drivers/virtual_amiibo.h b/src/input_common/drivers/virtual_amiibo.h index 0f9dad3338..13cacfc0af 100644 --- a/src/input_common/drivers/virtual_amiibo.h +++ b/src/input_common/drivers/virtual_amiibo.h @@ -36,7 +36,7 @@ public: ~VirtualAmiibo() override; // Sets polling mode to a controller - Common::Input::PollingError SetPollingMode( + Common::Input::DriverResult SetPollingMode( const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) override; Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override; diff --git a/src/input_common/helpers/joycon_driver.cpp b/src/input_common/helpers/joycon_driver.cpp new file mode 100644 index 0000000000..4159e5717a --- /dev/null +++ b/src/input_common/helpers/joycon_driver.cpp @@ -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(); +} + +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(hidapi_handle); + generic_protocol = std::make_unique(hidapi_handle); + irs_protocol = std::make_unique(hidapi_handle); + nfc_protocol = std::make_unique(hidapi_handle); + ring_protocol = std::make_unique(hidapi_handle); + rumble_protocol = std::make_unique(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(1 + port)); + + // Apply HW configuration + SetPollingMode(); + + // Initialize joycon poller + joycon_poller = std::make_unique(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 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 buffer) { + const auto report_mode = static_cast(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( + std::chrono::duration_cast(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 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 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, 2> supported_devices{ + std::pair{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(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 diff --git a/src/input_common/helpers/joycon_driver.h b/src/input_common/helpers/joycon_driver.h new file mode 100644 index 0000000000..c1e189fa52 --- /dev/null +++ b/src/input_common/helpers/joycon_driver.h @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#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 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 buffer); + + /// Returns a list of supported features that can be enabled on this device + SupportedFeatures GetSupportedFeatures(); + + // Protocol Features + std::unique_ptr calibration_protocol; + std::unique_ptr generic_protocol; + std::unique_ptr irs_protocol; + std::unique_ptr nfc_protocol; + std::unique_ptr joycon_poller; + std::unique_ptr ring_protocol; + std::unique_ptr rumble_protocol; + + // Connection status + std::atomic is_connected{}; + u64 delta_time; + std::size_t error_counter{}; + std::shared_ptr hidapi_handle; + std::chrono::time_point 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 diff --git a/src/input_common/helpers/joycon_protocol/calibration.cpp b/src/input_common/helpers/joycon_protocol/calibration.cpp new file mode 100644 index 0000000000..f6e7e97d59 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/calibration.cpp @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include + +#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 handle) + : JoyconCommonProtocol(std::move(handle)) {} + +DriverResult CalibrationProtocol::GetLeftJoyStickCalibration(JoyStickCalibration& calibration) { + ScopedSetBlocking sb(this); + std::vector 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(((buffer[1] & 0x0F) << 8) | buffer[0]); + calibration.y.max = static_cast((buffer[2] << 4) | (buffer[1] >> 4)); + calibration.x.center = static_cast(((buffer[4] & 0x0F) << 8) | buffer[3]); + calibration.y.center = static_cast((buffer[5] << 4) | (buffer[4] >> 4)); + calibration.x.min = static_cast(((buffer[7] & 0x0F) << 8) | buffer[6]); + calibration.y.min = static_cast((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 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(((buffer[1] & 0x0F) << 8) | buffer[0]); + calibration.y.center = static_cast((buffer[2] << 4) | (buffer[1] >> 4)); + calibration.x.min = static_cast(((buffer[4] & 0x0F) << 8) | buffer[3]); + calibration.y.min = static_cast((buffer[5] << 4) | (buffer[4] >> 4)); + calibration.x.max = static_cast(((buffer[7] & 0x0F) << 8) | buffer[6]); + calibration.y.max = static_cast((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 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 diff --git a/src/input_common/helpers/joycon_protocol/calibration.h b/src/input_common/helpers/joycon_protocol/calibration.h new file mode 100644 index 0000000000..afb52a36a4 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/calibration.h @@ -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 + +#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 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 diff --git a/src/input_common/helpers/joycon_protocol/common_protocol.cpp b/src/input_common/helpers/joycon_protocol/common_protocol.cpp new file mode 100644 index 0000000000..417d0dcc50 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/common_protocol.cpp @@ -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 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 buffer; + const auto result = ReadSPI(CalAddr::DEVICE_TYPE, 1, buffer); + controller_type = ControllerType::None; + + if (result == DriverResult::Success) { + controller_type = static_cast(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 buffer{static_cast(report_mode)}; + return SendSubCommand(SubCommand::SET_REPORT_MODE, buffer); +} + +DriverResult JoyconCommonProtocol::SendData(std::span 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& 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(sc)); + + if (output[0] != 0x21 && output[14] != static_cast(sc)) { + return DriverResult::WrongReply; + } + + return DriverResult::Success; +} + +DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span buffer, + std::vector& output) { + std::vector local_buffer(MaxResponseSize); + + local_buffer[0] = static_cast(OutputReport::RUMBLE_AND_SUBCMD); + local_buffer[1] = GetCounter(); + local_buffer[10] = static_cast(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 buffer) { + std::vector output; + return SendSubCommand(sc, buffer, output); +} + +DriverResult JoyconCommonProtocol::SendMCUCommand(SubCommand sc, std::span buffer) { + std::vector local_buffer(MaxResponseSize); + + local_buffer[0] = static_cast(OutputReport::MCU_DATA); + local_buffer[1] = GetCounter(); + local_buffer[10] = static_cast(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 buffer) { + std::vector local_buffer(MaxResponseSize); + + local_buffer[0] = static_cast(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& output) { + constexpr std::size_t MaxTries = 10; + std::size_t tries = 0; + std::array buffer = {0x00, 0x00, 0x00, 0x00, size}; + std::vector local_buffer(size + 20); + + buffer[0] = static_cast(static_cast(addr) & 0x00FF); + buffer[1] = static_cast((static_cast(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(local_buffer.begin() + 20, local_buffer.begin() + 20 + size); + return DriverResult::Success; +} + +DriverResult JoyconCommonProtocol::EnableMCU(bool enable) { + const std::array mcu_state{static_cast(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 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& output) { + const int report_mode = static_cast(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 buffer, + std::vector& output) { + std::vector local_buffer(MaxResponseSize); + + local_buffer[0] = static_cast(OutputReport::MCU_DATA); + local_buffer[1] = GetCounter(); + local_buffer[9] = static_cast(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 output; + constexpr std::size_t MaxTries{8}; + std::size_t tries{}; + + do { + const std::vector mcu_data{static_cast(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(mode)); + + return DriverResult::Success; +} + +// crc-8-ccitt / polynomial 0x07 look up table +constexpr std::array 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(crc8 ^ buffer[i])]; + } + return crc8; +} + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/common_protocol.h b/src/input_common/helpers/joycon_protocol/common_protocol.h new file mode 100644 index 0000000000..903bcf4025 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/common_protocol.h @@ -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 +#include +#include + +#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 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 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& 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 buffer, std::vector& 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 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 buffer); + + /** + * Sends vibration data to the joycon + * @param buffer data to be send + */ + DriverResult SendVibrationReport(std::span 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& 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& 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 buffer, + std::vector& 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 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 diff --git a/src/input_common/helpers/joycon_protocol/generic_functions.cpp b/src/input_common/helpers/joycon_protocol/generic_functions.cpp new file mode 100644 index 0000000000..52bb8b61aa --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/generic_functions.cpp @@ -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 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 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 buffer{static_cast(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 buffer{static_cast(gsen), static_cast(asen), + static_cast(gfrec), static_cast(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 buffer; + const auto result = ReadSPI(CalAddr::COLOR_DATA, 12, buffer); + + color = {}; + if (result == DriverResult::Success) { + color.body = static_cast((buffer[0] << 16) | (buffer[1] << 8) | buffer[2]); + color.buttons = static_cast((buffer[3] << 16) | (buffer[4] << 8) | buffer[5]); + color.left_grip = static_cast((buffer[6] << 16) | (buffer[7] << 8) | buffer[8]); + color.right_grip = static_cast((buffer[9] << 16) | (buffer[10] << 8) | buffer[11]); + } + + return result; +} + +DriverResult GenericProtocol::GetSerialNumber(SerialNumber& serial_number) { + ScopedSetBlocking sb(this); + std::vector 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 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 buffer{leds}; + return SendSubCommand(SubCommand::SET_PLAYER_LIGHTS, buffer); +} + +DriverResult GenericProtocol::SetLedBlinkPattern(u8 leds) { + return SetLedPattern(static_cast(leds << 4)); +} + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/generic_functions.h b/src/input_common/helpers/joycon_protocol/generic_functions.h new file mode 100644 index 0000000000..239bb7dbf9 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/generic_functions.h @@ -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 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 diff --git a/src/input_common/helpers/joycon_protocol/irs.cpp b/src/input_common/helpers/joycon_protocol/irs.cpp new file mode 100644 index 0000000000..09e17bc5b6 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/irs.cpp @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/logging/log.h" +#include "input_common/helpers/joycon_protocol/irs.h" + +namespace InputCommon::Joycon { + +IrsProtocol::IrsProtocol(std::shared_ptr 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 buffer) { + const u8 next_packet_fragment = + static_cast((packet_fragment + 1) % (static_cast(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 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(fragments) + 1) * 300); + + std::array 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 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(resolution_code)}, + {IrRegistersAddress::ExposureLSB, static_cast(exposure & 0xff)}, + {IrRegistersAddress::ExposureMSB, static_cast(exposure >> 8)}, + {IrRegistersAddress::ExposureTime, 0x00}, + {IrRegistersAddress::Leds, static_cast(leds)}, + {IrRegistersAddress::DigitalGainLSB, static_cast((digital_gain & 0x0f) << 4)}, + {IrRegistersAddress::DigitalGainMSB, static_cast((digital_gain & 0xf0) >> 4)}, + {IrRegistersAddress::LedFilter, static_cast(led_filter)}, + {IrRegistersAddress::WhitePixelThreshold, 0xc8}, + }, + .crc = {}, + }; + + std::array request_data{}; + memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters)); + request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36); + + std::array 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 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(led_intensity >> 8)}, + {IrRegistersAddress::LedIntensitiyLSB, static_cast(led_intensity & 0xff)}, + {IrRegistersAddress::ImageFlip, static_cast(image_flip)}, + {IrRegistersAddress::DenoiseSmoothing, static_cast((denoise >> 16) & 0xff)}, + {IrRegistersAddress::DenoiseEdge, static_cast((denoise >> 8) & 0xff)}, + {IrRegistersAddress::DenoiseColor, static_cast(denoise & 0xff)}, + {IrRegistersAddress::UpdateTime, 0x2d}, + {IrRegistersAddress::FinalizeConfig, 0x01}, + }, + .crc = {}, + }; + + std::array 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 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 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 IrsProtocol::GetImage() const { + return buf_image; +} + +IrsResolution IrsProtocol::GetIrsFormat() const { + return resolution; +} + +bool IrsProtocol::IsEnabled() const { + return is_enabled; +} + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/irs.h b/src/input_common/helpers/joycon_protocol/irs.h new file mode 100644 index 0000000000..76dfa02ea5 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/irs.h @@ -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 + +#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 handle); + + DriverResult EnableIrs(); + + DriverResult DisableIrs(); + + DriverResult SetIrsConfig(IrsMode mode, IrsResolution format); + + DriverResult RequestImage(std::span buffer); + + std::vector 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 buf_image; // 8bpp greyscale image. + + bool is_enabled{}; +}; + +} // 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 0000000000..e2d47349f0 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/joycon_types.h @@ -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 +#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 = 368; +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 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 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 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 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 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; + std::function&, IrsResolution)> on_camera_data; +}; + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/nfc.cpp b/src/input_common/helpers/joycon_protocol/nfc.cpp new file mode 100644 index 0000000000..5c0f717228 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/nfc.cpp @@ -0,0 +1,400 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include "common/logging/log.h" +#include "input_common/helpers/joycon_protocol/nfc.h" + +namespace InputCommon::Joycon { + +NfcProtocol::NfcProtocol(std::shared_ptr 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& 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 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 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 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(output[49]); + const auto nfc_status = static_cast(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& ntag_data) { + constexpr std::size_t timeout_limit = 10; + std::vector 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(output[49]); + const auto nfc_status = static_cast(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& 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 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& 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 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& 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 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& 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 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 diff --git a/src/input_common/helpers/joycon_protocol/nfc.h b/src/input_common/helpers/joycon_protocol/nfc.h new file mode 100644 index 0000000000..e63665aa97 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/nfc.h @@ -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 + +#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 handle); + + DriverResult EnableNfc(); + + DriverResult DisableNfc(); + + DriverResult StartNFCPollingMode(); + + DriverResult ScanAmiibo(std::vector& data); + + bool HasAmiibo(); + + bool IsEnabled() const; + +private: + struct TagFoundData { + u8 type; + std::vector uuid; + }; + + DriverResult WaitUntilNfcIsReady(); + + DriverResult StartPolling(TagFoundData& data); + + DriverResult ReadTag(const TagFoundData& data); + + DriverResult GetAmiiboData(std::vector& data); + + DriverResult SendStartPollingRequest(std::vector& output); + + DriverResult SendStopPollingRequest(std::vector& output); + + DriverResult SendStartWaitingRecieveRequest(std::vector& output); + + DriverResult SendReadAmiiboRequest(std::vector& output, NFCPages ntag_pages); + + NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const; + + bool is_enabled{}; +}; + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/poller.cpp b/src/input_common/helpers/joycon_protocol/poller.cpp new file mode 100644 index 0000000000..7f8e093faf --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/poller.cpp @@ -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 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 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 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& amiibo_data) { + callbacks.on_amiibo_data(amiibo_data); +} + +void JoyconPoller::UpdateCamera(const std::vector& 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(value - ring_status.default_value); + if (normalized_value > 0) { + normalized_value = normalized_value / + static_cast(ring_status.max_value - ring_status.default_value); + } + if (normalized_value < 0) { + normalized_value = normalized_value / + static_cast(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 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(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(left_buttons[i])) != 0; + const int button = static_cast(left_buttons[i]); + callbacks.on_button_data(button, button_status); + } + + const u16 raw_left_axis_x = + static_cast(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8)); + const u16 raw_left_axis_y = + static_cast((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(PadAxes::LeftStickX), left_axis_x); + callbacks.on_stick_data(static_cast(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(PadMotion::LeftMotion), left_motion); + } +} + +void JoyconPoller::UpdateActiveRightPadInput(const InputReportActive& input, + const MotionStatus& motion_status) { + static constexpr std::array 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((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(right_buttons[i])) != 0; + const int button = static_cast(right_buttons[i]); + callbacks.on_button_data(button, button_status); + } + + const u16 raw_right_axis_x = + static_cast(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8)); + const u16 raw_right_axis_y = + static_cast((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(PadAxes::RightStickX), right_axis_x); + callbacks.on_stick_data(static_cast(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(PadMotion::RightMotion), right_motion); + } +} + +void JoyconPoller::UpdateActiveProPadInput(const InputReportActive& input, + const MotionStatus& motion_status) { + static constexpr std::array 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(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(pro_buttons[i])) != 0; + const int button = static_cast(pro_buttons[i]); + callbacks.on_button_data(button, button_status); + } + + const u16 raw_left_axis_x = + static_cast(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8)); + const u16 raw_left_axis_y = + static_cast((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4)); + const u16 raw_right_axis_x = + static_cast(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8)); + const u16 raw_right_axis_y = + static_cast((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(PadAxes::LeftStickX), left_axis_x); + callbacks.on_stick_data(static_cast(PadAxes::LeftStickY), left_axis_y); + callbacks.on_stick_data(static_cast(PadAxes::RightStickX), right_axis_x); + callbacks.on_stick_data(static_cast(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(PadMotion::LeftMotion), pro_motion); + callbacks.on_motion_data(static_cast(PadMotion::RightMotion), pro_motion); + } +} + +void JoyconPoller::UpdatePasiveLeftPadInput(const InputReportPassive& input) { + static constexpr std::array 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(left_button)) != 0; + const int button = static_cast(left_button); + callbacks.on_button_data(button, button_status); + } +} + +void JoyconPoller::UpdatePasiveRightPadInput(const InputReportPassive& input) { + static constexpr std::array 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(right_button)) != 0; + const int button = static_cast(right_button); + callbacks.on_button_data(button, button_status); + } +} + +void JoyconPoller::UpdatePasiveProPadInput(const InputReportPassive& input) { + static constexpr std::array 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(pro_button)) != 0; + const int button = static_cast(pro_button); + callbacks.on_button_data(button, button_status); + } +} + +f32 JoyconPoller::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const { + const f32 value = static_cast(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 diff --git a/src/input_common/helpers/joycon_protocol/poller.h b/src/input_common/helpers/joycon_protocol/poller.h new file mode 100644 index 0000000000..354d41dad3 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/poller.h @@ -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 +#include + +#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 buffer); + + /// Handles data from active packages + void ReadActiveMode(std::span buffer, const MotionStatus& motion_status, + const RingStatus& ring_status); + + /// Handles data from nfc or ir packages + void ReadNfcIRMode(std::span buffer, const MotionStatus& motion_status); + + void UpdateColor(const Color& color); + void UpdateRing(s16 value, const RingStatus& ring_status); + void UpdateAmiibo(const std::vector& amiibo_data); + void UpdateCamera(const std::vector& 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 diff --git a/src/input_common/helpers/joycon_protocol/ringcon.cpp b/src/input_common/helpers/joycon_protocol/ringcon.cpp new file mode 100644 index 0000000000..12f81309e1 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/ringcon.cpp @@ -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 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 output; + std::size_t tries = 0; + is_connected = false; + + do { + std::array 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 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 ringcon_data{0x04, 0x01, 0x01, 0x02}; + return SendSubCommand(SubCommand::UNKNOWN_RINGCON2, ringcon_data); +} + +bool RingConProtocol::IsEnabled() const { + return is_enabled; +} + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/ringcon.h b/src/input_common/helpers/joycon_protocol/ringcon.h new file mode 100644 index 0000000000..6e858f3fcb --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/ringcon.h @@ -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 + +#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 handle); + + DriverResult EnableRingCon(); + + DriverResult DisableRingCon(); + + DriverResult StartRingconPolling(); + + bool IsEnabled() const; + +private: + DriverResult IsRingConnected(bool& is_connected); + + DriverResult ConfigureRing(); + + bool is_enabled{}; +}; + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/rumble.cpp b/src/input_common/helpers/joycon_protocol/rumble.cpp new file mode 100644 index 0000000000..63b60c9468 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/rumble.cpp @@ -0,0 +1,299 @@ +// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +#include "common/logging/log.h" +#include "input_common/helpers/joycon_protocol/rumble.h" + +namespace InputCommon::Joycon { + +RumbleProtocol::RumbleProtocol(std::shared_ptr handle) + : JoyconCommonProtocol(std::move(handle)) {} + +DriverResult RumbleProtocol::EnableRumble(bool is_enabled) { + LOG_DEBUG(Input, "Enable Rumble"); + ScopedSetBlocking sb(this); + const std::array buffer{static_cast(is_enabled ? 1 : 0)}; + return SendSubCommand(SubCommand::ENABLE_VIBRATION, buffer); +} + +DriverResult RumbleProtocol::SendVibration(const VibrationValue& vibration) { + std::array 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(encoded_high_frequency & 0xFF); + buffer[1] = static_cast(encoded_high_amplitude | ((encoded_high_frequency >> 8) & 0x01)); + buffer[2] = static_cast(encoded_low_frequency | ((encoded_low_amplitude >> 8) & 0x80)); + buffer[3] = static_cast(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(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f)); + return static_cast((new_frequency - 0x60) * 4); +} + +u8 RumbleProtocol::EncodeLowFrequency(f32 frequency) const { + const u8 new_frequency = + static_cast(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f)); + return static_cast(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, 101> high_fequency_amplitude{ + std::pair{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(code); + } + } + + return static_cast(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, 101> high_fequency_amplitude{ + std::pair{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(code); + } + } + + return static_cast(high_fequency_amplitude[high_fequency_amplitude.size() - 1].second); +} + +} // namespace InputCommon::Joycon diff --git a/src/input_common/helpers/joycon_protocol/rumble.h b/src/input_common/helpers/joycon_protocol/rumble.h new file mode 100644 index 0000000000..6c12b79258 --- /dev/null +++ b/src/input_common/helpers/joycon_protocol/rumble.h @@ -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 + +#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 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 diff --git a/src/input_common/input_engine.cpp b/src/input_common/input_engine.cpp index 61cfd0911c..91aa96aa73 100644 --- a/src/input_common/input_engine.cpp +++ b/src/input_common/input_engine.cpp @@ -79,6 +79,17 @@ void InputEngine::SetBattery(const PadIdentifier& identifier, Common::Input::Bat TriggerOnBatteryChange(identifier, value); } +void InputEngine::SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value) { + { + std::scoped_lock lock{mutex}; + ControllerData& controller = controller_list.at(identifier); + if (!configuring) { + controller.color = value; + } + } + TriggerOnColorChange(identifier, value); +} + void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value) { { std::scoped_lock lock{mutex}; @@ -176,6 +187,18 @@ Common::Input::BatteryLevel InputEngine::GetBattery(const PadIdentifier& identif return controller.battery; } +Common::Input::BodyColorStatus InputEngine::GetColor(const PadIdentifier& identifier) const { + std::scoped_lock lock{mutex}; + const auto controller_iter = controller_list.find(identifier); + if (controller_iter == controller_list.cend()) { + LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(), + identifier.pad, identifier.port); + return {}; + } + const ControllerData& controller = controller_iter->second; + return controller.color; +} + BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) const { std::scoped_lock lock{mutex}; const auto controller_iter = controller_list.find(identifier); @@ -328,6 +351,20 @@ void InputEngine::TriggerOnBatteryChange(const PadIdentifier& identifier, } } +void InputEngine::TriggerOnColorChange(const PadIdentifier& identifier, + [[maybe_unused]] Common::Input::BodyColorStatus value) { + std::scoped_lock lock{mutex_callback}; + for (const auto& poller_pair : callback_list) { + const InputIdentifier& poller = poller_pair.second; + if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Color, 0)) { + continue; + } + if (poller.callback.on_change) { + poller.callback.on_change(); + } + } +} + void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion, const BasicMotion& value) { std::scoped_lock lock{mutex_callback}; diff --git a/src/input_common/input_engine.h b/src/input_common/input_engine.h index 6cbcf5207e..50b5a3dc8c 100644 --- a/src/input_common/input_engine.h +++ b/src/input_common/input_engine.h @@ -40,6 +40,7 @@ enum class EngineInputType { Battery, Button, Camera, + Color, HatButton, Motion, Nfc, @@ -104,14 +105,17 @@ public: void EndConfiguration(); // Sets a led pattern for a controller - virtual void SetLeds([[maybe_unused]] const PadIdentifier& identifier, - [[maybe_unused]] const Common::Input::LedStatus& led_status) {} + virtual Common::Input::DriverResult SetLeds( + [[maybe_unused]] const PadIdentifier& identifier, + [[maybe_unused]] const Common::Input::LedStatus& led_status) { + return Common::Input::DriverResult::NotSupported; + } // Sets rumble to a controller - virtual Common::Input::VibrationError SetVibration( + virtual Common::Input::DriverResult SetVibration( [[maybe_unused]] const PadIdentifier& identifier, [[maybe_unused]] const Common::Input::VibrationStatus& vibration) { - return Common::Input::VibrationError::NotSupported; + return Common::Input::DriverResult::NotSupported; } // Returns true if device supports vibrations @@ -120,17 +124,17 @@ public: } // Sets polling mode to a controller - virtual Common::Input::PollingError SetPollingMode( + virtual Common::Input::DriverResult SetPollingMode( [[maybe_unused]] const PadIdentifier& identifier, [[maybe_unused]] const Common::Input::PollingMode polling_mode) { - return Common::Input::PollingError::NotSupported; + return Common::Input::DriverResult::NotSupported; } // Sets camera format to a controller - virtual Common::Input::CameraError SetCameraFormat( + virtual Common::Input::DriverResult SetCameraFormat( [[maybe_unused]] const PadIdentifier& identifier, [[maybe_unused]] Common::Input::CameraFormat camera_format) { - return Common::Input::CameraError::NotSupported; + return Common::Input::DriverResult::NotSupported; } // Returns success if nfc is supported @@ -199,6 +203,7 @@ public: bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const; f32 GetAxis(const PadIdentifier& identifier, int axis) const; Common::Input::BatteryLevel GetBattery(const PadIdentifier& identifier) const; + Common::Input::BodyColorStatus GetColor(const PadIdentifier& identifier) const; BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const; Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const; Common::Input::NfcStatus GetNfc(const PadIdentifier& identifier) const; @@ -212,6 +217,7 @@ protected: void SetHatButton(const PadIdentifier& identifier, int button, u8 value); void SetAxis(const PadIdentifier& identifier, int axis, f32 value); void SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value); + void SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value); void SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value); void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value); void SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value); @@ -227,6 +233,7 @@ private: std::unordered_map axes; std::unordered_map motions; Common::Input::BatteryLevel battery{}; + Common::Input::BodyColorStatus color{}; Common::Input::CameraStatus camera{}; Common::Input::NfcStatus nfc{}; }; @@ -235,6 +242,8 @@ private: void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value); void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value); void TriggerOnBatteryChange(const PadIdentifier& identifier, Common::Input::BatteryLevel value); + void TriggerOnColorChange(const PadIdentifier& identifier, + Common::Input::BodyColorStatus value); void TriggerOnMotionChange(const PadIdentifier& identifier, int motion, const BasicMotion& value); void TriggerOnCameraChange(const PadIdentifier& identifier, diff --git a/src/input_common/input_poller.cpp b/src/input_common/input_poller.cpp index fb8be42e2e..15cbf7e5f0 100644 --- a/src/input_common/input_poller.cpp +++ b/src/input_common/input_poller.cpp @@ -498,6 +498,58 @@ private: InputEngine* input_engine; }; +class InputFromColor final : public Common::Input::InputDevice { +public: + explicit InputFromColor(PadIdentifier identifier_, InputEngine* input_engine_) + : identifier(identifier_), input_engine(input_engine_) { + UpdateCallback engine_callback{[this]() { OnChange(); }}; + const InputIdentifier input_identifier{ + .identifier = identifier, + .type = EngineInputType::Color, + .index = 0, + .callback = engine_callback, + }; + last_color_value = {}; + callback_key = input_engine->SetCallback(input_identifier); + } + + ~InputFromColor() override { + input_engine->DeleteCallback(callback_key); + } + + Common::Input::BodyColorStatus GetStatus() const { + return input_engine->GetColor(identifier); + } + + void ForceUpdate() override { + const Common::Input::CallbackStatus status{ + .type = Common::Input::InputType::Color, + .color_status = GetStatus(), + }; + + last_color_value = status.color_status; + TriggerOnChange(status); + } + + void OnChange() { + const Common::Input::CallbackStatus status{ + .type = Common::Input::InputType::Color, + .color_status = GetStatus(), + }; + + if (status.color_status.body != last_color_value.body) { + last_color_value = status.color_status; + TriggerOnChange(status); + } + } + +private: + const PadIdentifier identifier; + int callback_key; + Common::Input::BodyColorStatus last_color_value; + InputEngine* input_engine; +}; + class InputFromMotion final : public Common::Input::InputDevice { public: explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_, @@ -754,11 +806,11 @@ public: explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_) : identifier(identifier_), input_engine(input_engine_) {} - void SetLED(const Common::Input::LedStatus& led_status) override { - input_engine->SetLeds(identifier, led_status); + Common::Input::DriverResult SetLED(const Common::Input::LedStatus& led_status) override { + return input_engine->SetLeds(identifier, led_status); } - Common::Input::VibrationError SetVibration( + Common::Input::DriverResult SetVibration( const Common::Input::VibrationStatus& vibration_status) override { return input_engine->SetVibration(identifier, vibration_status); } @@ -767,11 +819,12 @@ public: return input_engine->IsVibrationEnabled(identifier); } - Common::Input::PollingError SetPollingMode(Common::Input::PollingMode polling_mode) override { + Common::Input::DriverResult SetPollingMode(Common::Input::PollingMode polling_mode) override { return input_engine->SetPollingMode(identifier, polling_mode); } - Common::Input::CameraError SetCameraFormat(Common::Input::CameraFormat camera_format) override { + Common::Input::DriverResult SetCameraFormat( + Common::Input::CameraFormat camera_format) override { return input_engine->SetCameraFormat(identifier, camera_format); } @@ -966,6 +1019,18 @@ std::unique_ptr InputFactory::CreateBatteryDevice( return std::make_unique(identifier, input_engine.get()); } +std::unique_ptr InputFactory::CreateColorDevice( + const Common::ParamPackage& params) { + const PadIdentifier identifier = { + .guid = Common::UUID{params.Get("guid", "")}, + .port = static_cast(params.Get("port", 0)), + .pad = static_cast(params.Get("pad", 0)), + }; + + input_engine->PreSetController(identifier); + return std::make_unique(identifier, input_engine.get()); +} + std::unique_ptr InputFactory::CreateMotionDevice( Common::ParamPackage params) { const PadIdentifier identifier = { @@ -1053,6 +1118,9 @@ std::unique_ptr InputFactory::Create( if (params.Has("battery")) { return CreateBatteryDevice(params); } + if (params.Has("color")) { + return CreateColorDevice(params); + } if (params.Has("camera")) { return CreateCameraDevice(params); } diff --git a/src/input_common/input_poller.h b/src/input_common/input_poller.h index d7db13ce42..e097e254c8 100644 --- a/src/input_common/input_poller.h +++ b/src/input_common/input_poller.h @@ -190,6 +190,17 @@ private: std::unique_ptr CreateBatteryDevice( const Common::ParamPackage& params); + /** + * Creates a color device from the parameters given. + * @param params contains parameters for creating the device: + * - "guid": text string for identifying controllers + * - "port": port of the connected device + * - "pad": slot of the connected controller + * @returns a unique input device with the parameters specified + */ + std::unique_ptr CreateColorDevice( + const Common::ParamPackage& params); + /** * Creates a motion device from the parameters given. * @param params contains parameters for creating the device: diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp index e0b2131ed8..c77fc04ee6 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 }; diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index fd3bb30e1d..35fef506a5 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -440,6 +440,7 @@ void Config::ReadControlValues() { ReadBasicSetting(Settings::values.emulate_analog_keyboard); Settings::values.mouse_panning = false; ReadBasicSetting(Settings::values.mouse_panning_sensitivity); + ReadBasicSetting(Settings::values.enable_joycon_driver); ReadBasicSetting(Settings::values.tas_enable); ReadBasicSetting(Settings::values.tas_loop); @@ -1139,6 +1140,7 @@ void Config::SaveControlValues() { WriteGlobalSetting(Settings::values.enable_accurate_vibrations); WriteGlobalSetting(Settings::values.motion_enabled); WriteBasicSetting(Settings::values.enable_raw_input); + WriteBasicSetting(Settings::values.enable_joycon_driver); WriteBasicSetting(Settings::values.keyboard_enabled); WriteBasicSetting(Settings::values.emulate_analog_keyboard); WriteBasicSetting(Settings::values.mouse_panning_sensitivity); diff --git a/src/yuzu/configuration/configure_input_advanced.cpp b/src/yuzu/configuration/configure_input_advanced.cpp index 235b813d97..77b976e740 100644 --- a/src/yuzu/configuration/configure_input_advanced.cpp +++ b/src/yuzu/configuration/configure_input_advanced.cpp @@ -138,6 +138,7 @@ void ConfigureInputAdvanced::ApplyConfiguration() { Settings::values.controller_navigation = ui->controller_navigation->isChecked(); Settings::values.enable_ring_controller = ui->enable_ring_controller->isChecked(); Settings::values.enable_ir_sensor = ui->enable_ir_sensor->isChecked(); + Settings::values.enable_joycon_driver = ui->enable_joycon_driver->isChecked(); } void ConfigureInputAdvanced::LoadConfiguration() { @@ -172,6 +173,7 @@ void ConfigureInputAdvanced::LoadConfiguration() { ui->controller_navigation->setChecked(Settings::values.controller_navigation.GetValue()); ui->enable_ring_controller->setChecked(Settings::values.enable_ring_controller.GetValue()); ui->enable_ir_sensor->setChecked(Settings::values.enable_ir_sensor.GetValue()); + ui->enable_joycon_driver->setChecked(Settings::values.enable_joycon_driver.GetValue()); UpdateUIEnabled(); } diff --git a/src/yuzu/configuration/configure_input_advanced.ui b/src/yuzu/configuration/configure_input_advanced.ui index fac8cf827d..75d96d3ab5 100644 --- a/src/yuzu/configuration/configure_input_advanced.ui +++ b/src/yuzu/configuration/configure_input_advanced.ui @@ -2696,6 +2696,22 @@ + + + Requires restarting yuzu + + + + 0 + 23 + + + + Enable direct JoyCon driver + + + + @@ -2708,7 +2724,7 @@ - + Mouse sensitivity @@ -2730,14 +2746,14 @@ - + Motion / Touch - + Configure diff --git a/src/yuzu/configuration/configure_input_player.cpp b/src/yuzu/configuration/configure_input_player.cpp index c40d980c9a..4b7e3b01bf 100644 --- a/src/yuzu/configuration/configure_input_player.cpp +++ b/src/yuzu/configuration/configure_input_player.cpp @@ -66,6 +66,18 @@ QString GetButtonName(Common::Input::ButtonNames button_name) { return QObject::tr("R"); case Common::Input::ButtonNames::TriggerL: return QObject::tr("L"); + case Common::Input::ButtonNames::TriggerZR: + return QObject::tr("ZR"); + case Common::Input::ButtonNames::TriggerZL: + return QObject::tr("ZL"); + case Common::Input::ButtonNames::TriggerSR: + return QObject::tr("SR"); + case Common::Input::ButtonNames::TriggerSL: + return QObject::tr("SL"); + case Common::Input::ButtonNames::ButtonStickL: + return QObject::tr("Stick L"); + case Common::Input::ButtonNames::ButtonStickR: + return QObject::tr("Stick R"); case Common::Input::ButtonNames::ButtonA: return QObject::tr("A"); case Common::Input::ButtonNames::ButtonB: @@ -76,6 +88,14 @@ QString GetButtonName(Common::Input::ButtonNames button_name) { return QObject::tr("Y"); case Common::Input::ButtonNames::ButtonStart: return QObject::tr("Start"); + case Common::Input::ButtonNames::ButtonPlus: + return QObject::tr("Plus"); + case Common::Input::ButtonNames::ButtonMinus: + return QObject::tr("Minus"); + case Common::Input::ButtonNames::ButtonHome: + return QObject::tr("Home"); + case Common::Input::ButtonNames::ButtonCapture: + return QObject::tr("Capture"); case Common::Input::ButtonNames::L1: return QObject::tr("L1"); case Common::Input::ButtonNames::L2: diff --git a/src/yuzu/configuration/configure_input_player_widget.cpp b/src/yuzu/configuration/configure_input_player_widget.cpp index 11390fec06..68af6c20c9 100644 --- a/src/yuzu/configuration/configure_input_player_widget.cpp +++ b/src/yuzu/configuration/configure_input_player_widget.cpp @@ -103,9 +103,13 @@ void PlayerControlPreview::UpdateColors() { colors.left = colors.primary; colors.right = colors.primary; - // Possible alternative to set colors from settings - // colors.left = QColor(controller->GetColors().left.body); - // colors.right = QColor(controller->GetColors().right.body); + + const auto color_left = controller->GetColorsValues()[0].body; + const auto color_right = controller->GetColorsValues()[1].body; + if (color_left != 0 && color_right != 0) { + colors.left = QColor(color_left); + colors.right = QColor(color_right); + } } void PlayerControlPreview::ResetInputs() { diff --git a/src/yuzu/configuration/configure_ringcon.cpp b/src/yuzu/configuration/configure_ringcon.cpp index 688c2dd389..1275f10c89 100644 --- a/src/yuzu/configuration/configure_ringcon.cpp +++ b/src/yuzu/configuration/configure_ringcon.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include +#include -#include "core/hid/emulated_devices.h" +#include "core/hid/emulated_controller.h" #include "core/hid/hid_core.h" #include "input_common/drivers/keyboard.h" #include "input_common/drivers/mouse.h" @@ -126,9 +128,16 @@ ConfigureRingController::ConfigureRingController(QWidget* parent, ui->buttonRingAnalogPush, }; - emulated_device = hid_core_.GetEmulatedDevices(); - emulated_device->SaveCurrentConfig(); - emulated_device->EnableConfiguration(); + emulated_controller = hid_core_.GetEmulatedController(Core::HID::NpadIdType::Player1); + emulated_controller->SaveCurrentConfig(); + emulated_controller->EnableConfiguration(); + + Core::HID::ControllerUpdateCallback engine_callback{ + .on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdate(type); }, + .is_npad_service = false, + }; + callback_key = emulated_controller->SetCallback(engine_callback); + is_controller_set = true; LoadConfiguration(); @@ -143,9 +152,9 @@ ConfigureRingController::ConfigureRingController(QWidget* parent, HandleClick( analog_map_buttons[sub_button_id], [=, this](const Common::ParamPackage& params) { - Common::ParamPackage param = emulated_device->GetRingParam(); + Common::ParamPackage param = emulated_controller->GetRingParam(); SetAnalogParam(params, param, analog_sub_buttons[sub_button_id]); - emulated_device->SetRingParam(param); + emulated_controller->SetRingParam(param); }, InputCommon::Polling::InputType::Stick); }); @@ -155,16 +164,16 @@ ConfigureRingController::ConfigureRingController(QWidget* parent, connect(analog_button, &QPushButton::customContextMenuRequested, [=, this](const QPoint& menu_location) { QMenu context_menu; - Common::ParamPackage param = emulated_device->GetRingParam(); + Common::ParamPackage param = emulated_controller->GetRingParam(); context_menu.addAction(tr("Clear"), [&] { - emulated_device->SetRingParam({}); + emulated_controller->SetRingParam(param); analog_map_buttons[sub_button_id]->setText(tr("[not set]")); }); context_menu.addAction(tr("Invert axis"), [&] { const bool invert_value = param.Get("invert_x", "+") == "-"; const std::string invert_str = invert_value ? "+" : "-"; param.Set("invert_x", invert_str); - emulated_device->SetRingParam(param); + emulated_controller->SetRingParam(param); for (int sub_button_id2 = 0; sub_button_id2 < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id2) { analog_map_buttons[sub_button_id2]->setText( @@ -177,16 +186,19 @@ ConfigureRingController::ConfigureRingController(QWidget* parent, } connect(ui->sliderRingAnalogDeadzone, &QSlider::valueChanged, [=, this] { - Common::ParamPackage param = emulated_device->GetRingParam(); + Common::ParamPackage param = emulated_controller->GetRingParam(); const auto slider_value = ui->sliderRingAnalogDeadzone->value(); ui->labelRingAnalogDeadzone->setText(tr("Deadzone: %1%").arg(slider_value)); param.Set("deadzone", slider_value / 100.0f); - emulated_device->SetRingParam(param); + emulated_controller->SetRingParam(param); }); connect(ui->restore_defaults_button, &QPushButton::clicked, this, &ConfigureRingController::RestoreDefaults); + connect(ui->enable_ring_controller_button, &QPushButton::clicked, this, + &ConfigureRingController::EnableRingController); + timeout_timer->setSingleShot(true); connect(timeout_timer.get(), &QTimer::timeout, [this] { SetPollingResult({}, true); }); @@ -202,7 +214,14 @@ ConfigureRingController::ConfigureRingController(QWidget* parent, } ConfigureRingController::~ConfigureRingController() { - emulated_device->DisableConfiguration(); + emulated_controller->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex, + Common::Input::PollingMode::Active); + emulated_controller->DisableConfiguration(); + + if (is_controller_set) { + emulated_controller->DeleteCallback(callback_key); + is_controller_set = false; + } }; void ConfigureRingController::changeEvent(QEvent* event) { @@ -219,7 +238,7 @@ void ConfigureRingController::RetranslateUI() { void ConfigureRingController::UpdateUI() { RetranslateUI(); - const Common::ParamPackage param = emulated_device->GetRingParam(); + const Common::ParamPackage param = emulated_controller->GetRingParam(); for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) { auto* const analog_button = analog_map_buttons[sub_button_id]; @@ -240,9 +259,9 @@ void ConfigureRingController::UpdateUI() { } void ConfigureRingController::ApplyConfiguration() { - emulated_device->DisableConfiguration(); - emulated_device->SaveCurrentConfig(); - emulated_device->EnableConfiguration(); + emulated_controller->DisableConfiguration(); + emulated_controller->SaveCurrentConfig(); + emulated_controller->EnableConfiguration(); } void ConfigureRingController::LoadConfiguration() { @@ -252,10 +271,62 @@ void ConfigureRingController::LoadConfiguration() { void ConfigureRingController::RestoreDefaults() { const std::string default_ring_string = InputCommon::GenerateAnalogParamFromKeys( 0, 0, Config::default_ringcon_analogs[0], Config::default_ringcon_analogs[1], 0, 0.05f); - emulated_device->SetRingParam(Common::ParamPackage(default_ring_string)); + emulated_controller->SetRingParam(Common::ParamPackage(default_ring_string)); UpdateUI(); } +void ConfigureRingController::EnableRingController() { + const auto dialog_title = tr("Error enabling ring input"); + + is_ring_enabled = false; + ui->ring_controller_sensor_value->setText(tr("Not connected")); + + if (!Settings::values.enable_joycon_driver) { + QMessageBox::warning(this, dialog_title, tr("Direct Joycon driver is not enabled")); + return; + } + + ui->enable_ring_controller_button->setEnabled(false); + ui->enable_ring_controller_button->setText(tr("Configuring")); + // SetPollingMode is blocking. Allow to update the button status before calling the command + repaint(); + + const auto result = emulated_controller->SetPollingMode( + Core::HID::EmulatedDeviceIndex::RightIndex, Common::Input::PollingMode::Ring); + switch (result) { + case Common::Input::DriverResult::Success: + is_ring_enabled = true; + break; + case Common::Input::DriverResult::NotSupported: + QMessageBox::warning(this, dialog_title, + tr("The current mapped device doesn't support the ring controller")); + break; + case Common::Input::DriverResult::NoDeviceDetected: + QMessageBox::warning(this, dialog_title, + tr("The current mapped device doesn't have a ring attached")); + break; + default: + QMessageBox::warning(this, dialog_title, + tr("Unexpected driver result %1").arg(static_cast(result))); + break; + } + ui->enable_ring_controller_button->setEnabled(true); + ui->enable_ring_controller_button->setText(tr("Enable")); +} + +void ConfigureRingController::ControllerUpdate(Core::HID::ControllerTriggerType type) { + if (!is_ring_enabled) { + return; + } + if (type != Core::HID::ControllerTriggerType::RingController) { + return; + } + + const auto value = emulated_controller->GetRingSensorValues(); + const auto tex_value = QString::fromStdString(fmt::format("{:.3f}", value.raw_value)); + ui->ring_controller_sensor_value->setText(tex_value); +} + void ConfigureRingController::HandleClick( QPushButton* button, std::function new_input_setter, InputCommon::Polling::InputType type) { diff --git a/src/yuzu/configuration/configure_ringcon.h b/src/yuzu/configuration/configure_ringcon.h index 38a9cb7166..b23c279061 100644 --- a/src/yuzu/configuration/configure_ringcon.h +++ b/src/yuzu/configuration/configure_ringcon.h @@ -13,7 +13,7 @@ class InputSubsystem; namespace Core::HID { class HIDCore; -class EmulatedDevices; +class EmulatedController; } // namespace Core::HID namespace Ui { @@ -42,6 +42,12 @@ private: /// Restore all buttons to their default values. void RestoreDefaults(); + /// Sets current polling mode to ring input + void EnableRingController(); + + // Handles emulated controller events + void ControllerUpdate(Core::HID::ControllerTriggerType type); + /// Called when the button was pressed. void HandleClick(QPushButton* button, std::function new_input_setter, @@ -78,7 +84,11 @@ private: std::optional> input_setter; InputCommon::InputSubsystem* input_subsystem; - Core::HID::EmulatedDevices* emulated_device; + Core::HID::EmulatedController* emulated_controller; + + bool is_ring_enabled{}; + bool is_controller_set{}; + int callback_key; std::unique_ptr ui; }; diff --git a/src/yuzu/configuration/configure_ringcon.ui b/src/yuzu/configuration/configure_ringcon.ui index 9ec634dd49..514dff372a 100644 --- a/src/yuzu/configuration/configure_ringcon.ui +++ b/src/yuzu/configuration/configure_ringcon.ui @@ -6,8 +6,8 @@ 0 0 - 298 - 339 + 315 + 400 @@ -46,187 +46,283 @@ - - - - Ring Sensor Parameters - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - - 0 - - - QLayout::SetDefaultConstraint - - - 3 - - - 6 - - - 3 - - - 0 - - - + + + + Virtual Ring Sensor Parameters + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + - 3 - - - - - Pull - - - Qt::AlignCenter - - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - - - - 68 - 0 - - - - - 68 - 16777215 - - - - min-width: 68px; - - - Pull - - - - - - - - - - Push - - - Qt::AlignCenter - - - - 3 - - - 3 - - - 3 - - - 3 - - - 3 - - - - - - 68 - 0 - - - - - 68 - 16777215 - - - - min-width: 68px; - - - Push - - - - - - - - - - - - 3 + 0 - QLayout::SetDefaultConstraint + QLayout::SetDefaultConstraint - 0 + 3 - 10 + 6 - 0 + 3 - 3 + 0 - - - - - Deadzone: 0% + + + 3 + + + + + Pull - Qt::AlignHCenter + Qt::AlignCenter - + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 70 + 0 + + + + + 68 + 16777215 + + + + min-width: 68px; + + + Pull + + + + + - + + + + Push + + + Qt::AlignCenter + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + 70 + 0 + + + + + 68 + 16777215 + + + + min-width: 68px; + + + Push + + + + + + + - - - 100 + + + 3 - - Qt::Horizontal + + QLayout::SetDefaultConstraint - + + 0 + + + 10 + + + 0 + + + 3 + + + + + + + Deadzone: 0% + + + Qt::AlignHCenter + + + + + + + + + 100 + + + Qt::Horizontal + + + + - - - - - + + + + + + + Direct Joycon Driver + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + 0 + + + QLayout::SetDefaultConstraint + + + 3 + + + 6 + + + 3 + + + 10 + + + + + 10 + + + 6 + + + 10 + + + 10 + + + 10 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 76 + 20 + + + + + + + + Enable Ring Input + + + + + + + Enable + + + + + + + Ring Sensor Value + + + + + + + Not connected + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + @@ -273,6 +369,6 @@ rejected() ConfigureRingController reject() - +