mirror of
https://github.com/pkgforge-community/git.citron-emu.org-Citron-Citron.git
synced 2026-04-06 19:08:46 -04:00
Android: Implement TLB optimization to prevent deadlocks and improve performance
This commit is contained in:
committed by
github-actions[bot]
parent
452e20baff
commit
85b9b9442c
102
src/input_common/CMakeLists.txt
Normal file
102
src/input_common/CMakeLists.txt
Normal file
@@ -0,0 +1,102 @@
|
||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
add_library(input_common STATIC
|
||||
drivers/camera.cpp
|
||||
drivers/camera.h
|
||||
drivers/keyboard.cpp
|
||||
drivers/keyboard.h
|
||||
drivers/mouse.cpp
|
||||
drivers/mouse.h
|
||||
drivers/tas_input.cpp
|
||||
drivers/tas_input.h
|
||||
drivers/touch_screen.cpp
|
||||
drivers/touch_screen.h
|
||||
drivers/udp_client.cpp
|
||||
drivers/udp_client.h
|
||||
drivers/virtual_amiibo.cpp
|
||||
drivers/virtual_amiibo.h
|
||||
drivers/virtual_gamepad.cpp
|
||||
drivers/virtual_gamepad.h
|
||||
helpers/stick_from_buttons.cpp
|
||||
helpers/stick_from_buttons.h
|
||||
helpers/touch_from_buttons.cpp
|
||||
helpers/touch_from_buttons.h
|
||||
helpers/udp_protocol.cpp
|
||||
helpers/udp_protocol.h
|
||||
input_engine.cpp
|
||||
input_engine.h
|
||||
input_mapping.cpp
|
||||
input_mapping.h
|
||||
input_poller.cpp
|
||||
input_poller.h
|
||||
main.cpp
|
||||
main.h
|
||||
precompiled_headers.h
|
||||
)
|
||||
|
||||
if (MSVC)
|
||||
target_compile_options(input_common PRIVATE
|
||||
/we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data
|
||||
/we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data
|
||||
/we4800 # Implicit conversion from 'type' to bool. Possible information loss
|
||||
)
|
||||
else()
|
||||
target_compile_options(input_common PRIVATE
|
||||
-Werror=conversion
|
||||
)
|
||||
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)
|
||||
endif()
|
||||
|
||||
if (ENABLE_LIBUSB)
|
||||
target_sources(input_common PRIVATE
|
||||
drivers/gc_adapter.cpp
|
||||
drivers/gc_adapter.h
|
||||
)
|
||||
target_link_libraries(input_common PRIVATE libusb::usb)
|
||||
target_compile_definitions(input_common PRIVATE HAVE_LIBUSB)
|
||||
endif()
|
||||
|
||||
create_target_directory_groups(input_common)
|
||||
target_link_libraries(input_common PUBLIC hid_core PRIVATE common Boost::headers)
|
||||
|
||||
if (CITRON_USE_PRECOMPILED_HEADERS)
|
||||
target_precompile_headers(input_common PRIVATE precompiled_headers.h)
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
target_sources(input_common PRIVATE
|
||||
drivers/android.cpp
|
||||
drivers/android.h
|
||||
)
|
||||
target_link_libraries(input_common PRIVATE android)
|
||||
endif()
|
||||
367
src/input_common/drivers/android.cpp
Normal file
367
src/input_common/drivers/android.cpp
Normal file
@@ -0,0 +1,367 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <set>
|
||||
#include <common/settings_input.h>
|
||||
#include <common/thread.h>
|
||||
#include <jni.h>
|
||||
#include "common/android/android_common.h"
|
||||
#include "common/android/id_cache.h"
|
||||
#include "input_common/drivers/android.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
Android::Android(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
vibration_thread = std::jthread([this](std::stop_token token) {
|
||||
Common::SetCurrentThreadName("Android_Vibration");
|
||||
auto env = Common::Android::GetEnvForThread();
|
||||
using namespace std::chrono_literals;
|
||||
while (!token.stop_requested()) {
|
||||
SendVibrations(env, token);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Android::~Android() = default;
|
||||
|
||||
void Android::RegisterController(jobject j_input_device) {
|
||||
auto env = Common::Android::GetEnvForThread();
|
||||
const std::string guid = Common::Android::GetJString(
|
||||
env, static_cast<jstring>(
|
||||
env->CallObjectMethod(j_input_device, Common::Android::GetCitronDeviceGetGUID())));
|
||||
const s32 port = env->CallIntMethod(j_input_device, Common::Android::GetCitronDeviceGetPort());
|
||||
const auto identifier = GetIdentifier(guid, static_cast<size_t>(port));
|
||||
PreSetController(identifier);
|
||||
|
||||
if (input_devices.find(identifier) != input_devices.end()) {
|
||||
env->DeleteGlobalRef(input_devices[identifier]);
|
||||
}
|
||||
auto new_device = env->NewGlobalRef(j_input_device);
|
||||
input_devices[identifier] = new_device;
|
||||
}
|
||||
|
||||
void Android::SetButtonState(std::string guid, size_t port, int button_id, bool value) {
|
||||
const auto identifier = GetIdentifier(guid, port);
|
||||
SetButton(identifier, button_id, value);
|
||||
}
|
||||
|
||||
void Android::SetAxisPosition(std::string guid, size_t port, int axis_id, float value) {
|
||||
const auto identifier = GetIdentifier(guid, port);
|
||||
SetAxis(identifier, axis_id, value);
|
||||
}
|
||||
|
||||
void Android::SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
|
||||
float gyro_y, float gyro_z, float accel_x, float accel_y,
|
||||
float accel_z) {
|
||||
const auto identifier = GetIdentifier(guid, port);
|
||||
const BasicMotion motion_data{
|
||||
.gyro_x = gyro_x,
|
||||
.gyro_y = gyro_y,
|
||||
.gyro_z = gyro_z,
|
||||
.accel_x = accel_x,
|
||||
.accel_y = accel_y,
|
||||
.accel_z = accel_z,
|
||||
.delta_timestamp = delta_timestamp,
|
||||
};
|
||||
SetMotion(identifier, 0, motion_data);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult Android::SetVibration(
|
||||
[[maybe_unused]] const PadIdentifier& identifier,
|
||||
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
|
||||
vibration_queue.Push(VibrationRequest{
|
||||
.identifier = identifier,
|
||||
.vibration = vibration,
|
||||
});
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
bool Android::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
|
||||
auto device = input_devices.find(identifier);
|
||||
if (device != input_devices.end()) {
|
||||
return Common::Android::RunJNIOnFiber<bool>([&](JNIEnv* env) {
|
||||
return static_cast<bool>(env->CallBooleanMethod(
|
||||
device->second, Common::Android::GetCitronDeviceGetSupportsVibration()));
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> Android::GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices;
|
||||
auto env = Common::Android::GetEnvForThread();
|
||||
for (const auto& [key, value] : input_devices) {
|
||||
auto name_object = static_cast<jstring>(
|
||||
env->CallObjectMethod(value, Common::Android::GetCitronDeviceGetName()));
|
||||
const std::string name =
|
||||
fmt::format("{} {}", Common::Android::GetJString(env, name_object), key.port);
|
||||
devices.emplace_back(Common::ParamPackage{
|
||||
{"engine", GetEngineName()},
|
||||
{"display", std::move(name)},
|
||||
{"guid", key.guid.RawString()},
|
||||
{"port", std::to_string(key.port)},
|
||||
});
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
std::set<s32> Android::GetDeviceAxes(JNIEnv* env, jobject& j_device) const {
|
||||
auto j_axes = static_cast<jobjectArray>(
|
||||
env->CallObjectMethod(j_device, Common::Android::GetCitronDeviceGetAxes()));
|
||||
std::set<s32> axes;
|
||||
for (int i = 0; i < env->GetArrayLength(j_axes); ++i) {
|
||||
jobject axis = env->GetObjectArrayElement(j_axes, i);
|
||||
axes.insert(env->GetIntField(axis, Common::Android::GetIntegerValueField()));
|
||||
}
|
||||
return axes;
|
||||
}
|
||||
|
||||
Common::ParamPackage Android::BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
|
||||
int axis_y) const {
|
||||
Common::ParamPackage params;
|
||||
params.Set("engine", GetEngineName());
|
||||
params.Set("port", static_cast<int>(identifier.port));
|
||||
params.Set("guid", identifier.guid.RawString());
|
||||
params.Set("axis_x", axis_x);
|
||||
params.Set("axis_y", axis_y);
|
||||
params.Set("offset_x", 0);
|
||||
params.Set("offset_y", 0);
|
||||
params.Set("invert_x", "+");
|
||||
|
||||
// Invert Y-Axis by default
|
||||
params.Set("invert_y", "-");
|
||||
return params;
|
||||
}
|
||||
|
||||
Common::ParamPackage Android::BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
|
||||
bool invert) const {
|
||||
Common::ParamPackage params{};
|
||||
params.Set("engine", GetEngineName());
|
||||
params.Set("port", static_cast<int>(identifier.port));
|
||||
params.Set("guid", identifier.guid.RawString());
|
||||
params.Set("axis", axis);
|
||||
params.Set("threshold", "0.5");
|
||||
params.Set("invert", invert ? "-" : "+");
|
||||
return params;
|
||||
}
|
||||
|
||||
Common::ParamPackage Android::BuildButtonParamPackageForButton(PadIdentifier identifier,
|
||||
s32 button) const {
|
||||
Common::ParamPackage params{};
|
||||
params.Set("engine", GetEngineName());
|
||||
params.Set("port", static_cast<int>(identifier.port));
|
||||
params.Set("guid", identifier.guid.RawString());
|
||||
params.Set("button", button);
|
||||
return params;
|
||||
}
|
||||
|
||||
bool Android::MatchVID(Common::UUID device, const std::vector<std::string>& vids) const {
|
||||
for (size_t i = 0; i < vids.size(); ++i) {
|
||||
auto fucker = device.RawString();
|
||||
if (fucker.find(vids[i]) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
AnalogMapping Android::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("guid") || !params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto identifier =
|
||||
GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
|
||||
auto& j_device = input_devices[identifier];
|
||||
if (j_device == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto env = Common::Android::GetEnvForThread();
|
||||
std::set<s32> axes = GetDeviceAxes(env, j_device);
|
||||
if (axes.size() == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
AnalogMapping mapping = {};
|
||||
if (axes.find(AXIS_X) != axes.end() && axes.find(AXIS_Y) != axes.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::LStick,
|
||||
BuildParamPackageForAnalog(identifier, AXIS_X, AXIS_Y));
|
||||
}
|
||||
|
||||
if (axes.find(AXIS_RX) != axes.end() && axes.find(AXIS_RY) != axes.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
|
||||
BuildParamPackageForAnalog(identifier, AXIS_RX, AXIS_RY));
|
||||
} else if (axes.find(AXIS_Z) != axes.end() && axes.find(AXIS_RZ) != axes.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick,
|
||||
BuildParamPackageForAnalog(identifier, AXIS_Z, AXIS_RZ));
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
ButtonMapping Android::GetButtonMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("guid") || !params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto identifier =
|
||||
GetIdentifier(params.Get("guid", ""), static_cast<size_t>(params.Get("port", 0)));
|
||||
auto& j_device = input_devices[identifier];
|
||||
if (j_device == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto env = Common::Android::GetEnvForThread();
|
||||
jintArray j_keys = env->NewIntArray(static_cast<int>(keycode_ids.size()));
|
||||
env->SetIntArrayRegion(j_keys, 0, static_cast<int>(keycode_ids.size()), keycode_ids.data());
|
||||
auto j_has_keys_object = static_cast<jbooleanArray>(
|
||||
env->CallObjectMethod(j_device, Common::Android::GetCitronDeviceHasKeys(), j_keys));
|
||||
jboolean isCopy = false;
|
||||
jboolean* j_has_keys = env->GetBooleanArrayElements(j_has_keys_object, &isCopy);
|
||||
|
||||
std::set<s32> available_keys;
|
||||
for (size_t i = 0; i < keycode_ids.size(); ++i) {
|
||||
if (j_has_keys[i]) {
|
||||
available_keys.insert(keycode_ids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Some devices use axes instead of buttons for certain controls so we need all the axes here
|
||||
std::set<s32> axes = GetDeviceAxes(env, j_device);
|
||||
|
||||
ButtonMapping mapping = {};
|
||||
if (axes.find(AXIS_HAT_X) != axes.end() && axes.find(AXIS_HAT_Y) != axes.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::DUp,
|
||||
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, true));
|
||||
mapping.insert_or_assign(Settings::NativeButton::DDown,
|
||||
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_Y, false));
|
||||
mapping.insert_or_assign(Settings::NativeButton::DLeft,
|
||||
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, true));
|
||||
mapping.insert_or_assign(Settings::NativeButton::DRight,
|
||||
BuildAnalogParamPackageForButton(identifier, AXIS_HAT_X, false));
|
||||
} else if (available_keys.find(KEYCODE_DPAD_UP) != available_keys.end() &&
|
||||
available_keys.find(KEYCODE_DPAD_DOWN) != available_keys.end() &&
|
||||
available_keys.find(KEYCODE_DPAD_LEFT) != available_keys.end() &&
|
||||
available_keys.find(KEYCODE_DPAD_RIGHT) != available_keys.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::DUp,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_UP));
|
||||
mapping.insert_or_assign(Settings::NativeButton::DDown,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_DOWN));
|
||||
mapping.insert_or_assign(Settings::NativeButton::DLeft,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_LEFT));
|
||||
mapping.insert_or_assign(Settings::NativeButton::DRight,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_DPAD_RIGHT));
|
||||
}
|
||||
|
||||
if (axes.find(AXIS_LTRIGGER) != axes.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::ZL, BuildAnalogParamPackageForButton(
|
||||
identifier, AXIS_LTRIGGER, false));
|
||||
} else if (available_keys.find(KEYCODE_BUTTON_L2) != available_keys.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::ZL,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L2));
|
||||
}
|
||||
|
||||
if (axes.find(AXIS_RTRIGGER) != axes.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::ZR, BuildAnalogParamPackageForButton(
|
||||
identifier, AXIS_RTRIGGER, false));
|
||||
} else if (available_keys.find(KEYCODE_BUTTON_R2) != available_keys.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::ZR,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R2));
|
||||
}
|
||||
|
||||
if (available_keys.find(KEYCODE_BUTTON_A) != available_keys.end()) {
|
||||
if (MatchVID(identifier.guid, flipped_ab_vids)) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_A));
|
||||
} else {
|
||||
mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_A));
|
||||
}
|
||||
}
|
||||
if (available_keys.find(KEYCODE_BUTTON_B) != available_keys.end()) {
|
||||
if (MatchVID(identifier.guid, flipped_ab_vids)) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::A, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_B));
|
||||
} else {
|
||||
mapping.insert_or_assign(Settings::NativeButton::B, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_B));
|
||||
}
|
||||
}
|
||||
if (available_keys.find(KEYCODE_BUTTON_X) != available_keys.end()) {
|
||||
if (MatchVID(identifier.guid, flipped_xy_vids)) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_X));
|
||||
} else {
|
||||
mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_X));
|
||||
}
|
||||
}
|
||||
if (available_keys.find(KEYCODE_BUTTON_Y) != available_keys.end()) {
|
||||
if (MatchVID(identifier.guid, flipped_xy_vids)) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::X, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_Y));
|
||||
} else {
|
||||
mapping.insert_or_assign(Settings::NativeButton::Y, BuildButtonParamPackageForButton(
|
||||
identifier, KEYCODE_BUTTON_Y));
|
||||
}
|
||||
}
|
||||
|
||||
if (available_keys.find(KEYCODE_BUTTON_L1) != available_keys.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::L,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_L1));
|
||||
}
|
||||
if (available_keys.find(KEYCODE_BUTTON_R1) != available_keys.end()) {
|
||||
mapping.insert_or_assign(Settings::NativeButton::R,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_R1));
|
||||
}
|
||||
|
||||
if (available_keys.find(KEYCODE_BUTTON_THUMBL) != available_keys.end()) {
|
||||
mapping.insert_or_assign(
|
||||
Settings::NativeButton::LStick,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBL));
|
||||
}
|
||||
if (available_keys.find(KEYCODE_BUTTON_THUMBR) != available_keys.end()) {
|
||||
mapping.insert_or_assign(
|
||||
Settings::NativeButton::RStick,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_THUMBR));
|
||||
}
|
||||
|
||||
if (available_keys.find(KEYCODE_BUTTON_START) != available_keys.end()) {
|
||||
mapping.insert_or_assign(
|
||||
Settings::NativeButton::Plus,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_START));
|
||||
}
|
||||
if (available_keys.find(KEYCODE_BUTTON_SELECT) != available_keys.end()) {
|
||||
mapping.insert_or_assign(
|
||||
Settings::NativeButton::Minus,
|
||||
BuildButtonParamPackageForButton(identifier, KEYCODE_BUTTON_SELECT));
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames Android::GetUIName(
|
||||
[[maybe_unused]] const Common::ParamPackage& params) const {
|
||||
return Common::Input::ButtonNames::Value;
|
||||
}
|
||||
|
||||
PadIdentifier Android::GetIdentifier(const std::string& guid, size_t port) const {
|
||||
return {
|
||||
.guid = Common::UUID{guid},
|
||||
.port = port,
|
||||
.pad = 0,
|
||||
};
|
||||
}
|
||||
|
||||
void Android::SendVibrations(JNIEnv* env, std::stop_token token) {
|
||||
VibrationRequest request = vibration_queue.PopWait(token);
|
||||
auto device = input_devices.find(request.identifier);
|
||||
if (device != input_devices.end()) {
|
||||
float average_intensity = static_cast<float>(
|
||||
(request.vibration.high_amplitude + request.vibration.low_amplitude) / 2.0);
|
||||
env->CallVoidMethod(device->second, Common::Android::GetCitronDeviceVibrate(),
|
||||
average_intensity);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
148
src/input_common/drivers/android.h
Normal file
148
src/input_common/drivers/android.h
Normal file
@@ -0,0 +1,148 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <common/threadsafe_queue.h>
|
||||
#include <jni.h>
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* A virtual controller that is always assigned to the game input
|
||||
*/
|
||||
class Android final : public InputEngine {
|
||||
public:
|
||||
explicit Android(std::string input_engine_);
|
||||
|
||||
~Android() override;
|
||||
|
||||
/**
|
||||
* Registers controller number to accept new inputs.
|
||||
* @param j_input_device CitronInputDevice object from the Android frontend to register.
|
||||
*/
|
||||
void RegisterController(jobject j_input_device);
|
||||
|
||||
/**
|
||||
* Sets the status of a button on a specific controller.
|
||||
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
||||
* @param port Port determined by controller connection order.
|
||||
* @param button_id The Android Keycode corresponding to this event.
|
||||
* @param value Whether the button is pressed or not.
|
||||
*/
|
||||
void SetButtonState(std::string guid, size_t port, int button_id, bool value);
|
||||
|
||||
/**
|
||||
* Sets the status of an axis on a specific controller.
|
||||
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
||||
* @param port Port determined by controller connection order.
|
||||
* @param axis_id The Android axis ID corresponding to this event.
|
||||
* @param value Value along the given axis.
|
||||
*/
|
||||
void SetAxisPosition(std::string guid, size_t port, int axis_id, float value);
|
||||
|
||||
/**
|
||||
* Sets the status of the motion sensor on a specific controller
|
||||
* @param guid 32 character hexadecimal string consisting of the controller's PID+VID.
|
||||
* @param port Port determined by controller connection order.
|
||||
* @param delta_timestamp Time passed since the last read.
|
||||
* @param gyro_x,gyro_y,gyro_z Gyro sensor readings.
|
||||
* @param accel_x,accel_y,accel_z Accelerometer sensor readings.
|
||||
*/
|
||||
void SetMotionState(std::string guid, size_t port, u64 delta_timestamp, float gyro_x,
|
||||
float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
|
||||
|
||||
Common::Input::DriverResult SetVibration(
|
||||
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
|
||||
|
||||
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
|
||||
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
|
||||
/**
|
||||
* Gets the axes reported by the CitronInputDevice.
|
||||
* @param env JNI environment pointer.
|
||||
* @param j_device CitronInputDevice from the Android frontend.
|
||||
* @return Set of the axes reported by the underlying Android InputDevice
|
||||
*/
|
||||
std::set<s32> GetDeviceAxes(JNIEnv* env, jobject& j_device) const;
|
||||
|
||||
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
|
||||
int axis_y) const;
|
||||
|
||||
Common::ParamPackage BuildAnalogParamPackageForButton(PadIdentifier identifier, s32 axis,
|
||||
bool invert) const;
|
||||
|
||||
Common::ParamPackage BuildButtonParamPackageForButton(PadIdentifier identifier,
|
||||
s32 button) const;
|
||||
|
||||
bool MatchVID(Common::UUID device, const std::vector<std::string>& vids) const;
|
||||
|
||||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
|
||||
|
||||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
|
||||
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
private:
|
||||
std::unordered_map<PadIdentifier, jobject> input_devices;
|
||||
|
||||
/// Returns the correct identifier corresponding to the player index
|
||||
PadIdentifier GetIdentifier(const std::string& guid, size_t port) const;
|
||||
|
||||
/// Takes all vibrations from the queue and sends the command to the controller
|
||||
void SendVibrations(JNIEnv* env, std::stop_token token);
|
||||
|
||||
static constexpr s32 AXIS_X = 0;
|
||||
static constexpr s32 AXIS_Y = 1;
|
||||
static constexpr s32 AXIS_Z = 11;
|
||||
static constexpr s32 AXIS_RX = 12;
|
||||
static constexpr s32 AXIS_RY = 13;
|
||||
static constexpr s32 AXIS_RZ = 14;
|
||||
static constexpr s32 AXIS_HAT_X = 15;
|
||||
static constexpr s32 AXIS_HAT_Y = 16;
|
||||
static constexpr s32 AXIS_LTRIGGER = 17;
|
||||
static constexpr s32 AXIS_RTRIGGER = 18;
|
||||
|
||||
static constexpr s32 KEYCODE_DPAD_UP = 19;
|
||||
static constexpr s32 KEYCODE_DPAD_DOWN = 20;
|
||||
static constexpr s32 KEYCODE_DPAD_LEFT = 21;
|
||||
static constexpr s32 KEYCODE_DPAD_RIGHT = 22;
|
||||
static constexpr s32 KEYCODE_BUTTON_A = 96;
|
||||
static constexpr s32 KEYCODE_BUTTON_B = 97;
|
||||
static constexpr s32 KEYCODE_BUTTON_X = 99;
|
||||
static constexpr s32 KEYCODE_BUTTON_Y = 100;
|
||||
static constexpr s32 KEYCODE_BUTTON_L1 = 102;
|
||||
static constexpr s32 KEYCODE_BUTTON_R1 = 103;
|
||||
static constexpr s32 KEYCODE_BUTTON_L2 = 104;
|
||||
static constexpr s32 KEYCODE_BUTTON_R2 = 105;
|
||||
static constexpr s32 KEYCODE_BUTTON_THUMBL = 106;
|
||||
static constexpr s32 KEYCODE_BUTTON_THUMBR = 107;
|
||||
static constexpr s32 KEYCODE_BUTTON_START = 108;
|
||||
static constexpr s32 KEYCODE_BUTTON_SELECT = 109;
|
||||
const std::vector<s32> keycode_ids{
|
||||
KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT,
|
||||
KEYCODE_BUTTON_A, KEYCODE_BUTTON_B, KEYCODE_BUTTON_X, KEYCODE_BUTTON_Y,
|
||||
KEYCODE_BUTTON_L1, KEYCODE_BUTTON_R1, KEYCODE_BUTTON_L2, KEYCODE_BUTTON_R2,
|
||||
KEYCODE_BUTTON_THUMBL, KEYCODE_BUTTON_THUMBR, KEYCODE_BUTTON_START, KEYCODE_BUTTON_SELECT,
|
||||
};
|
||||
|
||||
const std::string sony_vid{"054c"};
|
||||
const std::string nintendo_vid{"057e"};
|
||||
const std::string razer_vid{"1532"};
|
||||
const std::string redmagic_vid{"3537"};
|
||||
const std::string backbone_labs_vid{"358a"};
|
||||
const std::string xbox_vid{"045e"};
|
||||
const std::vector<std::string> flipped_ab_vids{sony_vid, nintendo_vid, razer_vid,
|
||||
redmagic_vid, backbone_labs_vid, xbox_vid};
|
||||
const std::vector<std::string> flipped_xy_vids{sony_vid, razer_vid, redmagic_vid,
|
||||
backbone_labs_vid, xbox_vid};
|
||||
|
||||
/// Queue of vibration request to controllers
|
||||
Common::SPSCQueue<VibrationRequest> vibration_queue;
|
||||
std::jthread vibration_thread;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
82
src/input_common/drivers/camera.cpp
Normal file
82
src/input_common/drivers/camera.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "input_common/drivers/camera.h"
|
||||
|
||||
namespace InputCommon {
|
||||
constexpr PadIdentifier identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
Camera::Camera(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
PreSetController(identifier);
|
||||
}
|
||||
|
||||
void Camera::SetCameraData(std::size_t width, std::size_t height, std::span<const u32> data) {
|
||||
const std::size_t desired_width = getImageWidth();
|
||||
const std::size_t desired_height = getImageHeight();
|
||||
status.data.resize(desired_width * desired_height);
|
||||
|
||||
// Resize image to desired format
|
||||
for (std::size_t y = 0; y < desired_height; y++) {
|
||||
for (std::size_t x = 0; x < desired_width; x++) {
|
||||
const std::size_t pixel_index = y * desired_width + x;
|
||||
const std::size_t old_x = width * x / desired_width;
|
||||
const std::size_t old_y = height * y / desired_height;
|
||||
const std::size_t data_pixel_index = old_y * width + old_x;
|
||||
status.data[pixel_index] = static_cast<u8>(data[data_pixel_index] & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
SetCamera(identifier, status);
|
||||
}
|
||||
|
||||
std::size_t Camera::getImageWidth() const {
|
||||
switch (status.format) {
|
||||
case Common::Input::CameraFormat::Size320x240:
|
||||
return 320;
|
||||
case Common::Input::CameraFormat::Size160x120:
|
||||
return 160;
|
||||
case Common::Input::CameraFormat::Size80x60:
|
||||
return 80;
|
||||
case Common::Input::CameraFormat::Size40x30:
|
||||
return 40;
|
||||
case Common::Input::CameraFormat::Size20x15:
|
||||
return 20;
|
||||
case Common::Input::CameraFormat::None:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t Camera::getImageHeight() const {
|
||||
switch (status.format) {
|
||||
case Common::Input::CameraFormat::Size320x240:
|
||||
return 240;
|
||||
case Common::Input::CameraFormat::Size160x120:
|
||||
return 120;
|
||||
case Common::Input::CameraFormat::Size80x60:
|
||||
return 60;
|
||||
case Common::Input::CameraFormat::Size40x30:
|
||||
return 30;
|
||||
case Common::Input::CameraFormat::Size20x15:
|
||||
return 15;
|
||||
case Common::Input::CameraFormat::None:
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::DriverResult Camera::SetCameraFormat(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_,
|
||||
const Common::Input::CameraFormat camera_format) {
|
||||
status.format = camera_format;
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
32
src/input_common/drivers/camera.h
Normal file
32
src/input_common/drivers/camera.h
Normal file
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* A button device factory representing a keyboard. It receives keyboard events and forward them
|
||||
* to all button devices it created.
|
||||
*/
|
||||
class Camera final : public InputEngine {
|
||||
public:
|
||||
explicit Camera(std::string input_engine_);
|
||||
|
||||
void SetCameraData(std::size_t width, std::size_t height, std::span<const u32> data);
|
||||
|
||||
std::size_t getImageWidth() const;
|
||||
std::size_t getImageHeight() const;
|
||||
|
||||
Common::Input::DriverResult SetCameraFormat(const PadIdentifier& identifier_,
|
||||
Common::Input::CameraFormat camera_format) override;
|
||||
|
||||
private:
|
||||
Common::Input::CameraStatus status{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
548
src/input_common/drivers/gc_adapter.cpp
Normal file
548
src/input_common/drivers/gc_adapter.cpp
Normal file
@@ -0,0 +1,548 @@
|
||||
// SPDX-FileCopyrightText: 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
#include <libusb.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/param_package.h"
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "common/settings_input.h"
|
||||
#include "common/thread.h"
|
||||
#include "input_common/drivers/gc_adapter.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
class LibUSBContext {
|
||||
public:
|
||||
explicit LibUSBContext() {
|
||||
init_result = libusb_init(&ctx);
|
||||
}
|
||||
|
||||
~LibUSBContext() {
|
||||
libusb_exit(ctx);
|
||||
}
|
||||
|
||||
LibUSBContext& operator=(const LibUSBContext&) = delete;
|
||||
LibUSBContext(const LibUSBContext&) = delete;
|
||||
|
||||
LibUSBContext& operator=(LibUSBContext&&) noexcept = delete;
|
||||
LibUSBContext(LibUSBContext&&) noexcept = delete;
|
||||
|
||||
[[nodiscard]] int InitResult() const noexcept {
|
||||
return init_result;
|
||||
}
|
||||
|
||||
[[nodiscard]] libusb_context* get() noexcept {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private:
|
||||
libusb_context* ctx;
|
||||
int init_result{};
|
||||
};
|
||||
|
||||
class LibUSBDeviceHandle {
|
||||
public:
|
||||
explicit LibUSBDeviceHandle(libusb_context* ctx, uint16_t vid, uint16_t pid) noexcept {
|
||||
handle = libusb_open_device_with_vid_pid(ctx, vid, pid);
|
||||
}
|
||||
|
||||
~LibUSBDeviceHandle() noexcept {
|
||||
if (handle) {
|
||||
libusb_release_interface(handle, 1);
|
||||
libusb_close(handle);
|
||||
}
|
||||
}
|
||||
|
||||
LibUSBDeviceHandle& operator=(const LibUSBDeviceHandle&) = delete;
|
||||
LibUSBDeviceHandle(const LibUSBDeviceHandle&) = delete;
|
||||
|
||||
LibUSBDeviceHandle& operator=(LibUSBDeviceHandle&&) noexcept = delete;
|
||||
LibUSBDeviceHandle(LibUSBDeviceHandle&&) noexcept = delete;
|
||||
|
||||
[[nodiscard]] libusb_device_handle* get() noexcept {
|
||||
return handle;
|
||||
}
|
||||
|
||||
private:
|
||||
libusb_device_handle* handle{};
|
||||
};
|
||||
|
||||
GCAdapter::GCAdapter(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
if (usb_adapter_handle) {
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG(Input, "Initialization started");
|
||||
|
||||
libusb_ctx = std::make_unique<LibUSBContext>();
|
||||
const int init_res = libusb_ctx->InitResult();
|
||||
if (init_res == LIBUSB_SUCCESS) {
|
||||
adapter_scan_thread =
|
||||
std::jthread([this](std::stop_token stop_token) { AdapterScanThread(stop_token); });
|
||||
} else {
|
||||
LOG_ERROR(Input, "libusb could not be initialized. failed with error = {}", init_res);
|
||||
}
|
||||
}
|
||||
|
||||
GCAdapter::~GCAdapter() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
void GCAdapter::AdapterInputThread(std::stop_token stop_token) {
|
||||
LOG_DEBUG(Input, "Input thread started");
|
||||
Common::SetCurrentThreadName("GCAdapter");
|
||||
s32 payload_size{};
|
||||
AdapterPayload adapter_payload{};
|
||||
|
||||
adapter_scan_thread = {};
|
||||
|
||||
while (!stop_token.stop_requested()) {
|
||||
libusb_interrupt_transfer(usb_adapter_handle->get(), input_endpoint, adapter_payload.data(),
|
||||
static_cast<s32>(adapter_payload.size()), &payload_size, 16);
|
||||
if (IsPayloadCorrect(adapter_payload, payload_size)) {
|
||||
UpdateControllers(adapter_payload);
|
||||
UpdateVibrations();
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
if (restart_scan_thread) {
|
||||
adapter_scan_thread =
|
||||
std::jthread([this](std::stop_token token) { AdapterScanThread(token); });
|
||||
restart_scan_thread = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GCAdapter::IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size) {
|
||||
if (payload_size != static_cast<s32>(adapter_payload.size()) ||
|
||||
adapter_payload[0] != LIBUSB_DT_HID) {
|
||||
LOG_DEBUG(Input, "Error reading payload (size: {}, type: {:02x})", payload_size,
|
||||
adapter_payload[0]);
|
||||
if (input_error_counter++ > 20) {
|
||||
LOG_ERROR(Input, "Timeout, Is the adapter connected?");
|
||||
adapter_input_thread.request_stop();
|
||||
restart_scan_thread = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
input_error_counter = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GCAdapter::UpdateControllers(const AdapterPayload& adapter_payload) {
|
||||
for (std::size_t port = 0; port < pads.size(); ++port) {
|
||||
const std::size_t offset = 1 + (9 * port);
|
||||
const auto type = static_cast<ControllerTypes>(adapter_payload[offset] >> 4);
|
||||
UpdatePadType(port, type);
|
||||
if (DeviceConnected(port)) {
|
||||
const u8 b1 = adapter_payload[offset + 1];
|
||||
const u8 b2 = adapter_payload[offset + 2];
|
||||
UpdateStateButtons(port, b1, b2);
|
||||
UpdateStateAxes(port, adapter_payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GCAdapter::UpdatePadType(std::size_t port, ControllerTypes pad_type) {
|
||||
if (pads[port].type == pad_type) {
|
||||
return;
|
||||
}
|
||||
// Device changed reset device and set new type
|
||||
pads[port].axis_origin = {};
|
||||
pads[port].reset_origin_counter = {};
|
||||
pads[port].enable_vibration = {};
|
||||
pads[port].rumble_amplitude = {};
|
||||
pads[port].type = pad_type;
|
||||
}
|
||||
|
||||
void GCAdapter::UpdateStateButtons(std::size_t port, [[maybe_unused]] u8 b1,
|
||||
[[maybe_unused]] u8 b2) {
|
||||
if (port >= pads.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
static constexpr std::array<PadButton, 8> b1_buttons{
|
||||
PadButton::ButtonA, PadButton::ButtonB, PadButton::ButtonX, PadButton::ButtonY,
|
||||
PadButton::ButtonLeft, PadButton::ButtonRight, PadButton::ButtonDown, PadButton::ButtonUp,
|
||||
};
|
||||
|
||||
static constexpr std::array<PadButton, 4> b2_buttons{
|
||||
PadButton::ButtonStart,
|
||||
PadButton::TriggerZ,
|
||||
PadButton::TriggerR,
|
||||
PadButton::TriggerL,
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < b1_buttons.size(); ++i) {
|
||||
const bool button_status = (b1 & (1U << i)) != 0;
|
||||
const int button = static_cast<int>(b1_buttons[i]);
|
||||
SetButton(pads[port].identifier, button, button_status);
|
||||
}
|
||||
|
||||
for (std::size_t j = 0; j < b2_buttons.size(); ++j) {
|
||||
const bool button_status = (b2 & (1U << j)) != 0;
|
||||
const int button = static_cast<int>(b2_buttons[j]);
|
||||
SetButton(pads[port].identifier, button, button_status);
|
||||
}
|
||||
}
|
||||
|
||||
void GCAdapter::UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload) {
|
||||
if (port >= pads.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t offset = 1 + (9 * port);
|
||||
static constexpr std::array<PadAxes, 6> axes{
|
||||
PadAxes::StickX, PadAxes::StickY, PadAxes::SubstickX,
|
||||
PadAxes::SubstickY, PadAxes::TriggerLeft, PadAxes::TriggerRight,
|
||||
};
|
||||
|
||||
for (const PadAxes axis : axes) {
|
||||
const auto index = static_cast<std::size_t>(axis);
|
||||
const u8 axis_value = adapter_payload[offset + 3 + index];
|
||||
if (pads[port].reset_origin_counter <= 18) {
|
||||
if (pads[port].axis_origin[index] != axis_value) {
|
||||
pads[port].reset_origin_counter = 0;
|
||||
}
|
||||
pads[port].axis_origin[index] = axis_value;
|
||||
pads[port].reset_origin_counter++;
|
||||
}
|
||||
const f32 axis_status = (axis_value - pads[port].axis_origin[index]) / 100.0f;
|
||||
SetAxis(pads[port].identifier, static_cast<int>(index), axis_status);
|
||||
}
|
||||
}
|
||||
|
||||
void GCAdapter::AdapterScanThread(std::stop_token stop_token) {
|
||||
Common::SetCurrentThreadName("ScanGCAdapter");
|
||||
usb_adapter_handle = nullptr;
|
||||
pads = {};
|
||||
while (!Setup() && Common::StoppableTimedWait(stop_token, std::chrono::seconds{2})) {
|
||||
}
|
||||
}
|
||||
|
||||
bool GCAdapter::Setup() {
|
||||
constexpr u16 nintendo_vid = 0x057e;
|
||||
constexpr u16 gc_adapter_pid = 0x0337;
|
||||
usb_adapter_handle =
|
||||
std::make_unique<LibUSBDeviceHandle>(libusb_ctx->get(), nintendo_vid, gc_adapter_pid);
|
||||
if (!usb_adapter_handle->get()) {
|
||||
return false;
|
||||
}
|
||||
if (!CheckDeviceAccess()) {
|
||||
usb_adapter_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
libusb_device* const device = libusb_get_device(usb_adapter_handle->get());
|
||||
|
||||
LOG_INFO(Input, "GC adapter is now connected");
|
||||
// GC Adapter found and accessible, registering it
|
||||
if (GetGCEndpoint(device)) {
|
||||
rumble_enabled = true;
|
||||
input_error_counter = 0;
|
||||
output_error_counter = 0;
|
||||
|
||||
std::size_t port = 0;
|
||||
for (GCController& pad : pads) {
|
||||
pad.identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = port++,
|
||||
.pad = 0,
|
||||
};
|
||||
PreSetController(pad.identifier);
|
||||
}
|
||||
|
||||
adapter_input_thread =
|
||||
std::jthread([this](std::stop_token stop_token) { AdapterInputThread(stop_token); });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GCAdapter::CheckDeviceAccess() {
|
||||
s32 kernel_driver_error = libusb_kernel_driver_active(usb_adapter_handle->get(), 0);
|
||||
if (kernel_driver_error == 1) {
|
||||
kernel_driver_error = libusb_detach_kernel_driver(usb_adapter_handle->get(), 0);
|
||||
if (kernel_driver_error != 0 && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
|
||||
LOG_ERROR(Input, "libusb_detach_kernel_driver failed with error = {}",
|
||||
kernel_driver_error);
|
||||
}
|
||||
}
|
||||
|
||||
if (kernel_driver_error && kernel_driver_error != LIBUSB_ERROR_NOT_SUPPORTED) {
|
||||
usb_adapter_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
const int interface_claim_error = libusb_claim_interface(usb_adapter_handle->get(), 0);
|
||||
if (interface_claim_error) {
|
||||
LOG_ERROR(Input, "libusb_claim_interface failed with error = {}", interface_claim_error);
|
||||
usb_adapter_handle = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// This fixes payload problems from offbrand GCAdapters
|
||||
const s32 control_transfer_error =
|
||||
libusb_control_transfer(usb_adapter_handle->get(), 0x21, 11, 0x0001, 0, nullptr, 0, 1000);
|
||||
if (control_transfer_error < 0) {
|
||||
LOG_ERROR(Input, "libusb_control_transfer failed with error= {}", control_transfer_error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GCAdapter::GetGCEndpoint(libusb_device* device) {
|
||||
libusb_config_descriptor* config = nullptr;
|
||||
const int config_descriptor_return = libusb_get_config_descriptor(device, 0, &config);
|
||||
if (config_descriptor_return != LIBUSB_SUCCESS) {
|
||||
LOG_ERROR(Input, "libusb_get_config_descriptor failed with error = {}",
|
||||
config_descriptor_return);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (u8 ic = 0; ic < config->bNumInterfaces; ic++) {
|
||||
const libusb_interface* interfaceContainer = &config->interface[ic];
|
||||
for (int i = 0; i < interfaceContainer->num_altsetting; i++) {
|
||||
const libusb_interface_descriptor* interface = &interfaceContainer->altsetting[i];
|
||||
for (u8 e = 0; e < interface->bNumEndpoints; e++) {
|
||||
const libusb_endpoint_descriptor* endpoint = &interface->endpoint[e];
|
||||
if ((endpoint->bEndpointAddress & LIBUSB_ENDPOINT_IN) != 0) {
|
||||
input_endpoint = endpoint->bEndpointAddress;
|
||||
} else {
|
||||
output_endpoint = endpoint->bEndpointAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// This transfer seems to be responsible for clearing the state of the adapter
|
||||
// Used to clear the "busy" state of when the device is unexpectedly unplugged
|
||||
unsigned char clear_payload = 0x13;
|
||||
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, &clear_payload,
|
||||
sizeof(clear_payload), nullptr, 16);
|
||||
return true;
|
||||
}
|
||||
|
||||
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 =
|
||||
static_cast<u8>((mean_amplitude + std::pow(mean_amplitude, 0.3f)) * 0.5f * 0x8);
|
||||
|
||||
pads[identifier.port].rumble_amplitude = processed_amplitude;
|
||||
|
||||
if (!rumble_enabled) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
|
||||
return rumble_enabled;
|
||||
}
|
||||
|
||||
void GCAdapter::UpdateVibrations() {
|
||||
// Use 8 states to keep the switching between on/off fast enough for
|
||||
// a human to feel different vibration strength
|
||||
// More states == more rumble strengths == slower update time
|
||||
constexpr u8 vibration_states = 8;
|
||||
|
||||
vibration_counter = (vibration_counter + 1) % vibration_states;
|
||||
|
||||
for (GCController& pad : pads) {
|
||||
const bool vibrate = pad.rumble_amplitude > vibration_counter;
|
||||
vibration_changed |= vibrate != pad.enable_vibration;
|
||||
pad.enable_vibration = vibrate;
|
||||
}
|
||||
SendVibrations();
|
||||
}
|
||||
|
||||
void GCAdapter::SendVibrations() {
|
||||
if (!rumble_enabled || !vibration_changed) {
|
||||
return;
|
||||
}
|
||||
s32 size{};
|
||||
constexpr u8 rumble_command = 0x11;
|
||||
const u8 p1 = pads[0].enable_vibration;
|
||||
const u8 p2 = pads[1].enable_vibration;
|
||||
const u8 p3 = pads[2].enable_vibration;
|
||||
const u8 p4 = pads[3].enable_vibration;
|
||||
std::array<u8, 5> payload = {rumble_command, p1, p2, p3, p4};
|
||||
const int err =
|
||||
libusb_interrupt_transfer(usb_adapter_handle->get(), output_endpoint, payload.data(),
|
||||
static_cast<s32>(payload.size()), &size, 16);
|
||||
if (err) {
|
||||
LOG_DEBUG(Input, "Libusb write failed: {}", libusb_error_name(err));
|
||||
if (output_error_counter++ > 5) {
|
||||
LOG_ERROR(Input, "Output timeout, Rumble disabled");
|
||||
rumble_enabled = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
output_error_counter = 0;
|
||||
vibration_changed = false;
|
||||
}
|
||||
|
||||
bool GCAdapter::DeviceConnected(std::size_t port) const {
|
||||
return pads[port].type != ControllerTypes::None;
|
||||
}
|
||||
|
||||
void GCAdapter::Reset() {
|
||||
adapter_scan_thread = {};
|
||||
adapter_input_thread = {};
|
||||
usb_adapter_handle = nullptr;
|
||||
pads = {};
|
||||
libusb_ctx = nullptr;
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> GCAdapter::GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices;
|
||||
for (std::size_t port = 0; port < pads.size(); ++port) {
|
||||
if (!DeviceConnected(port)) {
|
||||
continue;
|
||||
}
|
||||
Common::ParamPackage identifier{};
|
||||
identifier.Set("engine", GetEngineName());
|
||||
identifier.Set("display", fmt::format("Gamecube Controller {}", port + 1));
|
||||
identifier.Set("port", static_cast<int>(port));
|
||||
devices.emplace_back(identifier);
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
ButtonMapping GCAdapter::GetButtonMappingForDevice(const Common::ParamPackage& params) {
|
||||
// This list is missing ZL/ZR since those are not considered buttons.
|
||||
// We will add those afterwards
|
||||
// This list also excludes any button that can't be really mapped
|
||||
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 14>
|
||||
switch_to_gcadapter_button = {
|
||||
std::pair{Settings::NativeButton::A, PadButton::ButtonA},
|
||||
{Settings::NativeButton::B, PadButton::ButtonB},
|
||||
{Settings::NativeButton::X, PadButton::ButtonX},
|
||||
{Settings::NativeButton::Y, PadButton::ButtonY},
|
||||
{Settings::NativeButton::Plus, PadButton::ButtonStart},
|
||||
{Settings::NativeButton::DLeft, PadButton::ButtonLeft},
|
||||
{Settings::NativeButton::DUp, PadButton::ButtonUp},
|
||||
{Settings::NativeButton::DRight, PadButton::ButtonRight},
|
||||
{Settings::NativeButton::DDown, PadButton::ButtonDown},
|
||||
{Settings::NativeButton::SLLeft, PadButton::TriggerL},
|
||||
{Settings::NativeButton::SRLeft, PadButton::TriggerR},
|
||||
{Settings::NativeButton::SLRight, PadButton::TriggerL},
|
||||
{Settings::NativeButton::SRRight, PadButton::TriggerR},
|
||||
{Settings::NativeButton::R, PadButton::TriggerZ},
|
||||
};
|
||||
if (!params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ButtonMapping mapping{};
|
||||
for (const auto& [switch_button, gcadapter_button] : switch_to_gcadapter_button) {
|
||||
Common::ParamPackage button_params{};
|
||||
button_params.Set("engine", GetEngineName());
|
||||
button_params.Set("port", params.Get("port", 0));
|
||||
button_params.Set("button", static_cast<int>(gcadapter_button));
|
||||
mapping.insert_or_assign(switch_button, std::move(button_params));
|
||||
}
|
||||
|
||||
// Add the missing bindings for ZL/ZR
|
||||
static constexpr std::array<std::tuple<Settings::NativeButton::Values, PadButton, PadAxes>, 2>
|
||||
switch_to_gcadapter_axis = {
|
||||
std::tuple{Settings::NativeButton::ZL, PadButton::TriggerL, PadAxes::TriggerLeft},
|
||||
{Settings::NativeButton::ZR, PadButton::TriggerR, PadAxes::TriggerRight},
|
||||
};
|
||||
for (const auto& [switch_button, gcadapter_button, gcadapter_axis] : switch_to_gcadapter_axis) {
|
||||
Common::ParamPackage button_params{};
|
||||
button_params.Set("engine", GetEngineName());
|
||||
button_params.Set("port", params.Get("port", 0));
|
||||
button_params.Set("button", static_cast<s32>(gcadapter_button));
|
||||
button_params.Set("axis", static_cast<s32>(gcadapter_axis));
|
||||
button_params.Set("threshold", 0.5f);
|
||||
button_params.Set("range", 1.9f);
|
||||
button_params.Set("direction", "+");
|
||||
mapping.insert_or_assign(switch_button, std::move(button_params));
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
AnalogMapping GCAdapter::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
AnalogMapping mapping = {};
|
||||
Common::ParamPackage left_analog_params;
|
||||
left_analog_params.Set("engine", GetEngineName());
|
||||
left_analog_params.Set("port", params.Get("port", 0));
|
||||
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::StickX));
|
||||
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::StickY));
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
|
||||
Common::ParamPackage right_analog_params;
|
||||
right_analog_params.Set("engine", GetEngineName());
|
||||
right_analog_params.Set("port", params.Get("port", 0));
|
||||
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::SubstickX));
|
||||
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::SubstickY));
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames GCAdapter::GetUIButtonName(const Common::ParamPackage& params) const {
|
||||
PadButton button = static_cast<PadButton>(params.Get("button", 0));
|
||||
switch (button) {
|
||||
case PadButton::ButtonLeft:
|
||||
return Common::Input::ButtonNames::ButtonLeft;
|
||||
case PadButton::ButtonRight:
|
||||
return Common::Input::ButtonNames::ButtonRight;
|
||||
case PadButton::ButtonDown:
|
||||
return Common::Input::ButtonNames::ButtonDown;
|
||||
case PadButton::ButtonUp:
|
||||
return Common::Input::ButtonNames::ButtonUp;
|
||||
case PadButton::TriggerZ:
|
||||
return Common::Input::ButtonNames::TriggerZ;
|
||||
case PadButton::TriggerR:
|
||||
return Common::Input::ButtonNames::TriggerR;
|
||||
case PadButton::TriggerL:
|
||||
return Common::Input::ButtonNames::TriggerL;
|
||||
case PadButton::ButtonA:
|
||||
return Common::Input::ButtonNames::ButtonA;
|
||||
case PadButton::ButtonB:
|
||||
return Common::Input::ButtonNames::ButtonB;
|
||||
case PadButton::ButtonX:
|
||||
return Common::Input::ButtonNames::ButtonX;
|
||||
case PadButton::ButtonY:
|
||||
return Common::Input::ButtonNames::ButtonY;
|
||||
case PadButton::ButtonStart:
|
||||
return Common::Input::ButtonNames::ButtonStart;
|
||||
default:
|
||||
return Common::Input::ButtonNames::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames GCAdapter::GetUIName(const Common::ParamPackage& params) const {
|
||||
if (params.Has("button")) {
|
||||
return GetUIButtonName(params);
|
||||
}
|
||||
if (params.Has("axis")) {
|
||||
return Common::Input::ButtonNames::Value;
|
||||
}
|
||||
|
||||
return Common::Input::ButtonNames::Invalid;
|
||||
}
|
||||
|
||||
bool GCAdapter::IsStickInverted(const Common::ParamPackage& params) {
|
||||
if (!params.Has("port")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto x_axis = static_cast<PadAxes>(params.Get("axis_x", 0));
|
||||
const auto y_axis = static_cast<PadAxes>(params.Get("axis_y", 0));
|
||||
if (x_axis != PadAxes::StickY && x_axis != PadAxes::SubstickY) {
|
||||
return false;
|
||||
}
|
||||
if (y_axis != PadAxes::StickX && y_axis != PadAxes::SubstickX) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
137
src/input_common/drivers/gc_adapter.h
Normal file
137
src/input_common/drivers/gc_adapter.h
Normal file
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: 2014 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
struct libusb_context;
|
||||
struct libusb_device;
|
||||
struct libusb_device_handle;
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
class LibUSBContext;
|
||||
class LibUSBDeviceHandle;
|
||||
|
||||
class GCAdapter : public InputEngine {
|
||||
public:
|
||||
explicit GCAdapter(std::string input_engine_);
|
||||
~GCAdapter() override;
|
||||
|
||||
Common::Input::DriverResult SetVibration(
|
||||
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
|
||||
|
||||
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
|
||||
|
||||
/// Used for automapping features
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
|
||||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
bool IsStickInverted(const Common::ParamPackage& params) override;
|
||||
|
||||
private:
|
||||
enum class PadButton {
|
||||
Undefined = 0x0000,
|
||||
ButtonLeft = 0x0001,
|
||||
ButtonRight = 0x0002,
|
||||
ButtonDown = 0x0004,
|
||||
ButtonUp = 0x0008,
|
||||
TriggerZ = 0x0010,
|
||||
TriggerR = 0x0020,
|
||||
TriggerL = 0x0040,
|
||||
ButtonA = 0x0100,
|
||||
ButtonB = 0x0200,
|
||||
ButtonX = 0x0400,
|
||||
ButtonY = 0x0800,
|
||||
ButtonStart = 0x1000,
|
||||
};
|
||||
|
||||
enum class PadAxes : u8 {
|
||||
StickX,
|
||||
StickY,
|
||||
SubstickX,
|
||||
SubstickY,
|
||||
TriggerLeft,
|
||||
TriggerRight,
|
||||
Undefined,
|
||||
};
|
||||
|
||||
enum class ControllerTypes {
|
||||
None,
|
||||
Wired,
|
||||
Wireless,
|
||||
};
|
||||
|
||||
struct GCController {
|
||||
ControllerTypes type = ControllerTypes::None;
|
||||
PadIdentifier identifier{};
|
||||
bool enable_vibration = false;
|
||||
u8 rumble_amplitude{};
|
||||
std::array<u8, 6> axis_origin{};
|
||||
u8 reset_origin_counter{};
|
||||
};
|
||||
|
||||
using AdapterPayload = std::array<u8, 37>;
|
||||
|
||||
void UpdatePadType(std::size_t port, ControllerTypes pad_type);
|
||||
void UpdateControllers(const AdapterPayload& adapter_payload);
|
||||
void UpdateStateButtons(std::size_t port, u8 b1, u8 b2);
|
||||
void UpdateStateAxes(std::size_t port, const AdapterPayload& adapter_payload);
|
||||
|
||||
void AdapterInputThread(std::stop_token stop_token);
|
||||
|
||||
void AdapterScanThread(std::stop_token stop_token);
|
||||
|
||||
bool IsPayloadCorrect(const AdapterPayload& adapter_payload, s32 payload_size);
|
||||
|
||||
/// For use in initialization, querying devices to find the adapter
|
||||
bool Setup();
|
||||
|
||||
/// Returns true if we successfully gain access to GC Adapter
|
||||
bool CheckDeviceAccess();
|
||||
|
||||
/// Captures GC Adapter endpoint address
|
||||
/// Returns true if the endpoint was set correctly
|
||||
bool GetGCEndpoint(libusb_device* device);
|
||||
|
||||
/// Returns true if there is a device connected to port
|
||||
bool DeviceConnected(std::size_t port) const;
|
||||
|
||||
/// For shutting down, clear all data, join all threads, release usb
|
||||
void Reset();
|
||||
|
||||
void UpdateVibrations();
|
||||
|
||||
/// Updates vibration state of all controllers
|
||||
void SendVibrations();
|
||||
|
||||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
||||
|
||||
std::unique_ptr<LibUSBDeviceHandle> usb_adapter_handle;
|
||||
std::array<GCController, 4> pads;
|
||||
|
||||
std::jthread adapter_input_thread;
|
||||
std::jthread adapter_scan_thread;
|
||||
bool restart_scan_thread{};
|
||||
|
||||
std::unique_ptr<LibUSBContext> libusb_ctx;
|
||||
|
||||
u8 input_endpoint{0};
|
||||
u8 output_endpoint{0};
|
||||
u8 input_error_counter{0};
|
||||
u8 output_error_counter{0};
|
||||
int vibration_counter{0};
|
||||
|
||||
bool rumble_enabled{true};
|
||||
bool vibration_changed{true};
|
||||
};
|
||||
} // namespace InputCommon
|
||||
843
src/input_common/drivers/joycon.cpp
Normal file
843
src/input_common/drivers/joycon.cpp
Normal file
@@ -0,0 +1,843 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "common/polyfill_ranges.h"
|
||||
#include "common/polyfill_thread.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 && !Settings::values.enable_procon_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();
|
||||
}
|
||||
for (const auto& device : pro_controller) {
|
||||
if (!device) {
|
||||
continue;
|
||||
}
|
||||
device->Stop();
|
||||
}
|
||||
SDL_hid_exit();
|
||||
}
|
||||
|
||||
void Joycons::Setup() {
|
||||
u32 port = 0;
|
||||
PreSetController(GetIdentifier(0, Joycon::ControllerType::None));
|
||||
for (auto& device : left_joycons) {
|
||||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Left));
|
||||
device = std::make_shared<Joycon::JoyconDriver>(port++);
|
||||
}
|
||||
port = 0;
|
||||
for (auto& device : right_joycons) {
|
||||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Right));
|
||||
device = std::make_shared<Joycon::JoyconDriver>(port++);
|
||||
}
|
||||
port = 0;
|
||||
for (auto& device : pro_controller) {
|
||||
PreSetController(GetIdentifier(port, Joycon::ControllerType::Pro));
|
||||
device = std::make_shared<Joycon::JoyconDriver>(port++);
|
||||
}
|
||||
|
||||
scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); });
|
||||
}
|
||||
|
||||
void Joycons::ScanThread(std::stop_token stop_token) {
|
||||
constexpr u16 nintendo_vendor_id = 0x057e;
|
||||
Common::SetCurrentThreadName("JoyconScanThread");
|
||||
|
||||
do {
|
||||
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);
|
||||
} while (Common::StoppableTimedWait(stop_token, 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 != Common::Input::DriverResult::Success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto result2 = Joycon::JoyconDriver::GetSerialNumber(device_info, serial_number);
|
||||
if (result2 != Common::Input::DriverResult::Success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto is_handle_identical = [serial_number](std::shared_ptr<Joycon::JoyconDriver> device) {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
if (!device->IsConnected()) {
|
||||
return false;
|
||||
}
|
||||
if (device->GetHandleSerialNumber() != serial_number) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Check if device already exist
|
||||
switch (type) {
|
||||
case Joycon::ControllerType::Left:
|
||||
if (!Settings::values.enable_joycon_driver) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& device : left_joycons) {
|
||||
if (is_handle_identical(device)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Joycon::ControllerType::Right:
|
||||
if (!Settings::values.enable_joycon_driver) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& device : right_joycons) {
|
||||
if (is_handle_identical(device)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Joycon::ControllerType::Pro:
|
||||
if (!Settings::values.enable_procon_driver) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& device : pro_controller) {
|
||||
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 == Common::Input::DriverResult::Success) {
|
||||
result = handle->RequestDeviceAccess(device_info);
|
||||
}
|
||||
if (result == Common::Input::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, type](const Joycon::TagInfo& tag_info) {
|
||||
OnAmiiboUpdate(port, type, tag_info);
|
||||
}},
|
||||
.on_camera_data = {[this, port](const std::vector<u8>& camera_data,
|
||||
Joycon::IrsResolution format) {
|
||||
OnCameraUpdate(port, camera_data, format);
|
||||
}},
|
||||
};
|
||||
|
||||
handle->InitializeDevice();
|
||||
handle->SetCallbacks(callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle(
|
||||
Joycon::ControllerType type) const {
|
||||
if (type == Joycon::ControllerType::Left) {
|
||||
const auto unconnected_device =
|
||||
std::ranges::find_if(left_joycons, [](auto& device) { return !device->IsConnected(); });
|
||||
if (unconnected_device != left_joycons.end()) {
|
||||
return *unconnected_device;
|
||||
}
|
||||
}
|
||||
if (type == Joycon::ControllerType::Right) {
|
||||
const auto unconnected_device = std::ranges::find_if(
|
||||
right_joycons, [](auto& device) { return !device->IsConnected(); });
|
||||
|
||||
if (unconnected_device != right_joycons.end()) {
|
||||
return *unconnected_device;
|
||||
}
|
||||
}
|
||||
if (type == Joycon::ControllerType::Pro) {
|
||||
const auto unconnected_device = std::ranges::find_if(
|
||||
pro_controller, [](auto& device) { return !device->IsConnected(); });
|
||||
|
||||
if (unconnected_device != pro_controller.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 handle->SetLedConfig(static_cast<u8>(led_config));
|
||||
}
|
||||
|
||||
Common::Input::DriverResult Joycons::SetCameraFormat(const PadIdentifier& identifier,
|
||||
Common::Input::CameraFormat camera_format) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::DriverResult::InvalidHandle;
|
||||
}
|
||||
return handle->SetIrsConfig(Joycon::IrsMode::ImageTransfer,
|
||||
static_cast<Joycon::IrsResolution>(camera_format));
|
||||
};
|
||||
|
||||
Common::Input::NfcState Joycons::SupportsNfc(const PadIdentifier& identifier_) const {
|
||||
return Common::Input::NfcState::Success;
|
||||
};
|
||||
|
||||
Common::Input::NfcState Joycons::StartNfcPolling(const PadIdentifier& identifier) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
return TranslateDriverResult(handle->StartNfcPolling());
|
||||
};
|
||||
|
||||
Common::Input::NfcState Joycons::StopNfcPolling(const PadIdentifier& identifier) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
return TranslateDriverResult(handle->StopNfcPolling());
|
||||
};
|
||||
|
||||
Common::Input::NfcState Joycons::ReadAmiiboData(const PadIdentifier& identifier,
|
||||
std::vector<u8>& out_data) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
return TranslateDriverResult(handle->ReadAmiiboData(out_data));
|
||||
}
|
||||
|
||||
Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier,
|
||||
const std::vector<u8>& data) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
return TranslateDriverResult(handle->WriteNfcData(data));
|
||||
};
|
||||
|
||||
Common::Input::NfcState Joycons::ReadMifareData(const PadIdentifier& identifier,
|
||||
const Common::Input::MifareRequest& request,
|
||||
Common::Input::MifareRequest& data) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
|
||||
const auto command = static_cast<Joycon::MifareCmd>(request.data[0].command);
|
||||
std::vector<Joycon::MifareReadChunk> read_request{};
|
||||
for (const auto& request_data : request.data) {
|
||||
if (request_data.command == 0) {
|
||||
continue;
|
||||
}
|
||||
Joycon::MifareReadChunk chunk = {
|
||||
.command = command,
|
||||
.sector_key = {},
|
||||
.sector = request_data.sector,
|
||||
};
|
||||
memcpy(chunk.sector_key.data(), request_data.key.data(),
|
||||
sizeof(Joycon::MifareReadChunk::sector_key));
|
||||
read_request.emplace_back(chunk);
|
||||
}
|
||||
|
||||
std::vector<Joycon::MifareReadData> read_data(read_request.size());
|
||||
const auto result = handle->ReadMifareData(read_request, read_data);
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
for (std::size_t i = 0; i < read_request.size(); i++) {
|
||||
data.data[i] = {
|
||||
.command = static_cast<u8>(command),
|
||||
.sector = read_data[i].sector,
|
||||
.key = {},
|
||||
.data = read_data[i].data,
|
||||
};
|
||||
}
|
||||
}
|
||||
return TranslateDriverResult(result);
|
||||
};
|
||||
|
||||
Common::Input::NfcState Joycons::WriteMifareData(const PadIdentifier& identifier,
|
||||
const Common::Input::MifareRequest& request) {
|
||||
auto handle = GetHandle(identifier);
|
||||
if (handle == nullptr) {
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
|
||||
const auto command = static_cast<Joycon::MifareCmd>(request.data[0].command);
|
||||
std::vector<Joycon::MifareWriteChunk> write_request{};
|
||||
for (const auto& request_data : request.data) {
|
||||
if (request_data.command == 0) {
|
||||
continue;
|
||||
}
|
||||
Joycon::MifareWriteChunk chunk = {
|
||||
.command = command,
|
||||
.sector_key = {},
|
||||
.sector = request_data.sector,
|
||||
.data = {},
|
||||
};
|
||||
memcpy(chunk.sector_key.data(), request_data.key.data(),
|
||||
sizeof(Joycon::MifareReadChunk::sector_key));
|
||||
memcpy(chunk.data.data(), request_data.data.data(), sizeof(Joycon::MifareWriteChunk::data));
|
||||
write_request.emplace_back(chunk);
|
||||
}
|
||||
|
||||
return TranslateDriverResult(handle->WriteMifareData(write_request));
|
||||
};
|
||||
|
||||
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 handle->SetActiveMode();
|
||||
case Common::Input::PollingMode::Passive:
|
||||
return handle->SetPassiveMode();
|
||||
case Common::Input::PollingMode::IR:
|
||||
return handle->SetIrMode();
|
||||
case Common::Input::PollingMode::NFC:
|
||||
return handle->SetNfcMode();
|
||||
case Common::Input::PollingMode::Ring:
|
||||
return 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
|
||||
static constexpr PadIdentifier identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
SetAxis(identifier, 100, ring_data);
|
||||
}
|
||||
|
||||
void Joycons::OnAmiiboUpdate(std::size_t port, Joycon::ControllerType type,
|
||||
const Joycon::TagInfo& tag_info) {
|
||||
const auto identifier = GetIdentifier(port, type);
|
||||
const auto nfc_state = tag_info.uuid_length == 0 ? Common::Input::NfcState::AmiiboRemoved
|
||||
: Common::Input::NfcState::NewAmiibo;
|
||||
|
||||
const Common::Input::NfcStatus nfc_status{
|
||||
.state = nfc_state,
|
||||
.uuid_length = tag_info.uuid_length,
|
||||
.protocol = tag_info.protocol,
|
||||
.tag_type = tag_info.tag_type,
|
||||
.uuid = tag_info.uuid,
|
||||
};
|
||||
|
||||
SetNfc(identifier, nfc_status);
|
||||
}
|
||||
|
||||
void Joycons::OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data,
|
||||
Joycon::IrsResolution format) {
|
||||
const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right);
|
||||
SetCamera(identifier, {static_cast<Common::Input::CameraFormat>(format), camera_data});
|
||||
}
|
||||
|
||||
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetHandle(PadIdentifier identifier) const {
|
||||
auto is_handle_active = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
if (!device->IsConnected()) {
|
||||
return false;
|
||||
}
|
||||
if (device->GetDevicePort() == identifier.port) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const auto type = static_cast<Joycon::ControllerType>(identifier.pad);
|
||||
|
||||
if (type == Joycon::ControllerType::Left) {
|
||||
const auto matching_device = std::ranges::find_if(
|
||||
left_joycons, [is_handle_active](auto& device) { return is_handle_active(device); });
|
||||
|
||||
if (matching_device != left_joycons.end()) {
|
||||
return *matching_device;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == Joycon::ControllerType::Right) {
|
||||
const auto matching_device = std::ranges::find_if(
|
||||
right_joycons, [is_handle_active](auto& device) { return is_handle_active(device); });
|
||||
|
||||
if (matching_device != right_joycons.end()) {
|
||||
return *matching_device;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == Joycon::ControllerType::Pro) {
|
||||
const auto matching_device = std::ranges::find_if(
|
||||
pro_controller, [is_handle_active](auto& device) { return is_handle_active(device); });
|
||||
|
||||
if (matching_device != pro_controller.end()) {
|
||||
return *matching_device;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const {
|
||||
const std::array<u8, 16> guid{0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, static_cast<u8>(type)};
|
||||
return {
|
||||
.guid = Common::UUID{guid},
|
||||
.port = port,
|
||||
.pad = static_cast<std::size_t>(type),
|
||||
};
|
||||
}
|
||||
|
||||
Common::ParamPackage Joycons::GetParamPackage(std::size_t port, Joycon::ControllerType type) const {
|
||||
const auto identifier = GetIdentifier(port, type);
|
||||
return {
|
||||
{"engine", GetEngineName()},
|
||||
{"guid", identifier.guid.RawString()},
|
||||
{"port", std::to_string(identifier.port)},
|
||||
{"pad", std::to_string(identifier.pad)},
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> Joycons::GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices{};
|
||||
|
||||
auto add_entry = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
if (!device->IsConnected()) {
|
||||
return;
|
||||
}
|
||||
auto param = GetParamPackage(device->GetDevicePort(), device->GetHandleDeviceType());
|
||||
std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()),
|
||||
device->GetDevicePort() + 1);
|
||||
param.Set("display", std::move(name));
|
||||
devices.emplace_back(param);
|
||||
};
|
||||
|
||||
for (const auto& controller : left_joycons) {
|
||||
add_entry(controller);
|
||||
}
|
||||
for (const auto& controller : right_joycons) {
|
||||
add_entry(controller);
|
||||
}
|
||||
for (const auto& controller : pro_controller) {
|
||||
add_entry(controller);
|
||||
}
|
||||
|
||||
// List dual joycon pairs
|
||||
for (std::size_t i = 0; i < MaxSupportedControllers; i++) {
|
||||
if (!left_joycons[i] || !right_joycons[i]) {
|
||||
continue;
|
||||
}
|
||||
if (!left_joycons[i]->IsConnected() || !right_joycons[i]->IsConnected()) {
|
||||
continue;
|
||||
}
|
||||
auto main_param = GetParamPackage(i, left_joycons[i]->GetHandleDeviceType());
|
||||
const auto second_param = GetParamPackage(i, right_joycons[i]->GetHandleDeviceType());
|
||||
const auto type = Joycon::ControllerType::Dual;
|
||||
std::string name = fmt::format("{} {}", JoyconName(type), i + 1);
|
||||
|
||||
main_param.Set("display", std::move(name));
|
||||
main_param.Set("guid2", second_param.Get("guid", ""));
|
||||
main_param.Set("pad", std::to_string(static_cast<size_t>(type)));
|
||||
devices.emplace_back(main_param);
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) {
|
||||
static constexpr std::array<std::tuple<Settings::NativeButton::Values, Joycon::PadButton, bool>,
|
||||
18>
|
||||
switch_to_joycon_button = {
|
||||
std::tuple{Settings::NativeButton::A, Joycon::PadButton::A, true},
|
||||
{Settings::NativeButton::B, Joycon::PadButton::B, true},
|
||||
{Settings::NativeButton::X, Joycon::PadButton::X, true},
|
||||
{Settings::NativeButton::Y, Joycon::PadButton::Y, true},
|
||||
{Settings::NativeButton::DLeft, Joycon::PadButton::Left, false},
|
||||
{Settings::NativeButton::DUp, Joycon::PadButton::Up, false},
|
||||
{Settings::NativeButton::DRight, Joycon::PadButton::Right, false},
|
||||
{Settings::NativeButton::DDown, Joycon::PadButton::Down, false},
|
||||
{Settings::NativeButton::L, Joycon::PadButton::L, false},
|
||||
{Settings::NativeButton::R, Joycon::PadButton::R, true},
|
||||
{Settings::NativeButton::ZL, Joycon::PadButton::ZL, false},
|
||||
{Settings::NativeButton::ZR, Joycon::PadButton::ZR, true},
|
||||
{Settings::NativeButton::Plus, Joycon::PadButton::Plus, true},
|
||||
{Settings::NativeButton::Minus, Joycon::PadButton::Minus, false},
|
||||
{Settings::NativeButton::Home, Joycon::PadButton::Home, true},
|
||||
{Settings::NativeButton::Screenshot, Joycon::PadButton::Capture, false},
|
||||
{Settings::NativeButton::LStick, Joycon::PadButton::StickL, false},
|
||||
{Settings::NativeButton::RStick, Joycon::PadButton::StickR, true},
|
||||
};
|
||||
|
||||
if (!params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ButtonMapping mapping{};
|
||||
for (const auto& [switch_button, joycon_button, side] : switch_to_joycon_button) {
|
||||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
|
||||
auto pad = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
|
||||
if (pad == Joycon::ControllerType::Dual) {
|
||||
pad = side ? Joycon::ControllerType::Right : Joycon::ControllerType::Left;
|
||||
}
|
||||
|
||||
Common::ParamPackage button_params = GetParamPackage(port, pad);
|
||||
button_params.Set("button", static_cast<int>(joycon_button));
|
||||
mapping.insert_or_assign(switch_button, std::move(button_params));
|
||||
}
|
||||
|
||||
// Map SL and SR buttons for left joycons
|
||||
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Left)) {
|
||||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
|
||||
Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Left);
|
||||
|
||||
Common::ParamPackage sl_button_params = button_params;
|
||||
Common::ParamPackage sr_button_params = button_params;
|
||||
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSL));
|
||||
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSR));
|
||||
mapping.insert_or_assign(Settings::NativeButton::SLLeft, std::move(sl_button_params));
|
||||
mapping.insert_or_assign(Settings::NativeButton::SRLeft, std::move(sr_button_params));
|
||||
}
|
||||
|
||||
// Map SL and SR buttons for right joycons
|
||||
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Right)) {
|
||||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
|
||||
Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Right);
|
||||
|
||||
Common::ParamPackage sl_button_params = button_params;
|
||||
Common::ParamPackage sr_button_params = button_params;
|
||||
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSL));
|
||||
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSR));
|
||||
mapping.insert_or_assign(Settings::NativeButton::SLRight, std::move(sl_button_params));
|
||||
mapping.insert_or_assign(Settings::NativeButton::SRRight, std::move(sr_button_params));
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
|
||||
auto pad_left = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
|
||||
auto pad_right = pad_left;
|
||||
if (pad_left == Joycon::ControllerType::Dual) {
|
||||
pad_left = Joycon::ControllerType::Left;
|
||||
pad_right = Joycon::ControllerType::Right;
|
||||
}
|
||||
|
||||
AnalogMapping mapping = {};
|
||||
Common::ParamPackage left_analog_params = GetParamPackage(port, pad_left);
|
||||
left_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::LeftStickX));
|
||||
left_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::LeftStickY));
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
|
||||
Common::ParamPackage right_analog_params = GetParamPackage(port, pad_right);
|
||||
right_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::RightStickX));
|
||||
right_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::RightStickY));
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
MotionMapping Joycons::GetMotionMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("port")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
|
||||
auto pad_left = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
|
||||
auto pad_right = pad_left;
|
||||
if (pad_left == Joycon::ControllerType::Dual) {
|
||||
pad_left = Joycon::ControllerType::Left;
|
||||
pad_right = Joycon::ControllerType::Right;
|
||||
}
|
||||
|
||||
MotionMapping mapping = {};
|
||||
Common::ParamPackage left_motion_params = GetParamPackage(port, pad_left);
|
||||
left_motion_params.Set("motion", 0);
|
||||
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params));
|
||||
Common::ParamPackage right_Motion_params = GetParamPackage(port, pad_right);
|
||||
right_Motion_params.Set("motion", 1);
|
||||
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_Motion_params));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames Joycons::GetUIButtonName(const Common::ParamPackage& params) const {
|
||||
const auto button = static_cast<Joycon::PadButton>(params.Get("button", 0));
|
||||
switch (button) {
|
||||
case Joycon::PadButton::Left:
|
||||
return Common::Input::ButtonNames::ButtonLeft;
|
||||
case Joycon::PadButton::Right:
|
||||
return Common::Input::ButtonNames::ButtonRight;
|
||||
case Joycon::PadButton::Down:
|
||||
return Common::Input::ButtonNames::ButtonDown;
|
||||
case Joycon::PadButton::Up:
|
||||
return Common::Input::ButtonNames::ButtonUp;
|
||||
case Joycon::PadButton::LeftSL:
|
||||
case Joycon::PadButton::RightSL:
|
||||
return Common::Input::ButtonNames::TriggerSL;
|
||||
case Joycon::PadButton::LeftSR:
|
||||
case Joycon::PadButton::RightSR:
|
||||
return Common::Input::ButtonNames::TriggerSR;
|
||||
case Joycon::PadButton::L:
|
||||
return Common::Input::ButtonNames::TriggerL;
|
||||
case Joycon::PadButton::R:
|
||||
return Common::Input::ButtonNames::TriggerR;
|
||||
case Joycon::PadButton::ZL:
|
||||
return Common::Input::ButtonNames::TriggerZL;
|
||||
case Joycon::PadButton::ZR:
|
||||
return Common::Input::ButtonNames::TriggerZR;
|
||||
case Joycon::PadButton::A:
|
||||
return Common::Input::ButtonNames::ButtonA;
|
||||
case Joycon::PadButton::B:
|
||||
return Common::Input::ButtonNames::ButtonB;
|
||||
case Joycon::PadButton::X:
|
||||
return Common::Input::ButtonNames::ButtonX;
|
||||
case Joycon::PadButton::Y:
|
||||
return Common::Input::ButtonNames::ButtonY;
|
||||
case Joycon::PadButton::Plus:
|
||||
return Common::Input::ButtonNames::ButtonPlus;
|
||||
case Joycon::PadButton::Minus:
|
||||
return Common::Input::ButtonNames::ButtonMinus;
|
||||
case Joycon::PadButton::Home:
|
||||
return Common::Input::ButtonNames::ButtonHome;
|
||||
case Joycon::PadButton::Capture:
|
||||
return Common::Input::ButtonNames::ButtonCapture;
|
||||
case Joycon::PadButton::StickL:
|
||||
return Common::Input::ButtonNames::ButtonStickL;
|
||||
case Joycon::PadButton::StickR:
|
||||
return Common::Input::ButtonNames::ButtonStickR;
|
||||
default:
|
||||
return Common::Input::ButtonNames::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames Joycons::GetUIName(const Common::ParamPackage& params) const {
|
||||
if (params.Has("button")) {
|
||||
return GetUIButtonName(params);
|
||||
}
|
||||
if (params.Has("axis")) {
|
||||
return Common::Input::ButtonNames::Value;
|
||||
}
|
||||
if (params.Has("motion")) {
|
||||
return Common::Input::ButtonNames::Engine;
|
||||
}
|
||||
|
||||
return Common::Input::ButtonNames::Invalid;
|
||||
}
|
||||
|
||||
std::string Joycons::JoyconName(Joycon::ControllerType type) const {
|
||||
switch (type) {
|
||||
case Joycon::ControllerType::Left:
|
||||
return "Left Joycon";
|
||||
case Joycon::ControllerType::Right:
|
||||
return "Right Joycon";
|
||||
case Joycon::ControllerType::Pro:
|
||||
return "Pro Controller";
|
||||
case Joycon::ControllerType::Dual:
|
||||
return "Dual Joycon";
|
||||
default:
|
||||
return "Unknown Switch Controller";
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::NfcState Joycons::TranslateDriverResult(Common::Input::DriverResult result) const {
|
||||
switch (result) {
|
||||
case Common::Input::DriverResult::Success:
|
||||
return Common::Input::NfcState::Success;
|
||||
case Common::Input::DriverResult::Disabled:
|
||||
return Common::Input::NfcState::WrongDeviceState;
|
||||
case Common::Input::DriverResult::NotSupported:
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
default:
|
||||
return Common::Input::NfcState::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
124
src/input_common/drivers/joycon.h
Normal file
124
src/input_common/drivers/joycon.h
Normal file
@@ -0,0 +1,124 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <thread>
|
||||
#include <SDL_hidapi.h>
|
||||
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
using SerialNumber = std::array<u8, 15>;
|
||||
struct Battery;
|
||||
struct Color;
|
||||
struct MotionData;
|
||||
struct TagInfo;
|
||||
enum class ControllerType : u8;
|
||||
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 StartNfcPolling(const PadIdentifier& identifier) override;
|
||||
Common::Input::NfcState StopNfcPolling(const PadIdentifier& identifier) override;
|
||||
Common::Input::NfcState ReadAmiiboData(const PadIdentifier& identifier,
|
||||
std::vector<u8>& out_data) override;
|
||||
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier,
|
||||
const std::vector<u8>& data) override;
|
||||
Common::Input::NfcState ReadMifareData(const PadIdentifier& identifier,
|
||||
const Common::Input::MifareRequest& request,
|
||||
Common::Input::MifareRequest& out_data) override;
|
||||
Common::Input::NfcState WriteMifareData(const PadIdentifier& identifier,
|
||||
const Common::Input::MifareRequest& request) override;
|
||||
|
||||
Common::Input::DriverResult SetPollingMode(
|
||||
const PadIdentifier& identifier, const Common::Input::PollingMode polling_mode) override;
|
||||
|
||||
/// Used for automapping features
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
|
||||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
|
||||
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
private:
|
||||
static constexpr std::size_t MaxSupportedControllers = 8;
|
||||
|
||||
/// For shutting down, clear all data, join all threads, release usb devices
|
||||
void Reset();
|
||||
|
||||
/// Registers controllers, clears all data and starts the scan thread
|
||||
void Setup();
|
||||
|
||||
/// Actively searches for new devices
|
||||
void ScanThread(std::stop_token stop_token);
|
||||
|
||||
/// Returns true if device is valid and not registered
|
||||
bool IsDeviceNew(SDL_hid_device_info* device_info) const;
|
||||
|
||||
/// Tries to connect to the new device
|
||||
void RegisterNewDevice(SDL_hid_device_info* device_info);
|
||||
|
||||
/// Returns the next free handle
|
||||
std::shared_ptr<Joycon::JoyconDriver> GetNextFreeHandle(Joycon::ControllerType type) const;
|
||||
|
||||
void OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, Joycon::Battery value);
|
||||
void OnColorUpdate(std::size_t port, Joycon::ControllerType type, const Joycon::Color& value);
|
||||
void OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value);
|
||||
void OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value);
|
||||
void OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
|
||||
const Joycon::MotionData& value);
|
||||
void OnRingConUpdate(f32 ring_data);
|
||||
void OnAmiiboUpdate(std::size_t port, Joycon::ControllerType type,
|
||||
const Joycon::TagInfo& amiibo_data);
|
||||
void OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data,
|
||||
Joycon::IrsResolution format);
|
||||
|
||||
/// Returns a JoyconHandle corresponding to a PadIdentifier
|
||||
std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const;
|
||||
|
||||
/// Returns a PadIdentifier corresponding to the port number and joycon type
|
||||
PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const;
|
||||
|
||||
/// Returns a ParamPackage corresponding to the port number and joycon type
|
||||
Common::ParamPackage GetParamPackage(std::size_t port, Joycon::ControllerType type) const;
|
||||
|
||||
std::string JoyconName(std::size_t port) const;
|
||||
|
||||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
||||
|
||||
/// Returns the name of the device in text format
|
||||
std::string JoyconName(Joycon::ControllerType type) const;
|
||||
|
||||
Common::Input::NfcState TranslateDriverResult(Common::Input::DriverResult result) const;
|
||||
|
||||
std::jthread scan_thread;
|
||||
|
||||
// Joycon types are split by type to ease supporting dualjoycon configurations
|
||||
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> left_joycons{};
|
||||
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> right_joycons{};
|
||||
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> pro_controller{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
111
src/input_common/drivers/keyboard.cpp
Normal file
111
src/input_common/drivers/keyboard.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "common/settings_input.h"
|
||||
#include "input_common/drivers/keyboard.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
constexpr PadIdentifier key_identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
constexpr PadIdentifier keyboard_key_identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 1,
|
||||
.pad = 0,
|
||||
};
|
||||
constexpr PadIdentifier keyboard_modifier_identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 1,
|
||||
.pad = 1,
|
||||
};
|
||||
|
||||
Keyboard::Keyboard(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
// Keyboard is broken into 3 different sets:
|
||||
// key: Unfiltered intended for controllers.
|
||||
// keyboard_key: Allows only Settings::NativeKeyboard::Keys intended for keyboard emulation.
|
||||
// keyboard_modifier: Allows only Settings::NativeKeyboard::Modifiers intended for keyboard
|
||||
// emulation.
|
||||
PreSetController(key_identifier);
|
||||
PreSetController(keyboard_key_identifier);
|
||||
PreSetController(keyboard_modifier_identifier);
|
||||
}
|
||||
|
||||
void Keyboard::PressKey(int key_code) {
|
||||
SetButton(key_identifier, key_code, true);
|
||||
}
|
||||
|
||||
void Keyboard::ReleaseKey(int key_code) {
|
||||
SetButton(key_identifier, key_code, false);
|
||||
}
|
||||
|
||||
void Keyboard::PressKeyboardKey(int key_index) {
|
||||
if (key_index == Settings::NativeKeyboard::None) {
|
||||
return;
|
||||
}
|
||||
SetButton(keyboard_key_identifier, key_index, true);
|
||||
}
|
||||
|
||||
void Keyboard::ReleaseKeyboardKey(int key_index) {
|
||||
if (key_index == Settings::NativeKeyboard::None) {
|
||||
return;
|
||||
}
|
||||
SetButton(keyboard_key_identifier, key_index, false);
|
||||
}
|
||||
|
||||
void Keyboard::SetKeyboardModifiers(int key_modifiers) {
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
bool key_value = ((key_modifiers >> i) & 0x1) != 0;
|
||||
SetButton(keyboard_modifier_identifier, i, key_value);
|
||||
// Use the modifier to press the key button equivalent
|
||||
switch (i) {
|
||||
case Settings::NativeKeyboard::LeftControl:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftControlKey, key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::LeftShift:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftShiftKey, key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::LeftAlt:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftAltKey, key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::LeftMeta:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::LeftMetaKey, key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::RightControl:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightControlKey,
|
||||
key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::RightShift:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightShiftKey, key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::RightAlt:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightAltKey, key_value);
|
||||
break;
|
||||
case Settings::NativeKeyboard::RightMeta:
|
||||
SetButton(keyboard_key_identifier, Settings::NativeKeyboard::RightMetaKey, key_value);
|
||||
break;
|
||||
default:
|
||||
// Other modifier keys should be pressed with PressKey since they stay enabled until
|
||||
// next press
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Keyboard::ReleaseAllKeys() {
|
||||
ResetButtonState();
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> Keyboard::GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices;
|
||||
devices.emplace_back(Common::ParamPackage{
|
||||
{"engine", GetEngineName()},
|
||||
{"display", "Keyboard Only"},
|
||||
});
|
||||
return devices;
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
55
src/input_common/drivers/keyboard.h
Normal file
55
src/input_common/drivers/keyboard.h
Normal file
@@ -0,0 +1,55 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* A button device factory representing a keyboard. It receives keyboard events and forward them
|
||||
* to all button devices it created.
|
||||
*/
|
||||
class Keyboard final : public InputEngine {
|
||||
public:
|
||||
explicit Keyboard(std::string input_engine_);
|
||||
|
||||
/**
|
||||
* Sets the status of all buttons bound with the key to pressed
|
||||
* @param key_code the code of the key to press
|
||||
*/
|
||||
void PressKey(int key_code);
|
||||
|
||||
/**
|
||||
* Sets the status of all buttons bound with the key to released
|
||||
* @param key_code the code of the key to release
|
||||
*/
|
||||
void ReleaseKey(int key_code);
|
||||
|
||||
/**
|
||||
* Sets the status of the keyboard key to pressed
|
||||
* @param key_index index of the key to press
|
||||
*/
|
||||
void PressKeyboardKey(int key_index);
|
||||
|
||||
/**
|
||||
* Sets the status of the keyboard key to released
|
||||
* @param key_index index of the key to release
|
||||
*/
|
||||
void ReleaseKeyboardKey(int key_index);
|
||||
|
||||
/**
|
||||
* Sets the status of all keyboard modifier keys
|
||||
* @param key_modifiers the code of the key to release
|
||||
*/
|
||||
void SetKeyboardModifiers(int key_modifiers);
|
||||
|
||||
/// Sets all keys to the non pressed state
|
||||
void ReleaseAllKeys();
|
||||
|
||||
/// Used for automapping features
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
316
src/input_common/drivers/mouse.cpp
Normal file
316
src/input_common/drivers/mouse.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <thread>
|
||||
#include <fmt/ranges.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "common/settings.h"
|
||||
#include "common/thread.h"
|
||||
#include "input_common/drivers/mouse.h"
|
||||
|
||||
namespace InputCommon {
|
||||
constexpr int update_time = 10;
|
||||
constexpr float default_panning_sensitivity = 0.0010f;
|
||||
constexpr float default_stick_sensitivity = 0.0006f;
|
||||
constexpr float default_deadzone_counterweight = 0.01f;
|
||||
constexpr float default_motion_panning_sensitivity = 2.5f;
|
||||
constexpr float default_motion_sensitivity = 0.416f;
|
||||
constexpr float maximum_rotation_speed = 2.0f;
|
||||
constexpr float maximum_stick_range = 1.5f;
|
||||
constexpr int mouse_axis_x = 0;
|
||||
constexpr int mouse_axis_y = 1;
|
||||
constexpr int wheel_axis_x = 2;
|
||||
constexpr int wheel_axis_y = 3;
|
||||
constexpr PadIdentifier identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
constexpr PadIdentifier motion_identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 1,
|
||||
};
|
||||
|
||||
constexpr PadIdentifier real_mouse_identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 1,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
constexpr PadIdentifier touch_identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 2,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
Mouse::Mouse(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
PreSetController(identifier);
|
||||
PreSetController(real_mouse_identifier);
|
||||
PreSetController(touch_identifier);
|
||||
PreSetController(motion_identifier);
|
||||
|
||||
// Initialize all mouse axis
|
||||
PreSetAxis(identifier, mouse_axis_x);
|
||||
PreSetAxis(identifier, mouse_axis_y);
|
||||
PreSetAxis(identifier, wheel_axis_x);
|
||||
PreSetAxis(identifier, wheel_axis_y);
|
||||
PreSetAxis(real_mouse_identifier, mouse_axis_x);
|
||||
PreSetAxis(real_mouse_identifier, mouse_axis_y);
|
||||
PreSetAxis(touch_identifier, mouse_axis_x);
|
||||
PreSetAxis(touch_identifier, mouse_axis_y);
|
||||
|
||||
// Initialize variables
|
||||
mouse_origin = {};
|
||||
last_mouse_position = {};
|
||||
wheel_position = {};
|
||||
last_mouse_change = {};
|
||||
last_motion_change = {};
|
||||
|
||||
update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
|
||||
}
|
||||
|
||||
void Mouse::UpdateThread(std::stop_token stop_token) {
|
||||
Common::SetCurrentThreadName("Mouse");
|
||||
|
||||
while (!stop_token.stop_requested()) {
|
||||
UpdateStickInput();
|
||||
UpdateMotionInput();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::UpdateStickInput() {
|
||||
if (!IsMousePanningEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float length = last_mouse_change.Length();
|
||||
|
||||
// Prevent input from exceeding the max range (1.0f) too much,
|
||||
// but allow some room to make it easier to sustain
|
||||
if (length > maximum_stick_range) {
|
||||
last_mouse_change /= length;
|
||||
last_mouse_change *= maximum_stick_range;
|
||||
}
|
||||
|
||||
SetAxis(identifier, mouse_axis_x, last_mouse_change.x);
|
||||
SetAxis(identifier, mouse_axis_y, -last_mouse_change.y);
|
||||
|
||||
// Decay input over time
|
||||
const float clamped_length = std::min(1.0f, length);
|
||||
const float decay_strength = Settings::values.mouse_panning_decay_strength.GetValue();
|
||||
const float decay = 1 - clamped_length * clamped_length * decay_strength * 0.01f;
|
||||
const float min_decay = Settings::values.mouse_panning_min_decay.GetValue();
|
||||
const float clamped_decay = std::min(1 - min_decay / 100.0f, decay);
|
||||
last_mouse_change *= clamped_decay;
|
||||
}
|
||||
|
||||
void Mouse::UpdateMotionInput() {
|
||||
const float sensitivity =
|
||||
IsMousePanningEnabled() ? default_motion_panning_sensitivity : default_motion_sensitivity;
|
||||
|
||||
const float rotation_velocity = std::sqrt(last_motion_change.x * last_motion_change.x +
|
||||
last_motion_change.y * last_motion_change.y);
|
||||
|
||||
// Clamp rotation speed
|
||||
if (rotation_velocity > maximum_rotation_speed / sensitivity) {
|
||||
const float multiplier = maximum_rotation_speed / rotation_velocity / sensitivity;
|
||||
last_motion_change.x = last_motion_change.x * multiplier;
|
||||
last_motion_change.y = last_motion_change.y * multiplier;
|
||||
}
|
||||
|
||||
const BasicMotion motion_data{
|
||||
.gyro_x = last_motion_change.x * sensitivity,
|
||||
.gyro_y = last_motion_change.y * sensitivity,
|
||||
.gyro_z = last_motion_change.z * sensitivity,
|
||||
.accel_x = 0,
|
||||
.accel_y = 0,
|
||||
.accel_z = 0,
|
||||
.delta_timestamp = update_time * 1000,
|
||||
};
|
||||
|
||||
if (IsMousePanningEnabled()) {
|
||||
last_motion_change.x = 0;
|
||||
last_motion_change.y = 0;
|
||||
}
|
||||
last_motion_change.z = 0;
|
||||
|
||||
SetMotion(motion_identifier, 0, motion_data);
|
||||
}
|
||||
|
||||
void Mouse::Move(int x, int y, int center_x, int center_y) {
|
||||
if (IsMousePanningEnabled()) {
|
||||
const auto mouse_change =
|
||||
(Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
|
||||
const float x_sensitivity =
|
||||
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_panning_sensitivity;
|
||||
const float y_sensitivity =
|
||||
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_panning_sensitivity;
|
||||
const float deadzone_counterweight =
|
||||
Settings::values.mouse_panning_deadzone_counterweight.GetValue() *
|
||||
default_deadzone_counterweight;
|
||||
|
||||
last_motion_change += {-mouse_change.y * x_sensitivity, -mouse_change.x * y_sensitivity, 0};
|
||||
last_mouse_change.x += mouse_change.x * x_sensitivity;
|
||||
last_mouse_change.y += mouse_change.y * y_sensitivity;
|
||||
|
||||
// Bind the mouse change to [0 <= deadzone_counterweight <= 1.0]
|
||||
const float length = last_mouse_change.Length();
|
||||
if (length < deadzone_counterweight && length != 0.0f) {
|
||||
last_mouse_change /= length;
|
||||
last_mouse_change *= deadzone_counterweight;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (button_pressed) {
|
||||
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
|
||||
const float x_sensitivity =
|
||||
Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
|
||||
const float y_sensitivity =
|
||||
Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
|
||||
SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * x_sensitivity);
|
||||
SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * y_sensitivity);
|
||||
|
||||
last_motion_change = {
|
||||
static_cast<float>(-mouse_move.y) * x_sensitivity,
|
||||
static_cast<float>(-mouse_move.x) * y_sensitivity,
|
||||
last_motion_change.z,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::MouseMove(f32 touch_x, f32 touch_y) {
|
||||
SetAxis(real_mouse_identifier, mouse_axis_x, touch_x);
|
||||
SetAxis(real_mouse_identifier, mouse_axis_y, touch_y);
|
||||
}
|
||||
|
||||
void Mouse::TouchMove(f32 touch_x, f32 touch_y) {
|
||||
SetAxis(touch_identifier, mouse_axis_x, touch_x);
|
||||
SetAxis(touch_identifier, mouse_axis_y, touch_y);
|
||||
}
|
||||
|
||||
void Mouse::PressButton(int x, int y, MouseButton button) {
|
||||
SetButton(identifier, static_cast<int>(button), true);
|
||||
|
||||
// Set initial analog parameters
|
||||
mouse_origin = {x, y};
|
||||
last_mouse_position = {x, y};
|
||||
button_pressed = true;
|
||||
}
|
||||
|
||||
void Mouse::PressMouseButton(MouseButton button) {
|
||||
SetButton(real_mouse_identifier, static_cast<int>(button), true);
|
||||
}
|
||||
|
||||
void Mouse::PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button) {
|
||||
SetAxis(touch_identifier, mouse_axis_x, touch_x);
|
||||
SetAxis(touch_identifier, mouse_axis_y, touch_y);
|
||||
SetButton(touch_identifier, static_cast<int>(button), true);
|
||||
}
|
||||
|
||||
void Mouse::ReleaseButton(MouseButton button) {
|
||||
SetButton(identifier, static_cast<int>(button), false);
|
||||
SetButton(real_mouse_identifier, static_cast<int>(button), false);
|
||||
SetButton(touch_identifier, static_cast<int>(button), false);
|
||||
|
||||
if (!IsMousePanningEnabled()) {
|
||||
SetAxis(identifier, mouse_axis_x, 0);
|
||||
SetAxis(identifier, mouse_axis_y, 0);
|
||||
}
|
||||
|
||||
last_motion_change.x = 0;
|
||||
last_motion_change.y = 0;
|
||||
|
||||
button_pressed = false;
|
||||
}
|
||||
|
||||
void Mouse::MouseWheelChange(int x, int y) {
|
||||
wheel_position.x += x;
|
||||
wheel_position.y += y;
|
||||
last_motion_change.z += static_cast<f32>(y);
|
||||
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
|
||||
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
|
||||
}
|
||||
|
||||
void Mouse::ReleaseAllButtons() {
|
||||
ResetButtonState();
|
||||
button_pressed = false;
|
||||
}
|
||||
|
||||
bool Mouse::IsMousePanningEnabled() {
|
||||
// Disable mouse panning when a real mouse is connected
|
||||
return Settings::values.mouse_panning && !Settings::values.mouse_enabled;
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices;
|
||||
devices.emplace_back(Common::ParamPackage{
|
||||
{"engine", GetEngineName()},
|
||||
{"display", "Keyboard/Mouse"},
|
||||
});
|
||||
return devices;
|
||||
}
|
||||
|
||||
AnalogMapping Mouse::GetAnalogMappingForDevice(
|
||||
[[maybe_unused]] const Common::ParamPackage& params) {
|
||||
// Only overwrite different buttons from default
|
||||
AnalogMapping mapping = {};
|
||||
Common::ParamPackage right_analog_params;
|
||||
right_analog_params.Set("engine", GetEngineName());
|
||||
right_analog_params.Set("axis_x", 0);
|
||||
right_analog_params.Set("axis_y", 1);
|
||||
right_analog_params.Set("threshold", 0.5f);
|
||||
right_analog_params.Set("range", 1.0f);
|
||||
right_analog_params.Set("deadzone", 0.0f);
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames Mouse::GetUIButtonName(const Common::ParamPackage& params) const {
|
||||
const auto button = static_cast<MouseButton>(params.Get("button", 0));
|
||||
switch (button) {
|
||||
case MouseButton::Left:
|
||||
return Common::Input::ButtonNames::ButtonLeft;
|
||||
case MouseButton::Right:
|
||||
return Common::Input::ButtonNames::ButtonRight;
|
||||
case MouseButton::Wheel:
|
||||
return Common::Input::ButtonNames::ButtonMouseWheel;
|
||||
case MouseButton::Backward:
|
||||
return Common::Input::ButtonNames::ButtonBackward;
|
||||
case MouseButton::Forward:
|
||||
return Common::Input::ButtonNames::ButtonForward;
|
||||
case MouseButton::Task:
|
||||
return Common::Input::ButtonNames::ButtonTask;
|
||||
case MouseButton::Extra:
|
||||
return Common::Input::ButtonNames::ButtonExtra;
|
||||
case MouseButton::Undefined:
|
||||
default:
|
||||
return Common::Input::ButtonNames::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames Mouse::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("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
|
||||
return Common::Input::ButtonNames::Engine;
|
||||
}
|
||||
if (params.Has("motion")) {
|
||||
return Common::Input::ButtonNames::Engine;
|
||||
}
|
||||
|
||||
return Common::Input::ButtonNames::Invalid;
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
115
src/input_common/drivers/mouse.h
Normal file
115
src/input_common/drivers/mouse.h
Normal file
@@ -0,0 +1,115 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "common/polyfill_thread.h"
|
||||
#include "common/vector_math.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
enum class MouseButton {
|
||||
Left,
|
||||
Right,
|
||||
Wheel,
|
||||
Backward,
|
||||
Forward,
|
||||
Task,
|
||||
Extra,
|
||||
Undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* A button device factory representing a keyboard. It receives keyboard events and forward them
|
||||
* to all button devices it created.
|
||||
*/
|
||||
class Mouse final : public InputEngine {
|
||||
public:
|
||||
explicit Mouse(std::string input_engine_);
|
||||
|
||||
/**
|
||||
* Signals that mouse has moved.
|
||||
* @param x the x-coordinate of the cursor
|
||||
* @param y the y-coordinate of the cursor
|
||||
* @param center_x the x-coordinate of the middle of the screen
|
||||
* @param center_y the y-coordinate of the middle of the screen
|
||||
*/
|
||||
void Move(int x, int y, int center_x, int center_y);
|
||||
|
||||
/**
|
||||
* Signals that real mouse has moved.
|
||||
* @param x the absolute position on the touchscreen of the cursor
|
||||
* @param y the absolute position on the touchscreen of the cursor
|
||||
*/
|
||||
void MouseMove(f32 touch_x, f32 touch_y);
|
||||
|
||||
/**
|
||||
* Signals that touch finger has moved.
|
||||
* @param x the absolute position on the touchscreen of the cursor
|
||||
* @param y the absolute position on the touchscreen of the cursor
|
||||
*/
|
||||
void TouchMove(f32 touch_x, f32 touch_y);
|
||||
|
||||
/**
|
||||
* Sets the status of a button to pressed
|
||||
* @param x the x-coordinate of the cursor
|
||||
* @param y the y-coordinate of the cursor
|
||||
* @param button the id of the button to press
|
||||
*/
|
||||
void PressButton(int x, int y, MouseButton button);
|
||||
|
||||
/**
|
||||
* Sets the status of a mouse button to pressed
|
||||
* @param button the id of the button to press
|
||||
*/
|
||||
void PressMouseButton(MouseButton button);
|
||||
|
||||
/**
|
||||
* Sets the status of touch finger to pressed
|
||||
* @param x the absolute position on the touchscreen of the cursor
|
||||
* @param y the absolute position on the touchscreen of the cursor
|
||||
* @param button the id of the button to press
|
||||
*/
|
||||
void PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button);
|
||||
|
||||
/**
|
||||
* Sets the status of all buttons bound with the key to released
|
||||
* @param key_code the code of the key to release
|
||||
*/
|
||||
void ReleaseButton(MouseButton button);
|
||||
|
||||
/**
|
||||
* Sets the status of the mouse wheel
|
||||
* @param x delta movement in the x direction
|
||||
* @param y delta movement in the y direction
|
||||
*/
|
||||
void MouseWheelChange(int x, int y);
|
||||
|
||||
void ReleaseAllButtons();
|
||||
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
private:
|
||||
void UpdateThread(std::stop_token stop_token);
|
||||
void UpdateStickInput();
|
||||
void UpdateMotionInput();
|
||||
|
||||
bool IsMousePanningEnabled();
|
||||
|
||||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
||||
|
||||
Common::Vec2<int> mouse_origin;
|
||||
Common::Vec2<int> last_mouse_position;
|
||||
Common::Vec2<float> last_mouse_change;
|
||||
Common::Vec3<float> last_motion_change;
|
||||
Common::Vec2<int> wheel_position;
|
||||
bool button_pressed;
|
||||
std::jthread update_thread;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
1123
src/input_common/drivers/sdl_driver.cpp
Normal file
1123
src/input_common/drivers/sdl_driver.cpp
Normal file
File diff suppressed because it is too large
Load Diff
127
src/input_common/drivers/sdl_driver.h
Normal file
127
src/input_common/drivers/sdl_driver.h
Normal file
@@ -0,0 +1,127 @@
|
||||
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/threadsafe_queue.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
union SDL_Event;
|
||||
using SDL_GameController = struct _SDL_GameController;
|
||||
using SDL_Joystick = struct _SDL_Joystick;
|
||||
using SDL_JoystickID = s32;
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
class SDLJoystick;
|
||||
|
||||
using ButtonBindings =
|
||||
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerButton>, 20>;
|
||||
using ZButtonBindings =
|
||||
std::array<std::pair<Settings::NativeButton::Values, SDL_GameControllerAxis>, 2>;
|
||||
|
||||
class SDLDriver : public InputEngine {
|
||||
public:
|
||||
/// Initializes and registers SDL device factories
|
||||
explicit SDLDriver(std::string input_engine_);
|
||||
|
||||
/// Unregisters SDL device factories and shut them down.
|
||||
~SDLDriver() override;
|
||||
|
||||
void PumpEvents() const;
|
||||
|
||||
/// Handle SDL_Events for joysticks from SDL_PollEvent
|
||||
void HandleGameControllerEvent(const SDL_Event& event);
|
||||
|
||||
/// Get the nth joystick with the corresponding GUID
|
||||
std::shared_ptr<SDLJoystick> GetSDLJoystickBySDLID(SDL_JoystickID sdl_id);
|
||||
|
||||
/**
|
||||
* Check how many identical joysticks (by guid) were connected before the one with sdl_id and so
|
||||
* tie it to a SDLJoystick with the same guid and that port
|
||||
*/
|
||||
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const Common::UUID& guid, int port);
|
||||
std::shared_ptr<SDLJoystick> GetSDLJoystickByGUID(const std::string& guid, int port);
|
||||
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
|
||||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
|
||||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
|
||||
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
std::string GetHatButtonName(u8 direction_value) const override;
|
||||
u8 GetHatButtonId(const std::string& direction_name) const override;
|
||||
|
||||
bool IsStickInverted(const Common::ParamPackage& params) override;
|
||||
|
||||
Common::Input::DriverResult SetVibration(
|
||||
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
|
||||
|
||||
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
|
||||
|
||||
private:
|
||||
void InitJoystick(int joystick_index);
|
||||
void CloseJoystick(SDL_Joystick* sdl_joystick);
|
||||
|
||||
/// Needs to be called before SDL_QuitSubSystem.
|
||||
void CloseJoysticks();
|
||||
|
||||
/// Takes all vibrations from the queue and sends the command to the controller
|
||||
void SendVibrations();
|
||||
|
||||
Common::ParamPackage BuildAnalogParamPackageForButton(int port, const Common::UUID& guid,
|
||||
s32 axis, float value = 0.1f) const;
|
||||
Common::ParamPackage BuildButtonParamPackageForButton(int port, const Common::UUID& guid,
|
||||
s32 button) const;
|
||||
|
||||
Common::ParamPackage BuildHatParamPackageForButton(int port, const Common::UUID& guid, s32 hat,
|
||||
u8 value) const;
|
||||
|
||||
Common::ParamPackage BuildMotionParam(int port, const Common::UUID& guid) const;
|
||||
|
||||
Common::ParamPackage BuildParamPackageForBinding(
|
||||
int port, const Common::UUID& guid, const SDL_GameControllerButtonBind& binding) const;
|
||||
|
||||
Common::ParamPackage BuildParamPackageForAnalog(PadIdentifier identifier, int axis_x,
|
||||
int axis_y, float offset_x,
|
||||
float offset_y) const;
|
||||
|
||||
/// Returns the default button bindings list
|
||||
ButtonBindings GetDefaultButtonBinding(const std::shared_ptr<SDLJoystick>& joystick) const;
|
||||
|
||||
/// Returns the button mappings from a single controller
|
||||
ButtonMapping GetSingleControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
|
||||
const ButtonBindings& switch_to_sdl_button,
|
||||
const ZButtonBindings& switch_to_sdl_axis) const;
|
||||
|
||||
/// Returns the button mappings from two different controllers
|
||||
ButtonMapping GetDualControllerMapping(const std::shared_ptr<SDLJoystick>& joystick,
|
||||
const std::shared_ptr<SDLJoystick>& joystick2,
|
||||
const ButtonBindings& switch_to_sdl_button,
|
||||
const ZButtonBindings& switch_to_sdl_axis) const;
|
||||
|
||||
/// Returns true if the button is on the left joycon
|
||||
bool IsButtonOnLeftSide(Settings::NativeButton::Values button) const;
|
||||
|
||||
/// Queue of vibration request to controllers
|
||||
Common::SPSCQueue<VibrationRequest> vibration_queue;
|
||||
|
||||
/// Map of GUID of a list of corresponding virtual Joysticks
|
||||
std::unordered_map<Common::UUID, std::vector<std::shared_ptr<SDLJoystick>>> joystick_map;
|
||||
std::mutex joystick_map_mutex;
|
||||
|
||||
bool start_thread = false;
|
||||
std::atomic<bool> initialized = false;
|
||||
|
||||
std::thread vibration_thread;
|
||||
};
|
||||
} // namespace InputCommon
|
||||
344
src/input_common/drivers/tas_input.cpp
Normal file
344
src/input_common/drivers/tas_input.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs_types.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "input_common/drivers/tas_input.h"
|
||||
|
||||
namespace InputCommon::TasInput {
|
||||
|
||||
enum class Tas::TasAxis : u8 {
|
||||
StickX,
|
||||
StickY,
|
||||
SubstickX,
|
||||
SubstickY,
|
||||
Undefined,
|
||||
};
|
||||
|
||||
// Supported keywords and buttons from a TAS file
|
||||
constexpr std::array<std::pair<std::string_view, TasButton>, 18> text_to_tas_button = {
|
||||
std::pair{"KEY_A", TasButton::BUTTON_A},
|
||||
{"KEY_B", TasButton::BUTTON_B},
|
||||
{"KEY_X", TasButton::BUTTON_X},
|
||||
{"KEY_Y", TasButton::BUTTON_Y},
|
||||
{"KEY_LSTICK", TasButton::STICK_L},
|
||||
{"KEY_RSTICK", TasButton::STICK_R},
|
||||
{"KEY_L", TasButton::TRIGGER_L},
|
||||
{"KEY_R", TasButton::TRIGGER_R},
|
||||
{"KEY_PLUS", TasButton::BUTTON_PLUS},
|
||||
{"KEY_MINUS", TasButton::BUTTON_MINUS},
|
||||
{"KEY_DLEFT", TasButton::BUTTON_LEFT},
|
||||
{"KEY_DUP", TasButton::BUTTON_UP},
|
||||
{"KEY_DRIGHT", TasButton::BUTTON_RIGHT},
|
||||
{"KEY_DDOWN", TasButton::BUTTON_DOWN},
|
||||
{"KEY_SL", TasButton::BUTTON_SL},
|
||||
{"KEY_SR", TasButton::BUTTON_SR},
|
||||
// These buttons are disabled to avoid TAS input from activating hotkeys
|
||||
// {"KEY_CAPTURE", TasButton::BUTTON_CAPTURE},
|
||||
// {"KEY_HOME", TasButton::BUTTON_HOME},
|
||||
{"KEY_ZL", TasButton::TRIGGER_ZL},
|
||||
{"KEY_ZR", TasButton::TRIGGER_ZR},
|
||||
};
|
||||
|
||||
Tas::Tas(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
for (size_t player_index = 0; player_index < PLAYER_NUMBER; player_index++) {
|
||||
PadIdentifier identifier{
|
||||
.guid = Common::UUID{},
|
||||
.port = player_index,
|
||||
.pad = 0,
|
||||
};
|
||||
PreSetController(identifier);
|
||||
}
|
||||
ClearInput();
|
||||
if (!Settings::values.tas_enable) {
|
||||
needs_reset = true;
|
||||
return;
|
||||
}
|
||||
LoadTasFiles();
|
||||
}
|
||||
|
||||
Tas::~Tas() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void Tas::LoadTasFiles() {
|
||||
script_length = 0;
|
||||
for (size_t i = 0; i < commands.size(); i++) {
|
||||
LoadTasFile(i, 0);
|
||||
if (commands[i].size() > script_length) {
|
||||
script_length = commands[i].size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Tas::LoadTasFile(size_t player_index, size_t file_index) {
|
||||
commands[player_index].clear();
|
||||
|
||||
std::string file = Common::FS::ReadStringFromFile(
|
||||
Common::FS::GetCitronPath(Common::FS::CitronPath::TASDir) /
|
||||
fmt::format("script{}-{}.txt", file_index, player_index + 1),
|
||||
Common::FS::FileType::BinaryFile);
|
||||
std::istringstream command_line(file);
|
||||
std::string line;
|
||||
int frame_no = 0;
|
||||
while (std::getline(command_line, line, '\n')) {
|
||||
if (line.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<std::string> seg_list;
|
||||
{
|
||||
std::istringstream line_stream(line);
|
||||
std::string segment;
|
||||
while (std::getline(line_stream, segment, ' ')) {
|
||||
seg_list.push_back(std::move(segment));
|
||||
}
|
||||
}
|
||||
|
||||
if (seg_list.size() < 4) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto num_frames = std::stoi(seg_list[0]);
|
||||
while (frame_no < num_frames) {
|
||||
commands[player_index].emplace_back();
|
||||
frame_no++;
|
||||
}
|
||||
} catch (const std::invalid_argument&) {
|
||||
LOG_ERROR(Input, "Invalid argument: '{}' at command {}", seg_list[0], frame_no);
|
||||
} catch (const std::out_of_range&) {
|
||||
LOG_ERROR(Input, "Out of range: '{}' at command {}", seg_list[0], frame_no);
|
||||
}
|
||||
|
||||
TASCommand command = {
|
||||
.buttons = ReadCommandButtons(seg_list[1]),
|
||||
.l_axis = ReadCommandAxis(seg_list[2]),
|
||||
.r_axis = ReadCommandAxis(seg_list[3]),
|
||||
};
|
||||
commands[player_index].push_back(command);
|
||||
frame_no++;
|
||||
}
|
||||
LOG_INFO(Input, "TAS file loaded! {} frames", frame_no);
|
||||
}
|
||||
|
||||
void Tas::WriteTasFile(std::u8string_view file_name) {
|
||||
std::string output_text;
|
||||
for (size_t frame = 0; frame < record_commands.size(); frame++) {
|
||||
const TASCommand& line = record_commands[frame];
|
||||
output_text += fmt::format("{} {} {} {}\n", frame, WriteCommandButtons(line.buttons),
|
||||
WriteCommandAxis(line.l_axis), WriteCommandAxis(line.r_axis));
|
||||
}
|
||||
|
||||
const auto tas_file_name = Common::FS::GetCitronPath(Common::FS::CitronPath::TASDir) / file_name;
|
||||
const auto bytes_written =
|
||||
Common::FS::WriteStringToFile(tas_file_name, Common::FS::FileType::TextFile, output_text);
|
||||
if (bytes_written == output_text.size()) {
|
||||
LOG_INFO(Input, "TAS file written to file!");
|
||||
} else {
|
||||
LOG_ERROR(Input, "Writing the TAS-file has failed! {} / {} bytes written", bytes_written,
|
||||
output_text.size());
|
||||
}
|
||||
}
|
||||
|
||||
void Tas::RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis) {
|
||||
last_input = {
|
||||
.buttons = buttons,
|
||||
.l_axis = left_axis,
|
||||
.r_axis = right_axis,
|
||||
};
|
||||
}
|
||||
|
||||
std::tuple<TasState, size_t, std::array<size_t, PLAYER_NUMBER>> Tas::GetStatus() const {
|
||||
TasState state;
|
||||
std::array<size_t, PLAYER_NUMBER> lengths{0};
|
||||
if (is_recording) {
|
||||
lengths[0] = record_commands.size();
|
||||
return {TasState::Recording, record_commands.size(), lengths};
|
||||
}
|
||||
|
||||
if (is_running) {
|
||||
state = TasState::Running;
|
||||
} else {
|
||||
state = TasState::Stopped;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < PLAYER_NUMBER; i++) {
|
||||
lengths[i] = commands[i].size();
|
||||
}
|
||||
|
||||
return {state, current_command, lengths};
|
||||
}
|
||||
|
||||
void Tas::UpdateThread() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
if (is_running) {
|
||||
Stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_recording) {
|
||||
record_commands.push_back(last_input);
|
||||
}
|
||||
if (needs_reset) {
|
||||
current_command = 0;
|
||||
needs_reset = false;
|
||||
LoadTasFiles();
|
||||
LOG_DEBUG(Input, "tas_reset done");
|
||||
}
|
||||
|
||||
if (!is_running) {
|
||||
ClearInput();
|
||||
return;
|
||||
}
|
||||
if (current_command < script_length) {
|
||||
LOG_DEBUG(Input, "Playing TAS {}/{}", current_command, script_length);
|
||||
const size_t frame = current_command++;
|
||||
for (size_t player_index = 0; player_index < commands.size(); player_index++) {
|
||||
TASCommand command{};
|
||||
if (frame < commands[player_index].size()) {
|
||||
command = commands[player_index][frame];
|
||||
}
|
||||
|
||||
PadIdentifier identifier{
|
||||
.guid = Common::UUID{},
|
||||
.port = player_index,
|
||||
.pad = 0,
|
||||
};
|
||||
for (std::size_t i = 0; i < sizeof(command.buttons) * 8; ++i) {
|
||||
const bool button_status = (command.buttons & (1LLU << i)) != 0;
|
||||
const int button = static_cast<int>(i);
|
||||
SetButton(identifier, button, button_status);
|
||||
}
|
||||
SetTasAxis(identifier, TasAxis::StickX, command.l_axis.x);
|
||||
SetTasAxis(identifier, TasAxis::StickY, command.l_axis.y);
|
||||
SetTasAxis(identifier, TasAxis::SubstickX, command.r_axis.x);
|
||||
SetTasAxis(identifier, TasAxis::SubstickY, command.r_axis.y);
|
||||
}
|
||||
} else {
|
||||
is_running = Settings::values.tas_loop.GetValue();
|
||||
LoadTasFiles();
|
||||
current_command = 0;
|
||||
ClearInput();
|
||||
}
|
||||
}
|
||||
|
||||
void Tas::ClearInput() {
|
||||
ResetButtonState();
|
||||
ResetAnalogState();
|
||||
}
|
||||
|
||||
TasAnalog Tas::ReadCommandAxis(const std::string& line) const {
|
||||
std::vector<std::string> seg_list;
|
||||
{
|
||||
std::istringstream line_stream(line);
|
||||
std::string segment;
|
||||
while (std::getline(line_stream, segment, ';')) {
|
||||
seg_list.push_back(std::move(segment));
|
||||
}
|
||||
}
|
||||
|
||||
if (seg_list.size() < 2) {
|
||||
LOG_ERROR(Input, "Invalid axis data: '{}'", line);
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const float x = std::stof(seg_list.at(0)) / 32767.0f;
|
||||
const float y = std::stof(seg_list.at(1)) / 32767.0f;
|
||||
return {x, y};
|
||||
} catch (const std::invalid_argument&) {
|
||||
LOG_ERROR(Input, "Invalid argument: '{}'", line);
|
||||
} catch (const std::out_of_range&) {
|
||||
LOG_ERROR(Input, "Out of range: '{}'", line);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
u64 Tas::ReadCommandButtons(const std::string& line) const {
|
||||
std::istringstream button_text(line);
|
||||
std::string button_line;
|
||||
u64 buttons = 0;
|
||||
while (std::getline(button_text, button_line, ';')) {
|
||||
for (const auto& [text, tas_button] : text_to_tas_button) {
|
||||
if (text == button_line) {
|
||||
buttons |= static_cast<u64>(tas_button);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
|
||||
std::string Tas::WriteCommandButtons(u64 buttons) const {
|
||||
std::string returns;
|
||||
for (const auto& [text_button, tas_button] : text_to_tas_button) {
|
||||
if ((buttons & static_cast<u64>(tas_button)) != 0) {
|
||||
returns += fmt::format("{};", text_button);
|
||||
}
|
||||
}
|
||||
return returns.empty() ? "NONE" : returns;
|
||||
}
|
||||
|
||||
std::string Tas::WriteCommandAxis(TasAnalog analog) const {
|
||||
return fmt::format("{};{}", analog.x * 32767, analog.y * 32767);
|
||||
}
|
||||
|
||||
void Tas::SetTasAxis(const PadIdentifier& identifier, TasAxis axis, f32 value) {
|
||||
SetAxis(identifier, static_cast<int>(axis), value);
|
||||
}
|
||||
|
||||
void Tas::StartStop() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
return;
|
||||
}
|
||||
if (is_running) {
|
||||
Stop();
|
||||
} else {
|
||||
is_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Tas::Stop() {
|
||||
is_running = false;
|
||||
}
|
||||
|
||||
void Tas::Reset() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
return;
|
||||
}
|
||||
needs_reset = true;
|
||||
}
|
||||
|
||||
bool Tas::Record() {
|
||||
if (!Settings::values.tas_enable) {
|
||||
return true;
|
||||
}
|
||||
is_recording = !is_recording;
|
||||
return is_recording;
|
||||
}
|
||||
|
||||
void Tas::SaveRecording(bool overwrite_file) {
|
||||
if (is_recording) {
|
||||
return;
|
||||
}
|
||||
if (record_commands.empty()) {
|
||||
return;
|
||||
}
|
||||
WriteTasFile(u8"record.txt");
|
||||
if (overwrite_file) {
|
||||
WriteTasFile(u8"script0-1.txt");
|
||||
}
|
||||
needs_reset = true;
|
||||
record_commands.clear();
|
||||
}
|
||||
|
||||
} // namespace InputCommon::TasInput
|
||||
200
src/input_common/drivers/tas_input.h
Normal file
200
src/input_common/drivers/tas_input.h
Normal file
@@ -0,0 +1,200 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
/*
|
||||
To play back TAS scripts on Citron, select the folder with scripts in the configuration menu below
|
||||
Tools -> Configure TAS. The file itself has normal text format and has to be called script0-1.txt
|
||||
for controller 1, script0-2.txt for controller 2 and so forth (with max. 8 players).
|
||||
|
||||
A script file has the same format as TAS-nx uses, so final files will look like this:
|
||||
|
||||
1 KEY_B 0;0 0;0
|
||||
6 KEY_ZL 0;0 0;0
|
||||
41 KEY_ZL;KEY_Y 0;0 0;0
|
||||
43 KEY_X;KEY_A 32767;0 0;0
|
||||
44 KEY_A 32767;0 0;0
|
||||
45 KEY_A 32767;0 0;0
|
||||
46 KEY_A 32767;0 0;0
|
||||
47 KEY_A 32767;0 0;0
|
||||
|
||||
After placing the file at the correct location, it can be read into Citron with the (default) hotkey
|
||||
CTRL+F6 (refresh). In the bottom left corner, it will display the amount of frames the script file
|
||||
has. Playback can be started or stopped using CTRL+F5.
|
||||
|
||||
However, for playback to actually work, the correct input device has to be selected: In the Controls
|
||||
menu, select TAS from the device list for the controller that the script should be played on.
|
||||
|
||||
Recording a new script file is really simple: Just make sure that the proper device (not TAS) is
|
||||
connected on P1, and press CTRL+F7 to start recording. When done, just press the same keystroke
|
||||
again (CTRL+F7). The new script will be saved at the location previously selected, as the filename
|
||||
record.txt.
|
||||
|
||||
For debugging purposes, the common controller debugger can be used (View -> Debugging -> Controller
|
||||
P1).
|
||||
*/
|
||||
|
||||
namespace InputCommon::TasInput {
|
||||
|
||||
constexpr size_t PLAYER_NUMBER = 10;
|
||||
|
||||
enum class TasButton : u64 {
|
||||
BUTTON_A = 1U << 0,
|
||||
BUTTON_B = 1U << 1,
|
||||
BUTTON_X = 1U << 2,
|
||||
BUTTON_Y = 1U << 3,
|
||||
STICK_L = 1U << 4,
|
||||
STICK_R = 1U << 5,
|
||||
TRIGGER_L = 1U << 6,
|
||||
TRIGGER_R = 1U << 7,
|
||||
TRIGGER_ZL = 1U << 8,
|
||||
TRIGGER_ZR = 1U << 9,
|
||||
BUTTON_PLUS = 1U << 10,
|
||||
BUTTON_MINUS = 1U << 11,
|
||||
BUTTON_LEFT = 1U << 12,
|
||||
BUTTON_UP = 1U << 13,
|
||||
BUTTON_RIGHT = 1U << 14,
|
||||
BUTTON_DOWN = 1U << 15,
|
||||
BUTTON_SL = 1U << 16,
|
||||
BUTTON_SR = 1U << 17,
|
||||
BUTTON_HOME = 1U << 18,
|
||||
BUTTON_CAPTURE = 1U << 19,
|
||||
};
|
||||
|
||||
struct TasAnalog {
|
||||
float x{};
|
||||
float y{};
|
||||
};
|
||||
|
||||
enum class TasState {
|
||||
Running,
|
||||
Recording,
|
||||
Stopped,
|
||||
};
|
||||
|
||||
class Tas final : public InputEngine {
|
||||
public:
|
||||
explicit Tas(std::string input_engine_);
|
||||
~Tas() override;
|
||||
|
||||
/**
|
||||
* Changes the input status that will be stored in each frame
|
||||
* @param buttons Bitfield with the status of the buttons
|
||||
* @param left_axis Value of the left axis
|
||||
* @param right_axis Value of the right axis
|
||||
*/
|
||||
void RecordInput(u64 buttons, TasAnalog left_axis, TasAnalog right_axis);
|
||||
|
||||
// Main loop that records or executes input
|
||||
void UpdateThread();
|
||||
|
||||
// Sets the flag to start or stop the TAS command execution and swaps controllers profiles
|
||||
void StartStop();
|
||||
|
||||
// Stop the TAS and reverts any controller profile
|
||||
void Stop();
|
||||
|
||||
// Sets the flag to reload the file and start from the beginning in the next update
|
||||
void Reset();
|
||||
|
||||
/**
|
||||
* Sets the flag to enable or disable recording of inputs
|
||||
* @returns true if the current recording status is enabled
|
||||
*/
|
||||
bool Record();
|
||||
|
||||
/**
|
||||
* Saves contents of record_commands on a file
|
||||
* @param overwrite_file Indicates if player 1 should be overwritten
|
||||
*/
|
||||
void SaveRecording(bool overwrite_file);
|
||||
|
||||
/**
|
||||
* Returns the current status values of TAS playback/recording
|
||||
* @returns A Tuple of
|
||||
* TasState indicating the current state out of Running ;
|
||||
* Current playback progress ;
|
||||
* Total length of script file currently loaded or being recorded
|
||||
*/
|
||||
std::tuple<TasState, size_t, std::array<size_t, PLAYER_NUMBER>> GetStatus() const;
|
||||
|
||||
private:
|
||||
enum class TasAxis : u8;
|
||||
|
||||
struct TASCommand {
|
||||
u64 buttons{};
|
||||
TasAnalog l_axis{};
|
||||
TasAnalog r_axis{};
|
||||
};
|
||||
|
||||
/// Loads TAS files from all players
|
||||
void LoadTasFiles();
|
||||
|
||||
/**
|
||||
* Loads TAS file from the specified player
|
||||
* @param player_index Player number to save the script
|
||||
* @param file_index Script number of the file
|
||||
*/
|
||||
void LoadTasFile(size_t player_index, size_t file_index);
|
||||
|
||||
/**
|
||||
* Writes a TAS file from the recorded commands
|
||||
* @param file_name Name of the file to be written
|
||||
*/
|
||||
void WriteTasFile(std::u8string_view file_name);
|
||||
|
||||
/**
|
||||
* Parses a string containing the axis values. X and Y have a range from -32767 to 32767
|
||||
* @param line String containing axis values with the following format "x;y"
|
||||
* @returns A TAS analog object with axis values with range from -1.0 to 1.0
|
||||
*/
|
||||
TasAnalog ReadCommandAxis(const std::string& line) const;
|
||||
|
||||
/**
|
||||
* Parses a string containing the button values. Each button is represented by it's text format
|
||||
* specified in text_to_tas_button array
|
||||
* @param line string containing button name with the following format "a;b;c;d..."
|
||||
* @returns A u64 with each bit representing the status of a button
|
||||
*/
|
||||
u64 ReadCommandButtons(const std::string& line) const;
|
||||
|
||||
/**
|
||||
* Reset state of all players
|
||||
*/
|
||||
void ClearInput();
|
||||
|
||||
/**
|
||||
* Converts an u64 containing the button status into the text equivalent
|
||||
* @param buttons Bitfield with the status of the buttons
|
||||
* @returns A string with the name of the buttons to be written to the file
|
||||
*/
|
||||
std::string WriteCommandButtons(u64 buttons) const;
|
||||
|
||||
/**
|
||||
* Converts an TAS analog object containing the axis status into the text equivalent
|
||||
* @param analog Value of the axis
|
||||
* @returns A string with the value of the axis to be written to the file
|
||||
*/
|
||||
std::string WriteCommandAxis(TasAnalog analog) const;
|
||||
|
||||
/// Sets an axis for a particular pad to the given value.
|
||||
void SetTasAxis(const PadIdentifier& identifier, TasAxis axis, f32 value);
|
||||
|
||||
size_t script_length{0};
|
||||
bool is_recording{false};
|
||||
bool is_running{false};
|
||||
bool needs_reset{false};
|
||||
std::array<std::vector<TASCommand>, PLAYER_NUMBER> commands{};
|
||||
std::vector<TASCommand> record_commands{};
|
||||
size_t current_command{0};
|
||||
TASCommand last_input{}; // only used for recording
|
||||
};
|
||||
} // namespace InputCommon::TasInput
|
||||
107
src/input_common/drivers/touch_screen.cpp
Normal file
107
src/input_common/drivers/touch_screen.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "input_common/drivers/touch_screen.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
constexpr PadIdentifier identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
TouchScreen::TouchScreen(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
PreSetController(identifier);
|
||||
ReleaseAllTouch();
|
||||
}
|
||||
|
||||
void TouchScreen::TouchMoved(float x, float y, std::size_t finger_id) {
|
||||
const auto index = GetIndexFromFingerId(finger_id);
|
||||
if (!index) {
|
||||
// Touch doesn't exist handle it as a new one
|
||||
TouchPressed(x, y, finger_id);
|
||||
return;
|
||||
}
|
||||
const auto i = index.value();
|
||||
fingers[i].is_active = true;
|
||||
SetButton(identifier, static_cast<int>(i), true);
|
||||
SetAxis(identifier, static_cast<int>(i * 2), x);
|
||||
SetAxis(identifier, static_cast<int>(i * 2 + 1), y);
|
||||
}
|
||||
|
||||
void TouchScreen::TouchPressed(float x, float y, std::size_t finger_id) {
|
||||
if (GetIndexFromFingerId(finger_id)) {
|
||||
// Touch already exist. Just update the data
|
||||
TouchMoved(x, y, finger_id);
|
||||
return;
|
||||
}
|
||||
const auto index = GetNextFreeIndex();
|
||||
if (!index) {
|
||||
// No free entries. Ignore input
|
||||
return;
|
||||
}
|
||||
const auto i = index.value();
|
||||
fingers[i].is_enabled = true;
|
||||
fingers[i].finger_id = finger_id;
|
||||
TouchMoved(x, y, finger_id);
|
||||
}
|
||||
|
||||
void TouchScreen::TouchReleased(std::size_t finger_id) {
|
||||
const auto index = GetIndexFromFingerId(finger_id);
|
||||
if (!index) {
|
||||
return;
|
||||
}
|
||||
const auto i = index.value();
|
||||
fingers[i].is_enabled = false;
|
||||
SetButton(identifier, static_cast<int>(i), false);
|
||||
SetAxis(identifier, static_cast<int>(i * 2), 0.0f);
|
||||
SetAxis(identifier, static_cast<int>(i * 2 + 1), 0.0f);
|
||||
}
|
||||
|
||||
std::optional<std::size_t> TouchScreen::GetIndexFromFingerId(std::size_t finger_id) const {
|
||||
for (std::size_t index = 0; index < MAX_FINGER_COUNT; ++index) {
|
||||
const auto& finger = fingers[index];
|
||||
if (!finger.is_enabled) {
|
||||
continue;
|
||||
}
|
||||
if (finger.finger_id == finger_id) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::size_t> TouchScreen::GetNextFreeIndex() const {
|
||||
for (std::size_t index = 0; index < MAX_FINGER_COUNT; ++index) {
|
||||
if (!fingers[index].is_enabled) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void TouchScreen::ClearActiveFlag() {
|
||||
for (auto& finger : fingers) {
|
||||
finger.is_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TouchScreen::ReleaseInactiveTouch() {
|
||||
for (const auto& finger : fingers) {
|
||||
if (!finger.is_active) {
|
||||
TouchReleased(finger.finger_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TouchScreen::ReleaseAllTouch() {
|
||||
for (const auto& finger : fingers) {
|
||||
if (finger.is_enabled) {
|
||||
TouchReleased(finger.finger_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
67
src/input_common/drivers/touch_screen.h
Normal file
67
src/input_common/drivers/touch_screen.h
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* A touch device factory representing a touch screen. It receives touch events and forward them
|
||||
* to all touch devices it created.
|
||||
*/
|
||||
class TouchScreen final : public InputEngine {
|
||||
public:
|
||||
explicit TouchScreen(std::string input_engine_);
|
||||
|
||||
/**
|
||||
* Signals that touch has moved and marks this touch point as active
|
||||
* @param x new horizontal position
|
||||
* @param y new vertical position
|
||||
* @param finger_id of the touch point to be updated
|
||||
*/
|
||||
void TouchMoved(float x, float y, std::size_t finger_id);
|
||||
|
||||
/**
|
||||
* Signals and creates a new touch point with this finger id
|
||||
* @param x starting horizontal position
|
||||
* @param y starting vertical position
|
||||
* @param finger_id to be assigned to the new touch point
|
||||
*/
|
||||
void TouchPressed(float x, float y, std::size_t finger_id);
|
||||
|
||||
/**
|
||||
* Signals and resets the touch point related to the this finger id
|
||||
* @param finger_id to be released
|
||||
*/
|
||||
void TouchReleased(std::size_t finger_id);
|
||||
|
||||
/// Resets the active flag for each touch point
|
||||
void ClearActiveFlag();
|
||||
|
||||
/// Releases all touch that haven't been marked as active
|
||||
void ReleaseInactiveTouch();
|
||||
|
||||
/// Resets all inputs to their initial value
|
||||
void ReleaseAllTouch();
|
||||
|
||||
private:
|
||||
static constexpr std::size_t MAX_FINGER_COUNT = 16;
|
||||
|
||||
struct TouchStatus {
|
||||
std::size_t finger_id{};
|
||||
bool is_enabled{};
|
||||
bool is_active{};
|
||||
};
|
||||
|
||||
std::optional<std::size_t> GetIndexFromFingerId(std::size_t finger_id) const;
|
||||
|
||||
std::optional<std::size_t> GetNextFreeIndex() const;
|
||||
|
||||
std::array<TouchStatus, MAX_FINGER_COUNT> fingers{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
653
src/input_common/drivers/udp_client.cpp
Normal file
653
src/input_common/drivers/udp_client.cpp
Normal file
@@ -0,0 +1,653 @@
|
||||
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <random>
|
||||
#include <boost/asio.hpp>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "common/param_package.h"
|
||||
#include "common/settings.h"
|
||||
#include "input_common/drivers/udp_client.h"
|
||||
#include "input_common/helpers/udp_protocol.h"
|
||||
|
||||
using boost::asio::ip::udp;
|
||||
|
||||
namespace InputCommon::CemuhookUDP {
|
||||
|
||||
struct SocketCallback {
|
||||
std::function<void(Response::Version)> version;
|
||||
std::function<void(Response::PortInfo)> port_info;
|
||||
std::function<void(Response::PadData)> pad_data;
|
||||
};
|
||||
|
||||
class Socket {
|
||||
public:
|
||||
using clock = std::chrono::system_clock;
|
||||
|
||||
explicit Socket(const std::string& host, u16 port, SocketCallback callback_)
|
||||
: callback(std::move(callback_)), timer(io_service),
|
||||
socket(io_service, udp::endpoint(udp::v4(), 0)), client_id(GenerateRandomClientId()) {
|
||||
boost::system::error_code ec{};
|
||||
auto ipv4 = boost::asio::ip::make_address_v4(host, ec);
|
||||
if (ec.value() != boost::system::errc::success) {
|
||||
LOG_ERROR(Input, "Invalid IPv4 address \"{}\" provided to socket", host);
|
||||
ipv4 = boost::asio::ip::address_v4{};
|
||||
}
|
||||
|
||||
send_endpoint = {udp::endpoint(ipv4, port)};
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
io_service.stop();
|
||||
}
|
||||
|
||||
void Loop() {
|
||||
io_service.run();
|
||||
}
|
||||
|
||||
void StartSend(const clock::time_point& from) {
|
||||
timer.expires_at(from + std::chrono::seconds(3));
|
||||
timer.async_wait([this](const boost::system::error_code& error) { HandleSend(error); });
|
||||
}
|
||||
|
||||
void StartReceive() {
|
||||
socket.async_receive_from(
|
||||
boost::asio::buffer(receive_buffer), receive_endpoint,
|
||||
[this](const boost::system::error_code& error, std::size_t bytes_transferred) {
|
||||
HandleReceive(error, bytes_transferred);
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
u32 GenerateRandomClientId() const {
|
||||
std::random_device device;
|
||||
return device();
|
||||
}
|
||||
|
||||
void HandleReceive(const boost::system::error_code&, std::size_t bytes_transferred) {
|
||||
if (auto type = Response::Validate(receive_buffer.data(), bytes_transferred)) {
|
||||
switch (*type) {
|
||||
case Type::Version: {
|
||||
Response::Version version;
|
||||
std::memcpy(&version, &receive_buffer[sizeof(Header)], sizeof(Response::Version));
|
||||
callback.version(std::move(version));
|
||||
break;
|
||||
}
|
||||
case Type::PortInfo: {
|
||||
Response::PortInfo port_info;
|
||||
std::memcpy(&port_info, &receive_buffer[sizeof(Header)],
|
||||
sizeof(Response::PortInfo));
|
||||
callback.port_info(std::move(port_info));
|
||||
break;
|
||||
}
|
||||
case Type::PadData: {
|
||||
Response::PadData pad_data;
|
||||
std::memcpy(&pad_data, &receive_buffer[sizeof(Header)], sizeof(Response::PadData));
|
||||
callback.pad_data(std::move(pad_data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
StartReceive();
|
||||
}
|
||||
|
||||
void HandleSend(const boost::system::error_code&) {
|
||||
boost::system::error_code _ignored{};
|
||||
// Send a request for getting port info for the pad
|
||||
const Request::PortInfo port_info{4, {0, 1, 2, 3}};
|
||||
const auto port_message = Request::Create(port_info, client_id);
|
||||
std::memcpy(&send_buffer1, &port_message, PORT_INFO_SIZE);
|
||||
socket.send_to(boost::asio::buffer(send_buffer1), send_endpoint, {}, _ignored);
|
||||
|
||||
// Send a request for getting pad data for the pad
|
||||
const Request::PadData pad_data{
|
||||
Request::RegisterFlags::AllPads,
|
||||
0,
|
||||
EMPTY_MAC_ADDRESS,
|
||||
};
|
||||
const auto pad_message = Request::Create(pad_data, client_id);
|
||||
std::memcpy(send_buffer2.data(), &pad_message, PAD_DATA_SIZE);
|
||||
socket.send_to(boost::asio::buffer(send_buffer2), send_endpoint, {}, _ignored);
|
||||
StartSend(timer.expiry());
|
||||
}
|
||||
|
||||
SocketCallback callback;
|
||||
boost::asio::io_service io_service;
|
||||
boost::asio::basic_waitable_timer<clock> timer;
|
||||
udp::socket socket;
|
||||
|
||||
const u32 client_id;
|
||||
|
||||
static constexpr std::size_t PORT_INFO_SIZE = sizeof(Message<Request::PortInfo>);
|
||||
static constexpr std::size_t PAD_DATA_SIZE = sizeof(Message<Request::PadData>);
|
||||
std::array<u8, PORT_INFO_SIZE> send_buffer1;
|
||||
std::array<u8, PAD_DATA_SIZE> send_buffer2;
|
||||
udp::endpoint send_endpoint;
|
||||
|
||||
std::array<u8, MAX_PACKET_SIZE> receive_buffer;
|
||||
udp::endpoint receive_endpoint;
|
||||
};
|
||||
|
||||
static void SocketLoop(Socket* socket) {
|
||||
socket->StartReceive();
|
||||
socket->StartSend(Socket::clock::now());
|
||||
socket->Loop();
|
||||
}
|
||||
|
||||
UDPClient::UDPClient(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
LOG_INFO(Input, "Udp Initialization started");
|
||||
ReloadSockets();
|
||||
}
|
||||
|
||||
UDPClient::~UDPClient() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
UDPClient::ClientConnection::ClientConnection() = default;
|
||||
|
||||
UDPClient::ClientConnection::~ClientConnection() = default;
|
||||
|
||||
void UDPClient::ReloadSockets() {
|
||||
Reset();
|
||||
|
||||
std::stringstream servers_ss(Settings::values.udp_input_servers.GetValue());
|
||||
std::string server_token;
|
||||
std::size_t client = 0;
|
||||
while (std::getline(servers_ss, server_token, ',')) {
|
||||
if (client == MAX_UDP_CLIENTS) {
|
||||
break;
|
||||
}
|
||||
std::stringstream server_ss(server_token);
|
||||
std::string token;
|
||||
std::getline(server_ss, token, ':');
|
||||
std::string udp_input_address = token;
|
||||
std::getline(server_ss, token, ':');
|
||||
char* temp;
|
||||
const u16 udp_input_port = static_cast<u16>(std::strtol(token.c_str(), &temp, 0));
|
||||
if (*temp != '\0') {
|
||||
LOG_ERROR(Input, "Port number is not valid {}", token);
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t client_number = GetClientNumber(udp_input_address, udp_input_port);
|
||||
if (client_number != MAX_UDP_CLIENTS) {
|
||||
LOG_ERROR(Input, "Duplicated UDP servers found");
|
||||
continue;
|
||||
}
|
||||
StartCommunication(client++, udp_input_address, udp_input_port);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t UDPClient::GetClientNumber(std::string_view host, u16 port) const {
|
||||
for (std::size_t client = 0; client < clients.size(); client++) {
|
||||
if (clients[client].active == -1) {
|
||||
continue;
|
||||
}
|
||||
if (clients[client].host == host && clients[client].port == port) {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
return MAX_UDP_CLIENTS;
|
||||
}
|
||||
|
||||
Common::Input::BatteryLevel UDPClient::GetBatteryLevel(Response::Battery battery) const {
|
||||
switch (battery) {
|
||||
case Response::Battery::Dying:
|
||||
return Common::Input::BatteryLevel::Empty;
|
||||
case Response::Battery::Low:
|
||||
return Common::Input::BatteryLevel::Critical;
|
||||
case Response::Battery::Medium:
|
||||
return Common::Input::BatteryLevel::Low;
|
||||
case Response::Battery::High:
|
||||
return Common::Input::BatteryLevel::Medium;
|
||||
case Response::Battery::Full:
|
||||
case Response::Battery::Charged:
|
||||
return Common::Input::BatteryLevel::Full;
|
||||
case Response::Battery::Charging:
|
||||
default:
|
||||
return Common::Input::BatteryLevel::Charging;
|
||||
}
|
||||
}
|
||||
|
||||
void UDPClient::OnVersion([[maybe_unused]] Response::Version data) {
|
||||
LOG_TRACE(Input, "Version packet received: {}", data.version);
|
||||
}
|
||||
|
||||
void UDPClient::OnPortInfo([[maybe_unused]] Response::PortInfo data) {
|
||||
LOG_TRACE(Input, "PortInfo packet received: {}", data.model);
|
||||
}
|
||||
|
||||
void UDPClient::OnPadData(Response::PadData data, std::size_t client) {
|
||||
const std::size_t pad_index = (client * PADS_PER_CLIENT) + data.info.id;
|
||||
|
||||
if (pad_index >= pads.size()) {
|
||||
LOG_ERROR(Input, "Invalid pad id {}", data.info.id);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_TRACE(Input, "PadData packet received");
|
||||
if (data.packet_counter == pads[pad_index].packet_sequence) {
|
||||
LOG_WARNING(
|
||||
Input,
|
||||
"PadData packet dropped because its stale info. Current count: {} Packet count: {}",
|
||||
pads[pad_index].packet_sequence, data.packet_counter);
|
||||
pads[pad_index].connected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
clients[client].active = 1;
|
||||
pads[pad_index].connected = true;
|
||||
pads[pad_index].packet_sequence = data.packet_counter;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto time_difference = static_cast<u64>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now - pads[pad_index].last_update)
|
||||
.count());
|
||||
pads[pad_index].last_update = now;
|
||||
|
||||
// Gyroscope values are not it the correct scale from better joy.
|
||||
// Dividing by 312 allows us to make one full turn = 1 turn
|
||||
// This must be a configurable valued called sensitivity
|
||||
const float gyro_scale = 1.0f / 312.0f;
|
||||
|
||||
const BasicMotion motion{
|
||||
.gyro_x = data.gyro.pitch * gyro_scale,
|
||||
.gyro_y = data.gyro.roll * gyro_scale,
|
||||
.gyro_z = -data.gyro.yaw * gyro_scale,
|
||||
.accel_x = data.accel.x,
|
||||
.accel_y = -data.accel.z,
|
||||
.accel_z = data.accel.y,
|
||||
.delta_timestamp = time_difference,
|
||||
};
|
||||
const PadIdentifier identifier = GetPadIdentifier(pad_index);
|
||||
SetMotion(identifier, 0, motion);
|
||||
|
||||
for (std::size_t id = 0; id < data.touch.size(); ++id) {
|
||||
const auto touch_pad = data.touch[id];
|
||||
const auto touch_axis_x_id =
|
||||
static_cast<int>(id == 0 ? PadAxes::Touch1X : PadAxes::Touch2X);
|
||||
const auto touch_axis_y_id =
|
||||
static_cast<int>(id == 0 ? PadAxes::Touch1Y : PadAxes::Touch2Y);
|
||||
const auto touch_button_id =
|
||||
static_cast<int>(id == 0 ? PadButton::Touch1 : PadButton::Touch2);
|
||||
|
||||
// TODO: Use custom calibration per device
|
||||
const Common::ParamPackage touch_param(Settings::values.touch_device.GetValue());
|
||||
const u16 min_x = static_cast<u16>(touch_param.Get("min_x", 100));
|
||||
const u16 min_y = static_cast<u16>(touch_param.Get("min_y", 50));
|
||||
const u16 max_x = static_cast<u16>(touch_param.Get("max_x", 1800));
|
||||
const u16 max_y = static_cast<u16>(touch_param.Get("max_y", 850));
|
||||
|
||||
const f32 x =
|
||||
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.x), min_x, max_x) - min_x) /
|
||||
static_cast<f32>(max_x - min_x);
|
||||
const f32 y =
|
||||
static_cast<f32>(std::clamp(static_cast<u16>(touch_pad.y), min_y, max_y) - min_y) /
|
||||
static_cast<f32>(max_y - min_y);
|
||||
|
||||
if (touch_pad.is_active) {
|
||||
SetAxis(identifier, touch_axis_x_id, x);
|
||||
SetAxis(identifier, touch_axis_y_id, y);
|
||||
SetButton(identifier, touch_button_id, true);
|
||||
continue;
|
||||
}
|
||||
SetAxis(identifier, touch_axis_x_id, 0);
|
||||
SetAxis(identifier, touch_axis_y_id, 0);
|
||||
SetButton(identifier, touch_button_id, false);
|
||||
}
|
||||
|
||||
SetAxis(identifier, static_cast<int>(PadAxes::LeftStickX),
|
||||
(data.left_stick_x - 127.0f) / 127.0f);
|
||||
SetAxis(identifier, static_cast<int>(PadAxes::LeftStickY),
|
||||
(data.left_stick_y - 127.0f) / 127.0f);
|
||||
SetAxis(identifier, static_cast<int>(PadAxes::RightStickX),
|
||||
(data.right_stick_x - 127.0f) / 127.0f);
|
||||
SetAxis(identifier, static_cast<int>(PadAxes::RightStickY),
|
||||
(data.right_stick_y - 127.0f) / 127.0f);
|
||||
|
||||
static constexpr std::array<PadButton, 16> buttons{
|
||||
PadButton::Share, PadButton::L3, PadButton::R3, PadButton::Options,
|
||||
PadButton::Up, PadButton::Right, PadButton::Down, PadButton::Left,
|
||||
PadButton::L2, PadButton::R2, PadButton::L1, PadButton::R1,
|
||||
PadButton::Triangle, PadButton::Circle, PadButton::Cross, PadButton::Square};
|
||||
|
||||
for (std::size_t i = 0; i < buttons.size(); ++i) {
|
||||
const bool button_status = (data.digital_button & (1U << i)) != 0;
|
||||
const int button = static_cast<int>(buttons[i]);
|
||||
SetButton(identifier, button, button_status);
|
||||
}
|
||||
|
||||
SetButton(identifier, static_cast<int>(PadButton::Home), data.home != 0);
|
||||
SetButton(identifier, static_cast<int>(PadButton::TouchHardPress), data.touch_hard_press != 0);
|
||||
|
||||
SetBattery(identifier, GetBatteryLevel(data.info.battery));
|
||||
}
|
||||
|
||||
void UDPClient::StartCommunication(std::size_t client, const std::string& host, u16 port) {
|
||||
SocketCallback callback{[this](Response::Version version) { OnVersion(version); },
|
||||
[this](Response::PortInfo info) { OnPortInfo(info); },
|
||||
[this, client](Response::PadData data) { OnPadData(data, client); }};
|
||||
LOG_INFO(Input, "Starting communication with UDP input server on {}:{}", host, port);
|
||||
clients[client].uuid = GetHostUUID(host);
|
||||
clients[client].host = host;
|
||||
clients[client].port = port;
|
||||
clients[client].active = 0;
|
||||
clients[client].socket = std::make_unique<Socket>(host, port, callback);
|
||||
clients[client].thread = std::thread{SocketLoop, clients[client].socket.get()};
|
||||
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
|
||||
const PadIdentifier identifier = GetPadIdentifier(client * PADS_PER_CLIENT + index);
|
||||
PreSetController(identifier);
|
||||
PreSetMotion(identifier, 0);
|
||||
}
|
||||
}
|
||||
|
||||
PadIdentifier UDPClient::GetPadIdentifier(std::size_t pad_index) const {
|
||||
const std::size_t client = pad_index / PADS_PER_CLIENT;
|
||||
return {
|
||||
.guid = clients[client].uuid,
|
||||
.port = static_cast<std::size_t>(clients[client].port),
|
||||
.pad = pad_index,
|
||||
};
|
||||
}
|
||||
|
||||
Common::UUID UDPClient::GetHostUUID(const std::string& host) const {
|
||||
const auto ip = boost::asio::ip::make_address_v4(host);
|
||||
const auto hex_host = fmt::format("00000000-0000-0000-0000-0000{:06x}", ip.to_uint());
|
||||
return Common::UUID{hex_host};
|
||||
}
|
||||
|
||||
void UDPClient::Reset() {
|
||||
for (auto& client : clients) {
|
||||
if (client.thread.joinable()) {
|
||||
client.active = -1;
|
||||
client.socket->Stop();
|
||||
client.thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> UDPClient::GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices;
|
||||
if (!Settings::values.enable_udp_controller) {
|
||||
return devices;
|
||||
}
|
||||
for (std::size_t client = 0; client < clients.size(); client++) {
|
||||
if (clients[client].active != 1) {
|
||||
continue;
|
||||
}
|
||||
for (std::size_t index = 0; index < PADS_PER_CLIENT; ++index) {
|
||||
const std::size_t pad_index = client * PADS_PER_CLIENT + index;
|
||||
if (!pads[pad_index].connected) {
|
||||
continue;
|
||||
}
|
||||
const auto pad_identifier = GetPadIdentifier(pad_index);
|
||||
Common::ParamPackage identifier{};
|
||||
identifier.Set("engine", GetEngineName());
|
||||
identifier.Set("display", fmt::format("UDP Controller {}", pad_identifier.pad));
|
||||
identifier.Set("guid", pad_identifier.guid.RawString());
|
||||
identifier.Set("port", static_cast<int>(pad_identifier.port));
|
||||
identifier.Set("pad", static_cast<int>(pad_identifier.pad));
|
||||
devices.emplace_back(identifier);
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
ButtonMapping UDPClient::GetButtonMappingForDevice(const Common::ParamPackage& params) {
|
||||
// This list excludes any button that can't be really mapped
|
||||
static constexpr std::array<std::pair<Settings::NativeButton::Values, PadButton>, 22>
|
||||
switch_to_dsu_button = {
|
||||
std::pair{Settings::NativeButton::A, PadButton::Circle},
|
||||
{Settings::NativeButton::B, PadButton::Cross},
|
||||
{Settings::NativeButton::X, PadButton::Triangle},
|
||||
{Settings::NativeButton::Y, PadButton::Square},
|
||||
{Settings::NativeButton::Plus, PadButton::Options},
|
||||
{Settings::NativeButton::Minus, PadButton::Share},
|
||||
{Settings::NativeButton::DLeft, PadButton::Left},
|
||||
{Settings::NativeButton::DUp, PadButton::Up},
|
||||
{Settings::NativeButton::DRight, PadButton::Right},
|
||||
{Settings::NativeButton::DDown, PadButton::Down},
|
||||
{Settings::NativeButton::L, PadButton::L1},
|
||||
{Settings::NativeButton::R, PadButton::R1},
|
||||
{Settings::NativeButton::ZL, PadButton::L2},
|
||||
{Settings::NativeButton::ZR, PadButton::R2},
|
||||
{Settings::NativeButton::SLLeft, PadButton::L2},
|
||||
{Settings::NativeButton::SRLeft, PadButton::R2},
|
||||
{Settings::NativeButton::SLRight, PadButton::L2},
|
||||
{Settings::NativeButton::SRRight, PadButton::R2},
|
||||
{Settings::NativeButton::LStick, PadButton::L3},
|
||||
{Settings::NativeButton::RStick, PadButton::R3},
|
||||
{Settings::NativeButton::Home, PadButton::Home},
|
||||
{Settings::NativeButton::Screenshot, PadButton::TouchHardPress},
|
||||
};
|
||||
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
ButtonMapping mapping{};
|
||||
for (const auto& [switch_button, dsu_button] : switch_to_dsu_button) {
|
||||
Common::ParamPackage button_params{};
|
||||
button_params.Set("engine", GetEngineName());
|
||||
button_params.Set("guid", params.Get("guid", ""));
|
||||
button_params.Set("port", params.Get("port", 0));
|
||||
button_params.Set("pad", params.Get("pad", 0));
|
||||
button_params.Set("button", static_cast<int>(dsu_button));
|
||||
mapping.insert_or_assign(switch_button, std::move(button_params));
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
AnalogMapping UDPClient::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
AnalogMapping mapping = {};
|
||||
Common::ParamPackage left_analog_params;
|
||||
left_analog_params.Set("engine", GetEngineName());
|
||||
left_analog_params.Set("guid", params.Get("guid", ""));
|
||||
left_analog_params.Set("port", params.Get("port", 0));
|
||||
left_analog_params.Set("pad", params.Get("pad", 0));
|
||||
left_analog_params.Set("axis_x", static_cast<int>(PadAxes::LeftStickX));
|
||||
left_analog_params.Set("axis_y", static_cast<int>(PadAxes::LeftStickY));
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
|
||||
Common::ParamPackage right_analog_params;
|
||||
right_analog_params.Set("engine", GetEngineName());
|
||||
right_analog_params.Set("guid", params.Get("guid", ""));
|
||||
right_analog_params.Set("port", params.Get("port", 0));
|
||||
right_analog_params.Set("pad", params.Get("pad", 0));
|
||||
right_analog_params.Set("axis_x", static_cast<int>(PadAxes::RightStickX));
|
||||
right_analog_params.Set("axis_y", static_cast<int>(PadAxes::RightStickY));
|
||||
mapping.insert_or_assign(Settings::NativeAnalog::RStick, std::move(right_analog_params));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
MotionMapping UDPClient::GetMotionMappingForDevice(const Common::ParamPackage& params) {
|
||||
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
MotionMapping mapping = {};
|
||||
Common::ParamPackage left_motion_params;
|
||||
left_motion_params.Set("engine", GetEngineName());
|
||||
left_motion_params.Set("guid", params.Get("guid", ""));
|
||||
left_motion_params.Set("port", params.Get("port", 0));
|
||||
left_motion_params.Set("pad", params.Get("pad", 0));
|
||||
left_motion_params.Set("motion", 0);
|
||||
|
||||
Common::ParamPackage right_motion_params;
|
||||
right_motion_params.Set("engine", GetEngineName());
|
||||
right_motion_params.Set("guid", params.Get("guid", ""));
|
||||
right_motion_params.Set("port", params.Get("port", 0));
|
||||
right_motion_params.Set("pad", params.Get("pad", 0));
|
||||
right_motion_params.Set("motion", 0);
|
||||
|
||||
mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params));
|
||||
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_motion_params));
|
||||
return mapping;
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames UDPClient::GetUIButtonName(const Common::ParamPackage& params) const {
|
||||
PadButton button = static_cast<PadButton>(params.Get("button", 0));
|
||||
switch (button) {
|
||||
case PadButton::Left:
|
||||
return Common::Input::ButtonNames::ButtonLeft;
|
||||
case PadButton::Right:
|
||||
return Common::Input::ButtonNames::ButtonRight;
|
||||
case PadButton::Down:
|
||||
return Common::Input::ButtonNames::ButtonDown;
|
||||
case PadButton::Up:
|
||||
return Common::Input::ButtonNames::ButtonUp;
|
||||
case PadButton::L1:
|
||||
return Common::Input::ButtonNames::L1;
|
||||
case PadButton::L2:
|
||||
return Common::Input::ButtonNames::L2;
|
||||
case PadButton::L3:
|
||||
return Common::Input::ButtonNames::L3;
|
||||
case PadButton::R1:
|
||||
return Common::Input::ButtonNames::R1;
|
||||
case PadButton::R2:
|
||||
return Common::Input::ButtonNames::R2;
|
||||
case PadButton::R3:
|
||||
return Common::Input::ButtonNames::R3;
|
||||
case PadButton::Circle:
|
||||
return Common::Input::ButtonNames::Circle;
|
||||
case PadButton::Cross:
|
||||
return Common::Input::ButtonNames::Cross;
|
||||
case PadButton::Square:
|
||||
return Common::Input::ButtonNames::Square;
|
||||
case PadButton::Triangle:
|
||||
return Common::Input::ButtonNames::Triangle;
|
||||
case PadButton::Share:
|
||||
return Common::Input::ButtonNames::Share;
|
||||
case PadButton::Options:
|
||||
return Common::Input::ButtonNames::Options;
|
||||
case PadButton::Home:
|
||||
return Common::Input::ButtonNames::Home;
|
||||
case PadButton::Touch1:
|
||||
case PadButton::Touch2:
|
||||
case PadButton::TouchHardPress:
|
||||
return Common::Input::ButtonNames::Touch;
|
||||
default:
|
||||
return Common::Input::ButtonNames::Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames UDPClient::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;
|
||||
}
|
||||
|
||||
bool UDPClient::IsStickInverted(const Common::ParamPackage& params) {
|
||||
if (!params.Has("guid") || !params.Has("port") || !params.Has("pad")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto x_axis = static_cast<PadAxes>(params.Get("axis_x", 0));
|
||||
const auto y_axis = static_cast<PadAxes>(params.Get("axis_y", 0));
|
||||
if (x_axis != PadAxes::LeftStickY && x_axis != PadAxes::RightStickY) {
|
||||
return false;
|
||||
}
|
||||
if (y_axis != PadAxes::LeftStickX && y_axis != PadAxes::RightStickX) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void TestCommunication(const std::string& host, u16 port,
|
||||
const std::function<void()>& success_callback,
|
||||
const std::function<void()>& failure_callback) {
|
||||
std::thread([=] {
|
||||
Common::Event success_event;
|
||||
SocketCallback callback{
|
||||
.version = [](Response::Version) {},
|
||||
.port_info = [](Response::PortInfo) {},
|
||||
.pad_data = [&](Response::PadData) { success_event.Set(); },
|
||||
};
|
||||
Socket socket{host, port, std::move(callback)};
|
||||
std::thread worker_thread{SocketLoop, &socket};
|
||||
const bool result =
|
||||
success_event.WaitUntil(std::chrono::steady_clock::now() + std::chrono::seconds(10));
|
||||
socket.Stop();
|
||||
worker_thread.join();
|
||||
if (result) {
|
||||
success_callback();
|
||||
} else {
|
||||
failure_callback();
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
|
||||
CalibrationConfigurationJob::CalibrationConfigurationJob(
|
||||
const std::string& host, u16 port, std::function<void(Status)> status_callback,
|
||||
std::function<void(u16, u16, u16, u16)> data_callback) {
|
||||
|
||||
std::thread([=, this] {
|
||||
u16 min_x{UINT16_MAX};
|
||||
u16 min_y{UINT16_MAX};
|
||||
u16 max_x{};
|
||||
u16 max_y{};
|
||||
|
||||
Status current_status{Status::Initialized};
|
||||
SocketCallback callback{[](Response::Version) {}, [](Response::PortInfo) {},
|
||||
[&](Response::PadData data) {
|
||||
constexpr u16 CALIBRATION_THRESHOLD = 100;
|
||||
|
||||
if (current_status == Status::Initialized) {
|
||||
// Receiving data means the communication is ready now
|
||||
current_status = Status::Ready;
|
||||
status_callback(current_status);
|
||||
}
|
||||
if (data.touch[0].is_active == 0) {
|
||||
return;
|
||||
}
|
||||
LOG_DEBUG(Input, "Current touch: {} {}", data.touch[0].x,
|
||||
data.touch[0].y);
|
||||
min_x = std::min(min_x, static_cast<u16>(data.touch[0].x));
|
||||
min_y = std::min(min_y, static_cast<u16>(data.touch[0].y));
|
||||
if (current_status == Status::Ready) {
|
||||
// First touch - min data (min_x/min_y)
|
||||
current_status = Status::Stage1Completed;
|
||||
status_callback(current_status);
|
||||
}
|
||||
if (data.touch[0].x - min_x > CALIBRATION_THRESHOLD &&
|
||||
data.touch[0].y - min_y > CALIBRATION_THRESHOLD) {
|
||||
// Set the current position as max value and finishes
|
||||
// configuration
|
||||
max_x = data.touch[0].x;
|
||||
max_y = data.touch[0].y;
|
||||
current_status = Status::Completed;
|
||||
data_callback(min_x, min_y, max_x, max_y);
|
||||
status_callback(current_status);
|
||||
|
||||
complete_event.Set();
|
||||
}
|
||||
}};
|
||||
Socket socket{host, port, std::move(callback)};
|
||||
std::thread worker_thread{SocketLoop, &socket};
|
||||
complete_event.Wait();
|
||||
socket.Stop();
|
||||
worker_thread.join();
|
||||
}).detach();
|
||||
}
|
||||
|
||||
CalibrationConfigurationJob::~CalibrationConfigurationJob() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void CalibrationConfigurationJob::Stop() {
|
||||
complete_event.Set();
|
||||
}
|
||||
|
||||
} // namespace InputCommon::CemuhookUDP
|
||||
192
src/input_common/drivers/udp_client.h
Normal file
192
src/input_common/drivers/udp_client.h
Normal file
@@ -0,0 +1,192 @@
|
||||
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/thread.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon::CemuhookUDP {
|
||||
|
||||
class Socket;
|
||||
|
||||
namespace Response {
|
||||
enum class Battery : u8;
|
||||
struct PadData;
|
||||
struct PortInfo;
|
||||
struct TouchPad;
|
||||
struct Version;
|
||||
} // namespace Response
|
||||
|
||||
enum class PadTouch {
|
||||
Click,
|
||||
Undefined,
|
||||
};
|
||||
|
||||
struct UDPPadStatus {
|
||||
std::string host{"127.0.0.1"};
|
||||
u16 port{26760};
|
||||
std::size_t pad_index{};
|
||||
};
|
||||
|
||||
struct DeviceStatus {
|
||||
std::mutex update_mutex;
|
||||
|
||||
// calibration data for scaling the device's touch area to 3ds
|
||||
struct CalibrationData {
|
||||
u16 min_x{};
|
||||
u16 min_y{};
|
||||
u16 max_x{};
|
||||
u16 max_y{};
|
||||
};
|
||||
std::optional<CalibrationData> touch_calibration;
|
||||
};
|
||||
|
||||
/**
|
||||
* A button device factory representing a keyboard. It receives keyboard events and forward them
|
||||
* to all button devices it created.
|
||||
*/
|
||||
class UDPClient final : public InputEngine {
|
||||
public:
|
||||
explicit UDPClient(std::string input_engine_);
|
||||
~UDPClient() override;
|
||||
|
||||
void ReloadSockets();
|
||||
|
||||
/// Used for automapping features
|
||||
std::vector<Common::ParamPackage> GetInputDevices() const override;
|
||||
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
|
||||
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
|
||||
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
|
||||
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
|
||||
|
||||
bool IsStickInverted(const Common::ParamPackage& params) override;
|
||||
|
||||
private:
|
||||
enum class PadButton {
|
||||
Undefined = 0x0000,
|
||||
Share = 0x0001,
|
||||
L3 = 0x0002,
|
||||
R3 = 0x0004,
|
||||
Options = 0x0008,
|
||||
Up = 0x0010,
|
||||
Right = 0x0020,
|
||||
Down = 0x0040,
|
||||
Left = 0x0080,
|
||||
L2 = 0x0100,
|
||||
R2 = 0x0200,
|
||||
L1 = 0x0400,
|
||||
R1 = 0x0800,
|
||||
Triangle = 0x1000,
|
||||
Circle = 0x2000,
|
||||
Cross = 0x4000,
|
||||
Square = 0x8000,
|
||||
Touch1 = 0x10000,
|
||||
Touch2 = 0x20000,
|
||||
Home = 0x40000,
|
||||
TouchHardPress = 0x80000,
|
||||
};
|
||||
|
||||
enum class PadAxes : u8 {
|
||||
LeftStickX,
|
||||
LeftStickY,
|
||||
RightStickX,
|
||||
RightStickY,
|
||||
AnalogLeft,
|
||||
AnalogDown,
|
||||
AnalogRight,
|
||||
AnalogUp,
|
||||
AnalogSquare,
|
||||
AnalogCross,
|
||||
AnalogCircle,
|
||||
AnalogTriangle,
|
||||
AnalogR1,
|
||||
AnalogL1,
|
||||
AnalogR2,
|
||||
AnalogL3,
|
||||
AnalogR3,
|
||||
Touch1X,
|
||||
Touch1Y,
|
||||
Touch2X,
|
||||
Touch2Y,
|
||||
Undefined,
|
||||
};
|
||||
|
||||
struct PadData {
|
||||
std::size_t pad_index{};
|
||||
bool connected{};
|
||||
DeviceStatus status;
|
||||
u64 packet_sequence{};
|
||||
|
||||
std::chrono::time_point<std::chrono::steady_clock> last_update;
|
||||
};
|
||||
|
||||
struct ClientConnection {
|
||||
ClientConnection();
|
||||
~ClientConnection();
|
||||
Common::UUID uuid{"00000000-0000-0000-0000-00007F000001"};
|
||||
std::string host{"127.0.0.1"};
|
||||
u16 port{26760};
|
||||
s8 active{-1};
|
||||
std::unique_ptr<Socket> socket;
|
||||
std::thread thread;
|
||||
};
|
||||
|
||||
// For shutting down, clear all data, join all threads, release usb
|
||||
void Reset();
|
||||
|
||||
// Translates configuration to client number
|
||||
std::size_t GetClientNumber(std::string_view host, u16 port) const;
|
||||
|
||||
// Translates UDP battery level to input engine battery level
|
||||
Common::Input::BatteryLevel GetBatteryLevel(Response::Battery battery) const;
|
||||
|
||||
void OnVersion(Response::Version);
|
||||
void OnPortInfo(Response::PortInfo);
|
||||
void OnPadData(Response::PadData, std::size_t client);
|
||||
void StartCommunication(std::size_t client, const std::string& host, u16 port);
|
||||
PadIdentifier GetPadIdentifier(std::size_t pad_index) const;
|
||||
Common::UUID GetHostUUID(const std::string& host) const;
|
||||
|
||||
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
|
||||
|
||||
// Allocate clients for 8 udp servers
|
||||
static constexpr std::size_t MAX_UDP_CLIENTS = 8;
|
||||
static constexpr std::size_t PADS_PER_CLIENT = 4;
|
||||
std::array<PadData, MAX_UDP_CLIENTS * PADS_PER_CLIENT> pads{};
|
||||
std::array<ClientConnection, MAX_UDP_CLIENTS> clients{};
|
||||
};
|
||||
|
||||
/// An async job allowing configuration of the touchpad calibration.
|
||||
class CalibrationConfigurationJob {
|
||||
public:
|
||||
enum class Status {
|
||||
Initialized,
|
||||
Ready,
|
||||
Stage1Completed,
|
||||
Completed,
|
||||
};
|
||||
/**
|
||||
* Constructs and starts the job with the specified parameter.
|
||||
*
|
||||
* @param status_callback Callback for job status updates
|
||||
* @param data_callback Called when calibration data is ready
|
||||
*/
|
||||
explicit CalibrationConfigurationJob(const std::string& host, u16 port,
|
||||
std::function<void(Status)> status_callback,
|
||||
std::function<void(u16, u16, u16, u16)> data_callback);
|
||||
~CalibrationConfigurationJob();
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
Common::Event complete_event;
|
||||
};
|
||||
|
||||
void TestCommunication(const std::string& host, u16 port,
|
||||
const std::function<void()>& success_callback,
|
||||
const std::function<void()>& failure_callback);
|
||||
|
||||
} // namespace InputCommon::CemuhookUDP
|
||||
257
src/input_common/drivers/virtual_amiibo.cpp
Normal file
257
src/input_common/drivers/virtual_amiibo.cpp
Normal file
@@ -0,0 +1,257 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/settings.h"
|
||||
#include "input_common/drivers/virtual_amiibo.h"
|
||||
|
||||
namespace InputCommon {
|
||||
constexpr PadIdentifier identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
VirtualAmiibo::VirtualAmiibo(std::string input_engine_) : InputEngine(std::move(input_engine_)) {}
|
||||
|
||||
VirtualAmiibo::~VirtualAmiibo() = default;
|
||||
|
||||
Common::Input::DriverResult VirtualAmiibo::SetPollingMode(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_,
|
||||
const Common::Input::PollingMode polling_mode_) {
|
||||
polling_mode = polling_mode_;
|
||||
|
||||
switch (polling_mode) {
|
||||
case Common::Input::PollingMode::NFC:
|
||||
state = State::Initialized;
|
||||
return Common::Input::DriverResult::Success;
|
||||
default:
|
||||
if (state == State::TagNearby) {
|
||||
CloseAmiibo();
|
||||
}
|
||||
state = State::Disabled;
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::NfcState VirtualAmiibo::SupportsNfc(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_) const {
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
Common::Input::NfcState VirtualAmiibo::StartNfcPolling(const PadIdentifier& identifier_) {
|
||||
if (state != State::Initialized) {
|
||||
return Common::Input::NfcState::WrongDeviceState;
|
||||
}
|
||||
state = State::WaitingForAmiibo;
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
|
||||
Common::Input::NfcState VirtualAmiibo::StopNfcPolling(const PadIdentifier& identifier_) {
|
||||
if (state == State::Disabled) {
|
||||
return Common::Input::NfcState::WrongDeviceState;
|
||||
}
|
||||
if (state == State::TagNearby) {
|
||||
CloseAmiibo();
|
||||
}
|
||||
state = State::Initialized;
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
|
||||
Common::Input::NfcState VirtualAmiibo::ReadAmiiboData(const PadIdentifier& identifier_,
|
||||
std::vector<u8>& out_data) {
|
||||
if (state != State::TagNearby) {
|
||||
return Common::Input::NfcState::WrongDeviceState;
|
||||
}
|
||||
|
||||
if (status.tag_type != 1U << 1) {
|
||||
return Common::Input::NfcState::InvalidTagType;
|
||||
}
|
||||
|
||||
out_data.resize(nfc_data.size());
|
||||
memcpy(out_data.data(), nfc_data.data(), nfc_data.size());
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
|
||||
Common::Input::NfcState VirtualAmiibo::WriteNfcData(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_, const std::vector<u8>& data) {
|
||||
const Common::FS::IOFile nfc_file{file_path, Common::FS::FileAccessMode::ReadWrite,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
|
||||
if (!nfc_file.IsOpen()) {
|
||||
LOG_ERROR(Core, "Amiibo is already on use");
|
||||
return Common::Input::NfcState::WriteFailed;
|
||||
}
|
||||
|
||||
if (!nfc_file.Write(data)) {
|
||||
LOG_ERROR(Service_NFP, "Error writing to file");
|
||||
return Common::Input::NfcState::WriteFailed;
|
||||
}
|
||||
|
||||
nfc_data = data;
|
||||
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
|
||||
Common::Input::NfcState VirtualAmiibo::ReadMifareData(const PadIdentifier& identifier_,
|
||||
const Common::Input::MifareRequest& request,
|
||||
Common::Input::MifareRequest& out_data) {
|
||||
if (state != State::TagNearby) {
|
||||
return Common::Input::NfcState::WrongDeviceState;
|
||||
}
|
||||
|
||||
if (status.tag_type != 1U << 6) {
|
||||
return Common::Input::NfcState::InvalidTagType;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < request.data.size(); i++) {
|
||||
if (request.data[i].command == 0) {
|
||||
continue;
|
||||
}
|
||||
out_data.data[i].command = request.data[i].command;
|
||||
out_data.data[i].sector = request.data[i].sector;
|
||||
|
||||
const std::size_t sector_index =
|
||||
request.data[i].sector * sizeof(Common::Input::MifareData::data);
|
||||
|
||||
if (nfc_data.size() < sector_index + sizeof(Common::Input::MifareData::data)) {
|
||||
return Common::Input::NfcState::WriteFailed;
|
||||
}
|
||||
|
||||
// Ignore the sector key as we don't support it
|
||||
memcpy(out_data.data[i].data.data(), nfc_data.data() + sector_index,
|
||||
sizeof(Common::Input::MifareData::data));
|
||||
}
|
||||
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
|
||||
Common::Input::NfcState VirtualAmiibo::WriteMifareData(
|
||||
const PadIdentifier& identifier_, const Common::Input::MifareRequest& request) {
|
||||
if (state != State::TagNearby) {
|
||||
return Common::Input::NfcState::WrongDeviceState;
|
||||
}
|
||||
|
||||
if (status.tag_type != 1U << 6) {
|
||||
return Common::Input::NfcState::InvalidTagType;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < request.data.size(); i++) {
|
||||
if (request.data[i].command == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t sector_index =
|
||||
request.data[i].sector * sizeof(Common::Input::MifareData::data);
|
||||
|
||||
if (nfc_data.size() < sector_index + sizeof(Common::Input::MifareData::data)) {
|
||||
return Common::Input::NfcState::WriteFailed;
|
||||
}
|
||||
|
||||
// Ignore the sector key as we don't support it
|
||||
memcpy(nfc_data.data() + sector_index, request.data[i].data.data(),
|
||||
sizeof(Common::Input::MifareData::data));
|
||||
}
|
||||
|
||||
return Common::Input::NfcState::Success;
|
||||
}
|
||||
|
||||
VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const {
|
||||
return state;
|
||||
}
|
||||
|
||||
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
|
||||
const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
std::vector<u8> data{};
|
||||
|
||||
if (!nfc_file.IsOpen()) {
|
||||
return Info::UnableToLoad;
|
||||
}
|
||||
|
||||
switch (nfc_file.GetSize()) {
|
||||
case AmiiboSize:
|
||||
case AmiiboSizeWithoutPassword:
|
||||
case AmiiboSizeWithSignature:
|
||||
data.resize(AmiiboSize);
|
||||
if (nfc_file.Read(data) < AmiiboSizeWithoutPassword) {
|
||||
return Info::NotAnAmiibo;
|
||||
}
|
||||
break;
|
||||
case MifareSize:
|
||||
data.resize(MifareSize);
|
||||
if (nfc_file.Read(data) < MifareSize) {
|
||||
return Info::NotAnAmiibo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return Info::NotAnAmiibo;
|
||||
}
|
||||
|
||||
file_path = filename;
|
||||
return LoadAmiibo(data);
|
||||
}
|
||||
|
||||
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span<u8> data) {
|
||||
if (state != State::WaitingForAmiibo) {
|
||||
return Info::WrongDeviceState;
|
||||
}
|
||||
|
||||
switch (data.size_bytes()) {
|
||||
case AmiiboSize:
|
||||
case AmiiboSizeWithoutPassword:
|
||||
case AmiiboSizeWithSignature:
|
||||
nfc_data.resize(AmiiboSize);
|
||||
status.tag_type = 1U << 1;
|
||||
status.uuid_length = 7;
|
||||
break;
|
||||
case MifareSize:
|
||||
nfc_data.resize(MifareSize);
|
||||
status.tag_type = 1U << 6;
|
||||
status.uuid_length = 4;
|
||||
break;
|
||||
default:
|
||||
return Info::NotAnAmiibo;
|
||||
}
|
||||
|
||||
status.uuid = {};
|
||||
status.protocol = 1;
|
||||
state = State::TagNearby;
|
||||
status.state = Common::Input::NfcState::NewAmiibo,
|
||||
memcpy(nfc_data.data(), data.data(), data.size_bytes());
|
||||
memcpy(status.uuid.data(), nfc_data.data(), status.uuid_length);
|
||||
SetNfc(identifier, status);
|
||||
return Info::Success;
|
||||
}
|
||||
|
||||
VirtualAmiibo::Info VirtualAmiibo::ReloadAmiibo() {
|
||||
if (state == State::TagNearby) {
|
||||
SetNfc(identifier, status);
|
||||
return Info::Success;
|
||||
}
|
||||
|
||||
return LoadAmiibo(file_path);
|
||||
}
|
||||
|
||||
VirtualAmiibo::Info VirtualAmiibo::CloseAmiibo() {
|
||||
if (state != State::TagNearby) {
|
||||
return Info::Success;
|
||||
}
|
||||
|
||||
state = State::WaitingForAmiibo;
|
||||
status.state = Common::Input::NfcState::AmiiboRemoved;
|
||||
SetNfc(identifier, status);
|
||||
status.tag_type = 0;
|
||||
return Info::Success;
|
||||
}
|
||||
|
||||
std::string VirtualAmiibo::GetLastFilePath() const {
|
||||
return file_path;
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
78
src/input_common/drivers/virtual_amiibo.h
Normal file
78
src/input_common/drivers/virtual_amiibo.h
Normal file
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace Common::FS {
|
||||
class IOFile;
|
||||
}
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
class VirtualAmiibo final : public InputEngine {
|
||||
public:
|
||||
enum class State {
|
||||
Disabled,
|
||||
Initialized,
|
||||
WaitingForAmiibo,
|
||||
TagNearby,
|
||||
};
|
||||
|
||||
enum class Info {
|
||||
Success,
|
||||
UnableToLoad,
|
||||
NotAnAmiibo,
|
||||
WrongDeviceState,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
explicit VirtualAmiibo(std::string input_engine_);
|
||||
~VirtualAmiibo() override;
|
||||
|
||||
// Sets polling mode to a controller
|
||||
Common::Input::DriverResult SetPollingMode(
|
||||
const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) override;
|
||||
|
||||
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override;
|
||||
Common::Input::NfcState StartNfcPolling(const PadIdentifier& identifier_) override;
|
||||
Common::Input::NfcState StopNfcPolling(const PadIdentifier& identifier_) override;
|
||||
Common::Input::NfcState ReadAmiiboData(const PadIdentifier& identifier_,
|
||||
std::vector<u8>& out_data) override;
|
||||
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier_,
|
||||
const std::vector<u8>& data) override;
|
||||
Common::Input::NfcState ReadMifareData(const PadIdentifier& identifier_,
|
||||
const Common::Input::MifareRequest& data,
|
||||
Common::Input::MifareRequest& out_data) override;
|
||||
Common::Input::NfcState WriteMifareData(const PadIdentifier& identifier_,
|
||||
const Common::Input::MifareRequest& data) override;
|
||||
|
||||
State GetCurrentState() const;
|
||||
|
||||
Info LoadAmiibo(const std::string& amiibo_file);
|
||||
Info LoadAmiibo(std::span<u8> data);
|
||||
Info ReloadAmiibo();
|
||||
Info CloseAmiibo();
|
||||
|
||||
std::string GetLastFilePath() const;
|
||||
|
||||
private:
|
||||
static constexpr std::size_t AmiiboSize = 0x21C;
|
||||
static constexpr std::size_t AmiiboSizeWithoutPassword = AmiiboSize - 0x8;
|
||||
static constexpr std::size_t AmiiboSizeWithSignature = AmiiboSize + 0x20;
|
||||
static constexpr std::size_t MifareSize = 0x400;
|
||||
|
||||
std::string file_path{};
|
||||
State state{State::Disabled};
|
||||
std::vector<u8> nfc_data;
|
||||
Common::Input::NfcStatus status;
|
||||
Common::Input::PollingMode polling_mode{Common::Input::PollingMode::Passive};
|
||||
};
|
||||
} // namespace InputCommon
|
||||
94
src/input_common/drivers/virtual_gamepad.cpp
Normal file
94
src/input_common/drivers/virtual_gamepad.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "input_common/drivers/virtual_gamepad.h"
|
||||
|
||||
namespace InputCommon {
|
||||
constexpr std::size_t PlayerIndexCount = 10;
|
||||
|
||||
VirtualGamepad::VirtualGamepad(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
|
||||
PreSetController(GetIdentifier(i));
|
||||
}
|
||||
}
|
||||
|
||||
void VirtualGamepad::SetButtonState(std::size_t player_index, int button_id, bool value) {
|
||||
if (player_index > PlayerIndexCount) {
|
||||
return;
|
||||
}
|
||||
const auto identifier = GetIdentifier(player_index);
|
||||
SetButton(identifier, button_id, value);
|
||||
}
|
||||
|
||||
void VirtualGamepad::SetButtonState(std::size_t player_index, VirtualButton button_id, bool value) {
|
||||
SetButtonState(player_index, static_cast<int>(button_id), value);
|
||||
}
|
||||
|
||||
void VirtualGamepad::SetStickPosition(std::size_t player_index, int axis_id, float x_value,
|
||||
float y_value) {
|
||||
if (player_index > PlayerIndexCount) {
|
||||
return;
|
||||
}
|
||||
const auto identifier = GetIdentifier(player_index);
|
||||
SetAxis(identifier, axis_id * 2, x_value);
|
||||
SetAxis(identifier, (axis_id * 2) + 1, y_value);
|
||||
}
|
||||
|
||||
void VirtualGamepad::SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
|
||||
float y_value) {
|
||||
SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value);
|
||||
}
|
||||
|
||||
void VirtualGamepad::SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x,
|
||||
float gyro_y, float gyro_z, float accel_x, float accel_y,
|
||||
float accel_z) {
|
||||
const auto identifier = GetIdentifier(player_index);
|
||||
const BasicMotion motion_data{
|
||||
.gyro_x = gyro_x,
|
||||
.gyro_y = gyro_y,
|
||||
.gyro_z = gyro_z,
|
||||
.accel_x = accel_x,
|
||||
.accel_y = accel_y,
|
||||
.accel_z = accel_z,
|
||||
.delta_timestamp = delta_timestamp,
|
||||
};
|
||||
SetMotion(identifier, 0, motion_data);
|
||||
}
|
||||
|
||||
void VirtualGamepad::ResetControllers() {
|
||||
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
|
||||
SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f);
|
||||
SetStickPosition(i, VirtualStick::Right, 0.0f, 0.0f);
|
||||
|
||||
SetButtonState(i, VirtualButton::ButtonA, false);
|
||||
SetButtonState(i, VirtualButton::ButtonB, false);
|
||||
SetButtonState(i, VirtualButton::ButtonX, false);
|
||||
SetButtonState(i, VirtualButton::ButtonY, false);
|
||||
SetButtonState(i, VirtualButton::StickL, false);
|
||||
SetButtonState(i, VirtualButton::StickR, false);
|
||||
SetButtonState(i, VirtualButton::TriggerL, false);
|
||||
SetButtonState(i, VirtualButton::TriggerR, false);
|
||||
SetButtonState(i, VirtualButton::TriggerZL, false);
|
||||
SetButtonState(i, VirtualButton::TriggerZR, false);
|
||||
SetButtonState(i, VirtualButton::ButtonPlus, false);
|
||||
SetButtonState(i, VirtualButton::ButtonMinus, false);
|
||||
SetButtonState(i, VirtualButton::ButtonLeft, false);
|
||||
SetButtonState(i, VirtualButton::ButtonUp, false);
|
||||
SetButtonState(i, VirtualButton::ButtonRight, false);
|
||||
SetButtonState(i, VirtualButton::ButtonDown, false);
|
||||
SetButtonState(i, VirtualButton::ButtonSL, false);
|
||||
SetButtonState(i, VirtualButton::ButtonSR, false);
|
||||
SetButtonState(i, VirtualButton::ButtonHome, false);
|
||||
SetButtonState(i, VirtualButton::ButtonCapture, false);
|
||||
}
|
||||
}
|
||||
|
||||
PadIdentifier VirtualGamepad::GetIdentifier(std::size_t player_index) const {
|
||||
return {
|
||||
.guid = Common::UUID{},
|
||||
.port = player_index,
|
||||
.pad = 0,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
83
src/input_common/drivers/virtual_gamepad.h
Normal file
83
src/input_common/drivers/virtual_gamepad.h
Normal file
@@ -0,0 +1,83 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* A virtual controller that is always assigned to the game input
|
||||
*/
|
||||
class VirtualGamepad final : public InputEngine {
|
||||
public:
|
||||
enum class VirtualButton {
|
||||
ButtonA,
|
||||
ButtonB,
|
||||
ButtonX,
|
||||
ButtonY,
|
||||
StickL,
|
||||
StickR,
|
||||
TriggerL,
|
||||
TriggerR,
|
||||
TriggerZL,
|
||||
TriggerZR,
|
||||
ButtonPlus,
|
||||
ButtonMinus,
|
||||
ButtonLeft,
|
||||
ButtonUp,
|
||||
ButtonRight,
|
||||
ButtonDown,
|
||||
ButtonSL,
|
||||
ButtonSR,
|
||||
ButtonHome,
|
||||
ButtonCapture,
|
||||
};
|
||||
|
||||
enum class VirtualStick {
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
};
|
||||
|
||||
explicit VirtualGamepad(std::string input_engine_);
|
||||
|
||||
/**
|
||||
* Sets the status of all buttons bound with the key to pressed
|
||||
* @param player_index the player number that will take this action
|
||||
* @param button_id the id of the button
|
||||
* @param value indicates if the button is pressed or not
|
||||
*/
|
||||
void SetButtonState(std::size_t player_index, int button_id, bool value);
|
||||
void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value);
|
||||
|
||||
/**
|
||||
* Sets the status of a stick to a specific player index
|
||||
* @param player_index the player number that will take this action
|
||||
* @param axis_id the id of the axis to move
|
||||
* @param x_value the position of the stick in the x axis
|
||||
* @param y_value the position of the stick in the y axis
|
||||
*/
|
||||
void SetStickPosition(std::size_t player_index, int axis_id, float x_value, float y_value);
|
||||
void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
|
||||
float y_value);
|
||||
|
||||
/**
|
||||
* Sets the status of the motion sensor to a specific player index
|
||||
* @param player_index the player number that will take this action
|
||||
* @param delta_timestamp time passed since last reading
|
||||
* @param gyro_x,gyro_y,gyro_z the gyro sensor readings
|
||||
* @param accel_x,accel_y,accel_z the accelerometer reading
|
||||
*/
|
||||
void SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
|
||||
float gyro_z, float accel_x, float accel_y, float accel_z);
|
||||
|
||||
/// Restores all inputs into the neutral position
|
||||
void ResetControllers();
|
||||
|
||||
private:
|
||||
/// Returns the correct identifier corresponding to the player index
|
||||
PadIdentifier GetIdentifier(std::size_t player_index) const;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
725
src/input_common/helpers/joycon_driver.cpp
Normal file
725
src/input_common/helpers/joycon_driver.cpp
Normal file
@@ -0,0 +1,725 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/scope_exit.h"
|
||||
#include "common/swap.h"
|
||||
#include "common/thread.h"
|
||||
#include "input_common/helpers/joycon_driver.h"
|
||||
#include "input_common/helpers/joycon_protocol/calibration.h"
|
||||
#include "input_common/helpers/joycon_protocol/generic_functions.h"
|
||||
#include "input_common/helpers/joycon_protocol/irs.h"
|
||||
#include "input_common/helpers/joycon_protocol/nfc.h"
|
||||
#include "input_common/helpers/joycon_protocol/poller.h"
|
||||
#include "input_common/helpers/joycon_protocol/ringcon.h"
|
||||
#include "input_common/helpers/joycon_protocol/rumble.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
JoyconDriver::JoyconDriver(std::size_t port_) : port{port_} {
|
||||
hidapi_handle = std::make_shared<JoyconHandle>();
|
||||
}
|
||||
|
||||
JoyconDriver::~JoyconDriver() {
|
||||
Stop();
|
||||
}
|
||||
|
||||
void JoyconDriver::Stop() {
|
||||
is_connected = false;
|
||||
input_thread = {};
|
||||
}
|
||||
|
||||
Common::Input::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 Common::Input::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, "Citron can't gain access to this device: ID {:04X}:{:04X}.",
|
||||
device_info->vendor_id, device_info->product_id);
|
||||
return Common::Input::DriverResult::HandleInUse;
|
||||
}
|
||||
SDL_hid_set_nonblocking(hidapi_handle->handle, 1);
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::InitializeDevice() {
|
||||
if (!hidapi_handle->handle) {
|
||||
return Common::Input::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;
|
||||
input_only_device = false;
|
||||
gyro_sensitivity = Joycon::GyroSensitivity::DPS2000;
|
||||
gyro_performance = Joycon::GyroPerformance::HZ833;
|
||||
accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8;
|
||||
accelerometer_performance = Joycon::AccelerometerPerformance::HZ100;
|
||||
|
||||
// Initialize HW Protocols
|
||||
calibration_protocol = std::make_unique<CalibrationProtocol>(hidapi_handle);
|
||||
generic_protocol = std::make_unique<GenericProtocol>(hidapi_handle);
|
||||
irs_protocol = std::make_unique<IrsProtocol>(hidapi_handle);
|
||||
nfc_protocol = std::make_unique<NfcProtocol>(hidapi_handle);
|
||||
ring_protocol = std::make_unique<RingConProtocol>(hidapi_handle);
|
||||
rumble_protocol = std::make_unique<RumbleProtocol>(hidapi_handle);
|
||||
|
||||
// Get fixed joycon info
|
||||
if (generic_protocol->GetVersionNumber(version) != Common::Input::DriverResult::Success) {
|
||||
// If this command fails the device doesn't accept configuration commands
|
||||
input_only_device = true;
|
||||
}
|
||||
|
||||
if (!input_only_device) {
|
||||
generic_protocol->SetLowPowerMode(false);
|
||||
generic_protocol->GetColor(color);
|
||||
if (handle_device_type == ControllerType::Pro) {
|
||||
// Some 3rd party controllers aren't pro controllers
|
||||
generic_protocol->GetControllerType(device_type);
|
||||
} else {
|
||||
device_type = handle_device_type;
|
||||
}
|
||||
generic_protocol->GetSerialNumber(serial_number);
|
||||
}
|
||||
|
||||
supported_features = GetSupportedFeatures();
|
||||
|
||||
// Get Calibration data
|
||||
calibration_protocol->GetLeftJoyStickCalibration(left_stick_calibration);
|
||||
calibration_protocol->GetRightJoyStickCalibration(right_stick_calibration);
|
||||
calibration_protocol->GetImuCalibration(motion_calibration);
|
||||
|
||||
// Set led status
|
||||
generic_protocol->SetLedBlinkPattern(static_cast<u8>(1 + port));
|
||||
|
||||
// Apply HW configuration
|
||||
SetPollingMode();
|
||||
|
||||
// Initialize joycon poller
|
||||
joycon_poller = std::make_unique<JoyconPoller>(device_type, left_stick_calibration,
|
||||
right_stick_calibration, motion_calibration);
|
||||
|
||||
// Start polling 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 Common::Input::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 = 3;
|
||||
std::vector<u8> buffer(MaxBufferSize);
|
||||
|
||||
while (!stop_token.stop_requested()) {
|
||||
int status = 0;
|
||||
|
||||
if (!IsInputThreadValid()) {
|
||||
input_thread.request_stop();
|
||||
continue;
|
||||
}
|
||||
|
||||
// By disabling the input thread we can ensure custom commands will succeed as no package is
|
||||
// skipped
|
||||
if (!disable_input_thread) {
|
||||
status = SDL_hid_read_timeout(hidapi_handle->handle, buffer.data(), buffer.size(),
|
||||
ThreadDelay);
|
||||
} else {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ThreadDelay));
|
||||
}
|
||||
|
||||
if (IsPayloadCorrect(status, buffer)) {
|
||||
OnNewData(buffer);
|
||||
}
|
||||
|
||||
if (!vibration_queue.Empty()) {
|
||||
VibrationValue vibration_value;
|
||||
vibration_queue.Pop(vibration_value);
|
||||
last_vibration_result = rumble_protocol->SendVibration(vibration_value);
|
||||
}
|
||||
|
||||
// We can't keep up with vibrations. Start skipping.
|
||||
while (vibration_queue.Size() > 6) {
|
||||
vibration_queue.Pop();
|
||||
}
|
||||
|
||||
std::this_thread::yield();
|
||||
}
|
||||
|
||||
is_connected = false;
|
||||
input_thread_running = false;
|
||||
LOG_INFO(Input, "Joycon Adapter input thread stopped");
|
||||
}
|
||||
|
||||
void JoyconDriver::OnNewData(std::span<u8> buffer) {
|
||||
const auto report_mode = static_cast<ReportMode>(buffer[0]);
|
||||
|
||||
// Packages can be a little bit inconsistent. Average the delta time to provide a smoother
|
||||
// motion experience
|
||||
switch (report_mode) {
|
||||
case ReportMode::STANDARD_FULL_60HZ:
|
||||
case ReportMode::NFC_IR_MODE_60HZ:
|
||||
case ReportMode::SIMPLE_HID_MODE: {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto new_delta_time = static_cast<u64>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count());
|
||||
delta_time = ((delta_time * 8) + (new_delta_time * 2)) / 10;
|
||||
last_update = now;
|
||||
joycon_poller->UpdateColor(color);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const MotionStatus motion_status{
|
||||
.is_enabled = motion_enabled,
|
||||
.delta_time = delta_time,
|
||||
.gyro_sensitivity = gyro_sensitivity,
|
||||
.accelerometer_sensitivity = accelerometer_sensitivity,
|
||||
};
|
||||
|
||||
// TODO: Remove this when calibration is properly loaded and not calculated
|
||||
if (ring_connected && report_mode == ReportMode::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->IsPolling()) {
|
||||
if (amiibo_detected) {
|
||||
if (!nfc_protocol->HasAmiibo()) {
|
||||
joycon_poller->UpdateAmiibo({});
|
||||
amiibo_detected = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!amiibo_detected) {
|
||||
Joycon::TagInfo tag_info;
|
||||
const auto result = nfc_protocol->GetTagInfo(tag_info);
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
joycon_poller->UpdateAmiibo(tag_info);
|
||||
amiibo_detected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (report_mode) {
|
||||
case ReportMode::STANDARD_FULL_60HZ:
|
||||
joycon_poller->ReadActiveMode(buffer, motion_status, ring_status);
|
||||
break;
|
||||
case ReportMode::NFC_IR_MODE_60HZ:
|
||||
joycon_poller->ReadNfcIRMode(buffer, motion_status);
|
||||
break;
|
||||
case ReportMode::SIMPLE_HID_MODE:
|
||||
joycon_poller->ReadPassiveMode(buffer);
|
||||
break;
|
||||
case ReportMode::SUBCMD_REPLY:
|
||||
LOG_DEBUG(Input, "Unhandled command reply");
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(Input, "Report mode not Implemented {}", report_mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetPollingMode() {
|
||||
SCOPE_EXIT {
|
||||
disable_input_thread = false;
|
||||
};
|
||||
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 (input_only_device) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
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 == Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
irs_protocol->DisableIrs();
|
||||
LOG_ERROR(Input, "Error enabling IRS");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (nfc_enabled && supported_features.nfc) {
|
||||
auto result = nfc_protocol->EnableNfc();
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
nfc_protocol->DisableNfc();
|
||||
LOG_ERROR(Input, "Error enabling NFC");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (hidbus_enabled && supported_features.hidbus) {
|
||||
auto result = ring_protocol->EnableRingCon();
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = ring_protocol->StartRingconPolling();
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
ring_connected = true;
|
||||
return result;
|
||||
}
|
||||
ring_connected = false;
|
||||
ring_protocol->DisableRingCon();
|
||||
LOG_ERROR(Input, "Error enabling Ringcon");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (passive_enabled && supported_features.passive) {
|
||||
const auto result = generic_protocol->EnablePassiveMode();
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
LOG_ERROR(Input, "Error enabling passive mode");
|
||||
}
|
||||
|
||||
// Default Mode
|
||||
const auto result = generic_protocol->EnableActiveMode();
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
LOG_ERROR(Input, "Error enabling active mode");
|
||||
}
|
||||
// Switch calls this function after enabling active mode
|
||||
generic_protocol->TriggersElapsed();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() {
|
||||
SupportedFeatures features{
|
||||
.passive = true,
|
||||
.motion = true,
|
||||
.vibration = true,
|
||||
};
|
||||
|
||||
if (input_only_device) {
|
||||
return features;
|
||||
}
|
||||
|
||||
if (device_type == ControllerType::Right) {
|
||||
features.nfc = true;
|
||||
features.irs = true;
|
||||
features.hidbus = true;
|
||||
}
|
||||
|
||||
if (device_type == ControllerType::Pro) {
|
||||
features.nfc = true;
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
bool JoyconDriver::IsInputThreadValid() const {
|
||||
if (!is_connected.load()) {
|
||||
return false;
|
||||
}
|
||||
if (hidapi_handle->handle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
// Controller is not responding. Terminate connection
|
||||
if (error_counter > MaxErrorCount) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JoyconDriver::IsPayloadCorrect(int status, std::span<const u8> buffer) {
|
||||
if (status <= -1) {
|
||||
error_counter++;
|
||||
return false;
|
||||
}
|
||||
// There's no new data
|
||||
if (status == 0) {
|
||||
return false;
|
||||
}
|
||||
// No reply ever starts with zero
|
||||
if (buffer[0] == 0x00) {
|
||||
error_counter++;
|
||||
return false;
|
||||
}
|
||||
error_counter = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetVibration(const VibrationValue& vibration) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (disable_input_thread) {
|
||||
return Common::Input::DriverResult::HandleInUse;
|
||||
}
|
||||
vibration_queue.Push(vibration);
|
||||
return last_vibration_result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetLedConfig(u8 led_pattern) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (disable_input_thread) {
|
||||
return Common::Input::DriverResult::HandleInUse;
|
||||
}
|
||||
return generic_protocol->SetLedPattern(led_pattern);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetIrsConfig(IrsMode mode_, IrsResolution format_) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (disable_input_thread) {
|
||||
return Common::Input::DriverResult::HandleInUse;
|
||||
}
|
||||
disable_input_thread = true;
|
||||
const auto result = irs_protocol->SetIrsConfig(mode_, format_);
|
||||
disable_input_thread = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetPassiveMode() {
|
||||
std::scoped_lock lock{mutex};
|
||||
motion_enabled = false;
|
||||
hidbus_enabled = false;
|
||||
nfc_enabled = false;
|
||||
passive_enabled = true;
|
||||
irs_enabled = false;
|
||||
return SetPollingMode();
|
||||
}
|
||||
|
||||
Common::Input::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();
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetIrMode() {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.irs) {
|
||||
return Common::Input::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();
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetNfcMode() {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
motion_enabled = true;
|
||||
hidbus_enabled = false;
|
||||
nfc_enabled = true;
|
||||
passive_enabled = false;
|
||||
irs_enabled = false;
|
||||
return SetPollingMode();
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::SetRingConMode() {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.hidbus) {
|
||||
return Common::Input::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 Common::Input::DriverResult::NoDeviceDetected;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::StartNfcPolling() {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
if (!nfc_protocol->IsEnabled()) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
|
||||
disable_input_thread = true;
|
||||
const auto result = nfc_protocol->StartNFCPollingMode();
|
||||
disable_input_thread = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::StopNfcPolling() {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
if (!nfc_protocol->IsEnabled()) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
|
||||
disable_input_thread = true;
|
||||
const auto result = nfc_protocol->StopNFCPollingMode();
|
||||
disable_input_thread = false;
|
||||
|
||||
if (amiibo_detected) {
|
||||
amiibo_detected = false;
|
||||
joycon_poller->UpdateAmiibo({});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::ReadAmiiboData(std::vector<u8>& out_data) {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
if (!nfc_protocol->IsEnabled()) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
if (!amiibo_detected) {
|
||||
return Common::Input::DriverResult::ErrorWritingData;
|
||||
}
|
||||
|
||||
out_data.resize(0x21C);
|
||||
disable_input_thread = true;
|
||||
const auto result = nfc_protocol->ReadAmiibo(out_data);
|
||||
disable_input_thread = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::WriteNfcData(std::span<const u8> data) {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
if (!nfc_protocol->IsEnabled()) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
if (!amiibo_detected) {
|
||||
return Common::Input::DriverResult::ErrorWritingData;
|
||||
}
|
||||
|
||||
disable_input_thread = true;
|
||||
const auto result = nfc_protocol->WriteAmiibo(data);
|
||||
disable_input_thread = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::ReadMifareData(std::span<const MifareReadChunk> data,
|
||||
std::span<MifareReadData> out_data) {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
if (!nfc_protocol->IsEnabled()) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
if (!amiibo_detected) {
|
||||
return Common::Input::DriverResult::ErrorWritingData;
|
||||
}
|
||||
|
||||
disable_input_thread = true;
|
||||
const auto result = nfc_protocol->ReadMifare(data, out_data);
|
||||
disable_input_thread = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::WriteMifareData(std::span<const MifareWriteChunk> data) {
|
||||
std::scoped_lock lock{mutex};
|
||||
|
||||
if (!supported_features.nfc) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
if (!nfc_protocol->IsEnabled()) {
|
||||
return Common::Input::DriverResult::Disabled;
|
||||
}
|
||||
if (!amiibo_detected) {
|
||||
return Common::Input::DriverResult::ErrorWritingData;
|
||||
}
|
||||
|
||||
disable_input_thread = true;
|
||||
const auto result = nfc_protocol->WriteMifare(data);
|
||||
disable_input_thread = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info,
|
||||
ControllerType& controller_type) {
|
||||
static constexpr std::array<std::pair<u32, ControllerType>, 6> supported_devices{
|
||||
std::pair<u32, ControllerType>{0x2006, ControllerType::Left},
|
||||
{0x2007, ControllerType::Right},
|
||||
{0x2009, ControllerType::Pro},
|
||||
};
|
||||
constexpr u16 nintendo_vendor_id = 0x057e;
|
||||
|
||||
controller_type = ControllerType::None;
|
||||
if (device_info->vendor_id != nintendo_vendor_id) {
|
||||
return Common::Input::DriverResult::UnsupportedControllerType;
|
||||
}
|
||||
|
||||
for (const auto& [product_id, type] : supported_devices) {
|
||||
if (device_info->product_id == static_cast<u16>(product_id)) {
|
||||
controller_type = type;
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
}
|
||||
return Common::Input::DriverResult::UnsupportedControllerType;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconDriver::GetSerialNumber(SDL_hid_device_info* device_info,
|
||||
SerialNumber& serial_number) {
|
||||
if (device_info->serial_number == nullptr) {
|
||||
return Common::Input::DriverResult::Unknown;
|
||||
}
|
||||
std::memcpy(&serial_number, device_info->serial_number, 15);
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
167
src/input_common/helpers/joycon_driver.h
Normal file
167
src/input_common/helpers/joycon_driver.h
Normal file
@@ -0,0 +1,167 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <thread>
|
||||
|
||||
#include "common/threadsafe_queue.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
Common::Input::DriverResult RequestDeviceAccess(SDL_hid_device_info* device_info);
|
||||
Common::Input::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;
|
||||
|
||||
Common::Input::DriverResult SetVibration(const VibrationValue& vibration);
|
||||
Common::Input::DriverResult SetLedConfig(u8 led_pattern);
|
||||
Common::Input::DriverResult SetIrsConfig(IrsMode mode_, IrsResolution format_);
|
||||
Common::Input::DriverResult SetPassiveMode();
|
||||
Common::Input::DriverResult SetActiveMode();
|
||||
Common::Input::DriverResult SetIrMode();
|
||||
Common::Input::DriverResult SetNfcMode();
|
||||
Common::Input::DriverResult SetRingConMode();
|
||||
Common::Input::DriverResult StartNfcPolling();
|
||||
Common::Input::DriverResult StopNfcPolling();
|
||||
Common::Input::DriverResult ReadAmiiboData(std::vector<u8>& out_data);
|
||||
Common::Input::DriverResult WriteNfcData(std::span<const u8> data);
|
||||
Common::Input::DriverResult ReadMifareData(std::span<const MifareReadChunk> request,
|
||||
std::span<MifareReadData> out_data);
|
||||
Common::Input::DriverResult WriteMifareData(std::span<const MifareWriteChunk> request);
|
||||
|
||||
void SetCallbacks(const JoyconCallbacks& callbacks);
|
||||
|
||||
// Returns device type from hidapi handle
|
||||
static Common::Input::DriverResult GetDeviceType(SDL_hid_device_info* device_info,
|
||||
ControllerType& controller_type);
|
||||
|
||||
// Returns serial number from hidapi handle
|
||||
static Common::Input::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 every time a valid package arrives
|
||||
void OnNewData(std::span<u8> buffer);
|
||||
|
||||
/// Updates device configuration to enable or disable features
|
||||
Common::Input::DriverResult SetPollingMode();
|
||||
|
||||
/// Returns true if input thread is valid and doesn't need to be stopped
|
||||
bool IsInputThreadValid() const;
|
||||
|
||||
/// Returns true if the data should be interpreted. Otherwise the error counter is incremented
|
||||
bool IsPayloadCorrect(int status, std::span<const u8> buffer);
|
||||
|
||||
/// Returns a list of supported features that can be enabled on this device
|
||||
SupportedFeatures GetSupportedFeatures();
|
||||
|
||||
// Protocol Features
|
||||
std::unique_ptr<CalibrationProtocol> calibration_protocol;
|
||||
std::unique_ptr<GenericProtocol> generic_protocol;
|
||||
std::unique_ptr<IrsProtocol> irs_protocol;
|
||||
std::unique_ptr<NfcProtocol> nfc_protocol;
|
||||
std::unique_ptr<JoyconPoller> joycon_poller;
|
||||
std::unique_ptr<RingConProtocol> ring_protocol;
|
||||
std::unique_ptr<RumbleProtocol> rumble_protocol;
|
||||
|
||||
// Connection status
|
||||
std::atomic<bool> is_connected{};
|
||||
u64 delta_time;
|
||||
std::size_t error_counter{};
|
||||
std::shared_ptr<JoyconHandle> hidapi_handle;
|
||||
std::chrono::time_point<std::chrono::steady_clock> last_update;
|
||||
|
||||
// External device status
|
||||
bool starlink_connected{};
|
||||
bool ring_connected{};
|
||||
bool amiibo_detected{};
|
||||
bool is_ring_disabled_by_irs{};
|
||||
|
||||
// Hardware configuration
|
||||
u8 leds{};
|
||||
ReportMode mode{};
|
||||
bool input_only_device{};
|
||||
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{};
|
||||
|
||||
/// Queue of vibration request to controllers
|
||||
Common::Input::DriverResult last_vibration_result{Common::Input::DriverResult::Success};
|
||||
Common::SPSCQueue<VibrationValue> vibration_queue;
|
||||
|
||||
// Thread related
|
||||
mutable std::mutex mutex;
|
||||
std::jthread input_thread;
|
||||
bool input_thread_running{};
|
||||
bool disable_input_thread{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
221
src/input_common/helpers/joycon_protocol/calibration.cpp
Normal file
221
src/input_common/helpers/joycon_protocol/calibration.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "common/input.h"
|
||||
#include "input_common/helpers/joycon_protocol/calibration.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
CalibrationProtocol::CalibrationProtocol(std::shared_ptr<JoyconHandle> handle)
|
||||
: JoyconCommonProtocol(std::move(handle)) {}
|
||||
|
||||
Common::Input::DriverResult CalibrationProtocol::GetLeftJoyStickCalibration(
|
||||
JoyStickCalibration& calibration) {
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
JoystickLeftSpiCalibration spi_calibration{};
|
||||
bool has_user_calibration = false;
|
||||
calibration = {};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = HasUserCalibration(SpiAddress::USER_LEFT_MAGIC, has_user_calibration);
|
||||
}
|
||||
|
||||
// Read User defined calibration
|
||||
if (result == Common::Input::DriverResult::Success && has_user_calibration) {
|
||||
result = ReadSPI(SpiAddress::USER_LEFT_DATA, spi_calibration);
|
||||
}
|
||||
|
||||
// Read Factory calibration
|
||||
if (result == Common::Input::DriverResult::Success && !has_user_calibration) {
|
||||
result = ReadSPI(SpiAddress::FACT_LEFT_DATA, spi_calibration);
|
||||
}
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
calibration.x.center = GetXAxisCalibrationValue(spi_calibration.center);
|
||||
calibration.y.center = GetYAxisCalibrationValue(spi_calibration.center);
|
||||
calibration.x.min = GetXAxisCalibrationValue(spi_calibration.min);
|
||||
calibration.y.min = GetYAxisCalibrationValue(spi_calibration.min);
|
||||
calibration.x.max = GetXAxisCalibrationValue(spi_calibration.max);
|
||||
calibration.y.max = GetYAxisCalibrationValue(spi_calibration.max);
|
||||
}
|
||||
|
||||
// Set a valid default calibration if data is missing
|
||||
ValidateCalibration(calibration);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult CalibrationProtocol::GetRightJoyStickCalibration(
|
||||
JoyStickCalibration& calibration) {
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
JoystickRightSpiCalibration spi_calibration{};
|
||||
bool has_user_calibration = false;
|
||||
calibration = {};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = HasUserCalibration(SpiAddress::USER_RIGHT_MAGIC, has_user_calibration);
|
||||
}
|
||||
|
||||
// Read User defined calibration
|
||||
if (result == Common::Input::DriverResult::Success && has_user_calibration) {
|
||||
result = ReadSPI(SpiAddress::USER_RIGHT_DATA, spi_calibration);
|
||||
}
|
||||
|
||||
// Read Factory calibration
|
||||
if (result == Common::Input::DriverResult::Success && !has_user_calibration) {
|
||||
result = ReadSPI(SpiAddress::FACT_RIGHT_DATA, spi_calibration);
|
||||
}
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
calibration.x.center = GetXAxisCalibrationValue(spi_calibration.center);
|
||||
calibration.y.center = GetYAxisCalibrationValue(spi_calibration.center);
|
||||
calibration.x.min = GetXAxisCalibrationValue(spi_calibration.min);
|
||||
calibration.y.min = GetYAxisCalibrationValue(spi_calibration.min);
|
||||
calibration.x.max = GetXAxisCalibrationValue(spi_calibration.max);
|
||||
calibration.y.max = GetYAxisCalibrationValue(spi_calibration.max);
|
||||
}
|
||||
|
||||
// Set a valid default calibration if data is missing
|
||||
ValidateCalibration(calibration);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult CalibrationProtocol::GetImuCalibration(MotionCalibration& calibration) {
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
ImuSpiCalibration spi_calibration{};
|
||||
bool has_user_calibration = false;
|
||||
calibration = {};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = HasUserCalibration(SpiAddress::USER_IMU_MAGIC, has_user_calibration);
|
||||
}
|
||||
|
||||
// Read User defined calibration
|
||||
if (result == Common::Input::DriverResult::Success && has_user_calibration) {
|
||||
result = ReadSPI(SpiAddress::USER_IMU_DATA, spi_calibration);
|
||||
}
|
||||
|
||||
// Read Factory calibration
|
||||
if (result == Common::Input::DriverResult::Success && !has_user_calibration) {
|
||||
result = ReadSPI(SpiAddress::FACT_IMU_DATA, spi_calibration);
|
||||
}
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
calibration.accelerometer[0].offset = spi_calibration.accelerometer_offset[0];
|
||||
calibration.accelerometer[1].offset = spi_calibration.accelerometer_offset[1];
|
||||
calibration.accelerometer[2].offset = spi_calibration.accelerometer_offset[2];
|
||||
|
||||
calibration.accelerometer[0].scale = spi_calibration.accelerometer_scale[0];
|
||||
calibration.accelerometer[1].scale = spi_calibration.accelerometer_scale[1];
|
||||
calibration.accelerometer[2].scale = spi_calibration.accelerometer_scale[2];
|
||||
|
||||
calibration.gyro[0].offset = spi_calibration.gyroscope_offset[0];
|
||||
calibration.gyro[1].offset = spi_calibration.gyroscope_offset[1];
|
||||
calibration.gyro[2].offset = spi_calibration.gyroscope_offset[2];
|
||||
|
||||
calibration.gyro[0].scale = spi_calibration.gyroscope_scale[0];
|
||||
calibration.gyro[1].scale = spi_calibration.gyroscope_scale[1];
|
||||
calibration.gyro[2].scale = spi_calibration.gyroscope_scale[2];
|
||||
}
|
||||
|
||||
ValidateCalibration(calibration);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult CalibrationProtocol::GetRingCalibration(RingCalibration& calibration,
|
||||
s16 current_value) {
|
||||
constexpr s16 DefaultRingRange{800};
|
||||
|
||||
// TODO: Get default calibration form ring itself
|
||||
if (ring_data_max == 0 && ring_data_min == 0) {
|
||||
ring_data_max = current_value + DefaultRingRange;
|
||||
ring_data_min = current_value - DefaultRingRange;
|
||||
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 Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult CalibrationProtocol::HasUserCalibration(SpiAddress address,
|
||||
bool& has_user_calibration) {
|
||||
MagicSpiCalibration spi_magic{};
|
||||
const Common::Input::DriverResult result{ReadSPI(address, spi_magic)};
|
||||
has_user_calibration = false;
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
has_user_calibration = spi_magic.first == CalibrationMagic::USR_MAGIC_0 &&
|
||||
spi_magic.second == CalibrationMagic::USR_MAGIC_1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
u16 CalibrationProtocol::GetXAxisCalibrationValue(std::span<u8> block) const {
|
||||
return static_cast<u16>(((block[1] & 0x0F) << 8) | block[0]);
|
||||
}
|
||||
|
||||
u16 CalibrationProtocol::GetYAxisCalibrationValue(std::span<u8> block) const {
|
||||
return static_cast<u16>((block[2] << 4) | (block[1] >> 4));
|
||||
}
|
||||
|
||||
void CalibrationProtocol::ValidateCalibration(JoyStickCalibration& calibration) {
|
||||
constexpr u16 DefaultStickCenter{0x800};
|
||||
constexpr u16 DefaultStickRange{0x6cc};
|
||||
|
||||
calibration.x.center = ValidateValue(calibration.x.center, DefaultStickCenter);
|
||||
calibration.x.max = ValidateValue(calibration.x.max, DefaultStickRange);
|
||||
calibration.x.min = ValidateValue(calibration.x.min, DefaultStickRange);
|
||||
|
||||
calibration.y.center = ValidateValue(calibration.y.center, DefaultStickCenter);
|
||||
calibration.y.max = ValidateValue(calibration.y.max, DefaultStickRange);
|
||||
calibration.y.min = ValidateValue(calibration.y.min, DefaultStickRange);
|
||||
}
|
||||
|
||||
void CalibrationProtocol::ValidateCalibration(MotionCalibration& calibration) {
|
||||
constexpr s16 DefaultAccelerometerScale{0x4000};
|
||||
constexpr s16 DefaultGyroScale{0x3be7};
|
||||
constexpr s16 DefaultOffset{0};
|
||||
|
||||
for (auto& sensor : calibration.accelerometer) {
|
||||
sensor.scale = ValidateValue(sensor.scale, DefaultAccelerometerScale);
|
||||
sensor.offset = ValidateValue(sensor.offset, DefaultOffset);
|
||||
}
|
||||
for (auto& sensor : calibration.gyro) {
|
||||
sensor.scale = ValidateValue(sensor.scale, DefaultGyroScale);
|
||||
sensor.offset = ValidateValue(sensor.offset, DefaultOffset);
|
||||
}
|
||||
}
|
||||
|
||||
u16 CalibrationProtocol::ValidateValue(u16 value, u16 default_value) const {
|
||||
if (value == 0) {
|
||||
return default_value;
|
||||
}
|
||||
if (value == 0xFFF) {
|
||||
return default_value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
s16 CalibrationProtocol::ValidateValue(s16 value, s16 default_value) const {
|
||||
if (value == 0) {
|
||||
return default_value;
|
||||
}
|
||||
if (value == 0xFFF) {
|
||||
return default_value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
85
src/input_common/helpers/joycon_protocol/calibration.h
Normal file
85
src/input_common/helpers/joycon_protocol/calibration.h
Normal file
@@ -0,0 +1,85 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
||||
|
||||
namespace Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
struct JoyStickCalibration;
|
||||
struct IMUCalibration;
|
||||
struct JoyconHandle;
|
||||
} // namespace InputCommon::Joycon
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
/// Driver functions related to retrieving calibration data from the device
|
||||
class CalibrationProtocol final : private JoyconCommonProtocol {
|
||||
public:
|
||||
explicit CalibrationProtocol(std::shared_ptr<JoyconHandle> handle);
|
||||
|
||||
/**
|
||||
* Sends a request to obtain the left stick calibration from memory
|
||||
* @param is_factory_calibration if true factory values will be returned
|
||||
* @returns JoyStickCalibration of the left joystick
|
||||
*/
|
||||
Common::Input::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
|
||||
*/
|
||||
Common::Input::DriverResult GetRightJoyStickCalibration(JoyStickCalibration& calibration);
|
||||
|
||||
/**
|
||||
* Sends a request to obtain the motion calibration from memory
|
||||
* @returns ImuCalibration of the motion sensor
|
||||
*/
|
||||
Common::Input::DriverResult GetImuCalibration(MotionCalibration& calibration);
|
||||
|
||||
/**
|
||||
* Calculates on run time the proper calibration of the ring controller
|
||||
* @returns RingCalibration of the ring sensor
|
||||
*/
|
||||
Common::Input::DriverResult GetRingCalibration(RingCalibration& calibration, s16 current_value);
|
||||
|
||||
private:
|
||||
/// Returns true if the specified address corresponds to the magic value of user calibration
|
||||
Common::Input::DriverResult HasUserCalibration(SpiAddress address, bool& has_user_calibration);
|
||||
|
||||
/// Converts a raw calibration block to an u16 value containing the x axis value
|
||||
u16 GetXAxisCalibrationValue(std::span<u8> block) const;
|
||||
|
||||
/// Converts a raw calibration block to an u16 value containing the y axis value
|
||||
u16 GetYAxisCalibrationValue(std::span<u8> block) const;
|
||||
|
||||
/// Ensures that all joystick calibration values are set
|
||||
void ValidateCalibration(JoyStickCalibration& calibration);
|
||||
|
||||
/// Ensures that all motion calibration values are set
|
||||
void ValidateCalibration(MotionCalibration& calibration);
|
||||
|
||||
/// Returns the default value if the value is either zero or 0xFFF
|
||||
u16 ValidateValue(u16 value, u16 default_value) const;
|
||||
|
||||
/// Returns the default value if the value is either zero or 0xFFF
|
||||
s16 ValidateValue(s16 value, s16 default_value) const;
|
||||
|
||||
s16 ring_data_max = 0;
|
||||
s16 ring_data_default = 0;
|
||||
s16 ring_data_min = 0;
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
321
src/input_common/helpers/joycon_protocol/common_protocol.cpp
Normal file
321
src/input_common/helpers/joycon_protocol/common_protocol.cpp
Normal file
@@ -0,0 +1,321 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
JoyconCommonProtocol::JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_)
|
||||
: hidapi_handle{std::move(hidapi_handle_)} {}
|
||||
|
||||
u8 JoyconCommonProtocol::GetCounter() {
|
||||
hidapi_handle->packet_counter = (hidapi_handle->packet_counter + 1) & 0x0F;
|
||||
return hidapi_handle->packet_counter;
|
||||
}
|
||||
|
||||
void JoyconCommonProtocol::SetBlocking() {
|
||||
SDL_hid_set_nonblocking(hidapi_handle->handle, 0);
|
||||
}
|
||||
|
||||
void JoyconCommonProtocol::SetNonBlocking() {
|
||||
SDL_hid_set_nonblocking(hidapi_handle->handle, 1);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::GetDeviceType(ControllerType& controller_type) {
|
||||
const auto result = ReadSPI(SpiAddress::DEVICE_TYPE, controller_type);
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
// Fallback to 3rd party pro controllers
|
||||
if (controller_type == ControllerType::None) {
|
||||
controller_type = ControllerType::Pro;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::CheckDeviceAccess(
|
||||
SDL_hid_device_info* device_info) {
|
||||
ControllerType controller_type{ControllerType::None};
|
||||
const auto result = GetDeviceType(controller_type);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success || controller_type == ControllerType::None) {
|
||||
return Common::Input::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, "Citron can't gain access to this device: ID {:04X}:{:04X}.",
|
||||
device_info->vendor_id, device_info->product_id);
|
||||
return Common::Input::DriverResult::HandleInUse;
|
||||
}
|
||||
|
||||
SetNonBlocking();
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SetReportMode(ReportMode report_mode) {
|
||||
const std::array<u8, 1> buffer{static_cast<u8>(report_mode)};
|
||||
return SendSubCommand(SubCommand::SET_REPORT_MODE, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SendRawData(std::span<const u8> buffer) {
|
||||
const auto result = SDL_hid_write(hidapi_handle->handle, buffer.data(), buffer.size());
|
||||
|
||||
if (result == -1) {
|
||||
return Common::Input::DriverResult::ErrorWritingData;
|
||||
}
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::GetSubCommandResponse(
|
||||
SubCommand sc, SubCommandResponse& output) {
|
||||
constexpr int timeout_mili = 66;
|
||||
constexpr int MaxTries = 10;
|
||||
int tries = 0;
|
||||
|
||||
do {
|
||||
int result = SDL_hid_read_timeout(hidapi_handle->handle, reinterpret_cast<u8*>(&output),
|
||||
sizeof(SubCommandResponse), timeout_mili);
|
||||
|
||||
if (result < 1) {
|
||||
LOG_ERROR(Input, "No response from joycon");
|
||||
}
|
||||
if (tries++ > MaxTries) {
|
||||
return Common::Input::DriverResult::Timeout;
|
||||
}
|
||||
} while (output.input_report.report_mode != ReportMode::SUBCMD_REPLY &&
|
||||
output.sub_command != sc);
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc,
|
||||
std::span<const u8> buffer,
|
||||
SubCommandResponse& output) {
|
||||
SubCommandPacket packet{
|
||||
.output_report = OutputReport::RUMBLE_AND_SUBCMD,
|
||||
.packet_counter = GetCounter(),
|
||||
.sub_command = sc,
|
||||
.command_data = {},
|
||||
};
|
||||
|
||||
if (buffer.size() > packet.command_data.size()) {
|
||||
return Common::Input::DriverResult::InvalidParameters;
|
||||
}
|
||||
|
||||
memcpy(packet.command_data.data(), buffer.data(), buffer.size());
|
||||
|
||||
auto result = SendData(packet);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return GetSubCommandResponse(sc, output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc,
|
||||
std::span<const u8> buffer) {
|
||||
SubCommandResponse output{};
|
||||
return SendSubCommand(sc, buffer, output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SendMCUCommand(SubCommand sc,
|
||||
std::span<const u8> buffer) {
|
||||
SubCommandPacket packet{
|
||||
.output_report = OutputReport::MCU_DATA,
|
||||
.packet_counter = GetCounter(),
|
||||
.sub_command = sc,
|
||||
.command_data = {},
|
||||
};
|
||||
|
||||
if (buffer.size() > packet.command_data.size()) {
|
||||
return Common::Input::DriverResult::InvalidParameters;
|
||||
}
|
||||
|
||||
memcpy(packet.command_data.data(), buffer.data(), buffer.size());
|
||||
|
||||
return SendData(packet);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SendVibrationReport(std::span<const u8> buffer) {
|
||||
VibrationPacket packet{
|
||||
.output_report = OutputReport::RUMBLE_ONLY,
|
||||
.packet_counter = GetCounter(),
|
||||
.vibration_data = {},
|
||||
};
|
||||
|
||||
if (buffer.size() > packet.vibration_data.size()) {
|
||||
return Common::Input::DriverResult::InvalidParameters;
|
||||
}
|
||||
|
||||
memcpy(packet.vibration_data.data(), buffer.data(), buffer.size());
|
||||
|
||||
return SendData(packet);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::ReadRawSPI(SpiAddress addr,
|
||||
std::span<u8> output) {
|
||||
constexpr std::size_t HeaderSize = 5;
|
||||
constexpr std::size_t MaxTries = 5;
|
||||
std::size_t tries = 0;
|
||||
SubCommandResponse response{};
|
||||
std::array<u8, sizeof(ReadSpiPacket)> buffer{};
|
||||
const ReadSpiPacket packet_data{
|
||||
.spi_address = addr,
|
||||
.size = static_cast<u8>(output.size()),
|
||||
};
|
||||
|
||||
memcpy(buffer.data(), &packet_data, sizeof(ReadSpiPacket));
|
||||
do {
|
||||
const auto result = SendSubCommand(SubCommand::SPI_FLASH_READ, buffer, response);
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (tries++ > MaxTries) {
|
||||
return Common::Input::DriverResult::Timeout;
|
||||
}
|
||||
} while (response.spi_address != addr);
|
||||
|
||||
if (response.command_data.size() < packet_data.size + HeaderSize) {
|
||||
return Common::Input::DriverResult::WrongReply;
|
||||
}
|
||||
|
||||
// Remove header from output
|
||||
memcpy(output.data(), response.command_data.data() + HeaderSize, packet_data.size);
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::EnableMCU(bool enable) {
|
||||
const std::array<u8, 1> mcu_state{static_cast<u8>(enable ? 1 : 0)};
|
||||
const auto result = SendSubCommand(SubCommand::SET_MCU_STATE, mcu_state);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
LOG_ERROR(Input, "Failed with error {}", result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::ConfigureMCU(const MCUConfig& config) {
|
||||
LOG_DEBUG(Input, "ConfigureMCU");
|
||||
std::array<u8, sizeof(MCUConfig)> config_buffer;
|
||||
memcpy(config_buffer.data(), &config, sizeof(MCUConfig));
|
||||
config_buffer[37] = CalculateMCU_CRC8(config_buffer.data() + 1, 36);
|
||||
|
||||
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, config_buffer);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
LOG_ERROR(Input, "Failed with error {}", result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::GetMCUDataResponse(ReportMode report_mode,
|
||||
MCUCommandResponse& output) {
|
||||
constexpr int TimeoutMili = 200;
|
||||
constexpr int MaxTries = 9;
|
||||
int tries = 0;
|
||||
|
||||
do {
|
||||
int result = SDL_hid_read_timeout(hidapi_handle->handle, reinterpret_cast<u8*>(&output),
|
||||
sizeof(MCUCommandResponse), TimeoutMili);
|
||||
|
||||
if (result < 1) {
|
||||
LOG_ERROR(Input, "No response from joycon attempt {}", tries);
|
||||
}
|
||||
if (tries++ > MaxTries) {
|
||||
return Common::Input::DriverResult::Timeout;
|
||||
}
|
||||
} while (output.input_report.report_mode != report_mode ||
|
||||
output.mcu_report == MCUReport::EmptyAwaitingCmd);
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::SendMCUData(ReportMode report_mode,
|
||||
MCUSubCommand sc,
|
||||
std::span<const u8> buffer,
|
||||
MCUCommandResponse& output) {
|
||||
SubCommandPacket packet{
|
||||
.output_report = OutputReport::MCU_DATA,
|
||||
.packet_counter = GetCounter(),
|
||||
.mcu_sub_command = sc,
|
||||
.command_data = {},
|
||||
};
|
||||
|
||||
if (buffer.size() > packet.command_data.size()) {
|
||||
return Common::Input::DriverResult::InvalidParameters;
|
||||
}
|
||||
|
||||
memcpy(packet.command_data.data(), buffer.data(), buffer.size());
|
||||
|
||||
auto result = SendData(packet);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = GetMCUDataResponse(report_mode, output);
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult JoyconCommonProtocol::WaitSetMCUMode(ReportMode report_mode,
|
||||
MCUMode mode) {
|
||||
MCUCommandResponse output{};
|
||||
constexpr std::size_t MaxTries{16};
|
||||
std::size_t tries{};
|
||||
|
||||
do {
|
||||
const auto result = SendMCUData(report_mode, MCUSubCommand::SetDeviceMode, {}, output);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (tries++ > MaxTries) {
|
||||
return Common::Input::DriverResult::WrongReply;
|
||||
}
|
||||
} while (output.mcu_report != MCUReport::StateReport ||
|
||||
output.mcu_data[6] != static_cast<u8>(mode));
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
// crc-8-ccitt / polynomial 0x07 look up table
|
||||
constexpr std::array<u8, 256> mcu_crc8_table = {
|
||||
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
|
||||
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
|
||||
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
|
||||
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
|
||||
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
|
||||
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
|
||||
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
|
||||
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
|
||||
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
|
||||
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
|
||||
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
|
||||
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
|
||||
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
|
||||
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
|
||||
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
|
||||
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3};
|
||||
|
||||
u8 JoyconCommonProtocol::CalculateMCU_CRC8(u8* buffer, u8 size) const {
|
||||
u8 crc8 = 0x0;
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
crc8 = mcu_crc8_table[static_cast<u8>(crc8 ^ buffer[i])];
|
||||
}
|
||||
return crc8;
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
203
src/input_common/helpers/joycon_protocol/common_protocol.h
Normal file
203
src/input_common/helpers/joycon_protocol/common_protocol.h
Normal file
@@ -0,0 +1,203 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
/// Joycon driver functions that handle low level communication
|
||||
class JoyconCommonProtocol {
|
||||
public:
|
||||
explicit JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_);
|
||||
|
||||
/**
|
||||
* Sets handle to blocking. In blocking mode, SDL_hid_read() will wait (block) until there is
|
||||
* data to read before returning.
|
||||
*/
|
||||
void SetBlocking();
|
||||
|
||||
/**
|
||||
* Sets handle to non blocking. In non-blocking mode calls to SDL_hid_read() will return
|
||||
* immediately with a value of 0 if there is no data to be read
|
||||
*/
|
||||
void SetNonBlocking();
|
||||
|
||||
/**
|
||||
* Sends a request to obtain the joycon type from device
|
||||
* @returns controller type of the joycon
|
||||
*/
|
||||
Common::Input::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
|
||||
*/
|
||||
Common::Input::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
|
||||
*/
|
||||
Common::Input::DriverResult SetReportMode(Joycon::ReportMode report_mode);
|
||||
|
||||
/**
|
||||
* Sends data to the joycon device
|
||||
* @param buffer data to be send
|
||||
*/
|
||||
Common::Input::DriverResult SendRawData(std::span<const u8> buffer);
|
||||
|
||||
template <typename Output>
|
||||
requires std::is_trivially_copyable_v<Output>
|
||||
Common::Input::DriverResult SendData(const Output& output) {
|
||||
std::array<u8, sizeof(Output)> buffer;
|
||||
std::memcpy(buffer.data(), &output, sizeof(Output));
|
||||
return SendRawData(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for incoming data of the joycon device that matches the subcommand
|
||||
* @param sub_command type of data to be returned
|
||||
* @returns a buffer containing the response
|
||||
*/
|
||||
Common::Input::DriverResult GetSubCommandResponse(SubCommand sub_command,
|
||||
SubCommandResponse& 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 response
|
||||
*/
|
||||
Common::Input::DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer,
|
||||
SubCommandResponse& 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
|
||||
*/
|
||||
Common::Input::DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer);
|
||||
|
||||
/**
|
||||
* Sends a mcu command to the device
|
||||
* @param sc sub command to be send
|
||||
* @param buffer data to be send
|
||||
*/
|
||||
Common::Input::DriverResult SendMCUCommand(SubCommand sc, std::span<const u8> buffer);
|
||||
|
||||
/**
|
||||
* Sends vibration data to the joycon
|
||||
* @param buffer data to be send
|
||||
*/
|
||||
Common::Input::DriverResult SendVibrationReport(std::span<const u8> buffer);
|
||||
|
||||
/**
|
||||
* Reads the SPI memory stored on the joycon
|
||||
* @param Initial address location
|
||||
* @returns output buffer containing the response
|
||||
*/
|
||||
Common::Input::DriverResult ReadRawSPI(SpiAddress addr, std::span<u8> output);
|
||||
|
||||
/**
|
||||
* Reads the SPI memory stored on the joycon
|
||||
* @param Initial address location
|
||||
* @returns output object containing the response
|
||||
*/
|
||||
template <typename Output>
|
||||
requires std::is_trivially_copyable_v<Output>
|
||||
Common::Input::DriverResult ReadSPI(SpiAddress addr, Output& output) {
|
||||
std::array<u8, sizeof(Output)> buffer;
|
||||
output = {};
|
||||
|
||||
const auto result = ReadRawSPI(addr, buffer);
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::memcpy(&output, buffer.data(), sizeof(Output));
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables MCU chip on the joycon
|
||||
* @param enable if true the chip will be enabled
|
||||
*/
|
||||
Common::Input::DriverResult EnableMCU(bool enable);
|
||||
|
||||
/**
|
||||
* Configures the MCU to the corresponding mode
|
||||
* @param MCUConfig configuration
|
||||
*/
|
||||
Common::Input::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 response
|
||||
*/
|
||||
Common::Input::DriverResult GetMCUDataResponse(ReportMode report_mode_,
|
||||
MCUCommandResponse& 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 response
|
||||
*/
|
||||
Common::Input::DriverResult SendMCUData(ReportMode report_mode, MCUSubCommand sc,
|
||||
std::span<const u8> buffer, MCUCommandResponse& output);
|
||||
|
||||
/**
|
||||
* Wait's until the MCU chip is on the specified mode
|
||||
* @param report mode of the expected reply
|
||||
* @param MCUMode configuration
|
||||
*/
|
||||
Common::Input::DriverResult WaitSetMCUMode(ReportMode report_mode, MCUMode mode);
|
||||
|
||||
/**
|
||||
* Calculates the checksum from the MCU data
|
||||
* @param buffer containing the data to be send
|
||||
* @param size of the buffer in bytes
|
||||
* @returns byte with the correct checksum
|
||||
*/
|
||||
u8 CalculateMCU_CRC8(u8* buffer, u8 size) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Increments and returns the packet counter of the handle
|
||||
* @param joycon_handle device to send the data
|
||||
* @returns packet counter value
|
||||
*/
|
||||
u8 GetCounter();
|
||||
|
||||
std::shared_ptr<JoyconHandle> hidapi_handle;
|
||||
};
|
||||
|
||||
class ScopedSetBlocking {
|
||||
public:
|
||||
explicit ScopedSetBlocking(JoyconCommonProtocol* self) : m_self{self} {
|
||||
m_self->SetBlocking();
|
||||
}
|
||||
|
||||
~ScopedSetBlocking() {
|
||||
m_self->SetNonBlocking();
|
||||
}
|
||||
|
||||
private:
|
||||
JoyconCommonProtocol* m_self{};
|
||||
};
|
||||
} // namespace InputCommon::Joycon
|
||||
138
src/input_common/helpers/joycon_protocol/generic_functions.cpp
Normal file
138
src/input_common/helpers/joycon_protocol/generic_functions.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/joycon_protocol/generic_functions.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
GenericProtocol::GenericProtocol(std::shared_ptr<JoyconHandle> handle)
|
||||
: JoyconCommonProtocol(std::move(handle)) {}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::EnablePassiveMode() {
|
||||
ScopedSetBlocking sb(this);
|
||||
return SetReportMode(ReportMode::SIMPLE_HID_MODE);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::EnableActiveMode() {
|
||||
ScopedSetBlocking sb(this);
|
||||
return SetReportMode(ReportMode::STANDARD_FULL_60HZ);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::SetLowPowerMode(bool enable) {
|
||||
ScopedSetBlocking sb(this);
|
||||
const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)};
|
||||
return SendSubCommand(SubCommand::LOW_POWER_MODE, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::TriggersElapsed() {
|
||||
ScopedSetBlocking sb(this);
|
||||
return SendSubCommand(SubCommand::TRIGGERS_ELAPSED, {});
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetDeviceInfo(DeviceInfo& device_info) {
|
||||
ScopedSetBlocking sb(this);
|
||||
SubCommandResponse output{};
|
||||
|
||||
const auto result = SendSubCommand(SubCommand::REQ_DEV_INFO, {}, output);
|
||||
|
||||
device_info = {};
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
device_info = output.device_info;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetControllerType(ControllerType& controller_type) {
|
||||
return GetDeviceType(controller_type);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::EnableImu(bool enable) {
|
||||
ScopedSetBlocking sb(this);
|
||||
const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)};
|
||||
return SendSubCommand(SubCommand::ENABLE_IMU, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::SetImuConfig(GyroSensitivity gsen,
|
||||
GyroPerformance gfrec,
|
||||
AccelerometerSensitivity asen,
|
||||
AccelerometerPerformance afrec) {
|
||||
ScopedSetBlocking sb(this);
|
||||
const std::array<u8, 4> buffer{static_cast<u8>(gsen), static_cast<u8>(asen),
|
||||
static_cast<u8>(gfrec), static_cast<u8>(afrec)};
|
||||
return SendSubCommand(SubCommand::SET_IMU_SENSITIVITY, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetBattery(u32& battery_level) {
|
||||
// This function is meant to request the high resolution battery status
|
||||
battery_level = 0;
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetColor(Color& color) {
|
||||
ScopedSetBlocking sb(this);
|
||||
std::array<u8, 12> buffer{};
|
||||
const auto result = ReadRawSPI(SpiAddress::COLOR_DATA, buffer);
|
||||
|
||||
color = {};
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
color.body = static_cast<u32>((buffer[0] << 16) | (buffer[1] << 8) | buffer[2]);
|
||||
color.buttons = static_cast<u32>((buffer[3] << 16) | (buffer[4] << 8) | buffer[5]);
|
||||
color.left_grip = static_cast<u32>((buffer[6] << 16) | (buffer[7] << 8) | buffer[8]);
|
||||
color.right_grip = static_cast<u32>((buffer[9] << 16) | (buffer[10] << 8) | buffer[11]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetSerialNumber(SerialNumber& serial_number) {
|
||||
ScopedSetBlocking sb(this);
|
||||
std::array<u8, 16> buffer{};
|
||||
const auto result = ReadRawSPI(SpiAddress::SERIAL_NUMBER, buffer);
|
||||
|
||||
serial_number = {};
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
memcpy(serial_number.data(), buffer.data() + 1, sizeof(SerialNumber));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetTemperature(u32& temperature) {
|
||||
// Not all devices have temperature sensor
|
||||
temperature = 25;
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::GetVersionNumber(FirmwareVersion& version) {
|
||||
DeviceInfo device_info{};
|
||||
|
||||
const auto result = GetDeviceInfo(device_info);
|
||||
version = device_info.firmware;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::SetHomeLight() {
|
||||
ScopedSetBlocking sb(this);
|
||||
static constexpr std::array<u8, 3> buffer{0x0f, 0xf0, 0x00};
|
||||
return SendSubCommand(SubCommand::SET_HOME_LIGHT, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::SetLedBusy() {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::SetLedPattern(u8 leds) {
|
||||
ScopedSetBlocking sb(this);
|
||||
const std::array<u8, 1> buffer{leds};
|
||||
return SendSubCommand(SubCommand::SET_PLAYER_LIGHTS, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult GenericProtocol::SetLedBlinkPattern(u8 leds) {
|
||||
return SetLedPattern(static_cast<u8>(leds << 4));
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
119
src/input_common/helpers/joycon_protocol/generic_functions.h
Normal file
119
src/input_common/helpers/joycon_protocol/generic_functions.h
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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 Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
/// Joycon driver functions that easily implemented
|
||||
class GenericProtocol final : private JoyconCommonProtocol {
|
||||
public:
|
||||
explicit GenericProtocol(std::shared_ptr<JoyconHandle> handle);
|
||||
|
||||
/// Enables passive mode. This mode only sends button data on change. Sticks will return digital
|
||||
/// data instead of analog. Motion will be disabled
|
||||
Common::Input::DriverResult EnablePassiveMode();
|
||||
|
||||
/// Enables active mode. This mode will return the current status every 5-15ms
|
||||
Common::Input::DriverResult EnableActiveMode();
|
||||
|
||||
/// Enables or disables the low power mode
|
||||
Common::Input::DriverResult SetLowPowerMode(bool enable);
|
||||
|
||||
/// Unknown function used by the switch
|
||||
Common::Input::DriverResult TriggersElapsed();
|
||||
|
||||
/**
|
||||
* Sends a request to obtain the joycon firmware and mac from handle
|
||||
* @returns controller device info
|
||||
*/
|
||||
Common::Input::DriverResult GetDeviceInfo(DeviceInfo& controller_type);
|
||||
|
||||
/**
|
||||
* Sends a request to obtain the joycon type from handle
|
||||
* @returns controller type of the joycon
|
||||
*/
|
||||
Common::Input::DriverResult GetControllerType(ControllerType& controller_type);
|
||||
|
||||
/**
|
||||
* Enables motion input
|
||||
* @param enable if true motion data will be enabled
|
||||
*/
|
||||
Common::Input::DriverResult EnableImu(bool enable);
|
||||
|
||||
/**
|
||||
* Configures the motion sensor with the specified parameters
|
||||
* @param gsen gyroscope sensor sensitivity in degrees per second
|
||||
* @param gfrec gyroscope sensor frequency in hertz
|
||||
* @param asen accelerometer sensitivity in G force
|
||||
* @param afrec accelerometer frequency in hertz
|
||||
*/
|
||||
Common::Input::DriverResult SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec,
|
||||
AccelerometerSensitivity asen,
|
||||
AccelerometerPerformance afrec);
|
||||
|
||||
/**
|
||||
* Request battery level from the device
|
||||
* @returns battery level
|
||||
*/
|
||||
Common::Input::DriverResult GetBattery(u32& battery_level);
|
||||
|
||||
/**
|
||||
* Request joycon colors from the device
|
||||
* @returns colors of the body and buttons
|
||||
*/
|
||||
Common::Input::DriverResult GetColor(Color& color);
|
||||
|
||||
/**
|
||||
* Request joycon serial number from the device
|
||||
* @returns 16 byte serial number
|
||||
*/
|
||||
Common::Input::DriverResult GetSerialNumber(SerialNumber& serial_number);
|
||||
|
||||
/**
|
||||
* Request joycon serial number from the device
|
||||
* @returns 16 byte serial number
|
||||
*/
|
||||
Common::Input::DriverResult GetTemperature(u32& temperature);
|
||||
|
||||
/**
|
||||
* Request joycon serial number from the device
|
||||
* @returns 16 byte serial number
|
||||
*/
|
||||
Common::Input::DriverResult GetVersionNumber(FirmwareVersion& version);
|
||||
|
||||
/**
|
||||
* Sets home led behaviour
|
||||
*/
|
||||
Common::Input::DriverResult SetHomeLight();
|
||||
|
||||
/**
|
||||
* Sets home led into a slow breathing state
|
||||
*/
|
||||
Common::Input::DriverResult SetLedBusy();
|
||||
|
||||
/**
|
||||
* Sets the 4 player leds on the joycon on a solid state
|
||||
* @params bit flag containing the led state
|
||||
*/
|
||||
Common::Input::DriverResult SetLedPattern(u8 leds);
|
||||
|
||||
/**
|
||||
* Sets the 4 player leds on the joycon on a blinking state
|
||||
* @returns bit flag containing the led state
|
||||
*/
|
||||
Common::Input::DriverResult SetLedBlinkPattern(u8 leds);
|
||||
};
|
||||
} // namespace InputCommon::Joycon
|
||||
299
src/input_common/helpers/joycon_protocol/irs.cpp
Normal file
299
src/input_common/helpers/joycon_protocol/irs.cpp
Normal file
@@ -0,0 +1,299 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/joycon_protocol/irs.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
IrsProtocol::IrsProtocol(std::shared_ptr<JoyconHandle> handle)
|
||||
: JoyconCommonProtocol(std::move(handle)) {}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::EnableIrs() {
|
||||
LOG_INFO(Input, "Enable IRS");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = EnableMCU(true);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
const MCUConfig config{
|
||||
.command = MCUCommand::ConfigureMCU,
|
||||
.sub_command = MCUSubCommand::SetMCUMode,
|
||||
.mode = MCUMode::IR,
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
result = ConfigureMCU(config);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::IR);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = ConfigureIrs();
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WriteRegistersStep1();
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WriteRegistersStep2();
|
||||
}
|
||||
|
||||
is_enabled = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::DisableIrs() {
|
||||
LOG_DEBUG(Input, "Disable IRS");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = EnableMCU(false);
|
||||
}
|
||||
|
||||
is_enabled = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::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 Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::RequestImage(std::span<u8> buffer) {
|
||||
const u8 next_packet_fragment =
|
||||
static_cast<u8>((packet_fragment + 1) % (static_cast<u8>(fragments) + 1));
|
||||
|
||||
if (buffer[0] == 0x31 && buffer[49] == 0x03) {
|
||||
u8 new_packet_fragment = buffer[52];
|
||||
if (new_packet_fragment == next_packet_fragment) {
|
||||
packet_fragment = next_packet_fragment;
|
||||
memcpy(buf_image.data() + (300 * packet_fragment), buffer.data() + 59, 300);
|
||||
|
||||
return RequestFrame(packet_fragment);
|
||||
}
|
||||
|
||||
if (new_packet_fragment == packet_fragment) {
|
||||
return RequestFrame(packet_fragment);
|
||||
}
|
||||
|
||||
return ResendFrame(next_packet_fragment);
|
||||
}
|
||||
|
||||
return RequestFrame(packet_fragment);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::ConfigureIrs() {
|
||||
LOG_DEBUG(Input, "Configure IRS");
|
||||
constexpr std::size_t max_tries = 28;
|
||||
SubCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
|
||||
const IrsConfigure irs_configuration{
|
||||
.command = MCUCommand::ConfigureIR,
|
||||
.sub_command = MCUSubCommand::SetDeviceMode,
|
||||
.irs_mode = IrsMode::ImageTransfer,
|
||||
.number_of_fragments = fragments,
|
||||
.mcu_major_version = 0x0500,
|
||||
.mcu_minor_version = 0x1800,
|
||||
.crc = {},
|
||||
};
|
||||
buf_image.resize((static_cast<u8>(fragments) + 1) * 300);
|
||||
|
||||
std::array<u8, sizeof(IrsConfigure)> request_data{};
|
||||
memcpy(request_data.data(), &irs_configuration, sizeof(IrsConfigure));
|
||||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
|
||||
do {
|
||||
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
if (tries++ >= max_tries) {
|
||||
return Common::Input::DriverResult::WrongReply;
|
||||
}
|
||||
} while (output.command_data[0] != 0x0b);
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::WriteRegistersStep1() {
|
||||
LOG_DEBUG(Input, "WriteRegistersStep1");
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
constexpr std::size_t max_tries = 28;
|
||||
SubCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
|
||||
const IrsWriteRegisters irs_registers{
|
||||
.command = MCUCommand::ConfigureIR,
|
||||
.sub_command = MCUSubCommand::WriteDeviceRegisters,
|
||||
.number_of_registers = 0x9,
|
||||
.registers =
|
||||
{
|
||||
IrsRegister{IrRegistersAddress::Resolution, static_cast<u8>(resolution_code)},
|
||||
{IrRegistersAddress::ExposureLSB, static_cast<u8>(exposure & 0xff)},
|
||||
{IrRegistersAddress::ExposureMSB, static_cast<u8>(exposure >> 8)},
|
||||
{IrRegistersAddress::ExposureTime, 0x00},
|
||||
{IrRegistersAddress::Leds, static_cast<u8>(leds)},
|
||||
{IrRegistersAddress::DigitalGainLSB, static_cast<u8>((digital_gain & 0x0f) << 4)},
|
||||
{IrRegistersAddress::DigitalGainMSB, static_cast<u8>((digital_gain & 0xf0) >> 4)},
|
||||
{IrRegistersAddress::LedFilter, static_cast<u8>(led_filter)},
|
||||
{IrRegistersAddress::WhitePixelThreshold, 0xc8},
|
||||
},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
|
||||
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
|
||||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
|
||||
|
||||
std::array<u8, 38> mcu_request{0x02};
|
||||
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
|
||||
mcu_request[37] = 0xFF;
|
||||
|
||||
if (result != Common::Input::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 == Common::Input::DriverResult::Success && tries == 0) {
|
||||
result = SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success && tries == 0) {
|
||||
GetSubCommandResponse(SubCommand::SET_MCU_CONFIG, output);
|
||||
}
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
if (tries++ >= max_tries) {
|
||||
return Common::Input::DriverResult::WrongReply;
|
||||
}
|
||||
} while (!(output.command_data[0] == 0x13 && output.command_data[2] == 0x07) &&
|
||||
output.command_data[0] != 0x23);
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::WriteRegistersStep2() {
|
||||
LOG_DEBUG(Input, "WriteRegistersStep2");
|
||||
constexpr std::size_t max_tries = 28;
|
||||
SubCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
|
||||
const IrsWriteRegisters irs_registers{
|
||||
.command = MCUCommand::ConfigureIR,
|
||||
.sub_command = MCUSubCommand::WriteDeviceRegisters,
|
||||
.number_of_registers = 0x8,
|
||||
.registers =
|
||||
{
|
||||
IrsRegister{IrRegistersAddress::LedIntensityMSB,
|
||||
static_cast<u8>(led_intensity >> 8)},
|
||||
{IrRegistersAddress::LedIntensityLSB, static_cast<u8>(led_intensity & 0xff)},
|
||||
{IrRegistersAddress::ImageFlip, static_cast<u8>(image_flip)},
|
||||
{IrRegistersAddress::DenoiseSmoothing, static_cast<u8>((denoise >> 16) & 0xff)},
|
||||
{IrRegistersAddress::DenoiseEdge, static_cast<u8>((denoise >> 8) & 0xff)},
|
||||
{IrRegistersAddress::DenoiseColor, static_cast<u8>(denoise & 0xff)},
|
||||
{IrRegistersAddress::UpdateTime, 0x2d},
|
||||
{IrRegistersAddress::FinalizeConfig, 0x01},
|
||||
},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
|
||||
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
|
||||
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
|
||||
do {
|
||||
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
if (tries++ >= max_tries) {
|
||||
return Common::Input::DriverResult::WrongReply;
|
||||
}
|
||||
} while (output.command_data[0] != 0x13 && output.command_data[0] != 0x23);
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::RequestFrame(u8 frame) {
|
||||
std::array<u8, 38> mcu_request{};
|
||||
mcu_request[3] = frame;
|
||||
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
|
||||
mcu_request[37] = 0xFF;
|
||||
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult IrsProtocol::ResendFrame(u8 frame) {
|
||||
std::array<u8, 38> mcu_request{};
|
||||
mcu_request[1] = 0x1;
|
||||
mcu_request[2] = frame;
|
||||
mcu_request[3] = 0x0;
|
||||
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
|
||||
mcu_request[37] = 0xFF;
|
||||
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
|
||||
}
|
||||
|
||||
std::vector<u8> IrsProtocol::GetImage() const {
|
||||
return buf_image;
|
||||
}
|
||||
|
||||
IrsResolution IrsProtocol::GetIrsFormat() const {
|
||||
return resolution;
|
||||
}
|
||||
|
||||
bool IrsProtocol::IsEnabled() const {
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
67
src/input_common/helpers/joycon_protocol/irs.h
Normal file
67
src/input_common/helpers/joycon_protocol/irs.h
Normal file
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
class IrsProtocol final : private JoyconCommonProtocol {
|
||||
public:
|
||||
explicit IrsProtocol(std::shared_ptr<JoyconHandle> handle);
|
||||
|
||||
Common::Input::DriverResult EnableIrs();
|
||||
|
||||
Common::Input::DriverResult DisableIrs();
|
||||
|
||||
Common::Input::DriverResult SetIrsConfig(IrsMode mode, IrsResolution format);
|
||||
|
||||
Common::Input::DriverResult RequestImage(std::span<u8> buffer);
|
||||
|
||||
std::vector<u8> GetImage() const;
|
||||
|
||||
IrsResolution GetIrsFormat() const;
|
||||
|
||||
bool IsEnabled() const;
|
||||
|
||||
private:
|
||||
Common::Input::DriverResult ConfigureIrs();
|
||||
|
||||
Common::Input::DriverResult WriteRegistersStep1();
|
||||
Common::Input::DriverResult WriteRegistersStep2();
|
||||
|
||||
Common::Input::DriverResult RequestFrame(u8 frame);
|
||||
Common::Input::DriverResult ResendFrame(u8 frame);
|
||||
|
||||
IrsMode irs_mode{IrsMode::ImageTransfer};
|
||||
IrsResolution resolution{IrsResolution::Size40x30};
|
||||
IrsResolutionCode resolution_code{IrsResolutionCode::Size40x30};
|
||||
IrsFragments fragments{IrsFragments::Size40x30};
|
||||
IrLeds leds{IrLeds::BrightAndDim};
|
||||
IrExLedFilter led_filter{IrExLedFilter::Enabled};
|
||||
IrImageFlip image_flip{IrImageFlip::Normal};
|
||||
u8 digital_gain{0x01};
|
||||
u16 exposure{0x2490};
|
||||
u16 led_intensity{0x0f10};
|
||||
u32 denoise{0x012344};
|
||||
|
||||
u8 packet_fragment{};
|
||||
std::vector<u8> buf_image; // 8bpp greyscale image.
|
||||
|
||||
bool is_enabled{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
791
src/input_common/helpers/joycon_protocol/joycon_types.h
Normal file
791
src/input_common/helpers/joycon_protocol/joycon_types.h
Normal file
@@ -0,0 +1,791 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <SDL_hidapi.h>
|
||||
|
||||
#include "common/bit_field.h"
|
||||
#include "common/common_funcs.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
constexpr u32 MaxErrorCount = 50;
|
||||
constexpr u32 MaxBufferSize = 368;
|
||||
constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40};
|
||||
|
||||
using MacAddress = std::array<u8, 6>;
|
||||
using SerialNumber = std::array<u8, 15>;
|
||||
using TagUUID = std::array<u8, 7>;
|
||||
using MifareUUID = std::array<u8, 4>;
|
||||
|
||||
enum class ControllerType : u8 {
|
||||
None = 0x00,
|
||||
Left = 0x01,
|
||||
Right = 0x02,
|
||||
Pro = 0x03,
|
||||
Dual = 0x05, // TODO: Verify this id
|
||||
LarkHvc1 = 0x07,
|
||||
LarkHvc2 = 0x08,
|
||||
LarkNesLeft = 0x09,
|
||||
LarkNesRight = 0x0A,
|
||||
Lucia = 0x0B,
|
||||
Lagon = 0x0C,
|
||||
Lager = 0x0D,
|
||||
};
|
||||
|
||||
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 PassivePadButton : 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 PassivePadStick : u8 {
|
||||
Right = 0x00,
|
||||
RightDown = 0x01,
|
||||
Down = 0x02,
|
||||
DownLeft = 0x03,
|
||||
Left = 0x04,
|
||||
LeftUp = 0x05,
|
||||
Up = 0x06,
|
||||
UpRight = 0x07,
|
||||
Neutral = 0x08,
|
||||
};
|
||||
|
||||
enum class OutputReport : u8 {
|
||||
RUMBLE_AND_SUBCMD = 0x01,
|
||||
FW_UPDATE_PKT = 0x03,
|
||||
RUMBLE_ONLY = 0x10,
|
||||
MCU_DATA = 0x11,
|
||||
USB_CMD = 0x80,
|
||||
};
|
||||
|
||||
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,
|
||||
SPI_SECTOR_ERASE = 0x12,
|
||||
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,
|
||||
GET_EXTERNAL_DEVICE_INFO = 0x59,
|
||||
ENABLE_EXTERNAL_POLLING = 0x5A,
|
||||
DISABLE_EXTERNAL_POLLING = 0x5B,
|
||||
SET_EXTERNAL_FORMAT_CONFIG = 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 CalibrationMagic : u8 {
|
||||
USR_MAGIC_0 = 0xB2,
|
||||
USR_MAGIC_1 = 0xA1,
|
||||
};
|
||||
|
||||
enum class SpiAddress : u16 {
|
||||
MAGIC = 0x0000,
|
||||
MAC_ADDRESS = 0x0015,
|
||||
PAIRING_INFO = 0x2000,
|
||||
SHIPMENT = 0x5000,
|
||||
SERIAL_NUMBER = 0x6000,
|
||||
DEVICE_TYPE = 0x6012,
|
||||
FORMAT_VERSION = 0x601B,
|
||||
FACT_IMU_DATA = 0x6020,
|
||||
FACT_LEFT_DATA = 0x603d,
|
||||
FACT_RIGHT_DATA = 0x6046,
|
||||
COLOR_DATA = 0x6050,
|
||||
DESIGN_VARIATION = 0x605C,
|
||||
SENSOR_DATA = 0x6080,
|
||||
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,
|
||||
SUBCMD_REPLY = 0x21,
|
||||
MCU_UPDATE_STATE = 0x23,
|
||||
STANDARD_FULL_60HZ = 0x30,
|
||||
NFC_IR_MODE_60HZ = 0x31,
|
||||
SIMPLE_HID_MODE = 0x3F,
|
||||
INPUT_USB_RESPONSE = 0x81,
|
||||
};
|
||||
|
||||
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 NFCCommand : u8 {
|
||||
CancelAll = 0x00,
|
||||
StartPolling = 0x01,
|
||||
StopPolling = 0x02,
|
||||
StartWaitingReceive = 0x04,
|
||||
ReadNtag = 0x06,
|
||||
WriteNtag = 0x08,
|
||||
Mifare = 0x0F,
|
||||
};
|
||||
|
||||
enum class NFCTagType : u8 {
|
||||
AllTags = 0x00,
|
||||
Ntag215 = 0x01,
|
||||
};
|
||||
|
||||
enum class NFCPages {
|
||||
Block0 = 0,
|
||||
Block3 = 3,
|
||||
Block45 = 45,
|
||||
Block135 = 135,
|
||||
Block231 = 231,
|
||||
};
|
||||
|
||||
enum class NFCStatus : u8 {
|
||||
Ready = 0x00,
|
||||
Polling = 0x01,
|
||||
LastPackage = 0x04,
|
||||
WriteDone = 0x05,
|
||||
TagLost = 0x07,
|
||||
WriteReady = 0x09,
|
||||
MifareDone = 0x10,
|
||||
};
|
||||
|
||||
enum class MifareCmd : u8 {
|
||||
None = 0x00,
|
||||
Read = 0x30,
|
||||
AuthA = 0x60,
|
||||
AuthB = 0x61,
|
||||
Write = 0xA0,
|
||||
Transfer = 0xB0,
|
||||
Decrement = 0xC0,
|
||||
Increment = 0xC1,
|
||||
Store = 0xC2
|
||||
};
|
||||
|
||||
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,
|
||||
LedIntensityMSB = 0x1100,
|
||||
LedIntensityLSB = 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 ExternalDeviceId : u16 {
|
||||
RingController = 0x2000,
|
||||
Starlink = 0x2800,
|
||||
};
|
||||
|
||||
struct MotionSensorCalibration {
|
||||
s16 offset;
|
||||
s16 scale;
|
||||
};
|
||||
|
||||
struct MotionCalibration {
|
||||
std::array<MotionSensorCalibration, 3> accelerometer;
|
||||
std::array<MotionSensorCalibration, 3> gyro;
|
||||
};
|
||||
|
||||
// Basic motion data containing data from the sensors and a timestamp in microseconds
|
||||
struct MotionData {
|
||||
float gyro_x{};
|
||||
float gyro_y{};
|
||||
float gyro_z{};
|
||||
float accel_x{};
|
||||
float accel_y{};
|
||||
float accel_z{};
|
||||
u64 delta_timestamp{};
|
||||
};
|
||||
|
||||
// Output from SPI read command containing user calibration magic
|
||||
struct MagicSpiCalibration {
|
||||
CalibrationMagic first;
|
||||
CalibrationMagic second;
|
||||
};
|
||||
static_assert(sizeof(MagicSpiCalibration) == 0x2, "MagicSpiCalibration is an invalid size");
|
||||
|
||||
// Output from SPI read command containing left joystick calibration
|
||||
struct JoystickLeftSpiCalibration {
|
||||
std::array<u8, 3> max;
|
||||
std::array<u8, 3> center;
|
||||
std::array<u8, 3> min;
|
||||
};
|
||||
static_assert(sizeof(JoystickLeftSpiCalibration) == 0x9,
|
||||
"JoystickLeftSpiCalibration is an invalid size");
|
||||
|
||||
// Output from SPI read command containing right joystick calibration
|
||||
struct JoystickRightSpiCalibration {
|
||||
std::array<u8, 3> center;
|
||||
std::array<u8, 3> min;
|
||||
std::array<u8, 3> max;
|
||||
};
|
||||
static_assert(sizeof(JoystickRightSpiCalibration) == 0x9,
|
||||
"JoystickRightSpiCalibration is an invalid size");
|
||||
|
||||
struct JoyStickAxisCalibration {
|
||||
u16 max;
|
||||
u16 min;
|
||||
u16 center;
|
||||
};
|
||||
|
||||
struct JoyStickCalibration {
|
||||
JoyStickAxisCalibration x;
|
||||
JoyStickAxisCalibration y;
|
||||
};
|
||||
|
||||
struct ImuSpiCalibration {
|
||||
std::array<s16, 3> accelerometer_offset;
|
||||
std::array<s16, 3> accelerometer_scale;
|
||||
std::array<s16, 3> gyroscope_offset;
|
||||
std::array<s16, 3> gyroscope_scale;
|
||||
};
|
||||
static_assert(sizeof(ImuSpiCalibration) == 0x18, "ImuSpiCalibration is an invalid size");
|
||||
|
||||
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 {
|
||||
ReportMode report_mode;
|
||||
u16 button_input;
|
||||
u8 stick_state;
|
||||
std::array<u8, 10> unknown_data;
|
||||
};
|
||||
static_assert(sizeof(InputReportPassive) == 0xE, "InputReportPassive is an invalid size");
|
||||
|
||||
struct InputReportActive {
|
||||
ReportMode report_mode;
|
||||
u8 packet_id;
|
||||
Battery battery_status;
|
||||
std::array<u8, 3> button_input;
|
||||
std::array<u8, 3> left_stick_state;
|
||||
std::array<u8, 3> right_stick_state;
|
||||
u8 vibration_code;
|
||||
std::array<s16, 6 * 2> motion_input;
|
||||
INSERT_PADDING_BYTES(0x2);
|
||||
s16 ring_input;
|
||||
};
|
||||
static_assert(sizeof(InputReportActive) == 0x29, "InputReportActive is an invalid size");
|
||||
|
||||
struct InputReportNfcIr {
|
||||
ReportMode report_mode;
|
||||
u8 packet_id;
|
||||
Battery battery_status;
|
||||
std::array<u8, 3> button_input;
|
||||
std::array<u8, 3> left_stick_state;
|
||||
std::array<u8, 3> right_stick_state;
|
||||
u8 vibration_code;
|
||||
std::array<s16, 6 * 2> motion_input;
|
||||
INSERT_PADDING_BYTES(0x4);
|
||||
};
|
||||
static_assert(sizeof(InputReportNfcIr) == 0x29, "InputReportNfcIr is an invalid size");
|
||||
#pragma pack(pop)
|
||||
|
||||
struct NFCReadBlock {
|
||||
u8 start;
|
||||
u8 end;
|
||||
};
|
||||
static_assert(sizeof(NFCReadBlock) == 0x2, "NFCReadBlock is an invalid size");
|
||||
|
||||
struct NFCReadBlockCommand {
|
||||
u8 block_count{};
|
||||
std::array<NFCReadBlock, 4> blocks{};
|
||||
};
|
||||
static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an invalid size");
|
||||
|
||||
struct NFCReadCommandData {
|
||||
u8 unknown;
|
||||
u8 uuid_length;
|
||||
TagUUID uid;
|
||||
NFCTagType tag_type;
|
||||
NFCReadBlockCommand read_block;
|
||||
};
|
||||
static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct NFCWriteCommandData {
|
||||
u8 unknown;
|
||||
u8 uuid_length;
|
||||
TagUUID uid;
|
||||
NFCTagType tag_type;
|
||||
u8 unknown2;
|
||||
u8 unknown3;
|
||||
u8 unknown4;
|
||||
u8 unknown5;
|
||||
u8 unknown6;
|
||||
u8 unknown7;
|
||||
u8 unknown8;
|
||||
u8 magic;
|
||||
u16_be write_count;
|
||||
u8 amiibo_version;
|
||||
};
|
||||
static_assert(sizeof(NFCWriteCommandData) == 0x15, "NFCWriteCommandData is an invalid size");
|
||||
#pragma pack(pop)
|
||||
|
||||
struct MifareCommandData {
|
||||
u8 unknown1;
|
||||
u8 unknown2;
|
||||
u8 number_of_short_bytes;
|
||||
MifareUUID uid;
|
||||
};
|
||||
static_assert(sizeof(MifareCommandData) == 0x7, "MifareCommandData 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 {
|
||||
NFCCommand command_argument;
|
||||
u8 block_id;
|
||||
u8 packet_id;
|
||||
MCUPacketFlag packet_flag;
|
||||
u8 data_length;
|
||||
union {
|
||||
std::array<u8, 0x1F> raw_data;
|
||||
NFCReadCommandData nfc_read;
|
||||
NFCPollingCommandData nfc_polling;
|
||||
};
|
||||
u8 crc;
|
||||
INSERT_PADDING_BYTES(0x1);
|
||||
};
|
||||
static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size");
|
||||
|
||||
struct NFCDataChunk {
|
||||
u8 nfc_page;
|
||||
u8 data_size;
|
||||
std::array<u8, 0xFF> data;
|
||||
};
|
||||
|
||||
struct NFCWritePackage {
|
||||
NFCWriteCommandData command_data;
|
||||
u8 number_of_chunks;
|
||||
std::array<NFCDataChunk, 4> data_chunks;
|
||||
};
|
||||
|
||||
struct MifareReadChunk {
|
||||
MifareCmd command;
|
||||
std::array<u8, 0x6> sector_key;
|
||||
u8 sector;
|
||||
};
|
||||
|
||||
struct MifareWriteChunk {
|
||||
MifareCmd command;
|
||||
std::array<u8, 0x6> sector_key;
|
||||
u8 sector;
|
||||
std::array<u8, 0x10> data;
|
||||
};
|
||||
|
||||
struct MifareReadData {
|
||||
u8 sector;
|
||||
std::array<u8, 0x10> data;
|
||||
};
|
||||
|
||||
struct MifareReadPackage {
|
||||
MifareCommandData command_data;
|
||||
std::array<MifareReadChunk, 0x10> data_chunks;
|
||||
};
|
||||
|
||||
struct MifareWritePackage {
|
||||
MifareCommandData command_data;
|
||||
std::array<MifareWriteChunk, 0x10> data_chunks;
|
||||
};
|
||||
|
||||
struct TagInfo {
|
||||
u8 uuid_length;
|
||||
u8 protocol;
|
||||
u8 tag_type;
|
||||
std::array<u8, 10> uuid;
|
||||
};
|
||||
|
||||
struct IrsConfigure {
|
||||
MCUCommand command;
|
||||
MCUSubCommand sub_command;
|
||||
IrsMode irs_mode;
|
||||
IrsFragments number_of_fragments;
|
||||
u16 mcu_major_version;
|
||||
u16 mcu_minor_version;
|
||||
INSERT_PADDING_BYTES(0x1D);
|
||||
u8 crc;
|
||||
};
|
||||
static_assert(sizeof(IrsConfigure) == 0x26, "IrsConfigure is an invalid size");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct IrsRegister {
|
||||
IrRegistersAddress address;
|
||||
u8 value;
|
||||
};
|
||||
static_assert(sizeof(IrsRegister) == 0x3, "IrsRegister is an invalid size");
|
||||
|
||||
struct IrsWriteRegisters {
|
||||
MCUCommand command;
|
||||
MCUSubCommand sub_command;
|
||||
u8 number_of_registers;
|
||||
std::array<IrsRegister, 9> registers;
|
||||
INSERT_PADDING_BYTES(0x7);
|
||||
u8 crc;
|
||||
};
|
||||
static_assert(sizeof(IrsWriteRegisters) == 0x26, "IrsWriteRegisters is an invalid size");
|
||||
#pragma pack(pop)
|
||||
|
||||
struct FirmwareVersion {
|
||||
u8 major;
|
||||
u8 minor;
|
||||
};
|
||||
static_assert(sizeof(FirmwareVersion) == 0x2, "FirmwareVersion is an invalid size");
|
||||
|
||||
struct DeviceInfo {
|
||||
FirmwareVersion firmware;
|
||||
std::array<u8, 2> unknown_1;
|
||||
MacAddress mac_address;
|
||||
std::array<u8, 2> unknown_2;
|
||||
};
|
||||
static_assert(sizeof(DeviceInfo) == 0xC, "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 VibrationPacket {
|
||||
OutputReport output_report;
|
||||
u8 packet_counter;
|
||||
std::array<u8, 0x8> vibration_data;
|
||||
};
|
||||
static_assert(sizeof(VibrationPacket) == 0xA, "VibrationPacket is an invalid size");
|
||||
|
||||
struct SubCommandPacket {
|
||||
OutputReport output_report;
|
||||
u8 packet_counter;
|
||||
INSERT_PADDING_BYTES(0x8); // This contains vibration data
|
||||
union {
|
||||
SubCommand sub_command;
|
||||
MCUSubCommand mcu_sub_command;
|
||||
};
|
||||
std::array<u8, 0x26> command_data;
|
||||
};
|
||||
static_assert(sizeof(SubCommandPacket) == 0x31, "SubCommandPacket is an invalid size");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct ReadSpiPacket {
|
||||
SpiAddress spi_address;
|
||||
INSERT_PADDING_BYTES(0x2);
|
||||
u8 size;
|
||||
};
|
||||
static_assert(sizeof(ReadSpiPacket) == 0x5, "ReadSpiPacket is an invalid size");
|
||||
|
||||
struct SubCommandResponse {
|
||||
InputReportPassive input_report;
|
||||
SubCommand sub_command;
|
||||
union {
|
||||
std::array<u8, 0x30> command_data;
|
||||
SpiAddress spi_address; // Reply from SPI_FLASH_READ subcommand
|
||||
ExternalDeviceId external_device_id; // Reply from GET_EXTERNAL_DEVICE_INFO subcommand
|
||||
DeviceInfo device_info; // Reply from REQ_DEV_INFO subcommand
|
||||
};
|
||||
u8 crc; // This is never used
|
||||
};
|
||||
static_assert(sizeof(SubCommandResponse) == 0x40, "SubCommandResponse is an invalid size");
|
||||
#pragma pack(pop)
|
||||
|
||||
struct MCUCommandResponse {
|
||||
InputReportNfcIr input_report;
|
||||
INSERT_PADDING_BYTES(0x8);
|
||||
MCUReport mcu_report;
|
||||
std::array<u8, 0x13D> mcu_data;
|
||||
u8 crc;
|
||||
};
|
||||
static_assert(sizeof(MCUCommandResponse) == 0x170, "MCUCommandResponse is an invalid size");
|
||||
|
||||
struct JoyconCallbacks {
|
||||
std::function<void(Battery)> on_battery_data;
|
||||
std::function<void(Color)> on_color_data;
|
||||
std::function<void(int, bool)> on_button_data;
|
||||
std::function<void(int, f32)> on_stick_data;
|
||||
std::function<void(int, const MotionData&)> on_motion_data;
|
||||
std::function<void(f32)> on_ring_data;
|
||||
std::function<void(const Joycon::TagInfo&)> on_amiibo_data;
|
||||
std::function<void(const std::vector<u8>&, IrsResolution)> on_camera_data;
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
992
src/input_common/helpers/joycon_protocol/nfc.cpp
Normal file
992
src/input_common/helpers/joycon_protocol/nfc.cpp
Normal file
@@ -0,0 +1,992 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/joycon_protocol/nfc.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
NfcProtocol::NfcProtocol(std::shared_ptr<JoyconHandle> handle)
|
||||
: JoyconCommonProtocol(std::move(handle)) {}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::EnableNfc() {
|
||||
LOG_INFO(Input, "Enable NFC");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = EnableMCU(true);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
const MCUConfig config{
|
||||
.command = MCUCommand::ConfigureMCU,
|
||||
.sub_command = MCUSubCommand::SetMCUMode,
|
||||
.mode = MCUMode::NFC,
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
result = ConfigureMCU(config);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::Ready);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStopPollingRequest(output);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::Ready);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
is_enabled = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::DisableNfc() {
|
||||
LOG_DEBUG(Input, "Disable NFC");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = EnableMCU(false);
|
||||
}
|
||||
|
||||
is_enabled = false;
|
||||
is_polling = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::StartNFCPollingMode() {
|
||||
LOG_DEBUG(Input, "Start NFC polling Mode");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStartPollingRequest(output);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::Polling);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
is_polling = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::StopNFCPollingMode() {
|
||||
LOG_DEBUG(Input, "Stop NFC polling Mode");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStopPollingRequest(output);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::WriteReady);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
is_polling = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::GetTagInfo(Joycon::TagInfo& tag_info) {
|
||||
if (update_counter++ < AMIIBO_UPDATE_DELAY) {
|
||||
return Common::Input::DriverResult::Delayed;
|
||||
}
|
||||
update_counter = 0;
|
||||
|
||||
LOG_DEBUG(Input, "Scan for amiibos");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
TagFoundData tag_data{};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsTagInRange(tag_data);
|
||||
}
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
tag_info = {
|
||||
.uuid_length = tag_data.uuid_size,
|
||||
.protocol = 1,
|
||||
.tag_type = tag_data.type,
|
||||
.uuid = {},
|
||||
};
|
||||
|
||||
memcpy(tag_info.uuid.data(), tag_data.uuid.data(), tag_data.uuid_size);
|
||||
|
||||
// Investigate why mifare type is not correct
|
||||
if (tag_info.tag_type == 144) {
|
||||
tag_info.tag_type = 1U << 6;
|
||||
}
|
||||
|
||||
std::string uuid_string;
|
||||
for (auto& content : tag_data.uuid) {
|
||||
uuid_string += fmt::format(" {:02x}", content);
|
||||
}
|
||||
LOG_INFO(Input, "Tag detected, type={}, uuid={}", tag_data.type, uuid_string);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::ReadAmiibo(std::vector<u8>& data) {
|
||||
LOG_DEBUG(Input, "Scan for amiibos");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
TagFoundData tag_data{};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsTagInRange(tag_data, 7);
|
||||
}
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = GetAmiiboData(data);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::WriteAmiibo(std::span<const u8> data) {
|
||||
LOG_DEBUG(Input, "Write amiibo");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
TagUUID tag_uuid = GetTagUUID(data);
|
||||
TagFoundData tag_data{};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsTagInRange(tag_data, 7);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
if (tag_data.uuid != tag_uuid) {
|
||||
result = Common::Input::DriverResult::InvalidParameters;
|
||||
}
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStopPollingRequest(output);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::Ready);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStartPollingRequest(output, true);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::WriteReady);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WriteAmiiboData(tag_uuid, data);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::WriteDone);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStopPollingRequest(output);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::ReadMifare(std::span<const MifareReadChunk> read_request,
|
||||
std::span<MifareReadData> out_data) {
|
||||
LOG_DEBUG(Input, "Read mifare");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
TagFoundData tag_data{};
|
||||
MifareUUID tag_uuid{};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsTagInRange(tag_data, 7);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
memcpy(tag_uuid.data(), tag_data.uuid.data(), sizeof(MifareUUID));
|
||||
result = GetMifareData(tag_uuid, read_request, out_data);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStopPollingRequest(output);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::Ready);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStartPollingRequest(output, true);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::WriteReady);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::WriteMifare(
|
||||
std::span<const MifareWriteChunk> write_request) {
|
||||
LOG_DEBUG(Input, "Write mifare");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
TagFoundData tag_data{};
|
||||
MifareUUID tag_uuid{};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsTagInRange(tag_data, 7);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
memcpy(tag_uuid.data(), tag_data.uuid.data(), sizeof(MifareUUID));
|
||||
result = WriteMifareData(tag_uuid, write_request);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStopPollingRequest(output);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::Ready);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
MCUCommandResponse output{};
|
||||
result = SendStartPollingRequest(output, true);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = WaitUntilNfcIs(NFCStatus::WriteReady);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool NfcProtocol::HasAmiibo() {
|
||||
if (update_counter++ < AMIIBO_UPDATE_DELAY) {
|
||||
return true;
|
||||
}
|
||||
update_counter = 0;
|
||||
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
TagFoundData tag_data{};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsTagInRange(tag_data, 7);
|
||||
}
|
||||
|
||||
return result == Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::WaitUntilNfcIs(NFCStatus status) {
|
||||
constexpr std::size_t timeout_limit = 10;
|
||||
MCUCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
|
||||
do {
|
||||
auto result = SendNextPackageRequest(output, {});
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
if (tries++ > timeout_limit) {
|
||||
return Common::Input::DriverResult::Timeout;
|
||||
}
|
||||
} while (output.mcu_report != MCUReport::NFCState ||
|
||||
(output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
|
||||
output.mcu_data[5] != 0x31 || output.mcu_data[6] != static_cast<u8>(status));
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::IsTagInRange(TagFoundData& data,
|
||||
std::size_t timeout_limit) {
|
||||
MCUCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
|
||||
do {
|
||||
const auto result = SendNextPackageRequest(output, {});
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
if (tries++ > timeout_limit) {
|
||||
return Common::Input::DriverResult::Timeout;
|
||||
}
|
||||
} while (output.mcu_report != MCUReport::NFCState ||
|
||||
(output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
|
||||
(output.mcu_data[6] != 0x09 && output.mcu_data[6] != 0x04));
|
||||
|
||||
data.type = output.mcu_data[12];
|
||||
data.uuid_size = std::min(output.mcu_data[14], static_cast<u8>(sizeof(TagUUID)));
|
||||
memcpy(data.uuid.data(), output.mcu_data.data() + 15, data.uuid.size());
|
||||
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::GetAmiiboData(std::vector<u8>& ntag_data) {
|
||||
constexpr std::size_t timeout_limit = 60;
|
||||
MCUCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
|
||||
u8 package_index = 0;
|
||||
std::size_t ntag_buffer_pos = 0;
|
||||
auto result = SendReadAmiiboRequest(output, NFCPages::Block135);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read Tag data
|
||||
while (tries++ < timeout_limit) {
|
||||
result = SendNextPackageRequest(output, package_index);
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if ((output.mcu_report == MCUReport::NFCReadData ||
|
||||
output.mcu_report == MCUReport::NFCState) &&
|
||||
nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
|
||||
std::size_t payload_size = (output.mcu_data[4] << 8 | output.mcu_data[5]) & 0x7FF;
|
||||
if (output.mcu_data[2] == 0x01) {
|
||||
memcpy(ntag_data.data() + ntag_buffer_pos, output.mcu_data.data() + 66,
|
||||
payload_size - 60);
|
||||
ntag_buffer_pos += payload_size - 60;
|
||||
} else {
|
||||
memcpy(ntag_data.data() + ntag_buffer_pos, output.mcu_data.data() + 6,
|
||||
payload_size);
|
||||
}
|
||||
package_index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
|
||||
LOG_INFO(Input, "Finished reading amiibo");
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
}
|
||||
|
||||
return Common::Input::DriverResult::Timeout;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::WriteAmiiboData(const TagUUID& tag_uuid,
|
||||
std::span<const u8> data) {
|
||||
constexpr std::size_t timeout_limit = 60;
|
||||
const auto nfc_data = MakeAmiiboWritePackage(tag_uuid, data);
|
||||
const std::vector<u8> nfc_buffer_data = SerializeWritePackage(nfc_data);
|
||||
std::span<const u8> buffer(nfc_buffer_data);
|
||||
MCUCommandResponse output{};
|
||||
u8 block_id = 1;
|
||||
u8 package_index = 0;
|
||||
std::size_t tries = 0;
|
||||
std::size_t current_position = 0;
|
||||
|
||||
LOG_INFO(Input, "Writing amiibo data");
|
||||
|
||||
auto result = SendWriteAmiiboRequest(output, tag_uuid);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Read Tag data but ignore the actual sent data
|
||||
while (tries++ < timeout_limit) {
|
||||
result = SendNextPackageRequest(output, package_index);
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if ((output.mcu_report == MCUReport::NFCReadData ||
|
||||
output.mcu_report == MCUReport::NFCState) &&
|
||||
nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
|
||||
package_index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
|
||||
LOG_INFO(Input, "Finished reading amiibo");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Send Data. Nfc buffer size is 31, Send the data in smaller packages
|
||||
while (current_position < buffer.size() && tries++ < timeout_limit) {
|
||||
const std::size_t next_position =
|
||||
std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
|
||||
const std::size_t block_size = next_position - current_position;
|
||||
const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
|
||||
|
||||
SendWriteDataAmiiboRequest(output, block_id, is_last_packet,
|
||||
buffer.subspan(current_position, block_size));
|
||||
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if ((output.mcu_report == MCUReport::NFCReadData ||
|
||||
output.mcu_report == MCUReport::NFCState) &&
|
||||
nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
// Increase position when data is confirmed by the joycon
|
||||
if (output.mcu_report == MCUReport::NFCState &&
|
||||
(output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
|
||||
output.mcu_data[3] == block_id) {
|
||||
block_id++;
|
||||
current_position = next_position;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::GetMifareData(
|
||||
const MifareUUID& tag_uuid, std::span<const MifareReadChunk> read_request,
|
||||
std::span<MifareReadData> out_data) {
|
||||
constexpr std::size_t timeout_limit = 60;
|
||||
const auto nfc_data = MakeMifareReadPackage(tag_uuid, read_request);
|
||||
const std::vector<u8> nfc_buffer_data = SerializeMifareReadPackage(nfc_data);
|
||||
std::span<const u8> buffer(nfc_buffer_data);
|
||||
Common::Input::DriverResult result = Common::Input::DriverResult::Success;
|
||||
MCUCommandResponse output{};
|
||||
u8 block_id = 1;
|
||||
u8 package_index = 0;
|
||||
std::size_t tries = 0;
|
||||
std::size_t current_position = 0;
|
||||
|
||||
LOG_INFO(Input, "Reading Mifare data");
|
||||
|
||||
// Send data request. Nfc buffer size is 31, Send the data in smaller packages
|
||||
while (current_position < buffer.size() && tries++ < timeout_limit) {
|
||||
const std::size_t next_position =
|
||||
std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
|
||||
const std::size_t block_size = next_position - current_position;
|
||||
const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
|
||||
|
||||
SendReadDataMifareRequest(output, block_id, is_last_packet,
|
||||
buffer.subspan(current_position, block_size));
|
||||
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
// Increase position when data is confirmed by the joycon
|
||||
if (output.mcu_report == MCUReport::NFCState &&
|
||||
(output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
|
||||
output.mcu_data[3] == block_id) {
|
||||
block_id++;
|
||||
current_position = next_position;
|
||||
}
|
||||
}
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Wait for reply and save the output data
|
||||
while (tries++ < timeout_limit) {
|
||||
result = SendNextPackageRequest(output, package_index);
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && output.mcu_data[1] == 0x10) {
|
||||
constexpr std::size_t DATA_LENGTH = 0x10 + 1;
|
||||
constexpr std::size_t DATA_START = 11;
|
||||
const u8 number_of_elements = output.mcu_data[10];
|
||||
for (std::size_t i = 0; i < number_of_elements; i++) {
|
||||
out_data[i].sector = output.mcu_data[DATA_START + (i * DATA_LENGTH)];
|
||||
memcpy(out_data[i].data.data(),
|
||||
output.mcu_data.data() + DATA_START + 1 + (i * DATA_LENGTH),
|
||||
sizeof(MifareReadData::data));
|
||||
}
|
||||
package_index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::MifareDone) {
|
||||
LOG_INFO(Input, "Finished reading mifare");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::WriteMifareData(
|
||||
const MifareUUID& tag_uuid, std::span<const MifareWriteChunk> write_request) {
|
||||
constexpr std::size_t timeout_limit = 60;
|
||||
const auto nfc_data = MakeMifareWritePackage(tag_uuid, write_request);
|
||||
const std::vector<u8> nfc_buffer_data = SerializeMifareWritePackage(nfc_data);
|
||||
std::span<const u8> buffer(nfc_buffer_data);
|
||||
Common::Input::DriverResult result = Common::Input::DriverResult::Success;
|
||||
MCUCommandResponse output{};
|
||||
u8 block_id = 1;
|
||||
u8 package_index = 0;
|
||||
std::size_t tries = 0;
|
||||
std::size_t current_position = 0;
|
||||
|
||||
LOG_INFO(Input, "Writing Mifare data");
|
||||
|
||||
// Send data request. Nfc buffer size is 31, Send the data in smaller packages
|
||||
while (current_position < buffer.size() && tries++ < timeout_limit) {
|
||||
const std::size_t next_position =
|
||||
std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
|
||||
const std::size_t block_size = next_position - current_position;
|
||||
const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
|
||||
|
||||
SendReadDataMifareRequest(output, block_id, is_last_packet,
|
||||
buffer.subspan(current_position, block_size));
|
||||
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
// Increase position when data is confirmed by the joycon
|
||||
if (output.mcu_report == MCUReport::NFCState &&
|
||||
(output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
|
||||
output.mcu_data[3] == block_id) {
|
||||
block_id++;
|
||||
current_position = next_position;
|
||||
}
|
||||
}
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Wait for reply and ignore the output data
|
||||
while (tries++ < timeout_limit) {
|
||||
result = SendNextPackageRequest(output, package_index);
|
||||
const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
|
||||
return Common::Input::DriverResult::ErrorReadingData;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && output.mcu_data[1] == 0x10) {
|
||||
package_index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::MifareDone) {
|
||||
LOG_INFO(Input, "Finished writing mifare");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output,
|
||||
bool is_second_attempt) {
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::StartPolling,
|
||||
.block_id = {},
|
||||
.packet_id = {},
|
||||
.packet_flag = MCUPacketFlag::LastCommandPacket,
|
||||
.data_length = sizeof(NFCPollingCommandData),
|
||||
.nfc_polling =
|
||||
{
|
||||
.enable_mifare = 0x00,
|
||||
.unknown_1 = static_cast<u8>(is_second_attempt ? 0xe8 : 0x00),
|
||||
.unknown_2 = static_cast<u8>(is_second_attempt ? 0x03 : 0x00),
|
||||
.unknown_3 = 0x2c,
|
||||
.unknown_4 = 0x01,
|
||||
},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(NFCRequestState)> request_data{};
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendStopPollingRequest(MCUCommandResponse& output) {
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::StopPolling,
|
||||
.block_id = {},
|
||||
.packet_id = {},
|
||||
.packet_flag = MCUPacketFlag::LastCommandPacket,
|
||||
.data_length = {},
|
||||
.raw_data = {},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(NFCRequestState)> request_data{};
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendNextPackageRequest(MCUCommandResponse& output,
|
||||
u8 packet_id) {
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::StartWaitingReceive,
|
||||
.block_id = {},
|
||||
.packet_id = packet_id,
|
||||
.packet_flag = MCUPacketFlag::LastCommandPacket,
|
||||
.data_length = {},
|
||||
.raw_data = {},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::vector<u8> request_data(sizeof(NFCRequestState));
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendReadAmiiboRequest(MCUCommandResponse& output,
|
||||
NFCPages ntag_pages) {
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::ReadNtag,
|
||||
.block_id = {},
|
||||
.packet_id = {},
|
||||
.packet_flag = MCUPacketFlag::LastCommandPacket,
|
||||
.data_length = sizeof(NFCReadCommandData),
|
||||
.nfc_read =
|
||||
{
|
||||
.unknown = 0xd0,
|
||||
.uuid_length = sizeof(NFCReadCommandData::uid),
|
||||
.uid = {},
|
||||
.tag_type = NFCTagType::Ntag215,
|
||||
.read_block = GetReadBlockCommand(ntag_pages),
|
||||
},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(NFCRequestState)> request_data{};
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendWriteAmiiboRequest(MCUCommandResponse& output,
|
||||
const TagUUID& tag_uuid) {
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::ReadNtag,
|
||||
.block_id = {},
|
||||
.packet_id = {},
|
||||
.packet_flag = MCUPacketFlag::LastCommandPacket,
|
||||
.data_length = sizeof(NFCReadCommandData),
|
||||
.nfc_read =
|
||||
{
|
||||
.unknown = 0xd0,
|
||||
.uuid_length = sizeof(NFCReadCommandData::uid),
|
||||
.uid = tag_uuid,
|
||||
.tag_type = NFCTagType::Ntag215,
|
||||
.read_block = GetReadBlockCommand(NFCPages::Block3),
|
||||
},
|
||||
.crc = {},
|
||||
};
|
||||
|
||||
std::array<u8, sizeof(NFCRequestState)> request_data{};
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendWriteDataAmiiboRequest(MCUCommandResponse& output,
|
||||
u8 block_id,
|
||||
bool is_last_packet,
|
||||
std::span<const u8> data) {
|
||||
const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::WriteNtag,
|
||||
.block_id = block_id,
|
||||
.packet_id = {},
|
||||
.packet_flag =
|
||||
is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
|
||||
.data_length = static_cast<u8>(data_size),
|
||||
.raw_data = {},
|
||||
.crc = {},
|
||||
};
|
||||
memcpy(request.raw_data.data(), data.data(), data_size);
|
||||
|
||||
std::array<u8, sizeof(NFCRequestState)> request_data{};
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult NfcProtocol::SendReadDataMifareRequest(MCUCommandResponse& output,
|
||||
u8 block_id, bool is_last_packet,
|
||||
std::span<const u8> data) {
|
||||
const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
|
||||
NFCRequestState request{
|
||||
.command_argument = NFCCommand::Mifare,
|
||||
.block_id = block_id,
|
||||
.packet_id = {},
|
||||
.packet_flag =
|
||||
is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
|
||||
.data_length = static_cast<u8>(data_size),
|
||||
.raw_data = {},
|
||||
.crc = {},
|
||||
};
|
||||
memcpy(request.raw_data.data(), data.data(), data_size);
|
||||
|
||||
std::array<u8, sizeof(NFCRequestState)> request_data{};
|
||||
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
|
||||
request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
|
||||
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
|
||||
output);
|
||||
}
|
||||
|
||||
std::vector<u8> NfcProtocol::SerializeWritePackage(const NFCWritePackage& package) const {
|
||||
const std::size_t header_size =
|
||||
sizeof(NFCWriteCommandData) + sizeof(NFCWritePackage::number_of_chunks);
|
||||
std::vector<u8> serialized_data(header_size);
|
||||
std::size_t start_index = 0;
|
||||
|
||||
memcpy(serialized_data.data(), &package, header_size);
|
||||
start_index += header_size;
|
||||
|
||||
for (const auto& data_chunk : package.data_chunks) {
|
||||
const std::size_t chunk_size =
|
||||
sizeof(NFCDataChunk::nfc_page) + sizeof(NFCDataChunk::data_size) + data_chunk.data_size;
|
||||
|
||||
serialized_data.resize(start_index + chunk_size);
|
||||
memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
|
||||
start_index += chunk_size;
|
||||
}
|
||||
|
||||
return serialized_data;
|
||||
}
|
||||
|
||||
std::vector<u8> NfcProtocol::SerializeMifareReadPackage(const MifareReadPackage& package) const {
|
||||
const std::size_t header_size = sizeof(MifareCommandData);
|
||||
std::vector<u8> serialized_data(header_size);
|
||||
std::size_t start_index = 0;
|
||||
|
||||
memcpy(serialized_data.data(), &package, header_size);
|
||||
start_index += header_size;
|
||||
|
||||
for (const auto& data_chunk : package.data_chunks) {
|
||||
const std::size_t chunk_size = sizeof(MifareReadChunk);
|
||||
if (data_chunk.command == MifareCmd::None) {
|
||||
continue;
|
||||
}
|
||||
serialized_data.resize(start_index + chunk_size);
|
||||
memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
|
||||
start_index += chunk_size;
|
||||
}
|
||||
|
||||
return serialized_data;
|
||||
}
|
||||
|
||||
std::vector<u8> NfcProtocol::SerializeMifareWritePackage(const MifareWritePackage& package) const {
|
||||
const std::size_t header_size = sizeof(MifareCommandData);
|
||||
std::vector<u8> serialized_data(header_size);
|
||||
std::size_t start_index = 0;
|
||||
|
||||
memcpy(serialized_data.data(), &package, header_size);
|
||||
start_index += header_size;
|
||||
|
||||
for (const auto& data_chunk : package.data_chunks) {
|
||||
const std::size_t chunk_size = sizeof(MifareWriteChunk);
|
||||
if (data_chunk.command == MifareCmd::None) {
|
||||
continue;
|
||||
}
|
||||
serialized_data.resize(start_index + chunk_size);
|
||||
memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
|
||||
start_index += chunk_size;
|
||||
}
|
||||
|
||||
return serialized_data;
|
||||
}
|
||||
|
||||
NFCWritePackage NfcProtocol::MakeAmiiboWritePackage(const TagUUID& tag_uuid,
|
||||
std::span<const u8> data) const {
|
||||
return {
|
||||
.command_data{
|
||||
.unknown = 0xd0,
|
||||
.uuid_length = sizeof(NFCReadCommandData::uid),
|
||||
.uid = tag_uuid,
|
||||
.tag_type = NFCTagType::Ntag215,
|
||||
.unknown2 = 0x00,
|
||||
.unknown3 = 0x01,
|
||||
.unknown4 = 0x04,
|
||||
.unknown5 = 0xff,
|
||||
.unknown6 = 0xff,
|
||||
.unknown7 = 0xff,
|
||||
.unknown8 = 0xff,
|
||||
.magic = data[16],
|
||||
.write_count = static_cast<u16>((data[17] << 8) + data[18]),
|
||||
.amiibo_version = data[19],
|
||||
},
|
||||
.number_of_chunks = 3,
|
||||
.data_chunks =
|
||||
{
|
||||
MakeAmiiboChunk(0x05, 0x20, data),
|
||||
MakeAmiiboChunk(0x20, 0xf0, data),
|
||||
MakeAmiiboChunk(0x5c, 0x98, data),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
MifareReadPackage NfcProtocol::MakeMifareReadPackage(
|
||||
const MifareUUID& tag_uuid, std::span<const MifareReadChunk> read_request) const {
|
||||
MifareReadPackage package{
|
||||
.command_data{
|
||||
.unknown1 = 0xd0,
|
||||
.unknown2 = 0x07,
|
||||
.number_of_short_bytes = static_cast<u8>(
|
||||
((read_request.size() * sizeof(MifareReadChunk)) + sizeof(MifareUUID)) / 2),
|
||||
.uid = tag_uuid,
|
||||
},
|
||||
.data_chunks = {},
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < read_request.size() && i < package.data_chunks.size(); ++i) {
|
||||
package.data_chunks[i] = read_request[i];
|
||||
}
|
||||
|
||||
return package;
|
||||
}
|
||||
|
||||
MifareWritePackage NfcProtocol::MakeMifareWritePackage(
|
||||
const MifareUUID& tag_uuid, std::span<const MifareWriteChunk> read_request) const {
|
||||
MifareWritePackage package{
|
||||
.command_data{
|
||||
.unknown1 = 0xd0,
|
||||
.unknown2 = 0x07,
|
||||
.number_of_short_bytes = static_cast<u8>(
|
||||
((read_request.size() * sizeof(MifareReadChunk)) + sizeof(MifareUUID) + 2) / 2),
|
||||
.uid = tag_uuid,
|
||||
},
|
||||
.data_chunks = {},
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < read_request.size() && i < package.data_chunks.size(); ++i) {
|
||||
package.data_chunks[i] = read_request[i];
|
||||
}
|
||||
|
||||
return package;
|
||||
}
|
||||
|
||||
NFCDataChunk NfcProtocol::MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const {
|
||||
constexpr u8 NFC_PAGE_SIZE = 4;
|
||||
|
||||
if (static_cast<std::size_t>(page * NFC_PAGE_SIZE) + size >= data.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
NFCDataChunk chunk{
|
||||
.nfc_page = page,
|
||||
.data_size = size,
|
||||
.data = {},
|
||||
};
|
||||
std::memcpy(chunk.data.data(), data.data() + (page * NFC_PAGE_SIZE), size);
|
||||
return chunk;
|
||||
}
|
||||
|
||||
NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(NFCPages pages) const {
|
||||
switch (pages) {
|
||||
case NFCPages::Block0:
|
||||
return {
|
||||
.block_count = 1,
|
||||
};
|
||||
case NFCPages::Block3:
|
||||
return {
|
||||
.block_count = 1,
|
||||
.blocks =
|
||||
{
|
||||
NFCReadBlock{0x03, 0x03},
|
||||
},
|
||||
};
|
||||
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 {};
|
||||
};
|
||||
}
|
||||
|
||||
TagUUID NfcProtocol::GetTagUUID(std::span<const u8> data) const {
|
||||
if (data.size() < 10) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// crc byte 3 is omitted in this operation
|
||||
return {
|
||||
data[0], data[1], data[2], data[4], data[5], data[6], data[7],
|
||||
};
|
||||
}
|
||||
|
||||
bool NfcProtocol::IsEnabled() const {
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
bool NfcProtocol::IsPolling() const {
|
||||
return is_polling;
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
122
src/input_common/helpers/joycon_protocol/nfc.h
Normal file
122
src/input_common/helpers/joycon_protocol/nfc.h
Normal file
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
class NfcProtocol final : private JoyconCommonProtocol {
|
||||
public:
|
||||
explicit NfcProtocol(std::shared_ptr<JoyconHandle> handle);
|
||||
|
||||
Common::Input::DriverResult EnableNfc();
|
||||
|
||||
Common::Input::DriverResult DisableNfc();
|
||||
|
||||
Common::Input::DriverResult StartNFCPollingMode();
|
||||
|
||||
Common::Input::DriverResult StopNFCPollingMode();
|
||||
|
||||
Common::Input::DriverResult GetTagInfo(Joycon::TagInfo& tag_info);
|
||||
|
||||
Common::Input::DriverResult ReadAmiibo(std::vector<u8>& data);
|
||||
|
||||
Common::Input::DriverResult WriteAmiibo(std::span<const u8> data);
|
||||
|
||||
Common::Input::DriverResult ReadMifare(std::span<const MifareReadChunk> read_request,
|
||||
std::span<MifareReadData> out_data);
|
||||
|
||||
Common::Input::DriverResult WriteMifare(std::span<const MifareWriteChunk> write_request);
|
||||
|
||||
bool HasAmiibo();
|
||||
|
||||
bool IsEnabled() const;
|
||||
|
||||
bool IsPolling() const;
|
||||
|
||||
private:
|
||||
// Number of times the function will be delayed until it outputs valid data
|
||||
static constexpr std::size_t AMIIBO_UPDATE_DELAY = 15;
|
||||
|
||||
struct TagFoundData {
|
||||
u8 type;
|
||||
u8 uuid_size;
|
||||
TagUUID uuid;
|
||||
};
|
||||
|
||||
Common::Input::DriverResult WaitUntilNfcIs(NFCStatus status);
|
||||
|
||||
Common::Input::DriverResult IsTagInRange(TagFoundData& data, std::size_t timeout_limit = 1);
|
||||
|
||||
Common::Input::DriverResult GetAmiiboData(std::vector<u8>& data);
|
||||
|
||||
Common::Input::DriverResult WriteAmiiboData(const TagUUID& tag_uuid, std::span<const u8> data);
|
||||
|
||||
Common::Input::DriverResult GetMifareData(const MifareUUID& tag_uuid,
|
||||
std::span<const MifareReadChunk> read_request,
|
||||
std::span<MifareReadData> out_data);
|
||||
|
||||
Common::Input::DriverResult WriteMifareData(const MifareUUID& tag_uuid,
|
||||
std::span<const MifareWriteChunk> write_request);
|
||||
|
||||
Common::Input::DriverResult SendStartPollingRequest(MCUCommandResponse& output,
|
||||
bool is_second_attempt = false);
|
||||
|
||||
Common::Input::DriverResult SendStopPollingRequest(MCUCommandResponse& output);
|
||||
|
||||
Common::Input::DriverResult SendNextPackageRequest(MCUCommandResponse& output, u8 packet_id);
|
||||
|
||||
Common::Input::DriverResult SendReadAmiiboRequest(MCUCommandResponse& output,
|
||||
NFCPages ntag_pages);
|
||||
|
||||
Common::Input::DriverResult SendWriteAmiiboRequest(MCUCommandResponse& output,
|
||||
const TagUUID& tag_uuid);
|
||||
|
||||
Common::Input::DriverResult SendWriteDataAmiiboRequest(MCUCommandResponse& output, u8 block_id,
|
||||
bool is_last_packet,
|
||||
std::span<const u8> data);
|
||||
|
||||
Common::Input::DriverResult SendReadDataMifareRequest(MCUCommandResponse& output, u8 block_id,
|
||||
bool is_last_packet,
|
||||
std::span<const u8> data);
|
||||
|
||||
std::vector<u8> SerializeWritePackage(const NFCWritePackage& package) const;
|
||||
|
||||
std::vector<u8> SerializeMifareReadPackage(const MifareReadPackage& package) const;
|
||||
|
||||
std::vector<u8> SerializeMifareWritePackage(const MifareWritePackage& package) const;
|
||||
|
||||
NFCWritePackage MakeAmiiboWritePackage(const TagUUID& tag_uuid, std::span<const u8> data) const;
|
||||
|
||||
NFCDataChunk MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const;
|
||||
|
||||
MifareReadPackage MakeMifareReadPackage(const MifareUUID& tag_uuid,
|
||||
std::span<const MifareReadChunk> read_request) const;
|
||||
|
||||
MifareWritePackage MakeMifareWritePackage(const MifareUUID& tag_uuid,
|
||||
std::span<const MifareWriteChunk> read_request) const;
|
||||
|
||||
NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const;
|
||||
|
||||
TagUUID GetTagUUID(std::span<const u8> data) const;
|
||||
|
||||
bool is_enabled{};
|
||||
bool is_polling{};
|
||||
std::size_t update_counter{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
374
src/input_common/helpers/joycon_protocol/poller.cpp
Normal file
374
src/input_common/helpers/joycon_protocol/poller.cpp
Normal file
@@ -0,0 +1,374 @@
|
||||
// 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 JoyconCallbacks& callbacks_) {
|
||||
callbacks = std::move(callbacks_);
|
||||
}
|
||||
|
||||
void JoyconPoller::ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
|
||||
const RingStatus& ring_status) {
|
||||
InputReportActive data{};
|
||||
memcpy(&data, buffer.data(), sizeof(InputReportActive));
|
||||
|
||||
switch (device_type) {
|
||||
case ControllerType::Left:
|
||||
UpdateActiveLeftPadInput(data, motion_status);
|
||||
break;
|
||||
case ControllerType::Right:
|
||||
UpdateActiveRightPadInput(data, motion_status);
|
||||
break;
|
||||
case ControllerType::Pro:
|
||||
UpdateActiveProPadInput(data, motion_status);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (ring_status.is_enabled) {
|
||||
UpdateRing(data.ring_input, ring_status);
|
||||
}
|
||||
|
||||
callbacks.on_battery_data(data.battery_status);
|
||||
}
|
||||
|
||||
void JoyconPoller::ReadPassiveMode(std::span<u8> buffer) {
|
||||
InputReportPassive data{};
|
||||
memcpy(&data, buffer.data(), sizeof(InputReportPassive));
|
||||
|
||||
switch (device_type) {
|
||||
case ControllerType::Left:
|
||||
UpdatePassiveLeftPadInput(data);
|
||||
break;
|
||||
case ControllerType::Right:
|
||||
UpdatePassiveRightPadInput(data);
|
||||
break;
|
||||
case ControllerType::Pro:
|
||||
UpdatePassiveProPadInput(data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void JoyconPoller::ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status) {
|
||||
// This mode is compatible with the active mode
|
||||
ReadActiveMode(buffer, motion_status, {});
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateColor(const Color& color) {
|
||||
callbacks.on_color_data(color);
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateAmiibo(const Joycon::TagInfo& tag_info) {
|
||||
callbacks.on_amiibo_data(tag_info);
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateCamera(const std::vector<u8>& camera_data, IrsResolution format) {
|
||||
callbacks.on_camera_data(camera_data, format);
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateRing(s16 value, const RingStatus& ring_status) {
|
||||
float normalized_value = static_cast<float>(value - ring_status.default_value);
|
||||
if (normalized_value > 0) {
|
||||
normalized_value = normalized_value /
|
||||
static_cast<float>(ring_status.max_value - ring_status.default_value);
|
||||
}
|
||||
if (normalized_value < 0) {
|
||||
normalized_value = normalized_value /
|
||||
static_cast<float>(ring_status.default_value - ring_status.min_value);
|
||||
}
|
||||
callbacks.on_ring_data(normalized_value);
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateActiveLeftPadInput(const InputReportActive& input,
|
||||
const MotionStatus& motion_status) {
|
||||
static constexpr std::array<Joycon::PadButton, 11> left_buttons{
|
||||
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
|
||||
Joycon::PadButton::Left, Joycon::PadButton::LeftSL, Joycon::PadButton::LeftSR,
|
||||
Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus,
|
||||
Joycon::PadButton::Capture, Joycon::PadButton::StickL,
|
||||
};
|
||||
|
||||
const u32 raw_button =
|
||||
static_cast<u32>(input.button_input[2] | ((input.button_input[1] & 0b00101001) << 16));
|
||||
for (std::size_t i = 0; i < left_buttons.size(); ++i) {
|
||||
const bool button_status = (raw_button & static_cast<u32>(left_buttons[i])) != 0;
|
||||
const int button = static_cast<int>(left_buttons[i]);
|
||||
callbacks.on_button_data(button, button_status);
|
||||
}
|
||||
|
||||
const u16 raw_left_axis_x =
|
||||
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
|
||||
const u16 raw_left_axis_y =
|
||||
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
|
||||
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
|
||||
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
|
||||
|
||||
if (motion_status.is_enabled) {
|
||||
auto left_motion = GetMotionInput(input, motion_status);
|
||||
// Rotate motion axis to the correct direction
|
||||
left_motion.accel_y = -left_motion.accel_y;
|
||||
left_motion.accel_z = -left_motion.accel_z;
|
||||
left_motion.gyro_x = -left_motion.gyro_x;
|
||||
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), left_motion);
|
||||
}
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateActiveRightPadInput(const InputReportActive& input,
|
||||
const MotionStatus& motion_status) {
|
||||
static constexpr std::array<Joycon::PadButton, 11> right_buttons{
|
||||
Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B,
|
||||
Joycon::PadButton::A, Joycon::PadButton::RightSL, Joycon::PadButton::RightSR,
|
||||
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
|
||||
Joycon::PadButton::Home, Joycon::PadButton::StickR,
|
||||
};
|
||||
|
||||
const u32 raw_button =
|
||||
static_cast<u32>((input.button_input[0] << 8) | (input.button_input[1] << 16));
|
||||
for (std::size_t i = 0; i < right_buttons.size(); ++i) {
|
||||
const bool button_status = (raw_button & static_cast<u32>(right_buttons[i])) != 0;
|
||||
const int button = static_cast<int>(right_buttons[i]);
|
||||
callbacks.on_button_data(button, button_status);
|
||||
}
|
||||
|
||||
const u16 raw_right_axis_x =
|
||||
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
|
||||
const u16 raw_right_axis_y =
|
||||
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
|
||||
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
|
||||
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
|
||||
|
||||
if (motion_status.is_enabled) {
|
||||
auto right_motion = GetMotionInput(input, motion_status);
|
||||
// Rotate motion axis to the correct direction
|
||||
right_motion.accel_x = -right_motion.accel_x;
|
||||
right_motion.accel_y = -right_motion.accel_y;
|
||||
right_motion.gyro_z = -right_motion.gyro_z;
|
||||
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), right_motion);
|
||||
}
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdateActiveProPadInput(const InputReportActive& input,
|
||||
const MotionStatus& motion_status) {
|
||||
static constexpr std::array<Joycon::PadButton, 18> pro_buttons{
|
||||
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
|
||||
Joycon::PadButton::Left, Joycon::PadButton::L, Joycon::PadButton::ZL,
|
||||
Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::Y,
|
||||
Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A,
|
||||
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
|
||||
Joycon::PadButton::Home, Joycon::PadButton::StickL, Joycon::PadButton::StickR,
|
||||
};
|
||||
|
||||
const u32 raw_button = static_cast<u32>(input.button_input[2] | (input.button_input[0] << 8) |
|
||||
(input.button_input[1] << 16));
|
||||
for (std::size_t i = 0; i < pro_buttons.size(); ++i) {
|
||||
const bool button_status = (raw_button & static_cast<u32>(pro_buttons[i])) != 0;
|
||||
const int button = static_cast<int>(pro_buttons[i]);
|
||||
callbacks.on_button_data(button, button_status);
|
||||
}
|
||||
|
||||
const u16 raw_left_axis_x =
|
||||
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
|
||||
const u16 raw_left_axis_y =
|
||||
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
|
||||
const u16 raw_right_axis_x =
|
||||
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
|
||||
const u16 raw_right_axis_y =
|
||||
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
|
||||
|
||||
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
|
||||
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
|
||||
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
|
||||
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
|
||||
|
||||
if (motion_status.is_enabled) {
|
||||
auto pro_motion = GetMotionInput(input, motion_status);
|
||||
pro_motion.gyro_x = -pro_motion.gyro_x;
|
||||
pro_motion.accel_y = -pro_motion.accel_y;
|
||||
pro_motion.accel_z = -pro_motion.accel_z;
|
||||
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), pro_motion);
|
||||
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), pro_motion);
|
||||
}
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdatePassiveLeftPadInput(const InputReportPassive& input) {
|
||||
static constexpr std::array<PassivePadButton, 11> left_buttons{
|
||||
PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
|
||||
PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
|
||||
PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Minus,
|
||||
PassivePadButton::Capture, PassivePadButton::StickL,
|
||||
};
|
||||
|
||||
for (auto left_button : left_buttons) {
|
||||
const bool button_status = (input.button_input & static_cast<u32>(left_button)) != 0;
|
||||
const int button = static_cast<int>(left_button);
|
||||
callbacks.on_button_data(button, button_status);
|
||||
}
|
||||
|
||||
const auto [left_axis_x, left_axis_y] =
|
||||
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state));
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdatePassiveRightPadInput(const InputReportPassive& input) {
|
||||
static constexpr std::array<PassivePadButton, 11> right_buttons{
|
||||
PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
|
||||
PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
|
||||
PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Plus,
|
||||
PassivePadButton::Home, PassivePadButton::StickR,
|
||||
};
|
||||
|
||||
for (auto right_button : right_buttons) {
|
||||
const bool button_status = (input.button_input & static_cast<u32>(right_button)) != 0;
|
||||
const int button = static_cast<int>(right_button);
|
||||
callbacks.on_button_data(button, button_status);
|
||||
}
|
||||
|
||||
const auto [right_axis_x, right_axis_y] =
|
||||
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state));
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
|
||||
}
|
||||
|
||||
void JoyconPoller::UpdatePassiveProPadInput(const InputReportPassive& input) {
|
||||
static constexpr std::array<PassivePadButton, 14> pro_buttons{
|
||||
PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
|
||||
PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
|
||||
PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Minus,
|
||||
PassivePadButton::Plus, PassivePadButton::Capture, PassivePadButton::Home,
|
||||
PassivePadButton::StickL, PassivePadButton::StickR,
|
||||
};
|
||||
|
||||
for (auto pro_button : pro_buttons) {
|
||||
const bool button_status = (input.button_input & static_cast<u32>(pro_button)) != 0;
|
||||
const int button = static_cast<int>(pro_button);
|
||||
callbacks.on_button_data(button, button_status);
|
||||
}
|
||||
|
||||
const auto [left_axis_x, left_axis_y] =
|
||||
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state & 0xf));
|
||||
const auto [right_axis_x, right_axis_y] =
|
||||
GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state >> 4));
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
|
||||
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
|
||||
}
|
||||
|
||||
f32 JoyconPoller::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const {
|
||||
const f32 value = static_cast<f32>(raw_value - calibration.center);
|
||||
if (value > 0.0f) {
|
||||
return value / calibration.max;
|
||||
}
|
||||
return value / calibration.min;
|
||||
}
|
||||
|
||||
std::pair<f32, f32> JoyconPoller::GetPassiveAxisValue(PassivePadStick raw_value) const {
|
||||
switch (raw_value) {
|
||||
case PassivePadStick::Right:
|
||||
return {1.0f, 0.0f};
|
||||
case PassivePadStick::RightDown:
|
||||
return {1.0f, -1.0f};
|
||||
case PassivePadStick::Down:
|
||||
return {0.0f, -1.0f};
|
||||
case PassivePadStick::DownLeft:
|
||||
return {-1.0f, -1.0f};
|
||||
case PassivePadStick::Left:
|
||||
return {-1.0f, 0.0f};
|
||||
case PassivePadStick::LeftUp:
|
||||
return {-1.0f, 1.0f};
|
||||
case PassivePadStick::Up:
|
||||
return {0.0f, 1.0f};
|
||||
case PassivePadStick::UpRight:
|
||||
return {1.0f, 1.0f};
|
||||
case PassivePadStick::Neutral:
|
||||
default:
|
||||
return {0.0f, 0.0f};
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
84
src/input_common/helpers/joycon_protocol/poller.h
Normal file
84
src/input_common/helpers/joycon_protocol/poller.h
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <span>
|
||||
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
// Handles input packages and triggers the corresponding input events
|
||||
class JoyconPoller {
|
||||
public:
|
||||
JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
|
||||
JoyStickCalibration right_stick_calibration_,
|
||||
MotionCalibration motion_calibration_);
|
||||
|
||||
void SetCallbacks(const JoyconCallbacks& callbacks_);
|
||||
|
||||
/// Handles data from passive packages
|
||||
void ReadPassiveMode(std::span<u8> buffer);
|
||||
|
||||
/// Handles data from active packages
|
||||
void ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
|
||||
const RingStatus& ring_status);
|
||||
|
||||
/// Handles data from nfc or ir packages
|
||||
void ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status);
|
||||
|
||||
void UpdateColor(const Color& color);
|
||||
void UpdateRing(s16 value, const RingStatus& ring_status);
|
||||
void UpdateAmiibo(const Joycon::TagInfo& tag_info);
|
||||
void UpdateCamera(const std::vector<u8>& camera_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 UpdatePassiveLeftPadInput(const InputReportPassive& buffer);
|
||||
void UpdatePassiveRightPadInput(const InputReportPassive& buffer);
|
||||
void UpdatePassiveProPadInput(const InputReportPassive& buffer);
|
||||
|
||||
/// Returns a calibrated joystick axis from raw axis data
|
||||
f32 GetAxisValue(u16 raw_value, JoyStickAxisCalibration calibration) const;
|
||||
|
||||
/// Returns a digital joystick axis from passive axis data
|
||||
std::pair<f32, f32> GetPassiveAxisValue(PassivePadStick raw_value) 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{};
|
||||
|
||||
JoyconCallbacks callbacks{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
118
src/input_common/helpers/joycon_protocol/ringcon.cpp
Normal file
118
src/input_common/helpers/joycon_protocol/ringcon.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/joycon_protocol/ringcon.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
RingConProtocol::RingConProtocol(std::shared_ptr<JoyconHandle> handle)
|
||||
: JoyconCommonProtocol(std::move(handle)) {}
|
||||
|
||||
Common::Input::DriverResult RingConProtocol::EnableRingCon() {
|
||||
LOG_DEBUG(Input, "Enable Ringcon");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = SetReportMode(ReportMode::STANDARD_FULL_60HZ);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = EnableMCU(true);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
const MCUConfig config{
|
||||
.command = MCUCommand::ConfigureMCU,
|
||||
.sub_command = MCUSubCommand::SetDeviceMode,
|
||||
.mode = MCUMode::Standby,
|
||||
.crc = {},
|
||||
};
|
||||
result = ConfigureMCU(config);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult RingConProtocol::DisableRingCon() {
|
||||
LOG_DEBUG(Input, "Disable RingCon");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = EnableMCU(false);
|
||||
}
|
||||
|
||||
is_enabled = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult RingConProtocol::StartRingconPolling() {
|
||||
LOG_DEBUG(Input, "Enable Ringcon");
|
||||
ScopedSetBlocking sb(this);
|
||||
Common::Input::DriverResult result{Common::Input::DriverResult::Success};
|
||||
bool is_connected = false;
|
||||
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
result = IsRingConnected(is_connected);
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success && is_connected) {
|
||||
LOG_INFO(Input, "Ringcon detected");
|
||||
result = ConfigureRing();
|
||||
}
|
||||
if (result == Common::Input::DriverResult::Success) {
|
||||
is_enabled = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult RingConProtocol::IsRingConnected(bool& is_connected) {
|
||||
LOG_DEBUG(Input, "IsRingConnected");
|
||||
constexpr std::size_t max_tries = 42;
|
||||
SubCommandResponse output{};
|
||||
std::size_t tries = 0;
|
||||
is_connected = false;
|
||||
|
||||
do {
|
||||
const auto result = SendSubCommand(SubCommand::GET_EXTERNAL_DEVICE_INFO, {}, output);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success &&
|
||||
result != Common::Input::DriverResult::Timeout) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (tries++ >= max_tries) {
|
||||
return Common::Input::DriverResult::NoDeviceDetected;
|
||||
}
|
||||
} while (output.external_device_id != ExternalDeviceId::RingController);
|
||||
|
||||
is_connected = true;
|
||||
return Common::Input::DriverResult::Success;
|
||||
}
|
||||
|
||||
Common::Input::DriverResult RingConProtocol::ConfigureRing() {
|
||||
LOG_DEBUG(Input, "ConfigureRing");
|
||||
|
||||
static constexpr std::array<u8, 37> ring_config{
|
||||
0x06, 0x03, 0x25, 0x06, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x16, 0xED, 0x34, 0x36,
|
||||
0x00, 0x00, 0x00, 0x0A, 0x64, 0x0B, 0xE6, 0xA9, 0x22, 0x00, 0x00, 0x04, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xA8, 0xE1, 0x34, 0x36};
|
||||
|
||||
const Common::Input::DriverResult result =
|
||||
SendSubCommand(SubCommand::SET_EXTERNAL_FORMAT_CONFIG, ring_config);
|
||||
|
||||
if (result != Common::Input::DriverResult::Success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static constexpr std::array<u8, 4> ringcon_data{0x04, 0x01, 0x01, 0x02};
|
||||
return SendSubCommand(SubCommand::ENABLE_EXTERNAL_POLLING, ringcon_data);
|
||||
}
|
||||
|
||||
bool RingConProtocol::IsEnabled() const {
|
||||
return is_enabled;
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
42
src/input_common/helpers/joycon_protocol/ringcon.h
Normal file
42
src/input_common/helpers/joycon_protocol/ringcon.h
Normal file
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
class RingConProtocol final : private JoyconCommonProtocol {
|
||||
public:
|
||||
explicit RingConProtocol(std::shared_ptr<JoyconHandle> handle);
|
||||
|
||||
Common::Input::DriverResult EnableRingCon();
|
||||
|
||||
Common::Input::DriverResult DisableRingCon();
|
||||
|
||||
Common::Input::DriverResult StartRingconPolling();
|
||||
|
||||
bool IsEnabled() const;
|
||||
|
||||
private:
|
||||
Common::Input::DriverResult IsRingConnected(bool& is_connected);
|
||||
|
||||
Common::Input::DriverResult ConfigureRing();
|
||||
|
||||
bool is_enabled{};
|
||||
};
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
300
src/input_common/helpers/joycon_protocol/rumble.cpp
Normal file
300
src/input_common/helpers/joycon_protocol/rumble.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "common/input.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/joycon_protocol/rumble.h"
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
RumbleProtocol::RumbleProtocol(std::shared_ptr<JoyconHandle> handle)
|
||||
: JoyconCommonProtocol(std::move(handle)) {}
|
||||
|
||||
Common::Input::DriverResult RumbleProtocol::EnableRumble(bool is_enabled) {
|
||||
LOG_DEBUG(Input, "Enable Rumble");
|
||||
ScopedSetBlocking sb(this);
|
||||
const std::array<u8, 1> buffer{static_cast<u8>(is_enabled ? 1 : 0)};
|
||||
return SendSubCommand(SubCommand::ENABLE_VIBRATION, buffer);
|
||||
}
|
||||
|
||||
Common::Input::DriverResult RumbleProtocol::SendVibration(const VibrationValue& vibration) {
|
||||
std::array<u8, sizeof(DefaultVibrationBuffer)> buffer{};
|
||||
|
||||
if (vibration.high_amplitude <= 0.0f && vibration.low_amplitude <= 0.0f) {
|
||||
return SendVibrationReport(DefaultVibrationBuffer);
|
||||
}
|
||||
|
||||
// Protect joycons from damage from strong vibrations
|
||||
const f32 clamp_amplitude =
|
||||
1.0f / std::max(1.0f, vibration.high_amplitude + vibration.low_amplitude);
|
||||
|
||||
const u16 encoded_high_frequency = EncodeHighFrequency(vibration.high_frequency);
|
||||
const u8 encoded_high_amplitude =
|
||||
EncodeHighAmplitude(vibration.high_amplitude * clamp_amplitude);
|
||||
const u8 encoded_low_frequency = EncodeLowFrequency(vibration.low_frequency);
|
||||
const u16 encoded_low_amplitude = EncodeLowAmplitude(vibration.low_amplitude * clamp_amplitude);
|
||||
|
||||
buffer[0] = static_cast<u8>(encoded_high_frequency & 0xFF);
|
||||
buffer[1] = static_cast<u8>(encoded_high_amplitude | ((encoded_high_frequency >> 8) & 0x01));
|
||||
buffer[2] = static_cast<u8>(encoded_low_frequency | ((encoded_low_amplitude >> 8) & 0x80));
|
||||
buffer[3] = static_cast<u8>(encoded_low_amplitude & 0xFF);
|
||||
|
||||
// Duplicate rumble for now
|
||||
buffer[4] = buffer[0];
|
||||
buffer[5] = buffer[1];
|
||||
buffer[6] = buffer[2];
|
||||
buffer[7] = buffer[3];
|
||||
|
||||
return SendVibrationReport(buffer);
|
||||
}
|
||||
|
||||
u16 RumbleProtocol::EncodeHighFrequency(f32 frequency) const {
|
||||
const u8 new_frequency =
|
||||
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f));
|
||||
return static_cast<u16>((new_frequency - 0x60) * 4);
|
||||
}
|
||||
|
||||
u8 RumbleProtocol::EncodeLowFrequency(f32 frequency) const {
|
||||
const u8 new_frequency =
|
||||
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f));
|
||||
return static_cast<u8>(new_frequency - 0x40);
|
||||
}
|
||||
|
||||
u8 RumbleProtocol::EncodeHighAmplitude(f32 amplitude) const {
|
||||
// More information about these values can be found here:
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
|
||||
|
||||
static constexpr std::array<std::pair<f32, int>, 101> high_frequency_amplitude{
|
||||
std::pair<f32, int>{0.0f, 0x0},
|
||||
{0.01f, 0x2},
|
||||
{0.012f, 0x4},
|
||||
{0.014f, 0x6},
|
||||
{0.017f, 0x8},
|
||||
{0.02f, 0x0a},
|
||||
{0.024f, 0x0c},
|
||||
{0.028f, 0x0e},
|
||||
{0.033f, 0x10},
|
||||
{0.04f, 0x12},
|
||||
{0.047f, 0x14},
|
||||
{0.056f, 0x16},
|
||||
{0.067f, 0x18},
|
||||
{0.08f, 0x1a},
|
||||
{0.095f, 0x1c},
|
||||
{0.112f, 0x1e},
|
||||
{0.117f, 0x20},
|
||||
{0.123f, 0x22},
|
||||
{0.128f, 0x24},
|
||||
{0.134f, 0x26},
|
||||
{0.14f, 0x28},
|
||||
{0.146f, 0x2a},
|
||||
{0.152f, 0x2c},
|
||||
{0.159f, 0x2e},
|
||||
{0.166f, 0x30},
|
||||
{0.173f, 0x32},
|
||||
{0.181f, 0x34},
|
||||
{0.189f, 0x36},
|
||||
{0.198f, 0x38},
|
||||
{0.206f, 0x3a},
|
||||
{0.215f, 0x3c},
|
||||
{0.225f, 0x3e},
|
||||
{0.23f, 0x40},
|
||||
{0.235f, 0x42},
|
||||
{0.24f, 0x44},
|
||||
{0.245f, 0x46},
|
||||
{0.251f, 0x48},
|
||||
{0.256f, 0x4a},
|
||||
{0.262f, 0x4c},
|
||||
{0.268f, 0x4e},
|
||||
{0.273f, 0x50},
|
||||
{0.279f, 0x52},
|
||||
{0.286f, 0x54},
|
||||
{0.292f, 0x56},
|
||||
{0.298f, 0x58},
|
||||
{0.305f, 0x5a},
|
||||
{0.311f, 0x5c},
|
||||
{0.318f, 0x5e},
|
||||
{0.325f, 0x60},
|
||||
{0.332f, 0x62},
|
||||
{0.34f, 0x64},
|
||||
{0.347f, 0x66},
|
||||
{0.355f, 0x68},
|
||||
{0.362f, 0x6a},
|
||||
{0.37f, 0x6c},
|
||||
{0.378f, 0x6e},
|
||||
{0.387f, 0x70},
|
||||
{0.395f, 0x72},
|
||||
{0.404f, 0x74},
|
||||
{0.413f, 0x76},
|
||||
{0.422f, 0x78},
|
||||
{0.431f, 0x7a},
|
||||
{0.44f, 0x7c},
|
||||
{0.45f, 0x7e},
|
||||
{0.46f, 0x80},
|
||||
{0.47f, 0x82},
|
||||
{0.48f, 0x84},
|
||||
{0.491f, 0x86},
|
||||
{0.501f, 0x88},
|
||||
{0.512f, 0x8a},
|
||||
{0.524f, 0x8c},
|
||||
{0.535f, 0x8e},
|
||||
{0.547f, 0x90},
|
||||
{0.559f, 0x92},
|
||||
{0.571f, 0x94},
|
||||
{0.584f, 0x96},
|
||||
{0.596f, 0x98},
|
||||
{0.609f, 0x9a},
|
||||
{0.623f, 0x9c},
|
||||
{0.636f, 0x9e},
|
||||
{0.65f, 0xa0},
|
||||
{0.665f, 0xa2},
|
||||
{0.679f, 0xa4},
|
||||
{0.694f, 0xa6},
|
||||
{0.709f, 0xa8},
|
||||
{0.725f, 0xaa},
|
||||
{0.741f, 0xac},
|
||||
{0.757f, 0xae},
|
||||
{0.773f, 0xb0},
|
||||
{0.79f, 0xb2},
|
||||
{0.808f, 0xb4},
|
||||
{0.825f, 0xb6},
|
||||
{0.843f, 0xb8},
|
||||
{0.862f, 0xba},
|
||||
{0.881f, 0xbc},
|
||||
{0.9f, 0xbe},
|
||||
{0.92f, 0xc0},
|
||||
{0.94f, 0xc2},
|
||||
{0.96f, 0xc4},
|
||||
{0.981f, 0xc6},
|
||||
{1.003f, 0xc8},
|
||||
};
|
||||
|
||||
for (const auto& [amplitude_value, code] : high_frequency_amplitude) {
|
||||
if (amplitude <= amplitude_value) {
|
||||
return static_cast<u8>(code);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<u8>(high_frequency_amplitude[high_frequency_amplitude.size() - 1].second);
|
||||
}
|
||||
|
||||
u16 RumbleProtocol::EncodeLowAmplitude(f32 amplitude) const {
|
||||
// More information about these values can be found here:
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
|
||||
|
||||
static constexpr std::array<std::pair<f32, int>, 101> high_frequency_amplitude{
|
||||
std::pair<f32, int>{0.0f, 0x0040},
|
||||
{0.01f, 0x8040},
|
||||
{0.012f, 0x0041},
|
||||
{0.014f, 0x8041},
|
||||
{0.017f, 0x0042},
|
||||
{0.02f, 0x8042},
|
||||
{0.024f, 0x0043},
|
||||
{0.028f, 0x8043},
|
||||
{0.033f, 0x0044},
|
||||
{0.04f, 0x8044},
|
||||
{0.047f, 0x0045},
|
||||
{0.056f, 0x8045},
|
||||
{0.067f, 0x0046},
|
||||
{0.08f, 0x8046},
|
||||
{0.095f, 0x0047},
|
||||
{0.112f, 0x8047},
|
||||
{0.117f, 0x0048},
|
||||
{0.123f, 0x8048},
|
||||
{0.128f, 0x0049},
|
||||
{0.134f, 0x8049},
|
||||
{0.14f, 0x004a},
|
||||
{0.146f, 0x804a},
|
||||
{0.152f, 0x004b},
|
||||
{0.159f, 0x804b},
|
||||
{0.166f, 0x004c},
|
||||
{0.173f, 0x804c},
|
||||
{0.181f, 0x004d},
|
||||
{0.189f, 0x804d},
|
||||
{0.198f, 0x004e},
|
||||
{0.206f, 0x804e},
|
||||
{0.215f, 0x004f},
|
||||
{0.225f, 0x804f},
|
||||
{0.23f, 0x0050},
|
||||
{0.235f, 0x8050},
|
||||
{0.24f, 0x0051},
|
||||
{0.245f, 0x8051},
|
||||
{0.251f, 0x0052},
|
||||
{0.256f, 0x8052},
|
||||
{0.262f, 0x0053},
|
||||
{0.268f, 0x8053},
|
||||
{0.273f, 0x0054},
|
||||
{0.279f, 0x8054},
|
||||
{0.286f, 0x0055},
|
||||
{0.292f, 0x8055},
|
||||
{0.298f, 0x0056},
|
||||
{0.305f, 0x8056},
|
||||
{0.311f, 0x0057},
|
||||
{0.318f, 0x8057},
|
||||
{0.325f, 0x0058},
|
||||
{0.332f, 0x8058},
|
||||
{0.34f, 0x0059},
|
||||
{0.347f, 0x8059},
|
||||
{0.355f, 0x005a},
|
||||
{0.362f, 0x805a},
|
||||
{0.37f, 0x005b},
|
||||
{0.378f, 0x805b},
|
||||
{0.387f, 0x005c},
|
||||
{0.395f, 0x805c},
|
||||
{0.404f, 0x005d},
|
||||
{0.413f, 0x805d},
|
||||
{0.422f, 0x005e},
|
||||
{0.431f, 0x805e},
|
||||
{0.44f, 0x005f},
|
||||
{0.45f, 0x805f},
|
||||
{0.46f, 0x0060},
|
||||
{0.47f, 0x8060},
|
||||
{0.48f, 0x0061},
|
||||
{0.491f, 0x8061},
|
||||
{0.501f, 0x0062},
|
||||
{0.512f, 0x8062},
|
||||
{0.524f, 0x0063},
|
||||
{0.535f, 0x8063},
|
||||
{0.547f, 0x0064},
|
||||
{0.559f, 0x8064},
|
||||
{0.571f, 0x0065},
|
||||
{0.584f, 0x8065},
|
||||
{0.596f, 0x0066},
|
||||
{0.609f, 0x8066},
|
||||
{0.623f, 0x0067},
|
||||
{0.636f, 0x8067},
|
||||
{0.65f, 0x0068},
|
||||
{0.665f, 0x8068},
|
||||
{0.679f, 0x0069},
|
||||
{0.694f, 0x8069},
|
||||
{0.709f, 0x006a},
|
||||
{0.725f, 0x806a},
|
||||
{0.741f, 0x006b},
|
||||
{0.757f, 0x806b},
|
||||
{0.773f, 0x006c},
|
||||
{0.79f, 0x806c},
|
||||
{0.808f, 0x006d},
|
||||
{0.825f, 0x806d},
|
||||
{0.843f, 0x006e},
|
||||
{0.862f, 0x806e},
|
||||
{0.881f, 0x006f},
|
||||
{0.9f, 0x806f},
|
||||
{0.92f, 0x0070},
|
||||
{0.94f, 0x8070},
|
||||
{0.96f, 0x0071},
|
||||
{0.981f, 0x8071},
|
||||
{1.003f, 0x0072},
|
||||
};
|
||||
|
||||
for (const auto& [amplitude_value, code] : high_frequency_amplitude) {
|
||||
if (amplitude <= amplitude_value) {
|
||||
return static_cast<u16>(code);
|
||||
}
|
||||
}
|
||||
|
||||
return static_cast<u16>(high_frequency_amplitude[high_frequency_amplitude.size() - 1].second);
|
||||
}
|
||||
|
||||
} // namespace InputCommon::Joycon
|
||||
37
src/input_common/helpers/joycon_protocol/rumble.h
Normal file
37
src/input_common/helpers/joycon_protocol/rumble.h
Normal file
@@ -0,0 +1,37 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
|
||||
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
|
||||
// https://github.com/CTCaer/jc_toolkit
|
||||
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "input_common/helpers/joycon_protocol/common_protocol.h"
|
||||
#include "input_common/helpers/joycon_protocol/joycon_types.h"
|
||||
|
||||
namespace Common::Input {
|
||||
enum class DriverResult;
|
||||
}
|
||||
|
||||
namespace InputCommon::Joycon {
|
||||
|
||||
class RumbleProtocol final : private JoyconCommonProtocol {
|
||||
public:
|
||||
explicit RumbleProtocol(std::shared_ptr<JoyconHandle> handle);
|
||||
|
||||
Common::Input::DriverResult EnableRumble(bool is_enabled);
|
||||
|
||||
Common::Input::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
|
||||
332
src/input_common/helpers/stick_from_buttons.cpp
Normal file
332
src/input_common/helpers/stick_from_buttons.cpp
Normal file
@@ -0,0 +1,332 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include "common/math_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "input_common/helpers/stick_from_buttons.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
class Stick final : public Common::Input::InputDevice {
|
||||
public:
|
||||
// Some games such as EARTH DEFENSE FORCE: WORLD BROTHERS
|
||||
// do not play nicely with the theoretical maximum range.
|
||||
// Using a value one lower from the maximum emulates real stick behavior.
|
||||
static constexpr float MAX_RANGE = 32766.0f / 32767.0f;
|
||||
static constexpr float TAU = Common::PI * 2.0f;
|
||||
// Use wider angle to ease the transition.
|
||||
static constexpr float APERTURE = TAU * 0.15f;
|
||||
|
||||
using Button = std::unique_ptr<Common::Input::InputDevice>;
|
||||
|
||||
Stick(Button up_, Button down_, Button left_, Button right_, Button modifier_, Button updater_,
|
||||
float modifier_scale_, float modifier_angle_)
|
||||
: up(std::move(up_)), down(std::move(down_)), left(std::move(left_)),
|
||||
right(std::move(right_)), modifier(std::move(modifier_)), updater(std::move(updater_)),
|
||||
modifier_scale(modifier_scale_), modifier_angle(modifier_angle_) {
|
||||
up->SetCallback({
|
||||
.on_change =
|
||||
[this](const Common::Input::CallbackStatus& callback_) {
|
||||
UpdateUpButtonStatus(callback_);
|
||||
},
|
||||
});
|
||||
down->SetCallback({
|
||||
.on_change =
|
||||
[this](const Common::Input::CallbackStatus& callback_) {
|
||||
UpdateDownButtonStatus(callback_);
|
||||
},
|
||||
});
|
||||
left->SetCallback({
|
||||
.on_change =
|
||||
[this](const Common::Input::CallbackStatus& callback_) {
|
||||
UpdateLeftButtonStatus(callback_);
|
||||
},
|
||||
});
|
||||
right->SetCallback({
|
||||
.on_change =
|
||||
[this](const Common::Input::CallbackStatus& callback_) {
|
||||
UpdateRightButtonStatus(callback_);
|
||||
},
|
||||
});
|
||||
modifier->SetCallback({
|
||||
.on_change =
|
||||
[this](const Common::Input::CallbackStatus& callback_) {
|
||||
UpdateModButtonStatus(callback_);
|
||||
},
|
||||
});
|
||||
updater->SetCallback({
|
||||
.on_change = [this](const Common::Input::CallbackStatus& callback_) { SoftUpdate(); },
|
||||
});
|
||||
last_x_axis_value = 0.0f;
|
||||
last_y_axis_value = 0.0f;
|
||||
}
|
||||
|
||||
bool IsAngleGreater(float old_angle, float new_angle) const {
|
||||
const float top_limit = new_angle + APERTURE;
|
||||
return (old_angle > new_angle && old_angle <= top_limit) ||
|
||||
(old_angle + TAU > new_angle && old_angle + TAU <= top_limit);
|
||||
}
|
||||
|
||||
bool IsAngleSmaller(float old_angle, float new_angle) const {
|
||||
const float bottom_limit = new_angle - APERTURE;
|
||||
return (old_angle >= bottom_limit && old_angle < new_angle) ||
|
||||
(old_angle - TAU >= bottom_limit && old_angle - TAU < new_angle);
|
||||
}
|
||||
|
||||
float GetAngle(std::chrono::time_point<std::chrono::steady_clock> now) const {
|
||||
float new_angle = angle;
|
||||
|
||||
auto time_difference = static_cast<float>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
|
||||
time_difference /= 1000.0f;
|
||||
if (time_difference > 0.5f) {
|
||||
time_difference = 0.5f;
|
||||
}
|
||||
|
||||
if (IsAngleGreater(new_angle, goal_angle)) {
|
||||
new_angle -= modifier_angle * time_difference;
|
||||
if (new_angle < 0) {
|
||||
new_angle += TAU;
|
||||
}
|
||||
if (!IsAngleGreater(new_angle, goal_angle)) {
|
||||
return goal_angle;
|
||||
}
|
||||
} else if (IsAngleSmaller(new_angle, goal_angle)) {
|
||||
new_angle += modifier_angle * time_difference;
|
||||
if (new_angle >= TAU) {
|
||||
new_angle -= TAU;
|
||||
}
|
||||
if (!IsAngleSmaller(new_angle, goal_angle)) {
|
||||
return goal_angle;
|
||||
}
|
||||
} else {
|
||||
return goal_angle;
|
||||
}
|
||||
return new_angle;
|
||||
}
|
||||
|
||||
void SetGoalAngle(bool r, bool l, bool u, bool d) {
|
||||
// Move to the right
|
||||
if (r && !u && !d) {
|
||||
goal_angle = 0.0f;
|
||||
}
|
||||
|
||||
// Move to the upper right
|
||||
if (r && u && !d) {
|
||||
goal_angle = Common::PI * 0.25f;
|
||||
}
|
||||
|
||||
// Move up
|
||||
if (u && !l && !r) {
|
||||
goal_angle = Common::PI * 0.5f;
|
||||
}
|
||||
|
||||
// Move to the upper left
|
||||
if (l && u && !d) {
|
||||
goal_angle = Common::PI * 0.75f;
|
||||
}
|
||||
|
||||
// Move to the left
|
||||
if (l && !u && !d) {
|
||||
goal_angle = Common::PI;
|
||||
}
|
||||
|
||||
// Move to the bottom left
|
||||
if (l && !u && d) {
|
||||
goal_angle = Common::PI * 1.25f;
|
||||
}
|
||||
|
||||
// Move down
|
||||
if (d && !l && !r) {
|
||||
goal_angle = Common::PI * 1.5f;
|
||||
}
|
||||
|
||||
// Move to the bottom right
|
||||
if (r && !u && d) {
|
||||
goal_angle = Common::PI * 1.75f;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateUpButtonStatus(const Common::Input::CallbackStatus& button_callback) {
|
||||
up_status = button_callback.button_status.value;
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void UpdateDownButtonStatus(const Common::Input::CallbackStatus& button_callback) {
|
||||
down_status = button_callback.button_status.value;
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void UpdateLeftButtonStatus(const Common::Input::CallbackStatus& button_callback) {
|
||||
left_status = button_callback.button_status.value;
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void UpdateRightButtonStatus(const Common::Input::CallbackStatus& button_callback) {
|
||||
right_status = button_callback.button_status.value;
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void UpdateModButtonStatus(const Common::Input::CallbackStatus& button_callback) {
|
||||
const auto& new_status = button_callback.button_status;
|
||||
const bool new_button_value = new_status.inverted ? !new_status.value : new_status.value;
|
||||
modifier_status.toggle = new_status.toggle;
|
||||
|
||||
// Update button status with current
|
||||
if (!modifier_status.toggle) {
|
||||
modifier_status.locked = false;
|
||||
if (modifier_status.value != new_button_value) {
|
||||
modifier_status.value = new_button_value;
|
||||
}
|
||||
} else {
|
||||
// Toggle button and lock status
|
||||
if (new_button_value && !modifier_status.locked) {
|
||||
modifier_status.locked = true;
|
||||
modifier_status.value = !modifier_status.value;
|
||||
}
|
||||
|
||||
// Unlock button ready for next press
|
||||
if (!new_button_value && modifier_status.locked) {
|
||||
modifier_status.locked = false;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateStatus();
|
||||
}
|
||||
|
||||
void UpdateStatus() {
|
||||
bool r = right_status;
|
||||
bool l = left_status;
|
||||
bool u = up_status;
|
||||
bool d = down_status;
|
||||
|
||||
// Eliminate contradictory movements
|
||||
if (r && l) {
|
||||
r = false;
|
||||
l = false;
|
||||
}
|
||||
if (u && d) {
|
||||
u = false;
|
||||
d = false;
|
||||
}
|
||||
|
||||
// Move if a key is pressed
|
||||
if (r || l || u || d) {
|
||||
amplitude = modifier_status.value ? modifier_scale : MAX_RANGE;
|
||||
} else {
|
||||
amplitude = 0;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto time_difference = static_cast<u64>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
|
||||
|
||||
if (time_difference < 10) {
|
||||
// Disable analog mode if inputs are too fast
|
||||
SetGoalAngle(r, l, u, d);
|
||||
angle = goal_angle;
|
||||
} else {
|
||||
angle = GetAngle(now);
|
||||
SetGoalAngle(r, l, u, d);
|
||||
}
|
||||
|
||||
last_update = now;
|
||||
Common::Input::CallbackStatus status{
|
||||
.type = Common::Input::InputType::Stick,
|
||||
.stick_status = GetStatus(),
|
||||
};
|
||||
last_x_axis_value = status.stick_status.x.raw_value;
|
||||
last_y_axis_value = status.stick_status.y.raw_value;
|
||||
TriggerOnChange(status);
|
||||
}
|
||||
|
||||
void ForceUpdate() override {
|
||||
up->ForceUpdate();
|
||||
down->ForceUpdate();
|
||||
left->ForceUpdate();
|
||||
right->ForceUpdate();
|
||||
modifier->ForceUpdate();
|
||||
}
|
||||
|
||||
void SoftUpdate() {
|
||||
Common::Input::CallbackStatus status{
|
||||
.type = Common::Input::InputType::Stick,
|
||||
.stick_status = GetStatus(),
|
||||
};
|
||||
if (last_x_axis_value == status.stick_status.x.raw_value &&
|
||||
last_y_axis_value == status.stick_status.y.raw_value) {
|
||||
return;
|
||||
}
|
||||
last_x_axis_value = status.stick_status.x.raw_value;
|
||||
last_y_axis_value = status.stick_status.y.raw_value;
|
||||
TriggerOnChange(status);
|
||||
}
|
||||
|
||||
Common::Input::StickStatus GetStatus() const {
|
||||
Common::Input::StickStatus status{};
|
||||
status.x.properties = properties;
|
||||
status.y.properties = properties;
|
||||
|
||||
if (Settings::values.emulate_analog_keyboard) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float angle_ = GetAngle(now);
|
||||
status.x.raw_value = std::cos(angle_) * amplitude;
|
||||
status.y.raw_value = std::sin(angle_) * amplitude;
|
||||
return status;
|
||||
}
|
||||
|
||||
status.x.raw_value = std::cos(goal_angle) * amplitude;
|
||||
status.y.raw_value = std::sin(goal_angle) * amplitude;
|
||||
return status;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr Common::Input::AnalogProperties properties{
|
||||
.deadzone = 0.0f,
|
||||
.range = 1.0f,
|
||||
.threshold = 0.5f,
|
||||
.offset = 0.0f,
|
||||
.inverted = false,
|
||||
.toggle = false,
|
||||
};
|
||||
|
||||
Button up;
|
||||
Button down;
|
||||
Button left;
|
||||
Button right;
|
||||
Button modifier;
|
||||
Button updater;
|
||||
float modifier_scale{};
|
||||
float modifier_angle{};
|
||||
float angle{};
|
||||
float goal_angle{};
|
||||
float amplitude{};
|
||||
bool up_status{};
|
||||
bool down_status{};
|
||||
bool left_status{};
|
||||
bool right_status{};
|
||||
float last_x_axis_value{};
|
||||
float last_y_axis_value{};
|
||||
Common::Input::ButtonStatus modifier_status{};
|
||||
std::chrono::time_point<std::chrono::steady_clock> last_update;
|
||||
};
|
||||
|
||||
std::unique_ptr<Common::Input::InputDevice> StickFromButton::Create(
|
||||
const Common::ParamPackage& params) {
|
||||
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
|
||||
auto up = Common::Input::CreateInputDeviceFromString(params.Get("up", null_engine));
|
||||
auto down = Common::Input::CreateInputDeviceFromString(params.Get("down", null_engine));
|
||||
auto left = Common::Input::CreateInputDeviceFromString(params.Get("left", null_engine));
|
||||
auto right = Common::Input::CreateInputDeviceFromString(params.Get("right", null_engine));
|
||||
auto modifier = Common::Input::CreateInputDeviceFromString(params.Get("modifier", null_engine));
|
||||
auto updater = Common::Input::CreateInputDeviceFromString("engine:updater,button:0");
|
||||
auto modifier_scale = params.Get("modifier_scale", 0.5f);
|
||||
auto modifier_angle = params.Get("modifier_angle", 5.5f);
|
||||
return std::make_unique<Stick>(std::move(up), std::move(down), std::move(left),
|
||||
std::move(right), std::move(modifier), std::move(updater),
|
||||
modifier_scale, modifier_angle);
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
29
src/input_common/helpers/stick_from_buttons.h
Normal file
29
src/input_common/helpers/stick_from_buttons.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/input.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* An analog device factory that takes direction button devices and combines them into a analog
|
||||
* device.
|
||||
*/
|
||||
class StickFromButton final : public Common::Input::Factory<Common::Input::InputDevice> {
|
||||
public:
|
||||
/**
|
||||
* Creates an analog device from direction button devices
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "up": a serialized ParamPackage for creating a button device for up direction
|
||||
* - "down": a serialized ParamPackage for creating a button device for down direction
|
||||
* - "left": a serialized ParamPackage for creating a button device for left direction
|
||||
* - "right": a serialized ParamPackage for creating a button device for right direction
|
||||
* - "modifier": a serialized ParamPackage for creating a button device as the modifier
|
||||
* - "modifier_scale": a float for the multiplier the modifier gives to the position
|
||||
*/
|
||||
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
86
src/input_common/helpers/touch_from_buttons.cpp
Normal file
86
src/input_common/helpers/touch_from_buttons.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include "common/settings.h"
|
||||
#include "input_common/helpers/touch_from_buttons.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
class TouchFromButtonDevice final : public Common::Input::InputDevice {
|
||||
public:
|
||||
using Button = std::unique_ptr<Common::Input::InputDevice>;
|
||||
TouchFromButtonDevice(Button button_, float x_, float y_)
|
||||
: button(std::move(button_)), x(x_), y(y_) {
|
||||
last_button_value = false;
|
||||
button->SetCallback({
|
||||
.on_change =
|
||||
[this](const Common::Input::CallbackStatus& callback_) {
|
||||
UpdateButtonStatus(callback_);
|
||||
},
|
||||
});
|
||||
button->ForceUpdate();
|
||||
}
|
||||
|
||||
void ForceUpdate() override {
|
||||
button->ForceUpdate();
|
||||
}
|
||||
|
||||
Common::Input::TouchStatus GetStatus(bool pressed) const {
|
||||
const Common::Input::ButtonStatus button_status{
|
||||
.value = pressed,
|
||||
};
|
||||
Common::Input::TouchStatus status{
|
||||
.pressed = button_status,
|
||||
.x = {},
|
||||
.y = {},
|
||||
};
|
||||
status.x.properties = properties;
|
||||
status.y.properties = properties;
|
||||
|
||||
if (!pressed) {
|
||||
return status;
|
||||
}
|
||||
|
||||
status.x.raw_value = x;
|
||||
status.y.raw_value = y;
|
||||
return status;
|
||||
}
|
||||
|
||||
void UpdateButtonStatus(const Common::Input::CallbackStatus& button_callback) {
|
||||
const Common::Input::CallbackStatus status{
|
||||
.type = Common::Input::InputType::Touch,
|
||||
.touch_status = GetStatus(button_callback.button_status.value),
|
||||
};
|
||||
if (last_button_value != button_callback.button_status.value) {
|
||||
last_button_value = button_callback.button_status.value;
|
||||
TriggerOnChange(status);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr Common::Input::AnalogProperties properties{
|
||||
.deadzone = 0.0f,
|
||||
.range = 1.0f,
|
||||
.threshold = 0.5f,
|
||||
.offset = 0.0f,
|
||||
.inverted = false,
|
||||
.toggle = false,
|
||||
};
|
||||
|
||||
Button button;
|
||||
bool last_button_value;
|
||||
const float x;
|
||||
const float y;
|
||||
};
|
||||
|
||||
std::unique_ptr<Common::Input::InputDevice> TouchFromButton::Create(
|
||||
const Common::ParamPackage& params) {
|
||||
const std::string null_engine = Common::ParamPackage{{"engine", "null"}}.Serialize();
|
||||
auto button = Common::Input::CreateInputDeviceFromString(params.Get("button", null_engine));
|
||||
const float x = params.Get("x", 0.0f) / 1280.0f;
|
||||
const float y = params.Get("y", 0.0f) / 720.0f;
|
||||
return std::make_unique<TouchFromButtonDevice>(std::move(button), x, y);
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
21
src/input_common/helpers/touch_from_buttons.h
Normal file
21
src/input_common/helpers/touch_from_buttons.h
Normal file
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2020 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/input.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/**
|
||||
* A touch device factory that takes a list of button devices and combines them into a touch device.
|
||||
*/
|
||||
class TouchFromButton final : public Common::Input::Factory<Common::Input::InputDevice> {
|
||||
public:
|
||||
/**
|
||||
* Creates a touch device from a list of button devices
|
||||
*/
|
||||
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
77
src/input_common/helpers/udp_protocol.cpp
Normal file
77
src/input_common/helpers/udp_protocol.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/helpers/udp_protocol.h"
|
||||
|
||||
namespace InputCommon::CemuhookUDP {
|
||||
|
||||
static constexpr std::size_t GetSizeOfResponseType(Type t) {
|
||||
switch (t) {
|
||||
case Type::Version:
|
||||
return sizeof(Response::Version);
|
||||
case Type::PortInfo:
|
||||
return sizeof(Response::PortInfo);
|
||||
case Type::PadData:
|
||||
return sizeof(Response::PadData);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace Response {
|
||||
|
||||
/**
|
||||
* Returns Type if the packet is valid, else none
|
||||
*
|
||||
* Note: Modifies the buffer to zero out the crc (since that's the easiest way to check without
|
||||
* copying the buffer)
|
||||
*/
|
||||
std::optional<Type> Validate(u8* data, std::size_t size) {
|
||||
if (size < sizeof(Header)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Header header{};
|
||||
std::memcpy(&header, data, sizeof(Header));
|
||||
if (header.magic != SERVER_MAGIC) {
|
||||
LOG_ERROR(Input, "UDP Packet has an unexpected magic value");
|
||||
return std::nullopt;
|
||||
}
|
||||
if (header.protocol_version != PROTOCOL_VERSION) {
|
||||
LOG_ERROR(Input, "UDP Packet protocol mismatch");
|
||||
return std::nullopt;
|
||||
}
|
||||
if (header.type < Type::Version || header.type > Type::PadData) {
|
||||
LOG_ERROR(Input, "UDP Packet is an unknown type");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Packet size must equal sizeof(Header) + sizeof(Data)
|
||||
// and also verify that the packet info mentions the correct size. Since the spec includes the
|
||||
// type of the packet as part of the data, we need to include it in size calculations here
|
||||
// ie: payload_length == sizeof(T) + sizeof(Type)
|
||||
const std::size_t data_len = GetSizeOfResponseType(header.type);
|
||||
if (header.payload_length != data_len + sizeof(Type) || size < data_len + sizeof(Header)) {
|
||||
LOG_ERROR(
|
||||
Input,
|
||||
"UDP Packet payload length doesn't match. Received: {} PayloadLength: {} Expected: {}",
|
||||
size, header.payload_length, data_len + sizeof(Type));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const u32 crc32 = header.crc;
|
||||
boost::crc_32_type result;
|
||||
// zero out the crc in the buffer and then run the crc against it
|
||||
std::memset(&data[offsetof(Header, crc)], 0, sizeof(u32_le));
|
||||
|
||||
result.process_bytes(data, data_len + sizeof(Header));
|
||||
if (crc32 != result.checksum()) {
|
||||
LOG_ERROR(Input, "UDP Packet CRC check failed. Offset: {}", offsetof(Header, crc));
|
||||
return std::nullopt;
|
||||
}
|
||||
return header.type;
|
||||
}
|
||||
} // namespace Response
|
||||
|
||||
} // namespace InputCommon::CemuhookUDP
|
||||
302
src/input_common/helpers/udp_protocol.h
Normal file
302
src/input_common/helpers/udp_protocol.h
Normal file
@@ -0,0 +1,302 @@
|
||||
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4701) // Potentially uninitialized local variable 'result' used
|
||||
#endif
|
||||
|
||||
#include <boost/crc.hpp>
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#include "common/swap.h"
|
||||
|
||||
namespace InputCommon::CemuhookUDP {
|
||||
|
||||
constexpr std::size_t MAX_PACKET_SIZE = 100;
|
||||
constexpr u16 PROTOCOL_VERSION = 1001;
|
||||
constexpr u32 CLIENT_MAGIC = 0x43555344; // DSUC (but flipped for LE)
|
||||
constexpr u32 SERVER_MAGIC = 0x53555344; // DSUS (but flipped for LE)
|
||||
|
||||
enum class Type : u32 {
|
||||
Version = 0x00100000,
|
||||
PortInfo = 0x00100001,
|
||||
PadData = 0x00100002,
|
||||
};
|
||||
|
||||
struct Header {
|
||||
u32_le magic{};
|
||||
u16_le protocol_version{};
|
||||
u16_le payload_length{};
|
||||
u32_le crc{};
|
||||
u32_le id{};
|
||||
///> In the protocol, the type of the packet is not part of the header, but its convenient to
|
||||
///> include in the header so the callee doesn't have to duplicate the type twice when building
|
||||
///> the data
|
||||
Type type{};
|
||||
};
|
||||
static_assert(sizeof(Header) == 20, "UDP Message Header struct has wrong size");
|
||||
static_assert(std::is_trivially_copyable_v<Header>, "UDP Message Header is not trivially copyable");
|
||||
|
||||
using MacAddress = std::array<u8, 6>;
|
||||
constexpr MacAddress EMPTY_MAC_ADDRESS = {0, 0, 0, 0, 0, 0};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
template <typename T>
|
||||
struct Message {
|
||||
Header header{};
|
||||
T data;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
template <typename T>
|
||||
constexpr Type GetMessageType();
|
||||
|
||||
template <typename T>
|
||||
Message<T> CreateMessage(const u32 magic, const T data, const u32 sender_id) {
|
||||
boost::crc_32_type crc;
|
||||
Header header{
|
||||
magic, PROTOCOL_VERSION, sizeof(T) + sizeof(Type), 0, sender_id, GetMessageType<T>(),
|
||||
};
|
||||
Message<T> message{header, data};
|
||||
crc.process_bytes(&message, sizeof(Message<T>));
|
||||
message.header.crc = crc.checksum();
|
||||
return message;
|
||||
}
|
||||
|
||||
namespace Request {
|
||||
|
||||
enum RegisterFlags : u8 {
|
||||
AllPads,
|
||||
PadID,
|
||||
PadMACAddress,
|
||||
};
|
||||
|
||||
struct Version {};
|
||||
/**
|
||||
* Requests the server to send information about what controllers are plugged into the ports
|
||||
* In citron's case, we only have one controller, so for simplicity's sake, we can just send a
|
||||
* request explicitly for the first controller port and leave it at that. In the future it would be
|
||||
* nice to make this configurable
|
||||
*/
|
||||
constexpr u32 MAX_PORTS = 4;
|
||||
struct PortInfo {
|
||||
u32_le pad_count{}; ///> Number of ports to request data for
|
||||
std::array<u8, MAX_PORTS> port;
|
||||
};
|
||||
static_assert(std::is_trivially_copyable_v<PortInfo>,
|
||||
"UDP Request PortInfo is not trivially copyable");
|
||||
|
||||
/**
|
||||
* Request the latest pad information from the server. If the server hasn't received this message
|
||||
* from the client in a reasonable time frame, the server will stop sending updates. The default
|
||||
* timeout seems to be 5 seconds.
|
||||
*/
|
||||
struct PadData {
|
||||
/// Determines which method will be used as a look up for the controller
|
||||
RegisterFlags flags{};
|
||||
/// Index of the port of the controller to retrieve data about
|
||||
u8 port_id{};
|
||||
/// Mac address of the controller to retrieve data about
|
||||
MacAddress mac;
|
||||
};
|
||||
static_assert(sizeof(PadData) == 8, "UDP Request PadData struct has wrong size");
|
||||
static_assert(std::is_trivially_copyable_v<PadData>,
|
||||
"UDP Request PadData is not trivially copyable");
|
||||
|
||||
/**
|
||||
* Creates a message with the proper header data that can be sent to the server.
|
||||
* @param data Request body to send
|
||||
* @param client_id ID of the udp client (usually not checked on the server)
|
||||
*/
|
||||
template <typename T>
|
||||
Message<T> Create(const T data, const u32 client_id = 0) {
|
||||
return CreateMessage(CLIENT_MAGIC, data, client_id);
|
||||
}
|
||||
} // namespace Request
|
||||
|
||||
namespace Response {
|
||||
|
||||
enum class ConnectionType : u8 {
|
||||
None,
|
||||
Usb,
|
||||
Bluetooth,
|
||||
};
|
||||
|
||||
enum class State : u8 {
|
||||
Disconnected,
|
||||
Reserved,
|
||||
Connected,
|
||||
};
|
||||
|
||||
enum class Model : u8 {
|
||||
None,
|
||||
PartialGyro,
|
||||
FullGyro,
|
||||
Generic,
|
||||
};
|
||||
|
||||
enum class Battery : u8 {
|
||||
None = 0x00,
|
||||
Dying = 0x01,
|
||||
Low = 0x02,
|
||||
Medium = 0x03,
|
||||
High = 0x04,
|
||||
Full = 0x05,
|
||||
Charging = 0xEE,
|
||||
Charged = 0xEF,
|
||||
};
|
||||
|
||||
struct Version {
|
||||
u16_le version{};
|
||||
};
|
||||
static_assert(sizeof(Version) == 2, "UDP Response Version struct has wrong size");
|
||||
static_assert(std::is_trivially_copyable_v<Version>,
|
||||
"UDP Response Version is not trivially copyable");
|
||||
|
||||
struct PortInfo {
|
||||
u8 id{};
|
||||
State state{};
|
||||
Model model{};
|
||||
ConnectionType connection_type{};
|
||||
MacAddress mac;
|
||||
Battery battery{};
|
||||
u8 is_pad_active{};
|
||||
};
|
||||
static_assert(sizeof(PortInfo) == 12, "UDP Response PortInfo struct has wrong size");
|
||||
static_assert(std::is_trivially_copyable_v<PortInfo>,
|
||||
"UDP Response PortInfo is not trivially copyable");
|
||||
|
||||
struct TouchPad {
|
||||
u8 is_active{};
|
||||
u8 id{};
|
||||
u16_le x{};
|
||||
u16_le y{};
|
||||
};
|
||||
static_assert(sizeof(TouchPad) == 6, "UDP Response TouchPad struct has wrong size ");
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct PadData {
|
||||
PortInfo info{};
|
||||
u32_le packet_counter{};
|
||||
|
||||
u16_le digital_button{};
|
||||
// The following union isn't trivially copyable but we don't use this input anyway.
|
||||
// union DigitalButton {
|
||||
// u16_le button;
|
||||
// BitField<0, 1, u16> button_1; // Share
|
||||
// BitField<1, 1, u16> button_2; // L3
|
||||
// BitField<2, 1, u16> button_3; // R3
|
||||
// BitField<3, 1, u16> button_4; // Options
|
||||
// BitField<4, 1, u16> button_5; // Up
|
||||
// BitField<5, 1, u16> button_6; // Right
|
||||
// BitField<6, 1, u16> button_7; // Down
|
||||
// BitField<7, 1, u16> button_8; // Left
|
||||
// BitField<8, 1, u16> button_9; // L2
|
||||
// BitField<9, 1, u16> button_10; // R2
|
||||
// BitField<10, 1, u16> button_11; // L1
|
||||
// BitField<11, 1, u16> button_12; // R1
|
||||
// BitField<12, 1, u16> button_13; // Triangle
|
||||
// BitField<13, 1, u16> button_14; // Circle
|
||||
// BitField<14, 1, u16> button_15; // Cross
|
||||
// BitField<15, 1, u16> button_16; // Square
|
||||
// } digital_button;
|
||||
|
||||
u8 home;
|
||||
/// If the device supports a "click" on the touchpad, this will change to 1 when a click happens
|
||||
u8 touch_hard_press{};
|
||||
u8 left_stick_x{};
|
||||
u8 left_stick_y{};
|
||||
u8 right_stick_x{};
|
||||
u8 right_stick_y{};
|
||||
|
||||
struct AnalogButton {
|
||||
u8 button_dpad_left_analog{};
|
||||
u8 button_dpad_down_analog{};
|
||||
u8 button_dpad_right_analog{};
|
||||
u8 button_dpad_up_analog{};
|
||||
u8 button_square_analog{};
|
||||
u8 button_cross_analog{};
|
||||
u8 button_circle_analog{};
|
||||
u8 button_triangle_analog{};
|
||||
u8 button_r1_analog{};
|
||||
u8 button_l1_analog{};
|
||||
u8 trigger_r2{};
|
||||
u8 trigger_l2{};
|
||||
} analog_button;
|
||||
|
||||
std::array<TouchPad, 2> touch;
|
||||
|
||||
u64_le motion_timestamp;
|
||||
|
||||
struct Accelerometer {
|
||||
float x{};
|
||||
float y{};
|
||||
float z{};
|
||||
} accel;
|
||||
|
||||
struct Gyroscope {
|
||||
float pitch{};
|
||||
float yaw{};
|
||||
float roll{};
|
||||
} gyro;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(PadData) == 80, "UDP Response PadData struct has wrong size ");
|
||||
static_assert(std::is_trivially_copyable_v<PadData>,
|
||||
"UDP Response PadData is not trivially copyable");
|
||||
|
||||
static_assert(sizeof(Message<PadData>) == MAX_PACKET_SIZE,
|
||||
"UDP MAX_PACKET_SIZE is no longer larger than Message<PadData>");
|
||||
|
||||
static_assert(sizeof(PadData::AnalogButton) == 12,
|
||||
"UDP Response AnalogButton struct has wrong size ");
|
||||
static_assert(sizeof(PadData::Accelerometer) == 12,
|
||||
"UDP Response Accelerometer struct has wrong size ");
|
||||
static_assert(sizeof(PadData::Gyroscope) == 12, "UDP Response Gyroscope struct has wrong size ");
|
||||
|
||||
/**
|
||||
* Create a Response Message from the data
|
||||
* @param data array of bytes sent from the server
|
||||
* @return boost::none if it failed to parse or Type if it succeeded. The client can then safely
|
||||
* copy the data into the appropriate struct for that Type
|
||||
*/
|
||||
std::optional<Type> Validate(u8* data, std::size_t size);
|
||||
|
||||
} // namespace Response
|
||||
|
||||
template <>
|
||||
constexpr Type GetMessageType<Request::Version>() {
|
||||
return Type::Version;
|
||||
}
|
||||
template <>
|
||||
constexpr Type GetMessageType<Request::PortInfo>() {
|
||||
return Type::PortInfo;
|
||||
}
|
||||
template <>
|
||||
constexpr Type GetMessageType<Request::PadData>() {
|
||||
return Type::PadData;
|
||||
}
|
||||
template <>
|
||||
constexpr Type GetMessageType<Response::Version>() {
|
||||
return Type::Version;
|
||||
}
|
||||
template <>
|
||||
constexpr Type GetMessageType<Response::PortInfo>() {
|
||||
return Type::PortInfo;
|
||||
}
|
||||
template <>
|
||||
constexpr Type GetMessageType<Response::PadData>() {
|
||||
return Type::PadData;
|
||||
}
|
||||
} // namespace InputCommon::CemuhookUDP
|
||||
483
src/input_common/input_engine.cpp
Normal file
483
src/input_common/input_engine.cpp
Normal file
@@ -0,0 +1,483 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/logging/log.h"
|
||||
#include "input_common/input_engine.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
void InputEngine::PreSetController(const PadIdentifier& identifier) {
|
||||
std::scoped_lock lock{mutex};
|
||||
controller_list.try_emplace(identifier);
|
||||
}
|
||||
|
||||
void InputEngine::PreSetButton(const PadIdentifier& identifier, int button) {
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
controller.buttons.try_emplace(button, false);
|
||||
}
|
||||
|
||||
void InputEngine::PreSetHatButton(const PadIdentifier& identifier, int button) {
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
controller.hat_buttons.try_emplace(button, u8{0});
|
||||
}
|
||||
|
||||
void InputEngine::PreSetAxis(const PadIdentifier& identifier, int axis) {
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
controller.axes.try_emplace(axis, 0.0f);
|
||||
}
|
||||
|
||||
void InputEngine::PreSetMotion(const PadIdentifier& identifier, int motion) {
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
controller.motions.try_emplace(motion);
|
||||
}
|
||||
|
||||
void InputEngine::SetButton(const PadIdentifier& identifier, int button, bool value) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.buttons.insert_or_assign(button, value);
|
||||
}
|
||||
}
|
||||
TriggerOnButtonChange(identifier, button, value);
|
||||
}
|
||||
|
||||
void InputEngine::SetHatButton(const PadIdentifier& identifier, int button, u8 value) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.hat_buttons.insert_or_assign(button, value);
|
||||
}
|
||||
}
|
||||
TriggerOnHatButtonChange(identifier, button, value);
|
||||
}
|
||||
|
||||
void InputEngine::SetAxis(const PadIdentifier& identifier, int axis, f32 value) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.axes.insert_or_assign(axis, value);
|
||||
}
|
||||
}
|
||||
TriggerOnAxisChange(identifier, axis, value);
|
||||
}
|
||||
|
||||
void InputEngine::SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.battery = value;
|
||||
}
|
||||
}
|
||||
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};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.motions.insert_or_assign(motion, value);
|
||||
}
|
||||
}
|
||||
TriggerOnMotionChange(identifier, motion, value);
|
||||
}
|
||||
|
||||
void InputEngine::SetCamera(const PadIdentifier& identifier,
|
||||
const Common::Input::CameraStatus& value) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.camera = value;
|
||||
}
|
||||
}
|
||||
TriggerOnCameraChange(identifier, value);
|
||||
}
|
||||
|
||||
void InputEngine::SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
ControllerData& controller = controller_list.at(identifier);
|
||||
if (!configuring) {
|
||||
controller.nfc = value;
|
||||
}
|
||||
}
|
||||
TriggerOnNfcChange(identifier, value);
|
||||
}
|
||||
|
||||
bool InputEngine::GetButton(const PadIdentifier& identifier, int button) 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 false;
|
||||
}
|
||||
const ControllerData& controller = controller_iter->second;
|
||||
const auto button_iter = controller.buttons.find(button);
|
||||
if (button_iter == controller.buttons.cend()) {
|
||||
LOG_ERROR(Input, "Invalid button {}", button);
|
||||
return false;
|
||||
}
|
||||
return button_iter->second;
|
||||
}
|
||||
|
||||
bool InputEngine::GetHatButton(const PadIdentifier& identifier, int button, u8 direction) 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 false;
|
||||
}
|
||||
const ControllerData& controller = controller_iter->second;
|
||||
const auto hat_iter = controller.hat_buttons.find(button);
|
||||
if (hat_iter == controller.hat_buttons.cend()) {
|
||||
LOG_ERROR(Input, "Invalid hat button {}", button);
|
||||
return false;
|
||||
}
|
||||
return (hat_iter->second & direction) != 0;
|
||||
}
|
||||
|
||||
f32 InputEngine::GetAxis(const PadIdentifier& identifier, int axis) 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 0.0f;
|
||||
}
|
||||
const ControllerData& controller = controller_iter->second;
|
||||
const auto axis_iter = controller.axes.find(axis);
|
||||
if (axis_iter == controller.axes.cend()) {
|
||||
LOG_ERROR(Input, "Invalid axis {}", axis);
|
||||
return 0.0f;
|
||||
}
|
||||
return axis_iter->second;
|
||||
}
|
||||
|
||||
Common::Input::BatteryLevel InputEngine::GetBattery(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 Common::Input::BatteryLevel::Charging;
|
||||
}
|
||||
const ControllerData& controller = controller_iter->second;
|
||||
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);
|
||||
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.motions.at(motion);
|
||||
}
|
||||
|
||||
Common::Input::CameraStatus InputEngine::GetCamera(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.camera;
|
||||
}
|
||||
|
||||
Common::Input::NfcStatus InputEngine::GetNfc(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.nfc;
|
||||
}
|
||||
|
||||
void InputEngine::ResetButtonState() {
|
||||
for (const auto& controller : controller_list) {
|
||||
for (const auto& button : controller.second.buttons) {
|
||||
SetButton(controller.first, button.first, false);
|
||||
}
|
||||
for (const auto& button : controller.second.hat_buttons) {
|
||||
SetHatButton(controller.first, button.first, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InputEngine::ResetAnalogState() {
|
||||
for (const auto& controller : controller_list) {
|
||||
for (const auto& axis : controller.second.axes) {
|
||||
SetAxis(controller.first, axis.first, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InputEngine::TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool 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::Button, button)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
if (!configuring || !mapping_callback.on_data) {
|
||||
return;
|
||||
}
|
||||
|
||||
PreSetButton(identifier, button);
|
||||
if (value == GetButton(identifier, button)) {
|
||||
return;
|
||||
}
|
||||
mapping_callback.on_data(MappingData{
|
||||
.engine = GetEngineName(),
|
||||
.pad = identifier,
|
||||
.type = EngineInputType::Button,
|
||||
.index = button,
|
||||
.button_value = value,
|
||||
});
|
||||
}
|
||||
|
||||
void InputEngine::TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 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::HatButton, button)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
if (!configuring || !mapping_callback.on_data) {
|
||||
return;
|
||||
}
|
||||
for (std::size_t index = 1; index < 0xff; index <<= 1) {
|
||||
bool button_value = (value & index) != 0;
|
||||
if (button_value == GetHatButton(identifier, button, static_cast<u8>(index))) {
|
||||
continue;
|
||||
}
|
||||
mapping_callback.on_data(MappingData{
|
||||
.engine = GetEngineName(),
|
||||
.pad = identifier,
|
||||
.type = EngineInputType::HatButton,
|
||||
.index = button,
|
||||
.hat_name = GetHatButtonName(static_cast<u8>(index)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void InputEngine::TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 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::Analog, axis)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
if (!configuring || !mapping_callback.on_data) {
|
||||
return;
|
||||
}
|
||||
if (std::abs(value - GetAxis(identifier, axis)) < 0.5f) {
|
||||
return;
|
||||
}
|
||||
mapping_callback.on_data(MappingData{
|
||||
.engine = GetEngineName(),
|
||||
.pad = identifier,
|
||||
.type = EngineInputType::Analog,
|
||||
.index = axis,
|
||||
.axis_value = value,
|
||||
});
|
||||
}
|
||||
|
||||
void InputEngine::TriggerOnBatteryChange(const PadIdentifier& identifier,
|
||||
[[maybe_unused]] Common::Input::BatteryLevel 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::Battery, 0)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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};
|
||||
for (const auto& poller_pair : callback_list) {
|
||||
const InputIdentifier& poller = poller_pair.second;
|
||||
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Motion, motion)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
if (!configuring || !mapping_callback.on_data) {
|
||||
return;
|
||||
}
|
||||
const auto old_value = GetMotion(identifier, motion);
|
||||
bool is_active = false;
|
||||
if (std::abs(value.accel_x - old_value.accel_x) > 1.5f ||
|
||||
std::abs(value.accel_y - old_value.accel_y) > 1.5f ||
|
||||
std::abs(value.accel_z - old_value.accel_z) > 1.5f) {
|
||||
is_active = true;
|
||||
}
|
||||
if (std::abs(value.gyro_x - old_value.gyro_x) > 0.6f ||
|
||||
std::abs(value.gyro_y - old_value.gyro_y) > 0.6f ||
|
||||
std::abs(value.gyro_z - old_value.gyro_z) > 0.6f) {
|
||||
is_active = true;
|
||||
}
|
||||
if (!is_active) {
|
||||
return;
|
||||
}
|
||||
mapping_callback.on_data(MappingData{
|
||||
.engine = GetEngineName(),
|
||||
.pad = identifier,
|
||||
.type = EngineInputType::Motion,
|
||||
.index = motion,
|
||||
.motion_value = value,
|
||||
});
|
||||
}
|
||||
|
||||
void InputEngine::TriggerOnCameraChange(const PadIdentifier& identifier,
|
||||
[[maybe_unused]] const Common::Input::CameraStatus& 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::Camera, 0)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InputEngine::TriggerOnNfcChange(const PadIdentifier& identifier,
|
||||
[[maybe_unused]] const Common::Input::NfcStatus& 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::Nfc, 0)) {
|
||||
continue;
|
||||
}
|
||||
if (poller.callback.on_change) {
|
||||
poller.callback.on_change();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool InputEngine::IsInputIdentifierEqual(const InputIdentifier& input_identifier,
|
||||
const PadIdentifier& identifier, EngineInputType type,
|
||||
int index) const {
|
||||
if (input_identifier.type != type) {
|
||||
return false;
|
||||
}
|
||||
if (input_identifier.index != index) {
|
||||
return false;
|
||||
}
|
||||
if (input_identifier.identifier != identifier) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputEngine::BeginConfiguration() {
|
||||
configuring = true;
|
||||
}
|
||||
|
||||
void InputEngine::EndConfiguration() {
|
||||
configuring = false;
|
||||
}
|
||||
|
||||
const std::string& InputEngine::GetEngineName() const {
|
||||
return input_engine;
|
||||
}
|
||||
|
||||
int InputEngine::SetCallback(InputIdentifier input_identifier) {
|
||||
std::scoped_lock lock{mutex_callback};
|
||||
callback_list.insert_or_assign(last_callback_key, std::move(input_identifier));
|
||||
return last_callback_key++;
|
||||
}
|
||||
|
||||
void InputEngine::SetMappingCallback(MappingCallback callback) {
|
||||
std::scoped_lock lock{mutex_callback};
|
||||
mapping_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void InputEngine::DeleteCallback(int key) {
|
||||
std::scoped_lock lock{mutex_callback};
|
||||
const auto& iterator = callback_list.find(key);
|
||||
if (iterator == callback_list.end()) {
|
||||
LOG_ERROR(Input, "Tried to delete non-existent callback {}", key);
|
||||
return;
|
||||
}
|
||||
callback_list.erase(iterator);
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
306
src/input_common/input_engine.h
Normal file
306
src/input_common/input_engine.h
Normal file
@@ -0,0 +1,306 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "common/input.h"
|
||||
#include "common/param_package.h"
|
||||
#include "common/uuid.h"
|
||||
#include "input_common/main.h"
|
||||
|
||||
// Pad Identifier of data source
|
||||
struct PadIdentifier {
|
||||
Common::UUID guid{};
|
||||
std::size_t port{};
|
||||
std::size_t pad{};
|
||||
|
||||
friend constexpr bool operator==(const PadIdentifier&, const PadIdentifier&) = default;
|
||||
};
|
||||
|
||||
// Basic motion data containing data from the sensors and a timestamp in microseconds
|
||||
struct BasicMotion {
|
||||
float gyro_x{};
|
||||
float gyro_y{};
|
||||
float gyro_z{};
|
||||
float accel_x{};
|
||||
float accel_y{};
|
||||
float accel_z{};
|
||||
u64 delta_timestamp{};
|
||||
};
|
||||
|
||||
// Types of input that are stored in the engine
|
||||
enum class EngineInputType {
|
||||
None,
|
||||
Analog,
|
||||
Battery,
|
||||
Button,
|
||||
Camera,
|
||||
Color,
|
||||
HatButton,
|
||||
Motion,
|
||||
Nfc,
|
||||
};
|
||||
|
||||
struct VibrationRequest {
|
||||
PadIdentifier identifier;
|
||||
Common::Input::VibrationStatus vibration;
|
||||
};
|
||||
|
||||
namespace std {
|
||||
// Hash used to create lists from PadIdentifier data
|
||||
template <>
|
||||
struct hash<PadIdentifier> {
|
||||
size_t operator()(const PadIdentifier& pad_id) const noexcept {
|
||||
u64 hash_value = pad_id.guid.Hash();
|
||||
hash_value ^= (static_cast<u64>(pad_id.port) << 32);
|
||||
hash_value ^= static_cast<u64>(pad_id.pad);
|
||||
return static_cast<size_t>(hash_value);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
// Data from the engine and device needed for creating a ParamPackage
|
||||
struct MappingData {
|
||||
std::string engine{};
|
||||
PadIdentifier pad{};
|
||||
EngineInputType type{};
|
||||
int index{};
|
||||
bool button_value{};
|
||||
std::string hat_name{};
|
||||
f32 axis_value{};
|
||||
BasicMotion motion_value{};
|
||||
};
|
||||
|
||||
// Triggered if data changed on the controller
|
||||
struct UpdateCallback {
|
||||
std::function<void()> on_change;
|
||||
};
|
||||
|
||||
// Triggered if data changed on the controller and the engine is on configuring mode
|
||||
struct MappingCallback {
|
||||
std::function<void(const MappingData&)> on_data;
|
||||
};
|
||||
|
||||
// Input Identifier of data source
|
||||
struct InputIdentifier {
|
||||
PadIdentifier identifier;
|
||||
EngineInputType type;
|
||||
int index;
|
||||
UpdateCallback callback;
|
||||
};
|
||||
|
||||
class InputEngine {
|
||||
public:
|
||||
explicit InputEngine(std::string input_engine_) : input_engine{std::move(input_engine_)} {}
|
||||
|
||||
virtual ~InputEngine() = default;
|
||||
|
||||
// Enable configuring mode for mapping
|
||||
void BeginConfiguration();
|
||||
|
||||
// Disable configuring mode for mapping
|
||||
void EndConfiguration();
|
||||
|
||||
// Sets a led pattern for a controller
|
||||
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::DriverResult SetVibration(
|
||||
[[maybe_unused]] const PadIdentifier& identifier,
|
||||
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
// Returns true if device supports vibrations
|
||||
virtual bool IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sets polling mode to a controller
|
||||
virtual Common::Input::DriverResult SetPollingMode(
|
||||
[[maybe_unused]] const PadIdentifier& identifier,
|
||||
[[maybe_unused]] const Common::Input::PollingMode polling_mode) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
// Sets camera format to a controller
|
||||
virtual Common::Input::DriverResult SetCameraFormat(
|
||||
[[maybe_unused]] const PadIdentifier& identifier,
|
||||
[[maybe_unused]] Common::Input::CameraFormat camera_format) {
|
||||
return Common::Input::DriverResult::NotSupported;
|
||||
}
|
||||
|
||||
// Returns success if nfc is supported
|
||||
virtual Common::Input::NfcState SupportsNfc(
|
||||
[[maybe_unused]] const PadIdentifier& identifier) const {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Start scanning for nfc tags
|
||||
virtual Common::Input::NfcState StartNfcPolling(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_) {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Start scanning for nfc tags
|
||||
virtual Common::Input::NfcState StopNfcPolling(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_) {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Reads data from amiibo tag
|
||||
virtual Common::Input::NfcState ReadAmiiboData(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_,
|
||||
[[maybe_unused]] std::vector<u8>& out_data) {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Writes data to an nfc tag
|
||||
virtual Common::Input::NfcState WriteNfcData([[maybe_unused]] const PadIdentifier& identifier,
|
||||
[[maybe_unused]] const std::vector<u8>& data) {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Reads data from mifare tag
|
||||
virtual Common::Input::NfcState ReadMifareData(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_,
|
||||
[[maybe_unused]] const Common::Input::MifareRequest& request,
|
||||
[[maybe_unused]] Common::Input::MifareRequest& out_data) {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Write data to mifare tag
|
||||
virtual Common::Input::NfcState WriteMifareData(
|
||||
[[maybe_unused]] const PadIdentifier& identifier_,
|
||||
[[maybe_unused]] const Common::Input::MifareRequest& request) {
|
||||
return Common::Input::NfcState::NotSupported;
|
||||
}
|
||||
|
||||
// Returns the engine name
|
||||
[[nodiscard]] const std::string& GetEngineName() const;
|
||||
|
||||
/// Used for automapping features
|
||||
virtual std::vector<Common::ParamPackage> GetInputDevices() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Retrieves the button mappings for the given device
|
||||
virtual ButtonMapping GetButtonMappingForDevice(
|
||||
[[maybe_unused]] const Common::ParamPackage& params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Retrieves the analog mappings for the given device
|
||||
virtual AnalogMapping GetAnalogMappingForDevice(
|
||||
[[maybe_unused]] const Common::ParamPackage& params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Retrieves the motion mappings for the given device
|
||||
virtual MotionMapping GetMotionMappingForDevice(
|
||||
[[maybe_unused]] const Common::ParamPackage& params) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/// Retrieves the name of the given input.
|
||||
virtual Common::Input::ButtonNames GetUIName(
|
||||
[[maybe_unused]] const Common::ParamPackage& params) const {
|
||||
return Common::Input::ButtonNames::Engine;
|
||||
}
|
||||
|
||||
/// Retrieves the index number of the given hat button direction
|
||||
virtual u8 GetHatButtonId([[maybe_unused]] const std::string& direction_name) const {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Returns true if axis of a stick aren't mapped in the correct direction
|
||||
virtual bool IsStickInverted([[maybe_unused]] const Common::ParamPackage& params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void PreSetController(const PadIdentifier& identifier);
|
||||
void PreSetButton(const PadIdentifier& identifier, int button);
|
||||
void PreSetHatButton(const PadIdentifier& identifier, int button);
|
||||
void PreSetAxis(const PadIdentifier& identifier, int axis);
|
||||
void PreSetMotion(const PadIdentifier& identifier, int motion);
|
||||
void ResetButtonState();
|
||||
void ResetAnalogState();
|
||||
|
||||
bool GetButton(const PadIdentifier& identifier, int button) const;
|
||||
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;
|
||||
|
||||
int SetCallback(InputIdentifier input_identifier);
|
||||
void SetMappingCallback(MappingCallback callback);
|
||||
void DeleteCallback(int key);
|
||||
|
||||
protected:
|
||||
void SetButton(const PadIdentifier& identifier, int button, bool value);
|
||||
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);
|
||||
|
||||
virtual std::string GetHatButtonName([[maybe_unused]] u8 direction_value) const {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private:
|
||||
struct ControllerData {
|
||||
std::unordered_map<int, bool> buttons;
|
||||
std::unordered_map<int, u8> hat_buttons;
|
||||
std::unordered_map<int, float> axes;
|
||||
std::unordered_map<int, BasicMotion> motions;
|
||||
Common::Input::BatteryLevel battery{};
|
||||
Common::Input::BodyColorStatus color{};
|
||||
Common::Input::CameraStatus camera{};
|
||||
Common::Input::NfcStatus nfc{};
|
||||
};
|
||||
|
||||
void TriggerOnButtonChange(const PadIdentifier& identifier, int button, bool value);
|
||||
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,
|
||||
const Common::Input::CameraStatus& value);
|
||||
void TriggerOnNfcChange(const PadIdentifier& identifier, const Common::Input::NfcStatus& value);
|
||||
|
||||
bool IsInputIdentifierEqual(const InputIdentifier& input_identifier,
|
||||
const PadIdentifier& identifier, EngineInputType type,
|
||||
int index) const;
|
||||
|
||||
mutable std::mutex mutex;
|
||||
mutable std::mutex mutex_callback;
|
||||
bool configuring{false};
|
||||
const std::string input_engine;
|
||||
int last_callback_key = 0;
|
||||
std::unordered_map<PadIdentifier, ControllerData> controller_list;
|
||||
std::unordered_map<int, InputIdentifier> callback_list;
|
||||
MappingCallback mapping_callback;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
219
src/input_common/input_mapping.cpp
Normal file
219
src/input_common/input_mapping.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "input_common/input_engine.h"
|
||||
#include "input_common/input_mapping.h"
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
MappingFactory::MappingFactory() = default;
|
||||
|
||||
void MappingFactory::BeginMapping(Polling::InputType type) {
|
||||
is_enabled = true;
|
||||
input_type = type;
|
||||
input_queue.Clear();
|
||||
first_axis = -1;
|
||||
second_axis = -1;
|
||||
}
|
||||
|
||||
Common::ParamPackage MappingFactory::GetNextInput() {
|
||||
Common::ParamPackage input;
|
||||
input_queue.Pop(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
void MappingFactory::RegisterInput(const MappingData& data) {
|
||||
if (!is_enabled) {
|
||||
return;
|
||||
}
|
||||
if (!IsDriverValid(data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (input_type) {
|
||||
case Polling::InputType::Button:
|
||||
RegisterButton(data);
|
||||
return;
|
||||
case Polling::InputType::Stick:
|
||||
RegisterStick(data);
|
||||
return;
|
||||
case Polling::InputType::Motion:
|
||||
RegisterMotion(data);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MappingFactory::StopMapping() {
|
||||
is_enabled = false;
|
||||
input_type = Polling::InputType::None;
|
||||
input_queue.Clear();
|
||||
}
|
||||
|
||||
void MappingFactory::RegisterButton(const MappingData& data) {
|
||||
Common::ParamPackage new_input;
|
||||
new_input.Set("engine", data.engine);
|
||||
if (data.pad.guid.IsValid()) {
|
||||
new_input.Set("guid", data.pad.guid.RawString());
|
||||
}
|
||||
new_input.Set("port", static_cast<int>(data.pad.port));
|
||||
new_input.Set("pad", static_cast<int>(data.pad.pad));
|
||||
|
||||
switch (data.type) {
|
||||
case EngineInputType::Button:
|
||||
// Workaround for old compatibility
|
||||
if (data.engine == "keyboard") {
|
||||
new_input.Set("code", data.index);
|
||||
break;
|
||||
}
|
||||
new_input.Set("button", data.index);
|
||||
break;
|
||||
case EngineInputType::HatButton:
|
||||
new_input.Set("hat", data.index);
|
||||
new_input.Set("direction", data.hat_name);
|
||||
break;
|
||||
case EngineInputType::Analog:
|
||||
// Ignore mouse axis when mapping buttons
|
||||
if (data.engine == "mouse" && data.index != 4) {
|
||||
return;
|
||||
}
|
||||
new_input.Set("axis", data.index);
|
||||
new_input.Set("threshold", 0.5f);
|
||||
break;
|
||||
case EngineInputType::Motion:
|
||||
new_input.Set("motion", data.index);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
input_queue.Push(new_input);
|
||||
}
|
||||
|
||||
void MappingFactory::RegisterStick(const MappingData& data) {
|
||||
Common::ParamPackage new_input;
|
||||
new_input.Set("engine", data.engine);
|
||||
if (data.pad.guid.IsValid()) {
|
||||
new_input.Set("guid", data.pad.guid.RawString());
|
||||
}
|
||||
new_input.Set("port", static_cast<int>(data.pad.port));
|
||||
new_input.Set("pad", static_cast<int>(data.pad.pad));
|
||||
|
||||
// If engine is mouse map the mouse position as a joystick
|
||||
if (data.engine == "mouse") {
|
||||
new_input.Set("axis_x", 0);
|
||||
new_input.Set("axis_y", 1);
|
||||
new_input.Set("threshold", 0.5f);
|
||||
new_input.Set("range", 1.0f);
|
||||
new_input.Set("deadzone", 0.0f);
|
||||
input_queue.Push(new_input);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case EngineInputType::Button:
|
||||
case EngineInputType::HatButton:
|
||||
RegisterButton(data);
|
||||
return;
|
||||
case EngineInputType::Analog:
|
||||
if (first_axis == data.index) {
|
||||
return;
|
||||
}
|
||||
if (first_axis == -1) {
|
||||
first_axis = data.index;
|
||||
return;
|
||||
}
|
||||
new_input.Set("axis_x", first_axis);
|
||||
new_input.Set("axis_y", data.index);
|
||||
new_input.Set("threshold", 0.5f);
|
||||
new_input.Set("range", 0.95f);
|
||||
new_input.Set("deadzone", 0.15f);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
input_queue.Push(new_input);
|
||||
}
|
||||
|
||||
void MappingFactory::RegisterMotion(const MappingData& data) {
|
||||
Common::ParamPackage new_input;
|
||||
new_input.Set("engine", data.engine);
|
||||
if (data.pad.guid.IsValid()) {
|
||||
new_input.Set("guid", data.pad.guid.RawString());
|
||||
}
|
||||
new_input.Set("port", static_cast<int>(data.pad.port));
|
||||
new_input.Set("pad", static_cast<int>(data.pad.pad));
|
||||
|
||||
// If engine is mouse map it automatically to mouse motion
|
||||
if (data.engine == "mouse") {
|
||||
new_input.Set("motion", 0);
|
||||
new_input.Set("pad", 1);
|
||||
new_input.Set("threshold", 0.001f);
|
||||
input_queue.Push(new_input);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (data.type) {
|
||||
case EngineInputType::Button:
|
||||
case EngineInputType::HatButton:
|
||||
RegisterButton(data);
|
||||
return;
|
||||
case EngineInputType::Analog:
|
||||
if (first_axis == data.index) {
|
||||
return;
|
||||
}
|
||||
if (second_axis == data.index) {
|
||||
return;
|
||||
}
|
||||
if (first_axis == -1) {
|
||||
first_axis = data.index;
|
||||
return;
|
||||
}
|
||||
if (second_axis == -1) {
|
||||
second_axis = data.index;
|
||||
return;
|
||||
}
|
||||
new_input.Set("axis_x", first_axis);
|
||||
new_input.Set("axis_y", second_axis);
|
||||
new_input.Set("axis_z", data.index);
|
||||
new_input.Set("range", 1.0f);
|
||||
new_input.Set("deadzone", 0.20f);
|
||||
break;
|
||||
case EngineInputType::Motion:
|
||||
new_input.Set("motion", data.index);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
input_queue.Push(new_input);
|
||||
}
|
||||
|
||||
bool MappingFactory::IsDriverValid(const MappingData& data) const {
|
||||
// Only port 0 can be mapped on the keyboard
|
||||
if (data.engine == "keyboard" && data.pad.port != 0) {
|
||||
return false;
|
||||
}
|
||||
// Only port 0 can be mapped on the mouse
|
||||
if (data.engine == "mouse" && data.pad.port != 0) {
|
||||
return false;
|
||||
}
|
||||
// To prevent mapping with two devices we disable any UDP except motion
|
||||
if (!Settings::values.enable_udp_controller && data.engine == "cemuhookudp" &&
|
||||
data.type != EngineInputType::Motion) {
|
||||
return false;
|
||||
}
|
||||
// The following drivers don't need to be mapped
|
||||
if (data.engine == "touch_from_button") {
|
||||
return false;
|
||||
}
|
||||
if (data.engine == "analog_from_button") {
|
||||
return false;
|
||||
}
|
||||
if (data.engine == "virtual_gamepad") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace InputCommon
|
||||
88
src/input_common/input_mapping.h
Normal file
88
src/input_common/input_mapping.h
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/param_package.h"
|
||||
#include "common/threadsafe_queue.h"
|
||||
|
||||
namespace InputCommon::Polling {
|
||||
enum class InputType;
|
||||
}
|
||||
|
||||
namespace InputCommon {
|
||||
class InputEngine;
|
||||
struct MappingData;
|
||||
|
||||
class MappingFactory {
|
||||
public:
|
||||
MappingFactory();
|
||||
|
||||
/**
|
||||
* Resets all variables to begin the mapping process
|
||||
* @param type type of input desired to be returned
|
||||
*/
|
||||
void BeginMapping(Polling::InputType type);
|
||||
|
||||
/// Returns an input event with mapping information from the input_queue
|
||||
[[nodiscard]] Common::ParamPackage GetNextInput();
|
||||
|
||||
/**
|
||||
* Registers mapping input data from the driver
|
||||
* @param data A struct containing all the information needed to create a proper
|
||||
* ParamPackage
|
||||
*/
|
||||
void RegisterInput(const MappingData& data);
|
||||
|
||||
/// Stop polling from all backends
|
||||
void StopMapping();
|
||||
|
||||
private:
|
||||
/**
|
||||
* If provided data satisfies the requirements it will push an element to the input_queue
|
||||
* Supported input:
|
||||
* - Button: Creates a basic button ParamPackage
|
||||
* - HatButton: Creates a basic hat button ParamPackage
|
||||
* - Analog: Creates a basic analog ParamPackage
|
||||
* @param data A struct containing all the information needed to create a proper
|
||||
* ParamPackage
|
||||
*/
|
||||
void RegisterButton(const MappingData& data);
|
||||
|
||||
/**
|
||||
* If provided data satisfies the requirements it will push an element to the input_queue
|
||||
* Supported input:
|
||||
* - Button, HatButton: Pass the data to RegisterButton
|
||||
* - Analog: Stores the first axis and on the second axis creates a basic stick ParamPackage
|
||||
* @param data A struct containing all the information needed to create a proper
|
||||
* ParamPackage
|
||||
*/
|
||||
void RegisterStick(const MappingData& data);
|
||||
|
||||
/**
|
||||
* If provided data satisfies the requirements it will push an element to the input_queue
|
||||
* Supported input:
|
||||
* - Button, HatButton: Pass the data to RegisterButton
|
||||
* - Analog: Stores the first two axis and on the third axis creates a basic Motion
|
||||
* ParamPackage
|
||||
* - Motion: Creates a basic Motion ParamPackage
|
||||
* @param data A struct containing all the information needed to create a proper
|
||||
* ParamPackage
|
||||
*/
|
||||
void RegisterMotion(const MappingData& data);
|
||||
|
||||
/**
|
||||
* Returns true if driver can be mapped
|
||||
* @param data A struct containing all the information needed to create a proper
|
||||
* ParamPackage
|
||||
*/
|
||||
bool IsDriverValid(const MappingData& data) const;
|
||||
|
||||
Common::SPSCQueue<Common::ParamPackage> input_queue;
|
||||
Polling::InputType input_type{Polling::InputType::None};
|
||||
bool is_enabled{};
|
||||
int first_axis = -1;
|
||||
int second_axis = -1;
|
||||
};
|
||||
|
||||
} // namespace InputCommon
|
||||
1200
src/input_common/input_poller.cpp
Normal file
1200
src/input_common/input_poller.cpp
Normal file
File diff suppressed because it is too large
Load Diff
248
src/input_common/input_poller.h
Normal file
248
src/input_common/input_poller.h
Normal file
@@ -0,0 +1,248 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Input {
|
||||
class InputDevice;
|
||||
|
||||
template <typename InputDevice>
|
||||
class Factory;
|
||||
}; // namespace Input
|
||||
|
||||
namespace InputCommon {
|
||||
class InputEngine;
|
||||
|
||||
class OutputFactory final : public Common::Input::Factory<Common::Input::OutputDevice> {
|
||||
public:
|
||||
explicit OutputFactory(std::shared_ptr<InputEngine> input_engine_);
|
||||
|
||||
/**
|
||||
* Creates an output 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 output device with the parameters specified
|
||||
*/
|
||||
std::unique_ptr<Common::Input::OutputDevice> Create(
|
||||
const Common::ParamPackage& params) override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<InputEngine> input_engine;
|
||||
};
|
||||
|
||||
/**
|
||||
* An Input factory. It receives input events and forward them to all input devices it created.
|
||||
*/
|
||||
class InputFactory final : public Common::Input::Factory<Common::Input::InputDevice> {
|
||||
public:
|
||||
explicit InputFactory(std::shared_ptr<InputEngine> input_engine_);
|
||||
|
||||
/**
|
||||
* Creates an input device from the parameters given. Identifies the type of input to be
|
||||
* returned if it contains the following parameters:
|
||||
* - button: Contains "button" or "code"
|
||||
* - hat_button: Contains "hat"
|
||||
* - analog: Contains "axis"
|
||||
* - trigger: Contains "button" and "axis"
|
||||
* - stick: Contains "axis_x" and "axis_y"
|
||||
* - motion: Contains "axis_x", "axis_y" and "axis_z"
|
||||
* - motion: Contains "motion"
|
||||
* - touch: Contains "button", "axis_x" and "axis_y"
|
||||
* - battery: Contains "battery"
|
||||
* - output: Contains "output"
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "code": the code of the keyboard key to bind with the input
|
||||
* - "button": same as "code" but for controller buttons
|
||||
* - "hat": similar as "button" but it's a group of hat buttons from SDL
|
||||
* - "axis": the axis number of the axis to bind with the input
|
||||
* - "motion": the motion number of the motion to bind with the input
|
||||
* - "axis_x": same as axis but specifying horizontal direction
|
||||
* - "axis_y": same as axis but specifying vertical direction
|
||||
* - "axis_z": same as axis but specifying forward direction
|
||||
* - "battery": Only used as a placeholder to set the input type
|
||||
* @returns a unique input device with the parameters specified
|
||||
*/
|
||||
std::unique_ptr<Common::Input::InputDevice> Create(const Common::ParamPackage& params) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Creates a button device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "code": the code of the keyboard key to bind with the input
|
||||
* - "button": same as "code" but for controller buttons
|
||||
* - "toggle": press once to enable, press again to disable
|
||||
* - "inverted": inverts the output of the button
|
||||
* - "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<Common::Input::InputDevice> CreateButtonDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a hat button device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "button": the controller hat id to bind with the input
|
||||
* - "direction": the direction id to be detected
|
||||
* - "toggle": press once to enable, press again to disable
|
||||
* - "inverted": inverts the output of the button
|
||||
* - "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<Common::Input::InputDevice> CreateHatButtonDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a stick device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "axis_x": the controller horizontal axis id to bind with the input
|
||||
* - "axis_y": the controller vertical axis id to bind with the input
|
||||
* - "deadzone": the minimum required value to be detected
|
||||
* - "range": the maximum value required to reach 100%
|
||||
* - "threshold": the minimum required value to considered pressed
|
||||
* - "offset_x": the amount of offset in the x axis
|
||||
* - "offset_y": the amount of offset in the y axis
|
||||
* - "invert_x": inverts the sign of the horizontal axis
|
||||
* - "invert_y": inverts the sign of the vertical axis
|
||||
* - "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<Common::Input::InputDevice> CreateStickDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates an analog device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "axis": the controller axis id to bind with the input
|
||||
* - "deadzone": the minimum required value to be detected
|
||||
* - "range": the maximum value required to reach 100%
|
||||
* - "threshold": the minimum required value to considered pressed
|
||||
* - "offset": the amount of offset in the axis
|
||||
* - "invert": inverts the sign of the axis
|
||||
* - "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<Common::Input::InputDevice> CreateAnalogDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a trigger device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "button": the controller hat id to bind with the input
|
||||
* - "direction": the direction id to be detected
|
||||
* - "toggle": press once to enable, press again to disable
|
||||
* - "inverted": inverts the output of the button
|
||||
* - "axis": the controller axis id to bind with the input
|
||||
* - "deadzone": the minimum required value to be detected
|
||||
* - "range": the maximum value required to reach 100%
|
||||
* - "threshold": the minimum required value to considered pressed
|
||||
* - "offset": the amount of offset in the axis
|
||||
* - "invert": inverts the sign of the axis
|
||||
* - "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<Common::Input::InputDevice> CreateTriggerDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a touch device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "button": the controller hat id to bind with the input
|
||||
* - "direction": the direction id to be detected
|
||||
* - "toggle": press once to enable, press again to disable
|
||||
* - "inverted": inverts the output of the button
|
||||
* - "axis_x": the controller horizontal axis id to bind with the input
|
||||
* - "axis_y": the controller vertical axis id to bind with the input
|
||||
* - "deadzone": the minimum required value to be detected
|
||||
* - "range": the maximum value required to reach 100%
|
||||
* - "threshold": the minimum required value to considered pressed
|
||||
* - "offset_x": the amount of offset in the x axis
|
||||
* - "offset_y": the amount of offset in the y axis
|
||||
* - "invert_x": inverts the sign of the horizontal axis
|
||||
* - "invert_y": inverts the sign of the vertical axis
|
||||
* - "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<Common::Input::InputDevice> CreateTouchDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a battery 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<Common::Input::InputDevice> 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<Common::Input::InputDevice> CreateColorDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a motion device from the parameters given.
|
||||
* @param params contains parameters for creating the device:
|
||||
* - "axis_x": the controller horizontal axis id to bind with the input
|
||||
* - "axis_y": the controller vertical axis id to bind with the input
|
||||
* - "axis_z": the controller forward axis id to bind with the input
|
||||
* - "deadzone": the minimum required value to be detected
|
||||
* - "range": the maximum value required to reach 100%
|
||||
* - "offset_x": the amount of offset in the x axis
|
||||
* - "offset_y": the amount of offset in the y axis
|
||||
* - "offset_z": the amount of offset in the z axis
|
||||
* - "invert_x": inverts the sign of the horizontal axis
|
||||
* - "invert_y": inverts the sign of the vertical axis
|
||||
* - "invert_z": inverts the sign of the forward axis
|
||||
* - "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<Common::Input::InputDevice> CreateMotionDevice(Common::ParamPackage params);
|
||||
|
||||
/**
|
||||
* Creates a camera 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<Common::Input::InputDevice> CreateCameraDevice(
|
||||
const Common::ParamPackage& params);
|
||||
|
||||
/**
|
||||
* Creates a nfc 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<Common::Input::InputDevice> CreateNfcDevice(const Common::ParamPackage& params);
|
||||
|
||||
std::shared_ptr<InputEngine> input_engine;
|
||||
};
|
||||
} // namespace InputCommon
|
||||
512
src/input_common/main.cpp
Normal file
512
src/input_common/main.cpp
Normal file
@@ -0,0 +1,512 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <memory>
|
||||
#include "common/input.h"
|
||||
#include "common/param_package.h"
|
||||
#include "input_common/drivers/camera.h"
|
||||
#include "input_common/drivers/keyboard.h"
|
||||
#include "input_common/drivers/mouse.h"
|
||||
#include "input_common/drivers/tas_input.h"
|
||||
#include "input_common/drivers/touch_screen.h"
|
||||
#include "input_common/drivers/udp_client.h"
|
||||
#include "input_common/drivers/virtual_amiibo.h"
|
||||
#include "input_common/drivers/virtual_gamepad.h"
|
||||
#include "input_common/helpers/stick_from_buttons.h"
|
||||
#include "input_common/helpers/touch_from_buttons.h"
|
||||
#include "input_common/input_engine.h"
|
||||
#include "input_common/input_mapping.h"
|
||||
#include "input_common/input_poller.h"
|
||||
#include "input_common/main.h"
|
||||
|
||||
#ifdef HAVE_LIBUSB
|
||||
#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
|
||||
|
||||
#ifdef ANDROID
|
||||
#include "input_common/drivers/android.h"
|
||||
#endif
|
||||
|
||||
namespace InputCommon {
|
||||
|
||||
/// Dummy engine to get periodic updates
|
||||
class UpdateEngine final : public InputEngine {
|
||||
public:
|
||||
explicit UpdateEngine(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
|
||||
PreSetController(identifier);
|
||||
}
|
||||
|
||||
void PumpEvents() {
|
||||
SetButton(identifier, 0, last_state);
|
||||
last_state = !last_state;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr PadIdentifier identifier = {
|
||||
.guid = Common::UUID{},
|
||||
.port = 0,
|
||||
.pad = 0,
|
||||
};
|
||||
|
||||
bool last_state{};
|
||||
};
|
||||
|
||||
struct InputSubsystem::Impl {
|
||||
template <typename Engine>
|
||||
void RegisterEngine(std::string name, std::shared_ptr<Engine>& engine) {
|
||||
MappingCallback mapping_callback{[this](const MappingData& data) { RegisterInput(data); }};
|
||||
|
||||
engine = std::make_shared<Engine>(name);
|
||||
engine->SetMappingCallback(mapping_callback);
|
||||
|
||||
std::shared_ptr<InputFactory> input_factory = std::make_shared<InputFactory>(engine);
|
||||
std::shared_ptr<OutputFactory> output_factory = std::make_shared<OutputFactory>(engine);
|
||||
Common::Input::RegisterInputFactory(engine->GetEngineName(), std::move(input_factory));
|
||||
Common::Input::RegisterOutputFactory(engine->GetEngineName(), std::move(output_factory));
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
mapping_factory = std::make_shared<MappingFactory>();
|
||||
|
||||
RegisterEngine("updater", update_engine);
|
||||
RegisterEngine("keyboard", keyboard);
|
||||
RegisterEngine("mouse", mouse);
|
||||
RegisterEngine("touch", touch_screen);
|
||||
#ifdef HAVE_LIBUSB
|
||||
RegisterEngine("gcpad", gcadapter);
|
||||
#endif
|
||||
RegisterEngine("cemuhookudp", udp_client);
|
||||
RegisterEngine("tas", tas_input);
|
||||
RegisterEngine("camera", camera);
|
||||
#ifdef ANDROID
|
||||
RegisterEngine("android", android);
|
||||
#endif
|
||||
RegisterEngine("virtual_amiibo", virtual_amiibo);
|
||||
RegisterEngine("virtual_gamepad", virtual_gamepad);
|
||||
#ifdef HAVE_SDL2
|
||||
RegisterEngine("sdl", sdl);
|
||||
RegisterEngine("joycon", joycon);
|
||||
#endif
|
||||
|
||||
Common::Input::RegisterInputFactory("touch_from_button",
|
||||
std::make_shared<TouchFromButton>());
|
||||
Common::Input::RegisterInputFactory("analog_from_button",
|
||||
std::make_shared<StickFromButton>());
|
||||
}
|
||||
|
||||
template <typename Engine>
|
||||
void UnregisterEngine(std::shared_ptr<Engine>& engine) {
|
||||
Common::Input::UnregisterInputFactory(engine->GetEngineName());
|
||||
Common::Input::UnregisterOutputFactory(engine->GetEngineName());
|
||||
engine.reset();
|
||||
}
|
||||
|
||||
void Shutdown() {
|
||||
UnregisterEngine(update_engine);
|
||||
UnregisterEngine(keyboard);
|
||||
UnregisterEngine(mouse);
|
||||
UnregisterEngine(touch_screen);
|
||||
#ifdef HAVE_LIBUSB
|
||||
UnregisterEngine(gcadapter);
|
||||
#endif
|
||||
UnregisterEngine(udp_client);
|
||||
UnregisterEngine(tas_input);
|
||||
UnregisterEngine(camera);
|
||||
#ifdef ANDROID
|
||||
UnregisterEngine(android);
|
||||
#endif
|
||||
UnregisterEngine(virtual_amiibo);
|
||||
UnregisterEngine(virtual_gamepad);
|
||||
#ifdef HAVE_SDL2
|
||||
UnregisterEngine(sdl);
|
||||
UnregisterEngine(joycon);
|
||||
#endif
|
||||
|
||||
Common::Input::UnregisterInputFactory("touch_from_button");
|
||||
Common::Input::UnregisterInputFactory("analog_from_button");
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const {
|
||||
std::vector<Common::ParamPackage> devices = {
|
||||
Common::ParamPackage{{"display", "Any"}, {"engine", "any"}},
|
||||
};
|
||||
|
||||
#ifndef ANDROID
|
||||
auto keyboard_devices = keyboard->GetInputDevices();
|
||||
devices.insert(devices.end(), keyboard_devices.begin(), keyboard_devices.end());
|
||||
auto mouse_devices = mouse->GetInputDevices();
|
||||
devices.insert(devices.end(), mouse_devices.begin(), mouse_devices.end());
|
||||
#endif
|
||||
#ifdef ANDROID
|
||||
auto android_devices = android->GetInputDevices();
|
||||
devices.insert(devices.end(), android_devices.begin(), android_devices.end());
|
||||
#endif
|
||||
#ifdef HAVE_LIBUSB
|
||||
auto gcadapter_devices = gcadapter->GetInputDevices();
|
||||
devices.insert(devices.end(), gcadapter_devices.begin(), gcadapter_devices.end());
|
||||
#endif
|
||||
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
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::shared_ptr<InputEngine> GetInputEngine(
|
||||
const Common::ParamPackage& params) const {
|
||||
if (!params.Has("engine") || params.Get("engine", "") == "any") {
|
||||
return nullptr;
|
||||
}
|
||||
const std::string engine = params.Get("engine", "");
|
||||
if (engine == keyboard->GetEngineName()) {
|
||||
return keyboard;
|
||||
}
|
||||
if (engine == mouse->GetEngineName()) {
|
||||
return mouse;
|
||||
}
|
||||
#ifdef ANDROID
|
||||
if (engine == android->GetEngineName()) {
|
||||
return android;
|
||||
}
|
||||
#endif
|
||||
#ifdef HAVE_LIBUSB
|
||||
if (engine == gcadapter->GetEngineName()) {
|
||||
return gcadapter;
|
||||
}
|
||||
#endif
|
||||
if (engine == udp_client->GetEngineName()) {
|
||||
return udp_client;
|
||||
}
|
||||
#ifdef HAVE_SDL2
|
||||
if (engine == sdl->GetEngineName()) {
|
||||
return sdl;
|
||||
}
|
||||
if (engine == joycon->GetEngineName()) {
|
||||
return joycon;
|
||||
}
|
||||
#endif
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(
|
||||
const Common::ParamPackage& params) const {
|
||||
const auto input_engine = GetInputEngine(params);
|
||||
|
||||
if (input_engine == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return input_engine->GetAnalogMappingForDevice(params);
|
||||
}
|
||||
|
||||
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(
|
||||
const Common::ParamPackage& params) const {
|
||||
const auto input_engine = GetInputEngine(params);
|
||||
|
||||
if (input_engine == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return input_engine->GetButtonMappingForDevice(params);
|
||||
}
|
||||
|
||||
[[nodiscard]] MotionMapping GetMotionMappingForDevice(
|
||||
const Common::ParamPackage& params) const {
|
||||
const auto input_engine = GetInputEngine(params);
|
||||
|
||||
if (input_engine == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return input_engine->GetMotionMappingForDevice(params);
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames GetButtonName(const Common::ParamPackage& params) const {
|
||||
if (!params.Has("engine") || params.Get("engine", "") == "any") {
|
||||
return Common::Input::ButtonNames::Undefined;
|
||||
}
|
||||
const auto input_engine = GetInputEngine(params);
|
||||
|
||||
if (input_engine == nullptr) {
|
||||
return Common::Input::ButtonNames::Invalid;
|
||||
}
|
||||
|
||||
return input_engine->GetUIName(params);
|
||||
}
|
||||
|
||||
bool IsStickInverted(const Common::ParamPackage& params) {
|
||||
const auto input_engine = GetInputEngine(params);
|
||||
|
||||
if (input_engine == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return input_engine->IsStickInverted(params);
|
||||
}
|
||||
|
||||
bool IsController(const Common::ParamPackage& params) {
|
||||
const std::string engine = params.Get("engine", "");
|
||||
if (engine == mouse->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
#ifdef ANDROID
|
||||
if (engine == android->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef HAVE_LIBUSB
|
||||
if (engine == gcadapter->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (engine == udp_client->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
if (engine == tas_input->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
if (engine == virtual_gamepad->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
#ifdef HAVE_SDL2
|
||||
if (engine == sdl->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
if (engine == joycon->GetEngineName()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
void BeginConfiguration() {
|
||||
keyboard->BeginConfiguration();
|
||||
mouse->BeginConfiguration();
|
||||
#ifdef ANDROID
|
||||
android->BeginConfiguration();
|
||||
#endif
|
||||
#ifdef HAVE_LIBUSB
|
||||
gcadapter->BeginConfiguration();
|
||||
#endif
|
||||
udp_client->BeginConfiguration();
|
||||
#ifdef HAVE_SDL2
|
||||
sdl->BeginConfiguration();
|
||||
joycon->BeginConfiguration();
|
||||
#endif
|
||||
}
|
||||
|
||||
void EndConfiguration() {
|
||||
keyboard->EndConfiguration();
|
||||
mouse->EndConfiguration();
|
||||
#ifdef ANDROID
|
||||
android->EndConfiguration();
|
||||
#endif
|
||||
#ifdef HAVE_LIBUSB
|
||||
gcadapter->EndConfiguration();
|
||||
#endif
|
||||
udp_client->EndConfiguration();
|
||||
#ifdef HAVE_SDL2
|
||||
sdl->EndConfiguration();
|
||||
joycon->EndConfiguration();
|
||||
#endif
|
||||
}
|
||||
|
||||
void PumpEvents() const {
|
||||
update_engine->PumpEvents();
|
||||
#ifdef HAVE_SDL2
|
||||
sdl->PumpEvents();
|
||||
#endif
|
||||
}
|
||||
|
||||
void RegisterInput(const MappingData& data) {
|
||||
mapping_factory->RegisterInput(data);
|
||||
}
|
||||
|
||||
std::shared_ptr<MappingFactory> mapping_factory;
|
||||
|
||||
std::shared_ptr<UpdateEngine> update_engine;
|
||||
std::shared_ptr<Keyboard> keyboard;
|
||||
std::shared_ptr<Mouse> mouse;
|
||||
std::shared_ptr<TouchScreen> touch_screen;
|
||||
std::shared_ptr<TasInput::Tas> tas_input;
|
||||
std::shared_ptr<CemuhookUDP::UDPClient> udp_client;
|
||||
std::shared_ptr<Camera> camera;
|
||||
std::shared_ptr<VirtualAmiibo> virtual_amiibo;
|
||||
std::shared_ptr<VirtualGamepad> virtual_gamepad;
|
||||
|
||||
#ifdef HAVE_LIBUSB
|
||||
std::shared_ptr<GCAdapter> gcadapter;
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_SDL2
|
||||
std::shared_ptr<SDLDriver> sdl;
|
||||
std::shared_ptr<Joycons> joycon;
|
||||
#endif
|
||||
|
||||
#ifdef ANDROID
|
||||
std::shared_ptr<Android> android;
|
||||
#endif
|
||||
};
|
||||
|
||||
InputSubsystem::InputSubsystem() : impl{std::make_unique<Impl>()} {}
|
||||
|
||||
InputSubsystem::~InputSubsystem() = default;
|
||||
|
||||
void InputSubsystem::Initialize() {
|
||||
impl->Initialize();
|
||||
}
|
||||
|
||||
void InputSubsystem::Shutdown() {
|
||||
impl->Shutdown();
|
||||
}
|
||||
|
||||
Keyboard* InputSubsystem::GetKeyboard() {
|
||||
return impl->keyboard.get();
|
||||
}
|
||||
|
||||
const Keyboard* InputSubsystem::GetKeyboard() const {
|
||||
return impl->keyboard.get();
|
||||
}
|
||||
|
||||
Mouse* InputSubsystem::GetMouse() {
|
||||
return impl->mouse.get();
|
||||
}
|
||||
|
||||
const Mouse* InputSubsystem::GetMouse() const {
|
||||
return impl->mouse.get();
|
||||
}
|
||||
|
||||
TouchScreen* InputSubsystem::GetTouchScreen() {
|
||||
return impl->touch_screen.get();
|
||||
}
|
||||
|
||||
const TouchScreen* InputSubsystem::GetTouchScreen() const {
|
||||
return impl->touch_screen.get();
|
||||
}
|
||||
|
||||
TasInput::Tas* InputSubsystem::GetTas() {
|
||||
return impl->tas_input.get();
|
||||
}
|
||||
|
||||
const TasInput::Tas* InputSubsystem::GetTas() const {
|
||||
return impl->tas_input.get();
|
||||
}
|
||||
|
||||
Camera* InputSubsystem::GetCamera() {
|
||||
return impl->camera.get();
|
||||
}
|
||||
|
||||
const Camera* InputSubsystem::GetCamera() const {
|
||||
return impl->camera.get();
|
||||
}
|
||||
|
||||
#ifdef ANDROID
|
||||
Android* InputSubsystem::GetAndroid() {
|
||||
return impl->android.get();
|
||||
}
|
||||
|
||||
const Android* InputSubsystem::GetAndroid() const {
|
||||
return impl->android.get();
|
||||
}
|
||||
#endif
|
||||
|
||||
VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() {
|
||||
return impl->virtual_amiibo.get();
|
||||
}
|
||||
|
||||
const VirtualAmiibo* InputSubsystem::GetVirtualAmiibo() const {
|
||||
return impl->virtual_amiibo.get();
|
||||
}
|
||||
|
||||
VirtualGamepad* InputSubsystem::GetVirtualGamepad() {
|
||||
return impl->virtual_gamepad.get();
|
||||
}
|
||||
|
||||
const VirtualGamepad* InputSubsystem::GetVirtualGamepad() const {
|
||||
return impl->virtual_gamepad.get();
|
||||
}
|
||||
|
||||
std::vector<Common::ParamPackage> InputSubsystem::GetInputDevices() const {
|
||||
return impl->GetInputDevices();
|
||||
}
|
||||
|
||||
AnalogMapping InputSubsystem::GetAnalogMappingForDevice(const Common::ParamPackage& device) const {
|
||||
return impl->GetAnalogMappingForDevice(device);
|
||||
}
|
||||
|
||||
ButtonMapping InputSubsystem::GetButtonMappingForDevice(const Common::ParamPackage& device) const {
|
||||
return impl->GetButtonMappingForDevice(device);
|
||||
}
|
||||
|
||||
MotionMapping InputSubsystem::GetMotionMappingForDevice(const Common::ParamPackage& device) const {
|
||||
return impl->GetMotionMappingForDevice(device);
|
||||
}
|
||||
|
||||
Common::Input::ButtonNames InputSubsystem::GetButtonName(const Common::ParamPackage& params) const {
|
||||
return impl->GetButtonName(params);
|
||||
}
|
||||
|
||||
bool InputSubsystem::IsController(const Common::ParamPackage& params) const {
|
||||
return impl->IsController(params);
|
||||
}
|
||||
|
||||
bool InputSubsystem::IsStickInverted(const Common::ParamPackage& params) const {
|
||||
if (params.Has("axis_x") && params.Has("axis_y")) {
|
||||
return impl->IsStickInverted(params);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void InputSubsystem::ReloadInputDevices() {
|
||||
impl->udp_client.get()->ReloadSockets();
|
||||
}
|
||||
|
||||
void InputSubsystem::BeginMapping(Polling::InputType type) {
|
||||
impl->BeginConfiguration();
|
||||
impl->mapping_factory->BeginMapping(type);
|
||||
}
|
||||
|
||||
Common::ParamPackage InputSubsystem::GetNextInput() const {
|
||||
return impl->mapping_factory->GetNextInput();
|
||||
}
|
||||
|
||||
void InputSubsystem::StopMapping() const {
|
||||
impl->EndConfiguration();
|
||||
impl->mapping_factory->StopMapping();
|
||||
}
|
||||
|
||||
void InputSubsystem::PumpEvents() const {
|
||||
impl->PumpEvents();
|
||||
}
|
||||
|
||||
std::string GenerateKeyboardParam(int key_code) {
|
||||
Common::ParamPackage param;
|
||||
param.Set("engine", "keyboard");
|
||||
param.Set("code", key_code);
|
||||
param.Set("toggle", false);
|
||||
return param.Serialize();
|
||||
}
|
||||
|
||||
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
|
||||
int key_modifier, float modifier_scale) {
|
||||
Common::ParamPackage circle_pad_param{
|
||||
{"engine", "analog_from_button"},
|
||||
{"up", GenerateKeyboardParam(key_up)},
|
||||
{"down", GenerateKeyboardParam(key_down)},
|
||||
{"left", GenerateKeyboardParam(key_left)},
|
||||
{"right", GenerateKeyboardParam(key_right)},
|
||||
{"modifier", GenerateKeyboardParam(key_modifier)},
|
||||
{"modifier_scale", std::to_string(modifier_scale)},
|
||||
};
|
||||
return circle_pad_param.Serialize();
|
||||
}
|
||||
} // namespace InputCommon
|
||||
178
src/input_common/main.h
Normal file
178
src/input_common/main.h
Normal file
@@ -0,0 +1,178 @@
|
||||
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Common {
|
||||
class ParamPackage;
|
||||
}
|
||||
|
||||
namespace Common::Input {
|
||||
enum class ButtonNames;
|
||||
}
|
||||
|
||||
namespace Settings::NativeAnalog {
|
||||
enum Values : int;
|
||||
}
|
||||
|
||||
namespace Settings::NativeButton {
|
||||
enum Values : int;
|
||||
}
|
||||
|
||||
namespace Settings::NativeMotion {
|
||||
enum Values : int;
|
||||
}
|
||||
|
||||
namespace InputCommon {
|
||||
class Android;
|
||||
class Camera;
|
||||
class Keyboard;
|
||||
class Mouse;
|
||||
class TouchScreen;
|
||||
class VirtualAmiibo;
|
||||
class VirtualGamepad;
|
||||
struct MappingData;
|
||||
} // namespace InputCommon
|
||||
|
||||
namespace InputCommon::TasInput {
|
||||
class Tas;
|
||||
} // namespace InputCommon::TasInput
|
||||
|
||||
namespace InputCommon {
|
||||
namespace Polling {
|
||||
/// Type of input desired for mapping purposes
|
||||
enum class InputType { None, Button, Stick, Motion, Touch };
|
||||
} // namespace Polling
|
||||
|
||||
/**
|
||||
* Given a ParamPackage for a Device returned from `GetInputDevices`, attempt to get the default
|
||||
* mapping for the device.
|
||||
*/
|
||||
using AnalogMapping = std::unordered_map<Settings::NativeAnalog::Values, Common::ParamPackage>;
|
||||
using ButtonMapping = std::unordered_map<Settings::NativeButton::Values, Common::ParamPackage>;
|
||||
using MotionMapping = std::unordered_map<Settings::NativeMotion::Values, Common::ParamPackage>;
|
||||
|
||||
class InputSubsystem {
|
||||
public:
|
||||
explicit InputSubsystem();
|
||||
~InputSubsystem();
|
||||
|
||||
InputSubsystem(const InputSubsystem&) = delete;
|
||||
InputSubsystem& operator=(const InputSubsystem&) = delete;
|
||||
|
||||
InputSubsystem(InputSubsystem&&) = delete;
|
||||
InputSubsystem& operator=(InputSubsystem&&) = delete;
|
||||
|
||||
/// Initializes and registers all built-in input device factories.
|
||||
void Initialize();
|
||||
|
||||
/// Unregisters all built-in input device factories and shuts them down.
|
||||
void Shutdown();
|
||||
|
||||
/// Retrieves the underlying keyboard device.
|
||||
[[nodiscard]] Keyboard* GetKeyboard();
|
||||
|
||||
/// Retrieves the underlying keyboard device.
|
||||
[[nodiscard]] const Keyboard* GetKeyboard() const;
|
||||
|
||||
/// Retrieves the underlying mouse device.
|
||||
[[nodiscard]] Mouse* GetMouse();
|
||||
|
||||
/// Retrieves the underlying mouse device.
|
||||
[[nodiscard]] const Mouse* GetMouse() const;
|
||||
|
||||
/// Retrieves the underlying touch screen device.
|
||||
[[nodiscard]] TouchScreen* GetTouchScreen();
|
||||
|
||||
/// Retrieves the underlying touch screen device.
|
||||
[[nodiscard]] const TouchScreen* GetTouchScreen() const;
|
||||
|
||||
/// Retrieves the underlying tas input device.
|
||||
[[nodiscard]] TasInput::Tas* GetTas();
|
||||
|
||||
/// Retrieves the underlying tas input device.
|
||||
[[nodiscard]] const TasInput::Tas* GetTas() const;
|
||||
|
||||
/// Retrieves the underlying camera input device.
|
||||
[[nodiscard]] Camera* GetCamera();
|
||||
|
||||
/// Retrieves the underlying camera input device.
|
||||
[[nodiscard]] const Camera* GetCamera() const;
|
||||
|
||||
/// Retrieves the underlying android input device.
|
||||
[[nodiscard]] Android* GetAndroid();
|
||||
|
||||
/// Retrieves the underlying android input device.
|
||||
[[nodiscard]] const Android* GetAndroid() const;
|
||||
|
||||
/// Retrieves the underlying virtual amiibo input device.
|
||||
[[nodiscard]] VirtualAmiibo* GetVirtualAmiibo();
|
||||
|
||||
/// Retrieves the underlying virtual amiibo input device.
|
||||
[[nodiscard]] const VirtualAmiibo* GetVirtualAmiibo() const;
|
||||
|
||||
/// Retrieves the underlying virtual gamepad input device.
|
||||
[[nodiscard]] VirtualGamepad* GetVirtualGamepad();
|
||||
|
||||
/// Retrieves the underlying virtual gamepad input device.
|
||||
[[nodiscard]] const VirtualGamepad* GetVirtualGamepad() const;
|
||||
|
||||
/**
|
||||
* Returns all available input devices that this Factory can create a new device with.
|
||||
* Each returned ParamPackage should have a `display` field used for display, a `engine` field
|
||||
* for backends to determine if this backend is meant to service the request and any other
|
||||
* information needed to identify this in the backend later.
|
||||
*/
|
||||
[[nodiscard]] std::vector<Common::ParamPackage> GetInputDevices() const;
|
||||
|
||||
/// Retrieves the analog mappings for the given device.
|
||||
[[nodiscard]] AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& device) const;
|
||||
|
||||
/// Retrieves the button mappings for the given device.
|
||||
[[nodiscard]] ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& device) const;
|
||||
|
||||
/// Retrieves the motion mappings for the given device.
|
||||
[[nodiscard]] MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& device) const;
|
||||
|
||||
/// Returns an enum containing the name to be displayed from the input engine.
|
||||
[[nodiscard]] Common::Input::ButtonNames GetButtonName(
|
||||
const Common::ParamPackage& params) const;
|
||||
|
||||
/// Returns true if device is a controller.
|
||||
[[nodiscard]] bool IsController(const Common::ParamPackage& params) const;
|
||||
|
||||
/// Returns true if axis of a stick aren't mapped in the correct direction
|
||||
[[nodiscard]] bool IsStickInverted(const Common::ParamPackage& device) const;
|
||||
|
||||
/// Reloads the input devices.
|
||||
void ReloadInputDevices();
|
||||
|
||||
/// Start polling from all backends for a desired input type.
|
||||
void BeginMapping(Polling::InputType type);
|
||||
|
||||
/// Returns an input event with mapping information.
|
||||
[[nodiscard]] Common::ParamPackage GetNextInput() const;
|
||||
|
||||
/// Stop polling from all backends.
|
||||
void StopMapping() const;
|
||||
|
||||
/// Signals SDL driver for new input events
|
||||
void PumpEvents() const;
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl;
|
||||
};
|
||||
|
||||
/// Generates a serialized param package for creating a keyboard button device.
|
||||
std::string GenerateKeyboardParam(int key_code);
|
||||
|
||||
/// Generates a serialized param package for creating an analog device taking input from keyboard.
|
||||
std::string GenerateAnalogParamFromKeys(int key_up, int key_down, int key_left, int key_right,
|
||||
int key_modifier, float modifier_scale);
|
||||
} // namespace InputCommon
|
||||
6
src/input_common/precompiled_headers.h
Normal file
6
src/input_common/precompiled_headers.h
Normal file
@@ -0,0 +1,6 @@
|
||||
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/common_precompiled_headers.h"
|
||||
Reference in New Issue
Block a user