early-access version 3244

This commit is contained in:
pineappleEA 2022-12-24 04:43:08 +01:00
parent 79ff2722d6
commit 984203b4d4
47 changed files with 4798 additions and 128 deletions

View file

@ -1,7 +1,7 @@
yuzu emulator early access yuzu emulator early access
============= =============
This is the source code for early-access 3242. This is the source code for early-access 3244.
## Legal Notice ## Legal Notice

View file

@ -51,6 +51,8 @@ enum class PollingMode {
NFC, NFC,
// Enable infrared camera polling // Enable infrared camera polling
IR, IR,
// Enable ring controller polling
Ring,
}; };
enum class CameraFormat { enum class CameraFormat {
@ -67,6 +69,7 @@ enum class VibrationError {
None, None,
NotSupported, NotSupported,
Disabled, Disabled,
InvalidHandle,
Unknown, Unknown,
}; };
@ -74,6 +77,7 @@ enum class VibrationError {
enum class PollingError { enum class PollingError {
None, None,
NotSupported, NotSupported,
InvalidHandle,
Unknown, Unknown,
}; };
@ -190,6 +194,8 @@ struct TouchStatus {
struct BodyColorStatus { struct BodyColorStatus {
u32 body{}; u32 body{};
u32 buttons{}; u32 buttons{};
u32 left_grip{};
u32 right_grip{};
}; };
// HD rumble data // HD rumble data
@ -228,17 +234,31 @@ enum class ButtonNames {
Engine, Engine,
// This will display the button by value instead of the button name // This will display the button by value instead of the button name
Value, Value,
// Joycon button names
ButtonLeft, ButtonLeft,
ButtonRight, ButtonRight,
ButtonDown, ButtonDown,
ButtonUp, ButtonUp,
TriggerZ,
TriggerR,
TriggerL,
ButtonA, ButtonA,
ButtonB, ButtonB,
ButtonX, ButtonX,
ButtonY, ButtonY,
ButtonPlus,
ButtonMinus,
ButtonHome,
ButtonCapture,
ButtonStickL,
ButtonStickR,
TriggerL,
TriggerZL,
TriggerSL,
TriggerR,
TriggerZR,
TriggerSR,
// GC button names
TriggerZ,
ButtonStart, ButtonStart,
// DS4 button names // DS4 button names

View file

@ -477,6 +477,7 @@ struct Values {
Setting<bool> enable_raw_input{false, "enable_raw_input"}; Setting<bool> enable_raw_input{false, "enable_raw_input"};
Setting<bool> controller_navigation{true, "controller_navigation"}; Setting<bool> controller_navigation{true, "controller_navigation"};
Setting<bool> enable_joycon_driver{false, "enable_joycon_driver"};
SwitchableSetting<bool> vibration_enabled{true, "vibration_enabled"}; SwitchableSetting<bool> vibration_enabled{true, "vibration_enabled"};
SwitchableSetting<bool> enable_accurate_vibrations{false, "enable_accurate_vibrations"}; SwitchableSetting<bool> enable_accurate_vibrations{false, "enable_accurate_vibrations"};

View file

@ -88,6 +88,7 @@ void EmulatedController::ReloadFromSettings() {
motion_params[index] = Common::ParamPackage(player.motions[index]); motion_params[index] = Common::ParamPackage(player.motions[index]);
} }
controller.color_values = {};
controller.colors_state.fullkey = { controller.colors_state.fullkey = {
.body = GetNpadColor(player.body_color_left), .body = GetNpadColor(player.body_color_left),
.button = GetNpadColor(player.button_color_left), .button = GetNpadColor(player.button_color_left),
@ -101,6 +102,8 @@ void EmulatedController::ReloadFromSettings() {
.button = GetNpadColor(player.button_color_right), .button = GetNpadColor(player.button_color_right),
}; };
ring_params[0] = Common::ParamPackage(Settings::values.ringcon_analogs);
// Other or debug controller should always be a pro controller // Other or debug controller should always be a pro controller
if (npad_id_type != NpadIdType::Other) { if (npad_id_type != NpadIdType::Other) {
SetNpadStyleIndex(MapSettingsTypeToNPad(player.controller_type)); SetNpadStyleIndex(MapSettingsTypeToNPad(player.controller_type));
@ -127,18 +130,26 @@ void EmulatedController::LoadDevices() {
trigger_params[LeftIndex] = button_params[Settings::NativeButton::ZL]; trigger_params[LeftIndex] = button_params[Settings::NativeButton::ZL];
trigger_params[RightIndex] = button_params[Settings::NativeButton::ZR]; trigger_params[RightIndex] = button_params[Settings::NativeButton::ZR];
color_params[LeftIndex] = left_joycon;
color_params[RightIndex] = right_joycon;
color_params[LeftIndex].Set("color", true);
color_params[RightIndex].Set("color", true);
battery_params[LeftIndex] = left_joycon; battery_params[LeftIndex] = left_joycon;
battery_params[RightIndex] = right_joycon; battery_params[RightIndex] = right_joycon;
battery_params[LeftIndex].Set("battery", true); battery_params[LeftIndex].Set("battery", true);
battery_params[RightIndex].Set("battery", true); battery_params[RightIndex].Set("battery", true);
camera_params = Common::ParamPackage{"engine:camera,camera:1"}; camera_params = Common::ParamPackage{"engine:camera,camera:1"};
nfc_params = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};
nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"};
nfc_params[1] = right_joycon;
nfc_params[1].Set("nfc", true);
output_params[LeftIndex] = left_joycon; output_params[LeftIndex] = left_joycon;
output_params[RightIndex] = right_joycon; output_params[RightIndex] = right_joycon;
output_params[2] = camera_params; output_params[2] = camera_params;
output_params[3] = nfc_params; output_params[3] = nfc_params[0];
output_params[LeftIndex].Set("output", true); output_params[LeftIndex].Set("output", true);
output_params[RightIndex].Set("output", true); output_params[RightIndex].Set("output", true);
output_params[2].Set("output", true); output_params[2].Set("output", true);
@ -154,8 +165,11 @@ void EmulatedController::LoadDevices() {
Common::Input::CreateInputDevice); Common::Input::CreateInputDevice);
std::ranges::transform(battery_params, battery_devices.begin(), std::ranges::transform(battery_params, battery_devices.begin(),
Common::Input::CreateInputDevice); Common::Input::CreateInputDevice);
std::ranges::transform(color_params, color_devices.begin(), Common::Input::CreateInputDevice);
camera_devices = Common::Input::CreateInputDevice(camera_params); camera_devices = Common::Input::CreateInputDevice(camera_params);
nfc_devices = Common::Input::CreateInputDevice(nfc_params); std::ranges::transform(ring_params, ring_analog_devices.begin(),
Common::Input::CreateInputDevice);
std::ranges::transform(nfc_params, nfc_devices.begin(), Common::Input::CreateInputDevice);
std::ranges::transform(output_params, output_devices.begin(), std::ranges::transform(output_params, output_devices.begin(),
Common::Input::CreateOutputDevice); Common::Input::CreateOutputDevice);
@ -310,6 +324,19 @@ void EmulatedController::ReloadInput() {
battery_devices[index]->ForceUpdate(); battery_devices[index]->ForceUpdate();
} }
for (std::size_t index = 0; index < color_devices.size(); ++index) {
if (!color_devices[index]) {
continue;
}
color_devices[index]->SetCallback({
.on_change =
[this, index](const Common::Input::CallbackStatus& callback) {
SetColors(callback, index);
},
});
color_devices[index]->ForceUpdate();
}
for (std::size_t index = 0; index < motion_devices.size(); ++index) { for (std::size_t index = 0; index < motion_devices.size(); ++index) {
if (!motion_devices[index]) { if (!motion_devices[index]) {
continue; continue;
@ -331,14 +358,26 @@ void EmulatedController::ReloadInput() {
camera_devices->ForceUpdate(); camera_devices->ForceUpdate();
} }
if (nfc_devices) { for (std::size_t index = 0; index < ring_analog_devices.size(); ++index) {
if (npad_id_type == NpadIdType::Handheld || npad_id_type == NpadIdType::Player1) { if (!ring_analog_devices[index]) {
nfc_devices->SetCallback({ continue;
.on_change =
[this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); },
});
nfc_devices->ForceUpdate();
} }
ring_analog_devices[index]->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback) { SetRingAnalog(callback); },
});
ring_analog_devices[index]->ForceUpdate();
}
for (std::size_t index = 0; index < nfc_devices.size(); ++index) {
if (!nfc_devices[index]) {
continue;
}
nfc_devices[index]->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); },
});
nfc_devices[index]->ForceUpdate();
} }
// Use a common UUID for TAS // Use a common UUID for TAS
@ -416,6 +455,9 @@ void EmulatedController::UnloadInput() {
for (auto& battery : battery_devices) { for (auto& battery : battery_devices) {
battery.reset(); battery.reset();
} }
for (auto& color : color_devices) {
color.reset();
}
for (auto& output : output_devices) { for (auto& output : output_devices) {
output.reset(); output.reset();
} }
@ -432,7 +474,12 @@ void EmulatedController::UnloadInput() {
stick.reset(); stick.reset();
} }
camera_devices.reset(); camera_devices.reset();
nfc_devices.reset(); for (auto& ring : ring_analog_devices) {
ring.reset();
}
for (auto& nfc : nfc_devices) {
nfc.reset();
}
} }
void EmulatedController::EnableConfiguration() { void EmulatedController::EnableConfiguration() {
@ -444,6 +491,11 @@ void EmulatedController::EnableConfiguration() {
void EmulatedController::DisableConfiguration() { void EmulatedController::DisableConfiguration() {
is_configuring = false; is_configuring = false;
// Get Joycon colors before turning on the controller
for (const auto& color_device : color_devices) {
color_device->ForceUpdate();
}
// Apply temporary npad type to the real controller // Apply temporary npad type to the real controller
if (tmp_npad_type != npad_type) { if (tmp_npad_type != npad_type) {
if (is_connected) { if (is_connected) {
@ -497,6 +549,9 @@ void EmulatedController::SaveCurrentConfig() {
for (std::size_t index = 0; index < player.motions.size(); ++index) { for (std::size_t index = 0; index < player.motions.size(); ++index) {
player.motions[index] = motion_params[index].Serialize(); player.motions[index] = motion_params[index].Serialize();
} }
if (npad_id_type == NpadIdType::Player1) {
Settings::values.ringcon_analogs = ring_params[0].Serialize();
}
} }
void EmulatedController::RestoreConfig() { void EmulatedController::RestoreConfig() {
@ -906,6 +961,58 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback
TriggerOnChange(ControllerTriggerType::Motion, true); TriggerOnChange(ControllerTriggerType::Motion, true);
} }
void EmulatedController::SetColors(const Common::Input::CallbackStatus& callback,
std::size_t index) {
if (index >= controller.color_values.size()) {
return;
}
std::unique_lock lock{mutex};
controller.color_values[index] = TransformToColor(callback);
if (is_configuring) {
lock.unlock();
TriggerOnChange(ControllerTriggerType::Color, false);
return;
}
if (controller.color_values[index].body == 0) {
return;
}
controller.colors_state.fullkey = {
.body = GetNpadColor(controller.color_values[index].body),
.button = GetNpadColor(controller.color_values[index].buttons),
};
if (npad_type == NpadStyleIndex::ProController) {
controller.colors_state.left = {
.body = GetNpadColor(controller.color_values[index].left_grip),
.button = GetNpadColor(controller.color_values[index].buttons),
};
controller.colors_state.right = {
.body = GetNpadColor(controller.color_values[index].right_grip),
.button = GetNpadColor(controller.color_values[index].buttons),
};
} else {
switch (index) {
case LeftIndex:
controller.colors_state.left = {
.body = GetNpadColor(controller.color_values[index].body),
.button = GetNpadColor(controller.color_values[index].buttons),
};
break;
case RightIndex:
controller.colors_state.right = {
.body = GetNpadColor(controller.color_values[index].body),
.button = GetNpadColor(controller.color_values[index].buttons),
};
break;
}
}
lock.unlock();
TriggerOnChange(ControllerTriggerType::Color, true);
}
void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback, void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback,
std::size_t index) { std::size_t index) {
if (index >= controller.battery_values.size()) { if (index >= controller.battery_values.size()) {
@ -996,6 +1103,24 @@ void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback
TriggerOnChange(ControllerTriggerType::IrSensor, true); TriggerOnChange(ControllerTriggerType::IrSensor, true);
} }
void EmulatedController::SetRingAnalog(const Common::Input::CallbackStatus& callback) {
std::unique_lock lock{mutex};
const auto force_value = TransformToStick(callback);
controller.ring_analog_value = force_value.x;
if (is_configuring) {
lock.unlock();
TriggerOnChange(ControllerTriggerType::RingController, false);
return;
}
controller.ring_analog_state.force = force_value.x.value;
lock.unlock();
TriggerOnChange(ControllerTriggerType::RingController, true);
}
void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) { void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) {
std::unique_lock lock{mutex}; std::unique_lock lock{mutex};
controller.nfc_values = TransformToNfc(callback); controller.nfc_values = TransformToNfc(callback);
@ -1095,6 +1220,15 @@ bool EmulatedController::SetCameraFormat(
camera_format)) == Common::Input::CameraError::None; camera_format)) == Common::Input::CameraError::None;
} }
Common::ParamPackage EmulatedController::GetRingParam() const {
return ring_params[0];
}
void EmulatedController::SetRingParam(Common::ParamPackage param) {
ring_params[0] = std::move(param);
ReloadInput();
}
bool EmulatedController::HasNfc() const { bool EmulatedController::HasNfc() const {
const auto& nfc_output_device = output_devices[3]; const auto& nfc_output_device = output_devices[3];
@ -1386,6 +1520,10 @@ CameraValues EmulatedController::GetCameraValues() const {
return controller.camera_values; return controller.camera_values;
} }
RingAnalogValue EmulatedController::GetRingSensorValues() const {
return controller.ring_analog_value;
}
HomeButtonState EmulatedController::GetHomeButtons() const { HomeButtonState EmulatedController::GetHomeButtons() const {
std::scoped_lock lock{mutex}; std::scoped_lock lock{mutex};
if (is_configuring) { if (is_configuring) {
@ -1479,6 +1617,10 @@ const CameraState& EmulatedController::GetCamera() const {
return controller.camera_state; return controller.camera_state;
} }
RingSensorForce EmulatedController::GetRingSensorForce() const {
return controller.ring_analog_state;
}
const NfcState& EmulatedController::GetNfc() const { const NfcState& EmulatedController::GetNfc() const {
std::scoped_lock lock{mutex}; std::scoped_lock lock{mutex};
return controller.nfc_state; return controller.nfc_state;

View file

@ -35,19 +35,26 @@ using ControllerMotionDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeMotion::NumMotions>; std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeMotion::NumMotions>;
using TriggerDevices = using TriggerDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeTrigger::NumTriggers>; std::array<std::unique_ptr<Common::Input::InputDevice>, Settings::NativeTrigger::NumTriggers>;
using ColorDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
using BatteryDevices = using BatteryDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>; std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
using CameraDevices = std::unique_ptr<Common::Input::InputDevice>; using CameraDevices = std::unique_ptr<Common::Input::InputDevice>;
using NfcDevices = std::unique_ptr<Common::Input::InputDevice>; using RingAnalogDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
using NfcDevices =
std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
using OutputDevices = std::array<std::unique_ptr<Common::Input::OutputDevice>, output_devices_size>; using OutputDevices = std::array<std::unique_ptr<Common::Input::OutputDevice>, output_devices_size>;
using ButtonParams = std::array<Common::ParamPackage, Settings::NativeButton::NumButtons>; using ButtonParams = std::array<Common::ParamPackage, Settings::NativeButton::NumButtons>;
using StickParams = std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs>; using StickParams = std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs>;
using ControllerMotionParams = std::array<Common::ParamPackage, Settings::NativeMotion::NumMotions>; using ControllerMotionParams = std::array<Common::ParamPackage, Settings::NativeMotion::NumMotions>;
using TriggerParams = std::array<Common::ParamPackage, Settings::NativeTrigger::NumTriggers>; using TriggerParams = std::array<Common::ParamPackage, Settings::NativeTrigger::NumTriggers>;
using ColorParams = std::array<Common::ParamPackage, max_emulated_controllers>;
using BatteryParams = std::array<Common::ParamPackage, max_emulated_controllers>; using BatteryParams = std::array<Common::ParamPackage, max_emulated_controllers>;
using CameraParams = Common::ParamPackage; using CameraParams = Common::ParamPackage;
using NfcParams = Common::ParamPackage; using RingAnalogParams = std::array<Common::ParamPackage, max_emulated_controllers>;
using NfcParams = std::array<Common::ParamPackage, max_emulated_controllers>;
using OutputParams = std::array<Common::ParamPackage, output_devices_size>; using OutputParams = std::array<Common::ParamPackage, output_devices_size>;
using ButtonValues = std::array<Common::Input::ButtonStatus, Settings::NativeButton::NumButtons>; using ButtonValues = std::array<Common::Input::ButtonStatus, Settings::NativeButton::NumButtons>;
@ -58,6 +65,7 @@ using ControllerMotionValues = std::array<ControllerMotionInfo, Settings::Native
using ColorValues = std::array<Common::Input::BodyColorStatus, max_emulated_controllers>; using ColorValues = std::array<Common::Input::BodyColorStatus, max_emulated_controllers>;
using BatteryValues = std::array<Common::Input::BatteryStatus, max_emulated_controllers>; using BatteryValues = std::array<Common::Input::BatteryStatus, max_emulated_controllers>;
using CameraValues = Common::Input::CameraStatus; using CameraValues = Common::Input::CameraStatus;
using RingAnalogValue = Common::Input::AnalogStatus;
using NfcValues = Common::Input::NfcStatus; using NfcValues = Common::Input::NfcStatus;
using VibrationValues = std::array<Common::Input::VibrationStatus, max_emulated_controllers>; using VibrationValues = std::array<Common::Input::VibrationStatus, max_emulated_controllers>;
@ -84,6 +92,10 @@ struct CameraState {
std::size_t sample{}; std::size_t sample{};
}; };
struct RingSensorForce {
f32 force;
};
struct NfcState { struct NfcState {
Common::Input::NfcState state{}; Common::Input::NfcState state{};
std::vector<u8> data{}; std::vector<u8> data{};
@ -116,6 +128,7 @@ struct ControllerStatus {
BatteryValues battery_values{}; BatteryValues battery_values{};
VibrationValues vibration_values{}; VibrationValues vibration_values{};
CameraValues camera_values{}; CameraValues camera_values{};
RingAnalogValue ring_analog_value{};
NfcValues nfc_values{}; NfcValues nfc_values{};
// Data for HID serices // Data for HID serices
@ -129,6 +142,7 @@ struct ControllerStatus {
ControllerColors colors_state{}; ControllerColors colors_state{};
BatteryLevelState battery_state{}; BatteryLevelState battery_state{};
CameraState camera_state{}; CameraState camera_state{};
RingSensorForce ring_analog_state{};
NfcState nfc_state{}; NfcState nfc_state{};
}; };
@ -141,6 +155,7 @@ enum class ControllerTriggerType {
Battery, Battery,
Vibration, Vibration,
IrSensor, IrSensor,
RingController,
Nfc, Nfc,
Connected, Connected,
Disconnected, Disconnected,
@ -294,6 +309,9 @@ public:
/// Returns the latest camera status from the controller with parameters /// Returns the latest camera status from the controller with parameters
CameraValues GetCameraValues() const; CameraValues GetCameraValues() const;
/// Returns the latest status of analog input from the ring sensor with parameters
RingAnalogValue GetRingSensorValues() const;
/// Returns the latest status of button input for the hid::HomeButton service /// Returns the latest status of button input for the hid::HomeButton service
HomeButtonState GetHomeButtons() const; HomeButtonState GetHomeButtons() const;
@ -324,6 +342,9 @@ public:
/// Returns the latest camera status from the controller /// Returns the latest camera status from the controller
const CameraState& GetCamera() const; const CameraState& GetCamera() const;
/// Returns the latest ringcon force sensor value
RingSensorForce GetRingSensorForce() const;
/// Returns the latest ntag status from the controller /// Returns the latest ntag status from the controller
const NfcState& GetNfc() const; const NfcState& GetNfc() const;
@ -353,6 +374,15 @@ public:
*/ */
bool SetCameraFormat(Core::IrSensor::ImageTransferProcessorFormat camera_format); bool SetCameraFormat(Core::IrSensor::ImageTransferProcessorFormat camera_format);
// Returns the current mapped ring device
Common::ParamPackage GetRingParam() const;
/**
* Updates the current mapped ring device
* @param param ParamPackage with ring sensor data to be mapped
*/
void SetRingParam(Common::ParamPackage param);
/// Returns true if the device has nfc support /// Returns true if the device has nfc support
bool HasNfc() const; bool HasNfc() const;
@ -432,10 +462,17 @@ private:
*/ */
void SetMotion(const Common::Input::CallbackStatus& callback, std::size_t index); void SetMotion(const Common::Input::CallbackStatus& callback, std::size_t index);
/**
* Updates the color status of the controller
* @param callback A CallbackStatus containing the color status
* @param index color ID of the to be updated
*/
void SetColors(const Common::Input::CallbackStatus& callback, std::size_t index);
/** /**
* Updates the battery status of the controller * Updates the battery status of the controller
* @param callback A CallbackStatus containing the battery status * @param callback A CallbackStatus containing the battery status
* @param index Button ID of the to be updated * @param index battery ID of the to be updated
*/ */
void SetBattery(const Common::Input::CallbackStatus& callback, std::size_t index); void SetBattery(const Common::Input::CallbackStatus& callback, std::size_t index);
@ -445,6 +482,12 @@ private:
*/ */
void SetCamera(const Common::Input::CallbackStatus& callback); void SetCamera(const Common::Input::CallbackStatus& callback);
/**
* Updates the ring analog sensor status of the ring controller
* @param callback A CallbackStatus containing the force status
*/
void SetRingAnalog(const Common::Input::CallbackStatus& callback);
/** /**
* Updates the nfc status of the controller * Updates the nfc status of the controller
* @param callback A CallbackStatus containing the nfc status * @param callback A CallbackStatus containing the nfc status
@ -484,7 +527,9 @@ private:
ControllerMotionParams motion_params; ControllerMotionParams motion_params;
TriggerParams trigger_params; TriggerParams trigger_params;
BatteryParams battery_params; BatteryParams battery_params;
ColorParams color_params;
CameraParams camera_params; CameraParams camera_params;
RingAnalogParams ring_params;
NfcParams nfc_params; NfcParams nfc_params;
OutputParams output_params; OutputParams output_params;
@ -493,7 +538,9 @@ private:
ControllerMotionDevices motion_devices; ControllerMotionDevices motion_devices;
TriggerDevices trigger_devices; TriggerDevices trigger_devices;
BatteryDevices battery_devices; BatteryDevices battery_devices;
ColorDevices color_devices;
CameraDevices camera_devices; CameraDevices camera_devices;
RingAnalogDevices ring_analog_devices;
NfcDevices nfc_devices; NfcDevices nfc_devices;
OutputDevices output_devices; OutputDevices output_devices;

View file

@ -14,7 +14,6 @@ EmulatedDevices::EmulatedDevices() = default;
EmulatedDevices::~EmulatedDevices() = default; EmulatedDevices::~EmulatedDevices() = default;
void EmulatedDevices::ReloadFromSettings() { void EmulatedDevices::ReloadFromSettings() {
ring_params = Common::ParamPackage(Settings::values.ringcon_analogs);
ReloadInput(); ReloadInput();
} }
@ -66,8 +65,6 @@ void EmulatedDevices::ReloadInput() {
key_index++; key_index++;
} }
ring_analog_device = Common::Input::CreateInputDevice(ring_params);
for (std::size_t index = 0; index < mouse_button_devices.size(); ++index) { for (std::size_t index = 0; index < mouse_button_devices.size(); ++index) {
if (!mouse_button_devices[index]) { if (!mouse_button_devices[index]) {
continue; continue;
@ -122,13 +119,6 @@ void EmulatedDevices::ReloadInput() {
}, },
}); });
} }
if (ring_analog_device) {
ring_analog_device->SetCallback({
.on_change =
[this](const Common::Input::CallbackStatus& callback) { SetRingAnalog(callback); },
});
}
} }
void EmulatedDevices::UnloadInput() { void EmulatedDevices::UnloadInput() {
@ -145,7 +135,6 @@ void EmulatedDevices::UnloadInput() {
for (auto& button : keyboard_modifier_devices) { for (auto& button : keyboard_modifier_devices) {
button.reset(); button.reset();
} }
ring_analog_device.reset();
} }
void EmulatedDevices::EnableConfiguration() { void EmulatedDevices::EnableConfiguration() {
@ -165,7 +154,6 @@ void EmulatedDevices::SaveCurrentConfig() {
if (!is_configuring) { if (!is_configuring) {
return; return;
} }
Settings::values.ringcon_analogs = ring_params.Serialize();
} }
void EmulatedDevices::RestoreConfig() { void EmulatedDevices::RestoreConfig() {
@ -175,15 +163,6 @@ void EmulatedDevices::RestoreConfig() {
ReloadFromSettings(); ReloadFromSettings();
} }
Common::ParamPackage EmulatedDevices::GetRingParam() const {
return ring_params;
}
void EmulatedDevices::SetRingParam(Common::ParamPackage param) {
ring_params = std::move(param);
ReloadInput();
}
void EmulatedDevices::SetKeyboardButton(const Common::Input::CallbackStatus& callback, void EmulatedDevices::SetKeyboardButton(const Common::Input::CallbackStatus& callback,
std::size_t index) { std::size_t index) {
if (index >= device_status.keyboard_values.size()) { if (index >= device_status.keyboard_values.size()) {
@ -430,23 +409,6 @@ void EmulatedDevices::SetMouseStick(const Common::Input::CallbackStatus& callbac
TriggerOnChange(DeviceTriggerType::Mouse); TriggerOnChange(DeviceTriggerType::Mouse);
} }
void EmulatedDevices::SetRingAnalog(const Common::Input::CallbackStatus& callback) {
std::lock_guard lock{mutex};
const auto force_value = TransformToStick(callback);
device_status.ring_analog_value = force_value.x;
if (is_configuring) {
device_status.ring_analog_value = {};
TriggerOnChange(DeviceTriggerType::RingController);
return;
}
device_status.ring_analog_state.force = force_value.x.value;
TriggerOnChange(DeviceTriggerType::RingController);
}
KeyboardValues EmulatedDevices::GetKeyboardValues() const { KeyboardValues EmulatedDevices::GetKeyboardValues() const {
std::scoped_lock lock{mutex}; std::scoped_lock lock{mutex};
return device_status.keyboard_values; return device_status.keyboard_values;
@ -462,10 +424,6 @@ MouseButtonValues EmulatedDevices::GetMouseButtonsValues() const {
return device_status.mouse_button_values; return device_status.mouse_button_values;
} }
RingAnalogValue EmulatedDevices::GetRingSensorValues() const {
return device_status.ring_analog_value;
}
KeyboardKey EmulatedDevices::GetKeyboard() const { KeyboardKey EmulatedDevices::GetKeyboard() const {
std::scoped_lock lock{mutex}; std::scoped_lock lock{mutex};
return device_status.keyboard_state; return device_status.keyboard_state;
@ -491,10 +449,6 @@ AnalogStickState EmulatedDevices::GetMouseWheel() const {
return device_status.mouse_wheel_state; return device_status.mouse_wheel_state;
} }
RingSensorForce EmulatedDevices::GetRingSensorForce() const {
return device_status.ring_analog_state;
}
void EmulatedDevices::TriggerOnChange(DeviceTriggerType type) { void EmulatedDevices::TriggerOnChange(DeviceTriggerType type) {
std::scoped_lock lock{callback_mutex}; std::scoped_lock lock{callback_mutex};
for (const auto& poller_pair : callback_list) { for (const auto& poller_pair : callback_list) {

View file

@ -26,11 +26,9 @@ using MouseButtonDevices = std::array<std::unique_ptr<Common::Input::InputDevice
using MouseAnalogDevices = std::array<std::unique_ptr<Common::Input::InputDevice>, using MouseAnalogDevices = std::array<std::unique_ptr<Common::Input::InputDevice>,
Settings::NativeMouseWheel::NumMouseWheels>; Settings::NativeMouseWheel::NumMouseWheels>;
using MouseStickDevice = std::unique_ptr<Common::Input::InputDevice>; using MouseStickDevice = std::unique_ptr<Common::Input::InputDevice>;
using RingAnalogDevice = std::unique_ptr<Common::Input::InputDevice>;
using MouseButtonParams = using MouseButtonParams =
std::array<Common::ParamPackage, Settings::NativeMouseButton::NumMouseButtons>; std::array<Common::ParamPackage, Settings::NativeMouseButton::NumMouseButtons>;
using RingAnalogParams = Common::ParamPackage;
using KeyboardValues = using KeyboardValues =
std::array<Common::Input::ButtonStatus, Settings::NativeKeyboard::NumKeyboardKeys>; std::array<Common::Input::ButtonStatus, Settings::NativeKeyboard::NumKeyboardKeys>;
@ -41,17 +39,12 @@ using MouseButtonValues =
using MouseAnalogValues = using MouseAnalogValues =
std::array<Common::Input::AnalogStatus, Settings::NativeMouseWheel::NumMouseWheels>; std::array<Common::Input::AnalogStatus, Settings::NativeMouseWheel::NumMouseWheels>;
using MouseStickValue = Common::Input::TouchStatus; using MouseStickValue = Common::Input::TouchStatus;
using RingAnalogValue = Common::Input::AnalogStatus;
struct MousePosition { struct MousePosition {
f32 x; f32 x;
f32 y; f32 y;
}; };
struct RingSensorForce {
f32 force;
};
struct DeviceStatus { struct DeviceStatus {
// Data from input_common // Data from input_common
KeyboardValues keyboard_values{}; KeyboardValues keyboard_values{};
@ -59,7 +52,6 @@ struct DeviceStatus {
MouseButtonValues mouse_button_values{}; MouseButtonValues mouse_button_values{};
MouseAnalogValues mouse_analog_values{}; MouseAnalogValues mouse_analog_values{};
MouseStickValue mouse_stick_value{}; MouseStickValue mouse_stick_value{};
RingAnalogValue ring_analog_value{};
// Data for HID serices // Data for HID serices
KeyboardKey keyboard_state{}; KeyboardKey keyboard_state{};
@ -67,7 +59,6 @@ struct DeviceStatus {
MouseButton mouse_button_state{}; MouseButton mouse_button_state{};
MousePosition mouse_position_state{}; MousePosition mouse_position_state{};
AnalogStickState mouse_wheel_state{}; AnalogStickState mouse_wheel_state{};
RingSensorForce ring_analog_state{};
}; };
enum class DeviceTriggerType { enum class DeviceTriggerType {
@ -138,9 +129,6 @@ public:
/// Returns the latest status of button input from the mouse with parameters /// Returns the latest status of button input from the mouse with parameters
MouseButtonValues GetMouseButtonsValues() const; MouseButtonValues GetMouseButtonsValues() const;
/// Returns the latest status of analog input from the ring sensor with parameters
RingAnalogValue GetRingSensorValues() const;
/// Returns the latest status of button input from the keyboard /// Returns the latest status of button input from the keyboard
KeyboardKey GetKeyboard() const; KeyboardKey GetKeyboard() const;
@ -156,9 +144,6 @@ public:
/// Returns the latest mouse wheel change /// Returns the latest mouse wheel change
AnalogStickState GetMouseWheel() const; AnalogStickState GetMouseWheel() const;
/// Returns the latest ringcon force sensor value
RingSensorForce GetRingSensorForce() const;
/** /**
* Adds a callback to the list of events * Adds a callback to the list of events
* @param update_callback InterfaceUpdateCallback that will be triggered * @param update_callback InterfaceUpdateCallback that will be triggered
@ -224,14 +209,11 @@ private:
bool is_configuring{false}; bool is_configuring{false};
RingAnalogParams ring_params;
KeyboardDevices keyboard_devices; KeyboardDevices keyboard_devices;
KeyboardModifierDevices keyboard_modifier_devices; KeyboardModifierDevices keyboard_modifier_devices;
MouseButtonDevices mouse_button_devices; MouseButtonDevices mouse_button_devices;
MouseAnalogDevices mouse_analog_devices; MouseAnalogDevices mouse_analog_devices;
MouseStickDevice mouse_stick_device; MouseStickDevice mouse_stick_device;
RingAnalogDevice ring_analog_device;
mutable std::mutex mutex; mutable std::mutex mutex;
mutable std::mutex callback_mutex; mutable std::mutex callback_mutex;

View file

@ -304,6 +304,18 @@ Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& cal
return nfc; return nfc;
} }
Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback) {
switch (callback.type) {
case Common::Input::InputType::Color:
return callback.color_status;
break;
default:
LOG_ERROR(Input, "Conversion from type {} to color not implemented", callback.type);
return {};
break;
}
}
void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) { void SanitizeAnalog(Common::Input::AnalogStatus& analog, bool clamp_value) {
const auto& properties = analog.properties; const auto& properties = analog.properties;
float& raw_value = analog.raw_value; float& raw_value = analog.raw_value;

View file

@ -88,10 +88,18 @@ Common::Input::CameraStatus TransformToCamera(const Common::Input::CallbackStatu
* Converts raw input data into a valid nfc status. * Converts raw input data into a valid nfc status.
* *
* @param callback Supported callbacks: Nfc. * @param callback Supported callbacks: Nfc.
* @return A valid CameraObject object. * @return A valid data tag vector.
*/ */
Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& callback); Common::Input::NfcStatus TransformToNfc(const Common::Input::CallbackStatus& callback);
/**
* Converts raw input data into a valid color status.
*
* @param callback Supported callbacks: Color.
* @return A valid Color object.
*/
Common::Input::BodyColorStatus TransformToColor(const Common::Input::CallbackStatus& callback);
/** /**
* Converts raw analog data into a valid analog value * Converts raw analog data into a valid analog value
* @param analog An analog object containing raw data and properties * @param analog An analog object containing raw data and properties

View file

@ -272,6 +272,8 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) {
} }
break; break;
case Core::HID::NpadStyleIndex::JoyconLeft: case Core::HID::NpadStyleIndex::JoyconLeft:
shared_memory->fullkey_color.attribute = ColorAttribute::Ok;
shared_memory->fullkey_color.fullkey = body_colors.left;
shared_memory->joycon_color.attribute = ColorAttribute::Ok; shared_memory->joycon_color.attribute = ColorAttribute::Ok;
shared_memory->joycon_color.left = body_colors.left; shared_memory->joycon_color.left = body_colors.left;
shared_memory->battery_level_dual = battery_level.left.battery_level; shared_memory->battery_level_dual = battery_level.left.battery_level;
@ -285,6 +287,8 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) {
shared_memory->sixaxis_left_properties.is_newly_assigned.Assign(1); shared_memory->sixaxis_left_properties.is_newly_assigned.Assign(1);
break; break;
case Core::HID::NpadStyleIndex::JoyconRight: case Core::HID::NpadStyleIndex::JoyconRight:
shared_memory->fullkey_color.attribute = ColorAttribute::Ok;
shared_memory->fullkey_color.fullkey = body_colors.right;
shared_memory->joycon_color.attribute = ColorAttribute::Ok; shared_memory->joycon_color.attribute = ColorAttribute::Ok;
shared_memory->joycon_color.right = body_colors.right; shared_memory->joycon_color.right = body_colors.right;
shared_memory->battery_level_right = battery_level.right.battery_level; shared_memory->battery_level_right = battery_level.right.battery_level;
@ -332,6 +336,8 @@ void Controller_NPad::InitNewlyAddedController(Core::HID::NpadIdType npad_id) {
controller.is_connected = true; controller.is_connected = true;
controller.device->Connect(); controller.device->Connect();
controller.device->SetLedPattern();
controller.device->SetPollingMode(Common::Input::PollingMode::Active);
SignalStyleSetChangedEvent(npad_id); SignalStyleSetChangedEvent(npad_id);
WriteEmptyEntry(controller.shared_memory); WriteEmptyEntry(controller.shared_memory);
} }

View file

@ -297,13 +297,13 @@ void HidBus::EnableExternalDevice(Kernel::HLERequestContext& ctx) {
const auto parameters{rp.PopRaw<Parameters>()}; const auto parameters{rp.PopRaw<Parameters>()};
LOG_INFO(Service_HID, LOG_DEBUG(Service_HID,
"called, enable={}, abstracted_pad_id={}, bus_type={}, internal_index={}, " "called, enable={}, abstracted_pad_id={}, bus_type={}, internal_index={}, "
"player_number={}, is_valid={}, inval={}, applet_resource_user_id{}", "player_number={}, is_valid={}, inval={}, applet_resource_user_id{}",
parameters.enable, parameters.bus_handle.abstracted_pad_id, parameters.enable, parameters.bus_handle.abstracted_pad_id,
parameters.bus_handle.bus_type, parameters.bus_handle.internal_index, parameters.bus_handle.bus_type, parameters.bus_handle.internal_index,
parameters.bus_handle.player_number, parameters.bus_handle.is_valid, parameters.inval, parameters.bus_handle.player_number, parameters.bus_handle.is_valid, parameters.inval,
parameters.applet_resource_user_id); parameters.applet_resource_user_id);
const auto device_index = GetDeviceIndexFromHandle(parameters.bus_handle); const auto device_index = GetDeviceIndexFromHandle(parameters.bus_handle);
@ -326,11 +326,11 @@ void HidBus::GetExternalDeviceId(Kernel::HLERequestContext& ctx) {
IPC::RequestParser rp{ctx}; IPC::RequestParser rp{ctx};
const auto bus_handle_{rp.PopRaw<BusHandle>()}; const auto bus_handle_{rp.PopRaw<BusHandle>()};
LOG_INFO(Service_HID, LOG_DEBUG(Service_HID,
"called, abstracted_pad_id={}, bus_type={}, internal_index={}, player_number={}, " "called, abstracted_pad_id={}, bus_type={}, internal_index={}, player_number={}, "
"is_valid={}", "is_valid={}",
bus_handle_.abstracted_pad_id, bus_handle_.bus_type, bus_handle_.internal_index, bus_handle_.abstracted_pad_id, bus_handle_.bus_type, bus_handle_.internal_index,
bus_handle_.player_number, bus_handle_.is_valid); bus_handle_.player_number, bus_handle_.is_valid);
const auto device_index = GetDeviceIndexFromHandle(bus_handle_); const auto device_index = GetDeviceIndexFromHandle(bus_handle_);

View file

@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include "core/hid/emulated_devices.h" #include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h" #include "core/hid/hid_core.h"
#include "core/hle/kernel/k_event.h" #include "core/hle/kernel/k_event.h"
#include "core/hle/kernel/k_readable_event.h" #include "core/hle/kernel/k_readable_event.h"
@ -12,16 +12,18 @@ namespace Service::HID {
RingController::RingController(Core::HID::HIDCore& hid_core_, RingController::RingController(Core::HID::HIDCore& hid_core_,
KernelHelpers::ServiceContext& service_context_) KernelHelpers::ServiceContext& service_context_)
: HidbusBase(service_context_) { : HidbusBase(service_context_) {
input = hid_core_.GetEmulatedDevices(); input = hid_core_.GetEmulatedController(Core::HID::NpadIdType::Player1);
} }
RingController::~RingController() = default; RingController::~RingController() = default;
void RingController::OnInit() { void RingController::OnInit() {
input->SetPollingMode(Common::Input::PollingMode::Ring);
return; return;
} }
void RingController::OnRelease() { void RingController::OnRelease() {
input->SetPollingMode(Common::Input::PollingMode::Active);
return; return;
}; };

View file

@ -9,7 +9,7 @@
#include "core/hle/service/hid/hidbus/hidbus_base.h" #include "core/hle/service/hid/hidbus/hidbus_base.h"
namespace Core::HID { namespace Core::HID {
class EmulatedDevices; class EmulatedController;
} // namespace Core::HID } // namespace Core::HID
namespace Service::HID { namespace Service::HID {
@ -248,6 +248,6 @@ private:
.zero = {.value = idle_value, .crc = 225}, .zero = {.value = idle_value, .crc = 225},
}; };
Core::HID::EmulatedDevices* input; Core::HID::EmulatedController* input;
}; };
} // namespace Service::HID } // namespace Service::HID

View file

@ -55,8 +55,27 @@ endif()
if (ENABLE_SDL2) if (ENABLE_SDL2)
target_sources(input_common PRIVATE target_sources(input_common PRIVATE
drivers/joycon.cpp
drivers/joycon.h
drivers/sdl_driver.cpp drivers/sdl_driver.cpp
drivers/sdl_driver.h 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/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_link_libraries(input_common PRIVATE SDL2::SDL2)
target_compile_definitions(input_common PRIVATE HAVE_SDL2) target_compile_definitions(input_common PRIVATE HAVE_SDL2)

View file

@ -0,0 +1,708 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <fmt/format.h>
#include "common/param_package.h"
#include "common/settings.h"
#include "common/thread.h"
#include "input_common/drivers/joycon.h"
#include "input_common/helpers/joycon_driver.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon {
Joycons::Joycons(const std::string& input_engine_) : InputEngine(input_engine_) {
// Avoid conflicting with SDL driver
if (!Settings::values.enable_joycon_driver) {
return;
}
LOG_INFO(Input, "Joycon driver Initialization started");
const int init_res = SDL_hid_init();
if (init_res == 0) {
Setup();
} else {
LOG_ERROR(Input, "Hidapi could not be initialized. failed with error = {}", init_res);
}
}
Joycons::~Joycons() {
Reset();
}
void Joycons::Reset() {
scan_thread = {};
for (const auto& device : left_joycons) {
if (!device) {
continue;
}
device->Stop();
}
for (const auto& device : right_joycons) {
if (!device) {
continue;
}
device->Stop();
}
for (const auto& device : pro_joycons) {
if (!device) {
continue;
}
device->Stop();
}
SDL_hid_exit();
}
void Joycons::Setup() {
u32 port = 0;
PreSetController(GetIdentifier(0, Joycon::ControllerType::None));
for (auto& device : left_joycons) {
PreSetController(GetIdentifier(port, Joycon::ControllerType::Left));
device = std::make_shared<Joycon::JoyconDriver>(port++);
}
port = 0;
for (auto& device : right_joycons) {
PreSetController(GetIdentifier(port, Joycon::ControllerType::Right));
device = std::make_shared<Joycon::JoyconDriver>(port++);
}
port = 0;
for (auto& device : pro_joycons) {
PreSetController(GetIdentifier(port, Joycon::ControllerType::Pro));
device = std::make_shared<Joycon::JoyconDriver>(port++);
}
if (!scan_thread_running) {
scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); });
}
}
void Joycons::ScanThread(std::stop_token stop_token) {
constexpr u16 nintendo_vendor_id = 0x057e;
Common::SetCurrentThreadName("yuzu:input:JoyconScanThread");
scan_thread_running = true;
while (!stop_token.stop_requested()) {
SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0);
SDL_hid_device_info* cur_dev = devs;
while (cur_dev) {
if (IsDeviceNew(cur_dev)) {
LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id,
cur_dev->product_id);
RegisterNewDevice(cur_dev);
}
cur_dev = cur_dev->next;
}
std::this_thread::sleep_for(std::chrono::seconds(5));
}
scan_thread_running = false;
}
bool Joycons::IsDeviceNew(SDL_hid_device_info* device_info) const {
Joycon::ControllerType type{};
Joycon::SerialNumber serial_number{};
const auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
if (result != Joycon::DriverResult::Success) {
return false;
}
const auto result2 = Joycon::JoyconDriver::GetSerialNumber(device_info, serial_number);
if (result2 != Joycon::DriverResult::Success) {
return false;
}
auto is_handle_identical = [serial_number](std::shared_ptr<Joycon::JoyconDriver> device) {
if (!device) {
return false;
}
if (!device->IsConnected()) {
return false;
}
if (device->GetHandleSerialNumber() != serial_number) {
return false;
}
return true;
};
// Check if device already exist
switch (type) {
case Joycon::ControllerType::Left:
for (const auto& device : left_joycons) {
if (is_handle_identical(device)) {
return false;
}
}
break;
case Joycon::ControllerType::Right:
for (const auto& device : right_joycons) {
if (is_handle_identical(device)) {
return false;
}
}
break;
case Joycon::ControllerType::Pro:
case Joycon::ControllerType::Grip:
for (const auto& device : pro_joycons) {
if (is_handle_identical(device)) {
return false;
}
}
break;
default:
return false;
}
return true;
}
void Joycons::RegisterNewDevice(SDL_hid_device_info* device_info) {
Joycon::ControllerType type{};
auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
auto handle = GetNextFreeHandle(type);
if (handle == nullptr) {
LOG_WARNING(Input, "No free handles available");
return;
}
if (result == Joycon::DriverResult::Success) {
result = handle->RequestDeviceAccess(device_info);
}
if (result == Joycon::DriverResult::Success) {
LOG_WARNING(Input, "Initialize device");
const std::size_t port = handle->GetDevicePort();
const Joycon::JoyconCallbacks callbacks{
.on_battery_data = {[this, port, type](Joycon::Battery value) {
OnBatteryUpdate(port, type, value);
}},
.on_color_data = {[this, port, type](Joycon::Color value) {
OnColorUpdate(port, type, value);
}},
.on_button_data = {[this, port, type](int id, bool value) {
OnButtonUpdate(port, type, id, value);
}},
.on_stick_data = {[this, port, type](int id, f32 value) {
OnStickUpdate(port, type, id, value);
}},
.on_motion_data = {[this, port, type](int id, const Joycon::MotionData& value) {
OnMotionUpdate(port, type, id, value);
}},
.on_ring_data = {[this](f32 ring_data) { OnRingConUpdate(ring_data); }},
.on_amiibo_data = {[this, port](const std::vector<u8>& amiibo_data) {
OnAmiiboUpdate(port, amiibo_data);
}},
};
handle->InitializeDevice();
handle->SetCallbacks(callbacks);
}
}
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle(
Joycon::ControllerType type) const {
if (type == Joycon::ControllerType::Left) {
for (const auto& device : left_joycons) {
if (!device->IsConnected()) {
return device;
}
}
}
if (type == Joycon::ControllerType::Right) {
for (const auto& device : right_joycons) {
if (!device->IsConnected()) {
return device;
}
}
}
if (type == Joycon::ControllerType::Pro || type == Joycon::ControllerType::Grip) {
for (const auto& device : pro_joycons) {
if (!device->IsConnected()) {
return device;
}
}
}
return nullptr;
}
bool Joycons::IsVibrationEnabled(const PadIdentifier& identifier) {
const auto handle = GetHandle(identifier);
if (handle == nullptr) {
return false;
}
return handle->IsVibrationEnabled();
}
Common::Input::VibrationError Joycons::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const Joycon::VibrationValue native_vibration{
.low_amplitude = vibration.low_amplitude,
.low_frequency = vibration.low_frequency,
.high_amplitude = vibration.high_amplitude,
.high_frequency = vibration.high_frequency,
};
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return Common::Input::VibrationError::InvalidHandle;
}
handle->SetVibration(native_vibration);
return Common::Input::VibrationError::None;
}
void Joycons::SetLeds(const PadIdentifier& identifier, const Common::Input::LedStatus& led_status) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
return;
}
int led_config = led_status.led_1 ? 1 : 0;
led_config += led_status.led_2 ? 2 : 0;
led_config += led_status.led_3 ? 4 : 0;
led_config += led_status.led_4 ? 8 : 0;
const auto result = handle->SetLedConfig(static_cast<u8>(led_config));
if (result != Joycon::DriverResult::Success) {
LOG_ERROR(Input, "Failed to set led config");
}
}
Common::Input::CameraError Joycons::SetCameraFormat(const PadIdentifier& identifier_,
Common::Input::CameraFormat camera_format) {
return Common::Input::CameraError::NotSupported;
};
Common::Input::NfcState Joycons::SupportsNfc(const PadIdentifier& identifier_) const {
return Common::Input::NfcState::Success;
};
Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier_,
const std::vector<u8>& data) {
return Common::Input::NfcState::NotSupported;
};
Common::Input::PollingError Joycons::SetPollingMode(const PadIdentifier& identifier,
const Common::Input::PollingMode polling_mode) {
auto handle = GetHandle(identifier);
if (handle == nullptr) {
LOG_ERROR(Input, "Invalid handle {}", identifier.port);
return Common::Input::PollingError::InvalidHandle;
}
switch (polling_mode) {
case Common::Input::PollingMode::NFC:
handle->SetNfcMode();
break;
case Common::Input::PollingMode::Active:
handle->SetActiveMode();
break;
case Common::Input::PollingMode::Pasive:
handle->SetPasiveMode();
break;
case Common::Input::PollingMode::Ring:
handle->SetRingConMode();
break;
default:
return Common::Input::PollingError::NotSupported;
}
return Common::Input::PollingError::None;
}
void Joycons::OnBatteryUpdate(std::size_t port, Joycon::ControllerType type,
Joycon::Battery value) {
const auto identifier = GetIdentifier(port, type);
if (value.charging != 0) {
SetBattery(identifier, Common::Input::BatteryLevel::Charging);
return;
}
Common::Input::BatteryLevel battery{};
switch (value.status) {
case 0:
battery = Common::Input::BatteryLevel::Empty;
break;
case 1:
battery = Common::Input::BatteryLevel::Critical;
break;
case 2:
battery = Common::Input::BatteryLevel::Low;
break;
case 3:
battery = Common::Input::BatteryLevel::Medium;
break;
case 4:
default:
battery = Common::Input::BatteryLevel::Full;
break;
}
SetBattery(identifier, battery);
}
void Joycons::OnColorUpdate(std::size_t port, Joycon::ControllerType type,
const Joycon::Color& value) {
const auto identifier = GetIdentifier(port, type);
Common::Input::BodyColorStatus color{
.body = value.body,
.buttons = value.buttons,
.left_grip = value.left_grip,
.right_grip = value.right_grip,
};
SetColor(identifier, color);
}
void Joycons::OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value) {
const auto identifier = GetIdentifier(port, type);
SetButton(identifier, id, value);
}
void Joycons::OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value) {
const auto identifier = GetIdentifier(port, type);
SetAxis(identifier, id, value);
}
void Joycons::OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
const Joycon::MotionData& value) {
const auto identifier = GetIdentifier(port, type);
BasicMotion motion_data{
.gyro_x = value.gyro_x,
.gyro_y = value.gyro_y,
.gyro_z = value.gyro_z,
.accel_x = value.accel_x,
.accel_y = value.accel_y,
.accel_z = value.accel_z,
.delta_timestamp = 15000,
};
SetMotion(identifier, id, motion_data);
}
void Joycons::OnRingConUpdate(f32 ring_data) {
// To simplify ring detection it will always be mapped to an empty identifier for all
// controllers
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
SetAxis(identifier, 100, ring_data);
}
void Joycons::OnAmiiboUpdate(std::size_t port, const std::vector<u8>& amiibo_data) {
const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right);
const auto nfc_state = amiibo_data.empty() ? Common::Input::NfcState::AmiiboRemoved
: Common::Input::NfcState::NewAmiibo;
SetNfc(identifier, {nfc_state, amiibo_data});
}
std::shared_ptr<Joycon::JoyconDriver> Joycons::GetHandle(PadIdentifier identifier) const {
auto is_handle_active = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
if (!device) {
return false;
}
if (!device->IsConnected()) {
return false;
}
if (device->GetDevicePort() == identifier.port) {
return true;
}
return false;
};
const auto type = static_cast<Joycon::ControllerType>(identifier.pad);
if (type == Joycon::ControllerType::Left) {
for (const auto& device : left_joycons) {
if (is_handle_active(device)) {
return device;
}
}
}
if (type == Joycon::ControllerType::Right) {
for (const auto& device : right_joycons) {
if (is_handle_active(device)) {
return device;
}
}
}
if (type == Joycon::ControllerType::Pro || type == Joycon::ControllerType::Grip) {
for (const auto& device : pro_joycons) {
if (is_handle_active(device)) {
return device;
}
}
}
return nullptr;
}
PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const {
return {
.guid = Common::UUID{Common::InvalidUUID},
.port = port,
.pad = static_cast<std::size_t>(type),
};
}
std::vector<Common::ParamPackage> Joycons::GetInputDevices() const {
std::vector<Common::ParamPackage> devices{};
auto add_entry = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
if (!device) {
return;
}
if (!device->IsConnected()) {
return;
}
std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()),
device->GetDevicePort() + 1);
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"port", std::to_string(device->GetDevicePort())},
{"pad", std::to_string(static_cast<std::size_t>(device->GetHandleDeviceType()))},
});
};
for (const auto& controller : left_joycons) {
add_entry(controller);
}
for (const auto& controller : right_joycons) {
add_entry(controller);
}
for (const auto& controller : pro_joycons) {
add_entry(controller);
}
// 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;
}
constexpr auto type = Joycon::ControllerType::Dual;
std::string name = fmt::format("{} {}", JoyconName(type), i + 1);
devices.emplace_back(Common::ParamPackage{
{"engine", GetEngineName()},
{"display", std::move(name)},
{"port", std::to_string(i)},
{"pad", std::to_string(static_cast<std::size_t>(type))},
});
}
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) {
int pad = params.Get("pad", 0);
if (pad == static_cast<int>(Joycon::ControllerType::Dual)) {
pad = side ? static_cast<int>(Joycon::ControllerType::Right)
: static_cast<int>(Joycon::ControllerType::Left);
}
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("pad", 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)) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("pad", static_cast<int>(Joycon::ControllerType::Left));
Common::ParamPackage sl_button_params = button_params;
Common::ParamPackage sr_button_params = button_params;
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSL));
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSR));
mapping.insert_or_assign(Settings::NativeButton::SL, std::move(sl_button_params));
mapping.insert_or_assign(Settings::NativeButton::SR, std::move(sr_button_params));
}
// Map SL and SR buttons for right joycons
if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Right)) {
Common::ParamPackage button_params{};
button_params.Set("engine", GetEngineName());
button_params.Set("port", params.Get("port", 0));
button_params.Set("pad", static_cast<int>(Joycon::ControllerType::Right));
Common::ParamPackage sl_button_params = button_params;
Common::ParamPackage sr_button_params = button_params;
sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSL));
sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::RightSR));
mapping.insert_or_assign(Settings::NativeButton::SL, std::move(sl_button_params));
mapping.insert_or_assign(Settings::NativeButton::SR, std::move(sr_button_params));
}
return mapping;
}
AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
if (!params.Has("port")) {
return {};
}
int pad_left = params.Get("pad", 0);
int pad_right = pad_left;
if (pad_left == static_cast<int>(Joycon::ControllerType::Dual)) {
pad_left = static_cast<int>(Joycon::ControllerType::Left);
pad_right = static_cast<int>(Joycon::ControllerType::Right);
}
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("pad", 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;
right_analog_params.Set("engine", GetEngineName());
right_analog_params.Set("port", params.Get("port", 0));
right_analog_params.Set("pad", 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 {};
}
int pad_left = params.Get("pad", 0);
int pad_right = pad_left;
if (pad_left == static_cast<int>(Joycon::ControllerType::Dual)) {
pad_left = static_cast<int>(Joycon::ControllerType::Left);
pad_right = static_cast<int>(Joycon::ControllerType::Right);
}
MotionMapping mapping = {};
Common::ParamPackage left_motion_params;
left_motion_params.Set("engine", GetEngineName());
left_motion_params.Set("port", params.Get("port", 0));
left_motion_params.Set("pad", 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;
right_Motion_params.Set("engine", GetEngineName());
right_Motion_params.Set("port", params.Get("port", 0));
right_Motion_params.Set("pad", pad_right);
right_Motion_params.Set("motion", 1);
mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_Motion_params));
return mapping;
}
Common::Input::ButtonNames Joycons::GetUIButtonName(const Common::ParamPackage& params) const {
const auto button = static_cast<Joycon::PadButton>(params.Get("button", 0));
switch (button) {
case Joycon::PadButton::Left:
return Common::Input::ButtonNames::ButtonLeft;
case Joycon::PadButton::Right:
return Common::Input::ButtonNames::ButtonRight;
case Joycon::PadButton::Down:
return Common::Input::ButtonNames::ButtonDown;
case Joycon::PadButton::Up:
return Common::Input::ButtonNames::ButtonUp;
case Joycon::PadButton::LeftSL:
case Joycon::PadButton::RightSL:
return Common::Input::ButtonNames::TriggerSL;
case Joycon::PadButton::LeftSR:
case Joycon::PadButton::RightSR:
return Common::Input::ButtonNames::TriggerSR;
case Joycon::PadButton::L:
return Common::Input::ButtonNames::TriggerL;
case Joycon::PadButton::R:
return Common::Input::ButtonNames::TriggerR;
case Joycon::PadButton::ZL:
return Common::Input::ButtonNames::TriggerZL;
case Joycon::PadButton::ZR:
return Common::Input::ButtonNames::TriggerZR;
case Joycon::PadButton::A:
return Common::Input::ButtonNames::ButtonA;
case Joycon::PadButton::B:
return Common::Input::ButtonNames::ButtonB;
case Joycon::PadButton::X:
return Common::Input::ButtonNames::ButtonX;
case Joycon::PadButton::Y:
return Common::Input::ButtonNames::ButtonY;
case Joycon::PadButton::Plus:
return Common::Input::ButtonNames::ButtonPlus;
case Joycon::PadButton::Minus:
return Common::Input::ButtonNames::ButtonMinus;
case Joycon::PadButton::Home:
return Common::Input::ButtonNames::ButtonHome;
case Joycon::PadButton::Capture:
return Common::Input::ButtonNames::ButtonCapture;
case Joycon::PadButton::StickL:
return Common::Input::ButtonNames::ButtonStickL;
case Joycon::PadButton::StickR:
return Common::Input::ButtonNames::ButtonStickR;
default:
return Common::Input::ButtonNames::Undefined;
}
}
Common::Input::ButtonNames Joycons::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
return GetUIButtonName(params);
}
if (params.Has("axis")) {
return Common::Input::ButtonNames::Value;
}
if (params.Has("motion")) {
return Common::Input::ButtonNames::Engine;
}
return Common::Input::ButtonNames::Invalid;
}
std::string Joycons::JoyconName(Joycon::ControllerType type) const {
switch (type) {
case Joycon::ControllerType::Left:
return "Left Joycon";
case Joycon::ControllerType::Right:
return "Right Joycon";
case Joycon::ControllerType::Pro:
return "Pro Controller";
case Joycon::ControllerType::Grip:
return "Grip Controller";
case Joycon::ControllerType::Dual:
return "Dual Joycon";
default:
return "Unknow Joycon";
}
}
} // namespace InputCommon

107
src/input_common/drivers/joycon.h Executable file
View file

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <span>
#include <thread>
#include <SDL_hidapi.h>
#include "input_common/input_engine.h"
namespace InputCommon::Joycon {
using SerialNumber = std::array<u8, 15>;
struct Battery;
struct Color;
struct MotionData;
enum class ControllerType;
enum class DriverResult;
class JoyconDriver;
} // namespace InputCommon::Joycon
namespace InputCommon {
class Joycons final : public InputCommon::InputEngine {
public:
explicit Joycons(const std::string& input_engine_);
~Joycons();
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
Common::Input::VibrationError SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
void SetLeds(const PadIdentifier& identifier,
const Common::Input::LedStatus& led_status) override;
Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_,
Common::Input::CameraFormat camera_format) override;
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override;
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier_,
const std::vector<u8>& data) override;
Common::Input::PollingError SetPollingMode(
const PadIdentifier& identifier, const Common::Input::PollingMode polling_mode) override;
/// Used for automapping features
std::vector<Common::ParamPackage> GetInputDevices() const override;
ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
private:
static constexpr std::size_t MaxSupportedControllers = 8;
/// For shutting down, clear all data, join all threads, release usb devices
void Reset();
/// Registers controllers, clears all data and starts the scan thread
void Setup();
/// Actively searchs for new devices
void ScanThread(std::stop_token stop_token);
/// Returns true if device is valid and not registered
bool IsDeviceNew(SDL_hid_device_info* device_info) const;
/// Tries to connect to the new device
void RegisterNewDevice(SDL_hid_device_info* device_info);
/// Returns the next free handle
std::shared_ptr<Joycon::JoyconDriver> GetNextFreeHandle(Joycon::ControllerType type) const;
void OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, Joycon::Battery value);
void OnColorUpdate(std::size_t port, Joycon::ControllerType type, const Joycon::Color& value);
void OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value);
void OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value);
void OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
const Joycon::MotionData& value);
void OnRingConUpdate(f32 ring_data);
void OnAmiiboUpdate(std::size_t port, const std::vector<u8>& amiibo_data);
/// Returns a JoyconHandle corresponding to a PadIdentifier
std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const;
/// Returns a PadIdentifier corresponding to the port number
PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const;
std::string JoyconName(std::size_t port) const;
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
/// Returns the name of the device in text format
std::string JoyconName(Joycon::ControllerType type) const;
std::jthread scan_thread;
bool scan_thread_running{};
// Joycon types are split by type to ease supporting dualjoycon configurations
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> left_joycons{};
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> right_joycons{};
std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> pro_joycons{};
};
} // namespace InputCommon

View file

@ -318,6 +318,14 @@ void SDLDriver::InitJoystick(int joystick_index) {
const auto guid = GetGUID(sdl_joystick); const auto guid = GetGUID(sdl_joystick);
if (Settings::values.enable_joycon_driver) {
if (guid.uuid[5] == 0x05 && guid.uuid[4] == 0x7e) {
LOG_ERROR(Input, "Device black listed {}", joystick_index);
SDL_JoystickClose(sdl_joystick);
return;
}
}
std::scoped_lock lock{joystick_map_mutex}; std::scoped_lock lock{joystick_map_mutex};
if (joystick_map.find(guid) == joystick_map.end()) { if (joystick_map.find(guid) == joystick_map.end()) {
auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller); auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller);
@ -440,9 +448,14 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
// Use hidapi driver for joycons. This will allow joycons to be detected as a GameController and // Disable hidapi drivers for switch controllers when the custom joycon driver is enabled
// not a generic one if (Settings::values.enable_joycon_driver) {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "0");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0");
} else {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1");
}
// Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native // Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native
// driver on Linux. // driver on Linux.

View file

@ -0,0 +1,499 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "common/swap.h"
#include "common/thread.h"
#include "input_common/helpers/joycon_driver.h"
#include "input_common/helpers/joycon_protocol/calibration.h"
#include "input_common/helpers/joycon_protocol/generic_functions.h"
#include "input_common/helpers/joycon_protocol/nfc.h"
#include "input_common/helpers/joycon_protocol/poller.h"
#include "input_common/helpers/joycon_protocol/ringcon.h"
#include "input_common/helpers/joycon_protocol/rumble.h"
namespace InputCommon::Joycon {
JoyconDriver::JoyconDriver(std::size_t port_) : port{port_} {
hidapi_handle = std::make_shared<JoyconHandle>();
}
JoyconDriver::~JoyconDriver() {
Stop();
}
void JoyconDriver::Stop() {
is_connected = false;
input_thread = {};
}
DriverResult JoyconDriver::RequestDeviceAccess(SDL_hid_device_info* device_info) {
std::scoped_lock lock{mutex};
handle_device_type = ControllerType::None;
GetDeviceType(device_info, handle_device_type);
if (handle_device_type == ControllerType::None) {
return DriverResult::UnsupportedControllerType;
}
hidapi_handle->handle =
SDL_hid_open(device_info->vendor_id, device_info->product_id, device_info->serial_number);
std::memcpy(&handle_serial_number, device_info->serial_number, 15);
if (!hidapi_handle->handle) {
LOG_ERROR(Input, "Yuzu can't gain access to this device: ID {:04X}:{:04X}.",
device_info->vendor_id, device_info->product_id);
return DriverResult::HandleInUse;
}
SDL_hid_set_nonblocking(hidapi_handle->handle, 1);
return DriverResult::Success;
}
DriverResult JoyconDriver::InitializeDevice() {
if (!hidapi_handle->handle) {
return DriverResult::InvalidHandle;
}
std::scoped_lock lock{mutex};
disable_input_thread = true;
// Reset Counters
error_counter = 0;
hidapi_handle->packet_counter = 0;
// Reset external device status
starlink_connected = false;
ring_connected = false;
amiibo_detected = false;
// Set HW default configuration
vibration_enabled = true;
motion_enabled = true;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = false;
irs_enabled = false;
gyro_sensitivity = Joycon::GyroSensitivity::DPS2000;
gyro_performance = Joycon::GyroPerformance::HZ833;
accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8;
accelerometer_performance = Joycon::AccelerometerPerformance::HZ100;
// Initialize HW Protocols
calibration_protocol = std::make_unique<CalibrationProtocol>(hidapi_handle);
generic_protocol = std::make_unique<GenericProtocol>(hidapi_handle);
nfc_protocol = std::make_unique<NfcProtocol>(hidapi_handle);
ring_protocol = std::make_unique<RingConProtocol>(hidapi_handle);
rumble_protocol = std::make_unique<RumbleProtocol>(hidapi_handle);
// Get fixed joycon info
generic_protocol->GetVersionNumber(version);
generic_protocol->GetColor(color);
if (handle_device_type == ControllerType::Pro) {
// Some 3rd party controllers aren't pro controllers
generic_protocol->GetControllerType(device_type);
} else {
device_type = handle_device_type;
}
generic_protocol->GetSerialNumber(serial_number);
supported_features = GetSupportedFeatures();
// Get Calibration data
calibration_protocol->GetLeftJoyStickCalibration(left_stick_calibration);
calibration_protocol->GetRightJoyStickCalibration(right_stick_calibration);
calibration_protocol->GetImuCalibration(motion_calibration);
// Set led status
generic_protocol->SetLedBlinkPattern(static_cast<u8>(1 + port));
// Apply HW configuration
SetPollingMode();
// Initialize joycon poller
joycon_poller = std::make_unique<JoyconPoller>(device_type, left_stick_calibration,
right_stick_calibration, motion_calibration);
// Start pooling for data
is_connected = true;
if (!input_thread_running) {
input_thread =
std::jthread([this](std::stop_token stop_token) { InputThread(stop_token); });
}
disable_input_thread = false;
return DriverResult::Success;
}
void JoyconDriver::InputThread(std::stop_token stop_token) {
LOG_INFO(Input, "JC Adapter input thread started");
Common::SetCurrentThreadName("JoyconInput");
input_thread_running = true;
// Max update rate is 5ms, ensure we are always able to read a bit faster
constexpr int ThreadDelay = 2;
std::vector<u8> buffer(MaxBufferSize);
while (!stop_token.stop_requested()) {
int status = 0;
if (!IsInputThreadValid()) {
input_thread.request_stop();
continue;
}
// By disabling the input thread we can ensure custom commands will succeed as no package is
// skipped
if (!disable_input_thread) {
status = SDL_hid_read_timeout(hidapi_handle->handle, buffer.data(), buffer.size(),
ThreadDelay);
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(ThreadDelay));
}
if (IsPayloadCorrect(status, buffer)) {
OnNewData(buffer);
}
std::this_thread::yield();
}
is_connected = false;
input_thread_running = false;
LOG_INFO(Input, "JC Adapter input thread stopped");
}
void JoyconDriver::OnNewData(std::span<u8> buffer) {
const auto report_mode = static_cast<InputReport>(buffer[0]);
// Packages can be a litte bit inconsistent. Average the delta time to provide a smoother motion
// experience
switch (report_mode) {
case InputReport::STANDARD_FULL_60HZ:
case InputReport::NFC_IR_MODE_60HZ:
case InputReport::SIMPLE_HID_MODE: {
const auto now = std::chrono::steady_clock::now();
const auto new_delta_time = static_cast<u64>(
std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count());
delta_time = ((delta_time * 8) + (new_delta_time * 2)) / 10;
last_update = now;
joycon_poller->UpdateColor(color);
break;
}
default:
break;
}
const MotionStatus motion_status{
.is_enabled = motion_enabled,
.delta_time = delta_time,
.gyro_sensitivity = gyro_sensitivity,
.accelerometer_sensitivity = accelerometer_sensitivity,
};
// TODO: Remove this when calibration is properly loaded and not calculated
if (ring_connected && report_mode == InputReport::STANDARD_FULL_60HZ) {
InputReportActive data{};
memcpy(&data, buffer.data(), sizeof(InputReportActive));
calibration_protocol->GetRingCalibration(ring_calibration, data.ring_input);
}
const RingStatus ring_status{
.is_enabled = ring_connected,
.default_value = ring_calibration.default_value,
.max_value = ring_calibration.max_value,
.min_value = ring_calibration.min_value,
};
if (nfc_protocol->IsEnabled()) {
if (amiibo_detected) {
if (!nfc_protocol->HasAmiibo()) {
joycon_poller->updateAmiibo({});
amiibo_detected = false;
return;
}
}
if (!amiibo_detected) {
std::vector<u8> data(0x21C);
const auto result = nfc_protocol->ScanAmiibo(data);
if (result == DriverResult::Success) {
joycon_poller->updateAmiibo(data);
amiibo_detected = true;
}
}
}
switch (report_mode) {
case InputReport::STANDARD_FULL_60HZ:
joycon_poller->ReadActiveMode(buffer, motion_status, ring_status);
break;
case InputReport::NFC_IR_MODE_60HZ:
joycon_poller->ReadNfcIRMode(buffer, motion_status);
break;
case InputReport::SIMPLE_HID_MODE:
joycon_poller->ReadPassiveMode(buffer);
break;
case InputReport::SUBCMD_REPLY:
LOG_DEBUG(Input, "Unhandled command reply");
break;
default:
LOG_ERROR(Input, "Report mode not Implemented {}", report_mode);
break;
}
}
void JoyconDriver::SetPollingMode() {
disable_input_thread = true;
rumble_protocol->EnableRumble(vibration_enabled && supported_features.vibration);
if (motion_enabled && supported_features.motion) {
generic_protocol->EnableImu(true);
generic_protocol->SetImuConfig(gyro_sensitivity, gyro_performance,
accelerometer_sensitivity, accelerometer_performance);
} else {
generic_protocol->EnableImu(false);
}
if (nfc_protocol->IsEnabled()) {
amiibo_detected = false;
nfc_protocol->DisableNfc();
}
if (nfc_enabled && supported_features.nfc) {
auto result = nfc_protocol->EnableNfc();
if (result == DriverResult::Success) {
result = nfc_protocol->StartNFCPollingMode();
}
if (result == DriverResult::Success) {
disable_input_thread = false;
return;
}
nfc_protocol->DisableNfc();
LOG_ERROR(Input, "Error enabling NFC");
}
if (ring_protocol->IsEnabled()) {
ring_connected = false;
ring_protocol->DisableRingCon();
}
if (hidbus_enabled && supported_features.hidbus) {
auto result = ring_protocol->EnableRingCon();
if (result == DriverResult::Success) {
result = ring_protocol->StartRingconPolling();
}
if (result == DriverResult::Success) {
ring_connected = true;
disable_input_thread = false;
return;
}
ring_connected = false;
ring_protocol->DisableRingCon();
LOG_ERROR(Input, "Error enabling Ringcon");
}
if (passive_enabled && supported_features.passive) {
const auto result = generic_protocol->EnablePassiveMode();
if (result == DriverResult::Success) {
disable_input_thread = false;
return;
}
LOG_ERROR(Input, "Error enabling passive mode");
}
// Default Mode
const auto result = generic_protocol->EnableActiveMode();
if (result != DriverResult::Success) {
LOG_ERROR(Input, "Error enabling active mode");
}
disable_input_thread = false;
}
JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() {
SupportedFeatures features{
.passive = true,
.motion = true,
.vibration = true,
};
if (device_type == ControllerType::Right) {
features.nfc = true;
features.irs = true;
features.hidbus = true;
}
if (device_type == ControllerType::Pro) {
features.nfc = true;
}
return features;
}
bool JoyconDriver::IsInputThreadValid() const {
if (!is_connected) {
return false;
}
if (hidapi_handle->handle == nullptr) {
return false;
}
// Controller is not responding. Terminate connection
if (error_counter > MaxErrorCount) {
return false;
}
return true;
}
bool JoyconDriver::IsPayloadCorrect(int status, std::span<const u8> buffer) {
if (status <= -1) {
error_counter++;
return false;
}
// There's no new data
if (status == 0) {
return false;
}
// No reply ever starts with zero
if (buffer[0] == 0x00) {
error_counter++;
return false;
}
error_counter = 0;
return true;
}
DriverResult JoyconDriver::SetVibration(const VibrationValue& vibration) {
std::scoped_lock lock{mutex};
if (disable_input_thread) {
return DriverResult::HandleInUse;
}
return rumble_protocol->SendVibration(vibration);
}
DriverResult JoyconDriver::SetLedConfig(u8 led_pattern) {
std::scoped_lock lock{mutex};
if (disable_input_thread) {
return DriverResult::HandleInUse;
}
return generic_protocol->SetLedPattern(led_pattern);
}
DriverResult JoyconDriver::SetPasiveMode() {
std::scoped_lock lock{mutex};
motion_enabled = false;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = true;
SetPollingMode();
return DriverResult::Success;
}
DriverResult JoyconDriver::SetActiveMode() {
std::scoped_lock lock{mutex};
motion_enabled = true;
hidbus_enabled = false;
nfc_enabled = false;
passive_enabled = false;
SetPollingMode();
return DriverResult::Success;
}
DriverResult JoyconDriver::SetNfcMode() {
std::scoped_lock lock{mutex};
motion_enabled = true;
hidbus_enabled = false;
nfc_enabled = true;
passive_enabled = false;
SetPollingMode();
return DriverResult::Success;
}
DriverResult JoyconDriver::SetRingConMode() {
std::scoped_lock lock{mutex};
motion_enabled = true;
hidbus_enabled = true;
nfc_enabled = false;
passive_enabled = false;
SetPollingMode();
return DriverResult::Success;
}
bool JoyconDriver::IsConnected() const {
std::scoped_lock lock{mutex};
return is_connected;
}
bool JoyconDriver::IsVibrationEnabled() const {
std::scoped_lock lock{mutex};
return vibration_enabled;
}
FirmwareVersion JoyconDriver::GetDeviceVersion() const {
std::scoped_lock lock{mutex};
return version;
}
Color JoyconDriver::GetDeviceColor() const {
std::scoped_lock lock{mutex};
return color;
}
std::size_t JoyconDriver::GetDevicePort() const {
std::scoped_lock lock{mutex};
return port;
}
ControllerType JoyconDriver::GetDeviceType() const {
std::scoped_lock lock{mutex};
return 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 Joycon::JoyconCallbacks& callbacks) {
joycon_poller->SetCallbacks(callbacks);
}
Joycon::DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info,
ControllerType& controller_type) {
static constexpr std::array<std::pair<u32, Joycon::ControllerType>, 4> supported_devices{
std::pair<u32, Joycon::ControllerType>{0x2006, Joycon::ControllerType::Left},
{0x2007, Joycon::ControllerType::Right},
{0x2009, Joycon::ControllerType::Pro},
{0x200E, Joycon::ControllerType::Grip},
};
constexpr u16 nintendo_vendor_id = 0x057e;
controller_type = Joycon::ControllerType::None;
if (device_info->vendor_id != nintendo_vendor_id) {
return Joycon::DriverResult::UnsupportedControllerType;
}
for (const auto& [product_id, type] : supported_devices) {
if (device_info->product_id == static_cast<u16>(product_id)) {
controller_type = type;
return Joycon::DriverResult::Success;
}
}
return Joycon::DriverResult::UnsupportedControllerType;
}
Joycon::DriverResult JoyconDriver::GetSerialNumber(SDL_hid_device_info* device_info,
Joycon::SerialNumber& serial_number) {
if (device_info->serial_number == nullptr) {
return Joycon::DriverResult::Unknown;
}
std::memcpy(&serial_number, device_info->serial_number, 15);
return Joycon::DriverResult::Success;
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,144 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <functional>
#include <mutex>
#include <span>
#include <thread>
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
class CalibrationProtocol;
class GenericProtocol;
class NfcProtocol;
class JoyconPoller;
class RingConProtocol;
class RumbleProtocol;
class JoyconDriver final {
public:
explicit JoyconDriver(std::size_t port_);
~JoyconDriver();
DriverResult RequestDeviceAccess(SDL_hid_device_info* device_info);
DriverResult InitializeDevice();
void Stop();
bool IsConnected() const;
bool IsVibrationEnabled() const;
FirmwareVersion GetDeviceVersion() const;
Color GetDeviceColor() const;
std::size_t GetDevicePort() const;
ControllerType GetDeviceType() const;
ControllerType GetHandleDeviceType() const;
SerialNumber GetSerialNumber() const;
SerialNumber GetHandleSerialNumber() const;
DriverResult SetVibration(const VibrationValue& vibration);
DriverResult SetLedConfig(u8 led_pattern);
DriverResult SetPasiveMode();
DriverResult SetActiveMode();
DriverResult SetNfcMode();
DriverResult SetRingConMode();
void SetCallbacks(const Joycon::JoyconCallbacks& callbacks);
// Returns device type from hidapi handle
static Joycon::DriverResult GetDeviceType(SDL_hid_device_info* device_info,
Joycon::ControllerType& controller_type);
// Returns serial number from hidapi handle
static Joycon::DriverResult GetSerialNumber(SDL_hid_device_info* device_info,
Joycon::SerialNumber& serial_number);
private:
struct SupportedFeatures {
bool passive{};
bool hidbus{};
bool irs{};
bool motion{};
bool nfc{};
bool vibration{};
};
/// Main thread, actively request new data from the handle
void InputThread(std::stop_token stop_token);
/// Called everytime a valid package arrives
void OnNewData(std::span<u8> buffer);
/// Updates device configuration to enable or disable features
void SetPollingMode();
/// Returns true if input thread is valid and doesn't need to be stopped
bool IsInputThreadValid() const;
/// Returns true if the data should be interpreted. Otherwise the error counter is incremented
bool IsPayloadCorrect(int status, std::span<const u8> buffer);
/// Returns a list of supported features that can be enabled on this device
SupportedFeatures GetSupportedFeatures();
// Protocol Features
std::unique_ptr<CalibrationProtocol> calibration_protocol;
std::unique_ptr<GenericProtocol> generic_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
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{};
// Harware configuration
u8 leds{};
ReportMode mode{};
bool passive_enabled{}; // Low power mode, Ideal for multiple controllers at the same time
bool hidbus_enabled{}; // External device support
bool irs_enabled{}; // Infrared camera input
bool motion_enabled{}; // Enables motion input
bool nfc_enabled{}; // Enables Amiibo detection
bool vibration_enabled{}; // Allows vibrations
// Calibration data
GyroSensitivity gyro_sensitivity{};
GyroPerformance gyro_performance{};
AccelerometerSensitivity accelerometer_sensitivity{};
AccelerometerPerformance accelerometer_performance{};
JoyStickCalibration left_stick_calibration{};
JoyStickCalibration right_stick_calibration{};
MotionCalibration motion_calibration{};
RingCalibration ring_calibration{};
// Fixed joycon info
FirmwareVersion version{};
Color color{};
std::size_t port{};
ControllerType device_type{}; // Device type reported by controller
ControllerType handle_device_type{}; // Device type reported by hidapi
SerialNumber serial_number{}; // Serial number reported by controller
SerialNumber handle_serial_number{}; // Serial number type reported by hidapi
SupportedFeatures supported_features{};
// Thread related
mutable std::mutex mutex;
std::jthread input_thread;
bool input_thread_running{};
bool disable_input_thread{};
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,187 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstring>
#include "input_common/helpers/joycon_protocol/calibration.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
CalibrationProtocol::CalibrationProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult CalibrationProtocol::GetLeftJoyStickCalibration(JoyStickCalibration& calibration) {
std::vector<u8> buffer;
DriverResult result{DriverResult::Success};
calibration = {};
SetBlocking();
result = ReadSPI(CalAddr::USER_LEFT_MAGIC, sizeof(u16), buffer);
if (result == DriverResult::Success) {
const bool has_user_calibration = buffer[0] == 0xB2 && buffer[1] == 0xA1;
if (has_user_calibration) {
result = ReadSPI(CalAddr::USER_LEFT_DATA, 9, buffer);
} else {
result = ReadSPI(CalAddr::FACT_LEFT_DATA, 9, buffer);
}
}
if (result == DriverResult::Success) {
calibration.x.max = static_cast<u16>(((buffer[1] & 0x0F) << 8) | buffer[0]);
calibration.y.max = static_cast<u16>((buffer[2] << 4) | (buffer[1] >> 4));
calibration.x.center = static_cast<u16>(((buffer[4] & 0x0F) << 8) | buffer[3]);
calibration.y.center = static_cast<u16>((buffer[5] << 4) | (buffer[4] >> 4));
calibration.x.min = static_cast<u16>(((buffer[7] & 0x0F) << 8) | buffer[6]);
calibration.y.min = static_cast<u16>((buffer[8] << 4) | (buffer[7] >> 4));
}
// Nintendo fix for drifting stick
// result = ReadSPI(0x60, 0x86 ,buffer, 16);
// calibration.deadzone = (u16)((buffer[4] << 8) & 0xF00 | buffer[3]);
// Set a valid default calibration if data is missing
ValidateCalibration(calibration);
SetNonBlocking();
return result;
}
DriverResult CalibrationProtocol::GetRightJoyStickCalibration(JoyStickCalibration& calibration) {
std::vector<u8> buffer;
DriverResult result{DriverResult::Success};
calibration = {};
SetBlocking();
result = ReadSPI(CalAddr::USER_RIGHT_MAGIC, sizeof(u16), buffer);
if (result == DriverResult::Success) {
const bool has_user_calibration = buffer[0] == 0xB2 && buffer[1] == 0xA1;
if (has_user_calibration) {
result = ReadSPI(CalAddr::USER_RIGHT_DATA, 9, buffer);
} else {
result = ReadSPI(CalAddr::FACT_RIGHT_DATA, 9, buffer);
}
}
if (result == DriverResult::Success) {
calibration.x.center = static_cast<u16>(((buffer[1] & 0x0F) << 8) | buffer[0]);
calibration.y.center = static_cast<u16>((buffer[2] << 4) | (buffer[1] >> 4));
calibration.x.min = static_cast<u16>(((buffer[4] & 0x0F) << 8) | buffer[3]);
calibration.y.min = static_cast<u16>((buffer[5] << 4) | (buffer[4] >> 4));
calibration.x.max = static_cast<u16>(((buffer[7] & 0x0F) << 8) | buffer[6]);
calibration.y.max = static_cast<u16>((buffer[8] << 4) | (buffer[7] >> 4));
}
// Nintendo fix for drifting stick
// buffer = ReadSPI(0x60, 0x98 , 16);
// joystick.deadzone = (u16)((buffer[4] << 8) & 0xF00 | buffer[3]);
// Set a valid default calibration if data is missing
ValidateCalibration(calibration);
SetNonBlocking();
return result;
}
DriverResult CalibrationProtocol::GetImuCalibration(MotionCalibration& calibration) {
std::vector<u8> buffer;
DriverResult result{DriverResult::Success};
calibration = {};
SetBlocking();
result = ReadSPI(CalAddr::USER_IMU_MAGIC, sizeof(u16), buffer);
if (result == DriverResult::Success) {
const bool has_user_calibration = buffer[0] == 0xB2 && buffer[1] == 0xA1;
if (has_user_calibration) {
result = ReadSPI(CalAddr::USER_IMU_DATA, sizeof(IMUCalibration), buffer);
} else {
result = ReadSPI(CalAddr::FACT_IMU_DATA, sizeof(IMUCalibration), buffer);
}
}
if (result == DriverResult::Success) {
IMUCalibration device_calibration{};
memcpy(&device_calibration, buffer.data(), sizeof(IMUCalibration));
calibration.accelerometer[0].offset = device_calibration.accelerometer_offset[0];
calibration.accelerometer[1].offset = device_calibration.accelerometer_offset[1];
calibration.accelerometer[2].offset = device_calibration.accelerometer_offset[2];
calibration.accelerometer[0].scale = device_calibration.accelerometer_scale[0];
calibration.accelerometer[1].scale = device_calibration.accelerometer_scale[1];
calibration.accelerometer[2].scale = device_calibration.accelerometer_scale[2];
calibration.gyro[0].offset = device_calibration.gyroscope_offset[0];
calibration.gyro[1].offset = device_calibration.gyroscope_offset[1];
calibration.gyro[2].offset = device_calibration.gyroscope_offset[2];
calibration.gyro[0].scale = device_calibration.gyroscope_scale[0];
calibration.gyro[1].scale = device_calibration.gyroscope_scale[1];
calibration.gyro[2].scale = device_calibration.gyroscope_scale[2];
}
ValidateCalibration(calibration);
SetNonBlocking();
return result;
}
DriverResult CalibrationProtocol::GetRingCalibration(RingCalibration& calibration,
s16 current_value) {
// TODO: Get default calibration form ring itself
if (ring_data_max == 0 && ring_data_min == 0) {
ring_data_max = current_value + 800;
ring_data_min = current_value - 800;
ring_data_default = current_value;
}
ring_data_max = std::max(ring_data_max, current_value);
ring_data_min = std::min(ring_data_min, current_value);
calibration = {
.default_value = ring_data_default,
.max_value = ring_data_max,
.min_value = ring_data_min,
};
return DriverResult::Success;
}
void CalibrationProtocol::ValidateCalibration(JoyStickCalibration& calibration) {
constexpr u16 DefaultStickCenter{2048};
constexpr u16 DefaultStickRange{1740};
if (calibration.x.center == 0xFFF || calibration.x.center == 0) {
calibration.x.center = DefaultStickCenter;
}
if (calibration.x.max == 0xFFF || calibration.x.max == 0) {
calibration.x.max = DefaultStickRange;
}
if (calibration.x.min == 0xFFF || calibration.x.min == 0) {
calibration.x.min = DefaultStickRange;
}
if (calibration.y.center == 0xFFF || calibration.y.center == 0) {
calibration.y.center = DefaultStickCenter;
}
if (calibration.y.max == 0xFFF || calibration.y.max == 0) {
calibration.y.max = DefaultStickRange;
}
if (calibration.y.min == 0xFFF || calibration.y.min == 0) {
calibration.y.min = DefaultStickRange;
}
}
void CalibrationProtocol::ValidateCalibration(MotionCalibration& calibration) {
for (auto& sensor : calibration.accelerometer) {
if (sensor.scale == 0) {
sensor.scale = 0x4000;
}
}
for (auto& sensor : calibration.gyro) {
if (sensor.scale == 0) {
sensor.scale = 0x3be7;
}
}
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
namespace InputCommon::Joycon {
enum class DriverResult;
struct JoyStickCalibration;
struct IMUCalibration;
struct JoyconHandle;
} // namespace InputCommon::Joycon
namespace InputCommon::Joycon {
/// Driver functions related to retrieving calibration data from the device
class CalibrationProtocol final : private JoyconCommonProtocol {
public:
explicit CalibrationProtocol(std::shared_ptr<JoyconHandle> handle);
/**
* Sends a request to obtain the left stick calibration from memory
* @param is_factory_calibration if true factory values will be returned
* @returns JoyStickCalibration of the left joystick
*/
DriverResult GetLeftJoyStickCalibration(JoyStickCalibration& calibration);
/**
* Sends a request to obtain the right stick calibration from memory
* @param is_factory_calibration if true factory values will be returned
* @returns JoyStickCalibration of the right joystick
*/
DriverResult GetRightJoyStickCalibration(JoyStickCalibration& calibration);
/**
* Sends a request to obtain the motion calibration from memory
* @returns ImuCalibration of the motion sensor
*/
DriverResult GetImuCalibration(MotionCalibration& calibration);
/**
* Calculates on run time the proper calibration of the ring controller
* @returns RingCalibration of the ring sensor
*/
DriverResult GetRingCalibration(RingCalibration& calibration, s16 current_value);
private:
void ValidateCalibration(JoyStickCalibration& calibration);
void ValidateCalibration(MotionCalibration& calibration);
s16 ring_data_max = 0;
s16 ring_data_default = 0;
s16 ring_data_min = 0;
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,286 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/common_protocol.h"
namespace InputCommon::Joycon {
JoyconCommonProtocol::JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_)
: hidapi_handle{std::move(hidapi_handle_)} {}
u8 JoyconCommonProtocol::GetCounter() {
hidapi_handle->packet_counter = (hidapi_handle->packet_counter + 1) & 0x0F;
return hidapi_handle->packet_counter;
}
void JoyconCommonProtocol::SetBlocking() {
SDL_hid_set_nonblocking(hidapi_handle->handle, 0);
}
void JoyconCommonProtocol::SetNonBlocking() {
SDL_hid_set_nonblocking(hidapi_handle->handle, 1);
}
DriverResult JoyconCommonProtocol::GetDeviceType(ControllerType& controller_type) {
std::vector<u8> buffer;
const auto result = ReadSPI(CalAddr::DEVICE_TYPE, 1, buffer);
controller_type = ControllerType::None;
if (result == DriverResult::Success) {
controller_type = static_cast<ControllerType>(buffer[0]);
// Fallback to 3rd party pro controllers
if (controller_type == ControllerType::None) {
controller_type = ControllerType::Pro;
}
}
return result;
}
DriverResult JoyconCommonProtocol::CheckDeviceAccess(SDL_hid_device_info* device_info) {
ControllerType controller_type{ControllerType::None};
const auto result = GetDeviceType(controller_type);
if (result != DriverResult::Success || controller_type == ControllerType::None) {
return DriverResult::UnsupportedControllerType;
}
hidapi_handle->handle =
SDL_hid_open(device_info->vendor_id, device_info->product_id, device_info->serial_number);
if (!hidapi_handle->handle) {
LOG_ERROR(Input, "Yuzu can't gain access to this device: ID {:04X}:{:04X}.",
device_info->vendor_id, device_info->product_id);
return DriverResult::HandleInUse;
}
SetNonBlocking();
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::SetReportMode(ReportMode report_mode) {
const std::vector<u8> buffer{static_cast<u8>(report_mode)};
std::vector<u8> output;
return SendSubCommand(SubCommand::SET_REPORT_MODE, buffer, output);
}
DriverResult JoyconCommonProtocol::SendData(std::span<const u8> buffer) {
const auto result = SDL_hid_write(hidapi_handle->handle, buffer.data(), buffer.size());
if (result == -1) {
return DriverResult::ErrorWritingData;
}
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::GetSubCommandResponse(SubCommand sc, std::vector<u8>& output) {
constexpr int timeout_mili = 100;
constexpr int MaxTries = 10;
int tries = 0;
output.resize(MaxSubCommandResponseSize);
do {
int result = SDL_hid_read_timeout(hidapi_handle->handle, output.data(),
MaxSubCommandResponseSize, timeout_mili);
if (result < 1) {
LOG_ERROR(Input, "No response from joycon");
}
if (tries++ > MaxTries) {
return DriverResult::Timeout;
}
} while (output[0] != 0x21 && output[14] != static_cast<u8>(sc));
if (output[0] != 0x21 && output[14] != static_cast<u8>(sc)) {
return DriverResult::WrongReply;
}
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const u8> buffer,
std::vector<u8>& output) {
std::vector<u8> local_buffer(MaxResponseSize);
local_buffer[0] = static_cast<u8>(OutputReport::RUMBLE_AND_SUBCMD);
local_buffer[1] = GetCounter();
local_buffer[10] = static_cast<u8>(sc);
for (std::size_t i = 0; i < buffer.size(); ++i) {
local_buffer[11 + i] = buffer[i];
}
auto result = SendData(local_buffer);
if (result != DriverResult::Success) {
return result;
}
result = GetSubCommandResponse(sc, output);
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::SendVibrationReport(std::span<const u8> buffer) {
std::vector<u8> local_buffer(MaxResponseSize);
local_buffer[0] = static_cast<u8>(Joycon::OutputReport::RUMBLE_ONLY);
local_buffer[1] = GetCounter();
memcpy(local_buffer.data() + 2, buffer.data(), buffer.size());
return SendData(local_buffer);
}
DriverResult JoyconCommonProtocol::ReadSPI(CalAddr addr, u8 size, std::vector<u8>& output) {
constexpr std::size_t MaxTries = 10;
std::size_t tries = 0;
std::vector<u8> buffer = {0x00, 0x00, 0x00, 0x00, size};
std::vector<u8> local_buffer(size + 20);
buffer[0] = static_cast<u8>(static_cast<u16>(addr) & 0x00FF);
buffer[1] = static_cast<u8>((static_cast<u16>(addr) & 0xFF00) >> 8);
do {
const auto result = SendSubCommand(SubCommand::SPI_FLASH_READ, buffer, local_buffer);
if (result != DriverResult::Success) {
return result;
}
if (tries++ > MaxTries) {
return DriverResult::Timeout;
}
} while (local_buffer[15] != buffer[0] || local_buffer[16] != buffer[1]);
// Remove header from output
output = std::vector<u8>(local_buffer.begin() + 20, local_buffer.begin() + 20 + size);
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::EnableMCU(bool enable) {
std::vector<u8> output;
const std::vector<u8> mcu_state{static_cast<u8>(enable ? 1 : 0)};
const auto result = SendSubCommand(SubCommand::SET_MCU_STATE, mcu_state, output);
if (result != DriverResult::Success) {
LOG_ERROR(Input, "SendMCUData failed with error {}", result);
}
return result;
}
DriverResult JoyconCommonProtocol::ConfigureMCU(const MCUConfig& config) {
LOG_DEBUG(Input, "ConfigureMCU");
std::vector<u8> output;
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, output);
if (result != DriverResult::Success) {
LOG_ERROR(Input, "Set MCU config failed with error {}", result);
}
return result;
}
DriverResult JoyconCommonProtocol::GetMCUDataResponse(ReportMode report_mode_,
std::vector<u8>& output) {
const int report_mode = static_cast<u8>(report_mode_);
constexpr int TimeoutMili = 200;
constexpr int MaxTries = 9;
int tries = 0;
output.resize(0x170);
do {
int result = SDL_hid_read_timeout(hidapi_handle->handle, output.data(), 0x170, TimeoutMili);
if (result < 1) {
LOG_ERROR(Input, "No response from joycon attempt {}", tries);
}
if (tries++ > MaxTries) {
return DriverResult::Timeout;
}
} while (output[0] != report_mode || output[49] == 0xFF);
if (output[0] != report_mode || output[49] == 0xFF) {
return DriverResult::WrongReply;
}
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::SendMCUData(ReportMode report_mode, SubCommand sc,
std::span<const u8> buffer,
std::vector<u8>& output) {
std::vector<u8> local_buffer(MaxResponseSize);
local_buffer[0] = static_cast<u8>(OutputReport::MCU_DATA);
local_buffer[1] = GetCounter();
local_buffer[9] = static_cast<u8>(sc);
for (std::size_t i = 0; i < buffer.size(); ++i) {
local_buffer[10 + i] = buffer[i];
}
auto result = SendData(local_buffer);
if (result != DriverResult::Success) {
return result;
}
result = GetMCUDataResponse(report_mode, output);
return DriverResult::Success;
}
DriverResult JoyconCommonProtocol::WaitSetMCUMode(ReportMode report_mode, MCUMode mode) {
std::vector<u8> output;
constexpr std::size_t MaxTries{8};
std::size_t tries{};
do {
const std::vector<u8> mcu_data{static_cast<u8>(MCUMode::Standby)};
const auto result = SendMCUData(report_mode, SubCommand::STATE, mcu_data, output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ > MaxTries) {
return DriverResult::WrongReply;
}
} while (output[49] != 1 || output[56] != static_cast<u8>(mode));
return DriverResult::Success;
}
// crc-8-ccitt / polynomial 0x07 look up table
constexpr std::array<u8, 256> mcu_crc8_table = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3};
u8 JoyconCommonProtocol::CalculateMCU_CRC8(u8* buffer, u8 size) const {
u8 crc8 = 0x0;
for (int i = 0; i < size; ++i) {
crc8 = mcu_crc8_table[static_cast<u8>(crc8 ^ buffer[i])];
}
return crc8;
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,146 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <memory>
#include <span>
#include <vector>
#include "common/common_types.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
/// Joycon driver functions that handle low level communication
class JoyconCommonProtocol {
public:
explicit JoyconCommonProtocol(std::shared_ptr<JoyconHandle> hidapi_handle_);
/**
* Sets handle to blocking. In blocking mode, SDL_hid_read() will wait (block) until there is
* data to read before returning.
*/
void SetBlocking();
/**
* Sets handle to non blocking. In non-blocking mode calls to SDL_hid_read() will return
* immediately with a value of 0 if there is no data to be read
*/
void SetNonBlocking();
/**
* Sends a request to obtain the joycon type from device
* @returns controller type of the joycon
*/
DriverResult GetDeviceType(ControllerType& controller_type);
/**
* Verifies and sets the joycon_handle if device is valid
* @param device info from the driver
* @returns success if the device is valid
*/
DriverResult CheckDeviceAccess(SDL_hid_device_info* device);
/**
* Sends a request to set the polling mode of the joycon
* @param report_mode polling mode to be set
*/
DriverResult SetReportMode(Joycon::ReportMode report_mode);
/**
* Sends data to the joycon device
* @param buffer data to be send
*/
DriverResult SendData(std::span<const u8> buffer);
/**
* Waits for incoming data of the joycon device that matchs the subcommand
* @param sub_command type of data to be returned
* @returns a buffer containing the responce
*/
DriverResult GetSubCommandResponse(SubCommand sub_command, std::vector<u8>& output);
/**
* Sends a sub command to the device and waits for it's reply
* @param sc sub command to be send
* @param buffer data to be send
* @returns output buffer containing the responce
*/
DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer, std::vector<u8>& output);
/**
* Sends vibration data to the joycon
* @param buffer data to be send
*/
DriverResult SendVibrationReport(std::span<const u8> buffer);
/**
* Reads the SPI memory stored on the joycon
* @param Initial address location
* @param size in bytes to be read
* @returns output buffer containing the responce
*/
DriverResult ReadSPI(CalAddr addr, u8 size, std::vector<u8>& output);
/**
* Enables MCU chip on the joycon
* @param enable if true the chip will be enabled
*/
DriverResult EnableMCU(bool enable);
/**
* Configures the MCU to the correspoinding mode
* @param MCUConfig configuration
*/
DriverResult ConfigureMCU(const MCUConfig& config);
/**
* Waits until there's MCU data available. On timeout returns error
* @param report mode of the expected reply
* @returns a buffer containing the responce
*/
DriverResult GetMCUDataResponse(ReportMode report_mode_, std::vector<u8>& output);
/**
* Sends data to the MCU chip and waits for it's reply
* @param report mode of the expected reply
* @param sub command to be send
* @param buffer data to be send
* @returns output buffer containing the responce
*/
DriverResult SendMCUData(ReportMode report_mode, SubCommand sc, std::span<const u8> buffer,
std::vector<u8>& output);
/**
* Wait's until the MCU chip is on the specified mode
* @param report mode of the expected reply
* @param MCUMode configuration
*/
DriverResult WaitSetMCUMode(ReportMode report_mode, MCUMode mode);
/**
* Calculates the checksum from the MCU data
* @param buffer containing the data to be send
* @param size of the buffer in bytes
* @returns byte with the correct checksum
*/
u8 CalculateMCU_CRC8(u8* buffer, u8 size) const;
private:
/**
* Increments and returns the packet counter of the handle
* @param joycon_handle device to send the data
* @returns packet counter value
*/
u8 GetCounter();
std::shared_ptr<JoyconHandle> hidapi_handle;
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/generic_functions.h"
namespace InputCommon::Joycon {
GenericProtocol::GenericProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult GenericProtocol::EnablePassiveMode() {
SetBlocking();
const auto result = SetReportMode(ReportMode::SIMPLE_HID_MODE);
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::EnableActiveMode() {
SetBlocking();
const auto result = SetReportMode(ReportMode::STANDARD_FULL_60HZ);
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::GetDeviceInfo(DeviceInfo& device_info) {
std::vector<u8> output;
SetBlocking();
const auto result = SendSubCommand(SubCommand::REQ_DEV_INFO, {}, output);
device_info = {};
if (result == DriverResult::Success) {
memcpy(&device_info, output.data(), sizeof(DeviceInfo));
}
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::GetControllerType(ControllerType& controller_type) {
return GetDeviceType(controller_type);
}
DriverResult GenericProtocol::EnableImu(bool enable) {
const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)};
std::vector<u8> output;
SetBlocking();
const auto result = SendSubCommand(SubCommand::ENABLE_IMU, buffer, output);
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec,
AccelerometerSensitivity asen,
AccelerometerPerformance afrec) {
const std::array<u8, 4> buffer{static_cast<u8>(gsen), static_cast<u8>(asen),
static_cast<u8>(gfrec), static_cast<u8>(afrec)};
std::vector<u8> output;
SetBlocking();
const auto result = SendSubCommand(SubCommand::SET_IMU_SENSITIVITY, buffer, output);
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::GetBattery(u32& battery_level) {
battery_level = 0;
return DriverResult::NotSupported;
}
DriverResult GenericProtocol::GetColor(Color& color) {
std::vector<u8> buffer;
SetBlocking();
const auto result = ReadSPI(CalAddr::COLOR_DATA, 12, buffer);
SetNonBlocking();
color = {};
if (result == DriverResult::Success) {
color.body = static_cast<u32>((buffer[0] << 16) | (buffer[1] << 8) | buffer[2]);
color.buttons = static_cast<u32>((buffer[3] << 16) | (buffer[4] << 8) | buffer[5]);
color.left_grip = static_cast<u32>((buffer[6] << 16) | (buffer[7] << 8) | buffer[8]);
color.right_grip = static_cast<u32>((buffer[9] << 16) | (buffer[10] << 8) | buffer[11]);
}
return result;
}
DriverResult GenericProtocol::GetSerialNumber(SerialNumber& serial_number) {
std::vector<u8> buffer;
SetBlocking();
const auto result = ReadSPI(CalAddr::SERIAL_NUMBER, 16, buffer);
SetNonBlocking();
serial_number = {};
if (result == DriverResult::Success) {
memcpy(serial_number.data(), buffer.data() + 1, sizeof(SerialNumber));
}
return result;
}
DriverResult GenericProtocol::GetTemperature(u32& temperature) {
// Not all devices have temperature sensor
temperature = 25;
return DriverResult::NotSupported;
}
DriverResult GenericProtocol::GetVersionNumber(FirmwareVersion& version) {
DeviceInfo device_info{};
const auto result = GetDeviceInfo(device_info);
version = device_info.firmware;
return result;
}
DriverResult GenericProtocol::SetHomeLight() {
static constexpr std::array<u8, 3> buffer{0x0f, 0xf0, 0x00};
std::vector<u8> output;
SetBlocking();
const auto result = SendSubCommand(SubCommand::SET_HOME_LIGHT, buffer, output);
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::SetLedBusy() {
return DriverResult::NotSupported;
}
DriverResult GenericProtocol::SetLedPattern(u8 leds) {
const std::array<u8, 1> buffer{leds};
std::vector<u8> output;
SetBlocking();
const auto result = SendSubCommand(SubCommand::SET_PLAYER_LIGHTS, buffer, output);
SetNonBlocking();
return result;
}
DriverResult GenericProtocol::SetLedBlinkPattern(u8 leds) {
return SetLedPattern(static_cast<u8>(leds << 4));
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
/// Joycon driver functions that easily implemented
class GenericProtocol final : private JoyconCommonProtocol {
public:
explicit GenericProtocol(std::shared_ptr<JoyconHandle> handle);
/// Enables passive mode. This mode only sends button data on change. Sticks will return digital
/// data instead of analog. Motion will be disabled
DriverResult EnablePassiveMode();
/// Enables active mode. This mode will return the current status every 5-15ms
DriverResult EnableActiveMode();
/**
* Sends a request to obtain the joycon firmware and mac from handle
* @returns controller device info
*/
DriverResult GetDeviceInfo(DeviceInfo& controller_type);
/**
* Sends a request to obtain the joycon type from handle
* @returns controller type of the joycon
*/
DriverResult GetControllerType(ControllerType& controller_type);
/**
* Enables motion input
* @param enable if true motion data will be enabled
*/
DriverResult EnableImu(bool enable);
/**
* Configures the motion sensor with the specified parameters
* @param gsen gyroscope sensor sensitvity in degrees per second
* @param gfrec gyroscope sensor frequency in hertz
* @param asen accelerometer sensitivity in G force
* @param afrec accelerometer frequency in hertz
*/
DriverResult SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec,
AccelerometerSensitivity asen, AccelerometerPerformance afrec);
/**
* Request battery level from the device
* @returns battery level
*/
DriverResult GetBattery(u32& battery_level);
/**
* Request joycon colors from the device
* @returns colors of the body and buttons
*/
DriverResult GetColor(Color& color);
/**
* Request joycon serial number from the device
* @returns 16 byte serial number
*/
DriverResult GetSerialNumber(SerialNumber& serial_number);
/**
* Request joycon serial number from the device
* @returns 16 byte serial number
*/
DriverResult GetTemperature(u32& temperature);
/**
* Request joycon serial number from the device
* @returns 16 byte serial number
*/
DriverResult GetVersionNumber(FirmwareVersion& version);
/**
* Sets home led behaviour
*/
DriverResult SetHomeLight();
/**
* Sets home led into a slow breathing state
*/
DriverResult SetLedBusy();
/**
* Sets the 4 player leds on the joycon on a solid state
* @params bit flag containing the led state
*/
DriverResult SetLedPattern(u8 leds);
/**
* Sets the 4 player leds on the joycon on a blinking state
* @returns bit flag containing the led state
*/
DriverResult SetLedBlinkPattern(u8 leds);
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,494 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <array>
#include <functional>
#include <SDL_hidapi.h>
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
namespace InputCommon::Joycon {
constexpr u32 MaxErrorCount = 50;
constexpr u32 MaxBufferSize = 60;
constexpr u32 MaxResponseSize = 49;
constexpr u32 MaxSubCommandResponseSize = 64;
constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40};
using MacAddress = std::array<u8, 6>;
using SerialNumber = std::array<u8, 15>;
enum class ControllerType {
None,
Left,
Right,
Pro,
Grip,
Dual,
};
enum class PadAxes {
LeftStickX,
LeftStickY,
RightStickX,
RightStickY,
Undefined,
};
enum class PadMotion {
LeftMotion,
RightMotion,
Undefined,
};
enum class PadButton : u32 {
Down = 0x000001,
Up = 0x000002,
Right = 0x000004,
Left = 0x000008,
LeftSR = 0x000010,
LeftSL = 0x000020,
L = 0x000040,
ZL = 0x000080,
Y = 0x000100,
X = 0x000200,
B = 0x000400,
A = 0x000800,
RightSR = 0x001000,
RightSL = 0x002000,
R = 0x004000,
ZR = 0x008000,
Minus = 0x010000,
Plus = 0x020000,
StickR = 0x040000,
StickL = 0x080000,
Home = 0x100000,
Capture = 0x200000,
};
enum class PasivePadButton : u32 {
Down_A = 0x0001,
Right_X = 0x0002,
Left_B = 0x0004,
Up_Y = 0x0008,
SL = 0x0010,
SR = 0x0020,
Minus = 0x0100,
Plus = 0x0200,
StickL = 0x0400,
StickR = 0x0800,
Home = 0x1000,
Capture = 0x2000,
L_R = 0x4000,
ZL_ZR = 0x8000,
};
enum class OutputReport : u8 {
RUMBLE_AND_SUBCMD = 0x01,
FW_UPDATE_PKT = 0x03,
RUMBLE_ONLY = 0x10,
MCU_DATA = 0x11,
USB_CMD = 0x80,
};
enum class InputReport : u8 {
SUBCMD_REPLY = 0x21,
STANDARD_FULL_60HZ = 0x30,
NFC_IR_MODE_60HZ = 0x31,
SIMPLE_HID_MODE = 0x3F,
INPUT_USB_RESPONSE = 0x81,
};
enum class FeatureReport : u8 {
Last_SUBCMD = 0x02,
OTA_GW_UPGRADE = 0x70,
SETUP_MEM_READ = 0x71,
MEM_READ = 0x72,
ERASE_MEM_SECTOR = 0x73,
MEM_WRITE = 0x74,
LAUNCH = 0x75,
};
enum class SubCommand : u8 {
STATE = 0x00,
MANUAL_BT_PAIRING = 0x01,
REQ_DEV_INFO = 0x02,
SET_REPORT_MODE = 0x03,
TRIGGERS_ELAPSED = 0x04,
GET_PAGE_LIST_STATE = 0x05,
SET_HCI_STATE = 0x06,
RESET_PAIRING_INFO = 0x07,
LOW_POWER_MODE = 0x08,
SPI_FLASH_READ = 0x10,
SPI_FLASH_WRITE = 0x11,
RESET_MCU = 0x20,
SET_MCU_CONFIG = 0x21,
SET_MCU_STATE = 0x22,
SET_PLAYER_LIGHTS = 0x30,
GET_PLAYER_LIGHTS = 0x31,
SET_HOME_LIGHT = 0x38,
ENABLE_IMU = 0x40,
SET_IMU_SENSITIVITY = 0x41,
WRITE_IMU_REG = 0x42,
READ_IMU_REG = 0x43,
ENABLE_VIBRATION = 0x48,
GET_REGULATED_VOLTAGE = 0x50,
SET_EXTERNAL_CONFIG = 0x58,
UNKNOWN_RINGCON = 0x59,
UNKNOWN_RINGCON2 = 0x5A,
UNKNOWN_RINGCON3 = 0x5C,
};
enum class UsbSubCommand : u8 {
CONN_STATUS = 0x01,
HADSHAKE = 0x02,
BAUDRATE_3M = 0x03,
NO_TIMEOUT = 0x04,
EN_TIMEOUT = 0x05,
RESET = 0x06,
PRE_HANDSHAKE = 0x91,
SEND_UART = 0x92,
};
enum class CalMagic : u8 {
USR_MAGIC_0 = 0xB2,
USR_MAGIC_1 = 0xA1,
USRR_MAGI_SIZE = 2,
};
enum class CalAddr {
SERIAL_NUMBER = 0X6000,
DEVICE_TYPE = 0X6012,
COLOR_EXIST = 0X601B,
FACT_LEFT_DATA = 0X603d,
FACT_RIGHT_DATA = 0X6046,
COLOR_DATA = 0X6050,
FACT_IMU_DATA = 0X6020,
USER_LEFT_MAGIC = 0X8010,
USER_LEFT_DATA = 0X8012,
USER_RIGHT_MAGIC = 0X801B,
USER_RIGHT_DATA = 0X801D,
USER_IMU_MAGIC = 0X8026,
USER_IMU_DATA = 0X8028,
};
enum class ReportMode : u8 {
ACTIVE_POLLING_NFC_IR_CAMERA_DATA = 0x00,
ACTIVE_POLLING_NFC_IR_CAMERA_CONFIGURATION = 0x01,
ACTIVE_POLLING_NFC_IR_CAMERA_DATA_CONFIGURATION = 0x02,
ACTIVE_POLLING_IR_CAMERA_DATA = 0x03,
MCU_UPDATE_STATE = 0x23,
STANDARD_FULL_60HZ = 0x30,
NFC_IR_MODE_60HZ = 0x31,
SIMPLE_HID_MODE = 0x3F,
};
enum class GyroSensitivity : u8 {
DPS250,
DPS500,
DPS1000,
DPS2000, // Default
};
enum class AccelerometerSensitivity : u8 {
G8, // Default
G4,
G2,
G16,
};
enum class GyroPerformance : u8 {
HZ833,
HZ208, // Default
};
enum class AccelerometerPerformance : u8 {
HZ200,
HZ100, // Default
};
enum class MCUCommand : u8 {
ConfigureMCU = 0x21,
ConfigureIR = 0x23,
};
enum class MCUSubCommand : u8 {
SetMCUMode = 0x0,
SetDeviceMode = 0x1,
ReadDeviceMode = 0x02,
WriteDeviceRegisters = 0x4,
};
enum class MCUMode : u8 {
Suspend = 0,
Standby = 1,
Ringcon = 3,
NFC = 4,
IR = 5,
MaybeFWUpdate = 6,
};
enum class MCURequest : u8 {
GetMCUStatus = 1,
GetNFCData = 2,
GetIRData = 3,
};
enum class MCUReport : u8 {
Empty = 0x00,
StateReport = 0x01,
IRData = 0x03,
BusyInitializing = 0x0b,
IRStatus = 0x13,
IRRegisters = 0x1b,
NFCState = 0x2a,
NFCReadData = 0x3a,
EmptyAwaitingCmd = 0xff,
};
enum class MCUPacketFlag : u8 {
MorePacketsRemaining = 0x00,
LastCommandPacket = 0x08,
};
enum class NFCReadCommand : u8 {
CancelAll = 0x00,
StartPolling = 0x01,
StopPolling = 0x02,
StartWaitingRecieve = 0x04,
Ntag = 0x06,
Mifare = 0x0F,
};
enum class NFCTagType : u8 {
AllTags = 0x00,
Ntag215 = 0x01,
};
enum class DriverResult {
Success,
WrongReply,
Timeout,
UnsupportedControllerType,
HandleInUse,
ErrorReadingData,
ErrorWritingData,
NoDeviceDetected,
InvalidHandle,
NotSupported,
Unknown,
};
struct MotionSensorCalibration {
s16 offset;
s16 scale;
};
struct MotionCalibration {
std::array<MotionSensorCalibration, 3> accelerometer;
std::array<MotionSensorCalibration, 3> gyro;
};
// Basic motion data containing data from the sensors and a timestamp in microseconds
struct MotionData {
float gyro_x{};
float gyro_y{};
float gyro_z{};
float accel_x{};
float accel_y{};
float accel_z{};
u64 delta_timestamp{};
};
struct JoyStickAxisCalibration {
u16 max{1};
u16 min{1};
u16 center{0};
};
struct JoyStickCalibration {
JoyStickAxisCalibration x;
JoyStickAxisCalibration y;
};
struct RingCalibration {
s16 default_value;
s16 max_value;
s16 min_value;
};
struct Color {
u32 body;
u32 buttons;
u32 left_grip;
u32 right_grip;
};
struct Battery {
union {
u8 raw{};
BitField<0, 4, u8> unknown;
BitField<4, 1, u8> charging;
BitField<5, 3, u8> status;
};
};
struct VibrationValue {
f32 low_amplitude;
f32 low_frequency;
f32 high_amplitude;
f32 high_frequency;
};
struct JoyconHandle {
SDL_hid_device* handle = nullptr;
u8 packet_counter{};
};
struct MCUConfig {
MCUCommand command;
MCUSubCommand sub_command;
MCUMode mode;
INSERT_PADDING_BYTES(0x22);
u8 crc;
};
static_assert(sizeof(MCUConfig) == 0x26, "MCUConfig is an invalid size");
#pragma pack(push, 1)
struct InputReportPassive {
InputReport report_mode;
u16 button_input;
u8 stick_state;
std::array<u8, 10> unknown_data;
};
static_assert(sizeof(InputReportPassive) == 0xE, "InputReportPassive is an invalid size");
struct InputReportActive {
InputReport report_mode;
u8 packet_id;
Battery battery_status;
std::array<u8, 3> button_input;
std::array<u8, 3> left_stick_state;
std::array<u8, 3> right_stick_state;
u8 vibration_code;
std::array<s16, 6 * 2> motion_input;
INSERT_PADDING_BYTES(0x2);
s16 ring_input;
};
static_assert(sizeof(InputReportActive) == 0x29, "InputReportActive is an invalid size");
struct InputReportNfcIr {
InputReport report_mode;
u8 packet_id;
Battery battery_status;
std::array<u8, 3> button_input;
std::array<u8, 3> left_stick_state;
std::array<u8, 3> right_stick_state;
u8 vibration_code;
std::array<s16, 6 * 2> motion_input;
INSERT_PADDING_BYTES(0x4);
};
static_assert(sizeof(InputReportNfcIr) == 0x29, "InputReportNfcIr is an invalid size");
#pragma pack(pop)
struct IMUCalibration {
std::array<s16, 3> accelerometer_offset;
std::array<s16, 3> accelerometer_scale;
std::array<s16, 3> gyroscope_offset;
std::array<s16, 3> gyroscope_scale;
};
static_assert(sizeof(IMUCalibration) == 0x18, "IMUCalibration is an invalid size");
struct NFCReadBlock {
u8 start;
u8 end;
};
static_assert(sizeof(NFCReadBlock) == 0x2, "NFCReadBlock is an invalid size");
struct NFCReadBlockCommand {
u8 block_count{};
std::array<NFCReadBlock, 4> blocks{};
};
static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an invalid size");
struct NFCReadCommandData {
u8 unknown;
u8 uuid_length;
u8 unknown_2;
std::array<u8, 6> uid;
NFCTagType tag_type;
NFCReadBlockCommand read_block;
};
static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size");
struct NFCPollingCommandData {
u8 enable_mifare;
u8 unknown_1;
u8 unknown_2;
u8 unknown_3;
u8 unknown_4;
};
static_assert(sizeof(NFCPollingCommandData) == 0x05, "NFCPollingCommandData is an invalid size");
struct NFCRequestState {
MCUSubCommand sub_command;
NFCReadCommand command_argument;
u8 packet_id;
INSERT_PADDING_BYTES(0x1);
MCUPacketFlag packet_flag;
u8 data_length;
union {
std::array<u8, 0x1F> raw_data;
NFCReadCommandData nfc_read;
NFCPollingCommandData nfc_polling;
};
u8 crc;
};
static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size");
struct FirmwareVersion {
u8 major;
u8 minor;
};
static_assert(sizeof(FirmwareVersion) == 0x2, "FirmwareVersion is an invalid size");
struct DeviceInfo {
FirmwareVersion firmware;
MacAddress mac_address;
};
static_assert(sizeof(DeviceInfo) == 0x8, "DeviceInfo is an invalid size");
struct MotionStatus {
bool is_enabled;
u64 delta_time;
GyroSensitivity gyro_sensitivity;
AccelerometerSensitivity accelerometer_sensitivity;
};
struct RingStatus {
bool is_enabled;
s16 default_value;
s16 max_value;
s16 min_value;
};
struct JoyconCallbacks {
std::function<void(Battery)> on_battery_data;
std::function<void(Color)> on_color_data;
std::function<void(int, bool)> on_button_data;
std::function<void(int, f32)> on_stick_data;
std::function<void(int, const MotionData&)> on_motion_data;
std::function<void(f32)> on_ring_data;
std::function<void(const std::vector<u8>&)> on_amiibo_data;
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,415 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/nfc.h"
namespace InputCommon::Joycon {
NfcProtocol::NfcProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult NfcProtocol::EnableNfc() {
LOG_INFO(Input, "Enable NFC");
DriverResult result{DriverResult::Success};
SetBlocking();
if (result == DriverResult::Success) {
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ);
}
if (result == DriverResult::Success) {
result = EnableMCU(true);
}
if (result == DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby);
}
if (result == DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetMCUMode,
.mode = MCUMode::NFC,
.crc = {},
};
result = ConfigureMCU(config);
}
SetNonBlocking();
return result;
}
DriverResult NfcProtocol::DisableNfc() {
LOG_DEBUG(Input, "Disable NFC");
DriverResult result{DriverResult::Success};
SetBlocking();
if (result == DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
SetNonBlocking();
return result;
}
DriverResult NfcProtocol::StartNFCPollingMode() {
LOG_DEBUG(Input, "Start NFC pooling Mode");
DriverResult result{DriverResult::Success};
TagFoundData tag_data{};
SetBlocking();
if (result == DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC);
}
if (result == DriverResult::Success) {
result = WaitUntilNfcIsReady();
}
if (result == DriverResult::Success) {
is_enabled = true;
}
SetNonBlocking();
return result;
}
DriverResult NfcProtocol::ScanAmiibo(std::vector<u8>& data) {
LOG_DEBUG(Input, "Start NFC pooling Mode");
DriverResult result{DriverResult::Success};
TagFoundData tag_data{};
SetBlocking();
if (result == DriverResult::Success) {
result = StartPolling(tag_data);
}
if (result == DriverResult::Success) {
result = ReadTag(tag_data);
}
if (result == DriverResult::Success) {
result = WaitUntilNfcIsReady();
}
if (result == DriverResult::Success) {
result = StartPolling(tag_data);
}
if (result == DriverResult::Success) {
result = GetAmiiboData(data);
}
SetNonBlocking();
return result;
}
bool NfcProtocol::HasAmiibo() {
DriverResult result{DriverResult::Success};
TagFoundData tag_data{};
SetBlocking();
if (result == DriverResult::Success) {
result = StartPolling(tag_data);
}
SetNonBlocking();
return result == DriverResult::Success;
}
DriverResult NfcProtocol::WaitUntilNfcIsReady() {
constexpr std::size_t timeout_limit = 10;
std::vector<u8> output;
std::size_t tries = 0;
do {
auto result = SendStartWaitingRecieveRequest(output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ > timeout_limit) {
return DriverResult::Timeout;
}
} while (output[49] != 0x2a || (output[51] << 8) + output[50] != 0x0500 || output[55] != 0x31 ||
output[56] != 0x00);
return DriverResult::Success;
}
DriverResult NfcProtocol::StartPolling(TagFoundData& data) {
LOG_DEBUG(Input, "Start Polling for tag");
constexpr std::size_t timeout_limit = 7;
std::vector<u8> output;
std::size_t tries = 0;
do {
const auto result = SendStartPollingRequest(output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ > timeout_limit) {
return DriverResult::Timeout;
}
} while (output[49] != 0x2a || (output[51] << 8) + output[50] != 0x0500 || output[56] != 0x09);
data.type = output[62];
data.uuid.resize(output[64]);
memcpy(data.uuid.data(), output.data() + 65, data.uuid.size());
return DriverResult::Success;
}
DriverResult NfcProtocol::ReadTag(const TagFoundData& data) {
constexpr std::size_t timeout_limit = 10;
std::vector<u8> output;
std::size_t tries = 0;
std::string uuid_string;
for (auto& content : data.uuid) {
uuid_string += fmt::format(" {:02x}", content);
}
LOG_INFO(Input, "Tag detected, type={}, uuid={}", data.type, uuid_string);
tries = 0;
std::size_t ntag_pages = 0;
// Read Tag data
loop1:
while (true) {
auto result = SendReadAmiiboRequest(output, ntag_pages);
int attempt = 0;
while (1) {
if (attempt != 0) {
result = GetMCUDataResponse(ReportMode::NFC_IR_MODE_60HZ, output);
}
if ((output[49] == 0x3a || output[49] == 0x2a) && output[56] == 0x07) {
return DriverResult::ErrorReadingData;
}
if (output[49] == 0x3a && output[51] == 0x07 && output[52] == 0x01) {
if (data.type != 2) {
goto loop1;
}
switch (output[74]) {
case 0:
ntag_pages = 135;
break;
case 3:
ntag_pages = 45;
break;
case 4:
ntag_pages = 231;
break;
default:
return DriverResult::ErrorReadingData;
}
goto loop1;
}
if (output[49] == 0x2a && output[56] == 0x04) {
// finished
SendStopPollingRequest(output);
return DriverResult::Success;
}
if (output[49] == 0x2a) {
goto loop1;
}
if (attempt++ > 6) {
goto loop1;
}
}
if (result != DriverResult::Success) {
return result;
}
if (tries++ > timeout_limit) {
return DriverResult::Timeout;
}
}
return DriverResult::Success;
}
DriverResult NfcProtocol::GetAmiiboData(std::vector<u8>& ntag_data) {
constexpr std::size_t timeout_limit = 10;
std::vector<u8> output;
std::size_t tries = 0;
std::size_t ntag_pages = 135;
std::size_t ntag_buffer_pos = 0;
// Read Tag data
loop1:
while (true) {
auto result = SendReadAmiiboRequest(output, ntag_pages);
int attempt = 0;
while (1) {
if (attempt != 0) {
result = GetMCUDataResponse(ReportMode::NFC_IR_MODE_60HZ, output);
}
if ((output[49] == 0x3a || output[49] == 0x2a) && output[56] == 0x07) {
return DriverResult::ErrorReadingData;
}
if (output[49] == 0x3a && output[51] == 0x07) {
std::size_t payload_size = (output[54] << 8 | output[55]) & 0x7FF;
if (output[52] == 0x01) {
memcpy(ntag_data.data() + ntag_buffer_pos, output.data() + 116,
payload_size - 60);
ntag_buffer_pos += payload_size - 60;
} else {
memcpy(ntag_data.data() + ntag_buffer_pos, output.data() + 56, payload_size);
}
goto loop1;
}
if (output[49] == 0x2a && output[56] == 0x04) {
LOG_INFO(Input, "Finished reading amiibo");
return DriverResult::Success;
}
if (output[49] == 0x2a) {
goto loop1;
}
if (attempt++ > 4) {
goto loop1;
}
}
if (result != DriverResult::Success) {
return result;
}
if (tries++ > timeout_limit) {
return DriverResult::Timeout;
}
}
return DriverResult::Success;
}
DriverResult NfcProtocol::SendStartPollingRequest(std::vector<u8>& output) {
NFCRequestState request{
.sub_command = MCUSubCommand::ReadDeviceMode,
.command_argument = NFCReadCommand::StartPolling,
.packet_id = 0x0,
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = sizeof(NFCPollingCommandData),
.nfc_polling =
{
.enable_mifare = 0x01,
.unknown_1 = 0x00,
.unknown_2 = 0x00,
.unknown_3 = 0x2c,
.unknown_4 = 0x01,
},
.crc = {},
};
std::vector<u8> request_data(sizeof(NFCRequestState));
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output);
}
DriverResult NfcProtocol::SendStopPollingRequest(std::vector<u8>& output) {
NFCRequestState request{
.sub_command = MCUSubCommand::ReadDeviceMode,
.command_argument = NFCReadCommand::StopPolling,
.packet_id = 0x0,
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = 0,
.raw_data = {},
.crc = {},
};
std::vector<u8> request_data(sizeof(NFCRequestState));
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output);
}
DriverResult NfcProtocol::SendStartWaitingRecieveRequest(std::vector<u8>& output) {
NFCRequestState request{
.sub_command = MCUSubCommand::ReadDeviceMode,
.command_argument = NFCReadCommand::StartWaitingRecieve,
.packet_id = 0x0,
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = 0,
.raw_data = {},
.crc = {},
};
std::vector<u8> request_data(sizeof(NFCRequestState));
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output);
}
DriverResult NfcProtocol::SendReadAmiiboRequest(std::vector<u8>& output, std::size_t ntag_pages) {
NFCRequestState request{
.sub_command = MCUSubCommand::ReadDeviceMode,
.command_argument = NFCReadCommand::Ntag,
.packet_id = 0x0,
.packet_flag = MCUPacketFlag::LastCommandPacket,
.data_length = sizeof(NFCReadCommandData),
.nfc_read =
{
.unknown = 0xd0,
.uuid_length = 0x07,
.unknown_2 = 0x00,
.uid = {},
.tag_type = NFCTagType::AllTags,
.read_block = GetReadBlockCommand(ntag_pages),
},
.crc = {},
};
std::vector<u8> request_data(sizeof(NFCRequestState));
memcpy(request_data.data(), &request, sizeof(NFCRequestState));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, SubCommand::STATE, request_data, output);
}
NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(std::size_t pages) const {
if (pages == 0) {
return {
.block_count = 1,
};
}
if (pages == 45) {
return {
.block_count = 1,
.blocks =
{
NFCReadBlock{0x00, 0x2C},
},
};
}
if (pages == 135) {
return {
.block_count = 3,
.blocks =
{
NFCReadBlock{0x00, 0x3b},
{0x3c, 0x77},
{0x78, 0x86},
},
};
}
if (pages == 231) {
return {
.block_count = 4,
.blocks =
{
NFCReadBlock{0x00, 0x3b},
{0x3c, 0x77},
{0x78, 0x83},
{0xb4, 0xe6},
},
};
}
return {};
}
bool NfcProtocol::IsEnabled() const {
return is_enabled;
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
class NfcProtocol final : private JoyconCommonProtocol {
public:
explicit NfcProtocol(std::shared_ptr<JoyconHandle> handle);
DriverResult EnableNfc();
DriverResult DisableNfc();
DriverResult StartNFCPollingMode();
DriverResult ScanAmiibo(std::vector<u8>& data);
bool HasAmiibo();
bool IsEnabled() const;
private:
struct TagFoundData {
u8 type;
std::vector<u8> uuid;
};
DriverResult WaitUntilNfcIsReady();
DriverResult StartPolling(TagFoundData& data);
DriverResult ReadTag(const TagFoundData& data);
DriverResult GetAmiiboData(std::vector<u8>& data);
DriverResult SendStartPollingRequest(std::vector<u8>& output);
DriverResult SendStopPollingRequest(std::vector<u8>& output);
DriverResult SendStartWaitingRecieveRequest(std::vector<u8>& output);
DriverResult SendReadAmiiboRequest(std::vector<u8>& output, std::size_t ntag_pages);
NFCReadBlockCommand GetReadBlockCommand(std::size_t pages) const;
bool is_enabled{};
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,337 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/poller.h"
namespace InputCommon::Joycon {
JoyconPoller::JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
JoyStickCalibration right_stick_calibration_,
MotionCalibration motion_calibration_)
: device_type{device_type_}, left_stick_calibration{left_stick_calibration_},
right_stick_calibration{right_stick_calibration_}, motion_calibration{motion_calibration_} {}
void JoyconPoller::SetCallbacks(const Joycon::JoyconCallbacks& callbacks_) {
callbacks = std::move(callbacks_);
}
void JoyconPoller::ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
const RingStatus& ring_status) {
InputReportActive data{};
memcpy(&data, buffer.data(), sizeof(InputReportActive));
switch (device_type) {
case Joycon::ControllerType::Left:
UpdateActiveLeftPadInput(data, motion_status);
break;
case Joycon::ControllerType::Right:
UpdateActiveRightPadInput(data, motion_status);
break;
case Joycon::ControllerType::Pro:
UpdateActiveProPadInput(data, motion_status);
break;
case Joycon::ControllerType::Grip:
case Joycon::ControllerType::Dual:
case Joycon::ControllerType::None:
break;
}
if (ring_status.is_enabled) {
UpdateRing(data.ring_input, ring_status);
}
callbacks.on_battery_data(data.battery_status);
}
void JoyconPoller::ReadPassiveMode(std::span<u8> buffer) {
InputReportPassive data{};
memcpy(&data, buffer.data(), sizeof(InputReportPassive));
switch (device_type) {
case Joycon::ControllerType::Left:
UpdatePasiveLeftPadInput(data);
break;
case Joycon::ControllerType::Right:
UpdatePasiveRightPadInput(data);
break;
case Joycon::ControllerType::Pro:
UpdatePasiveProPadInput(data);
break;
case Joycon::ControllerType::Grip:
case Joycon::ControllerType::Dual:
case Joycon::ControllerType::None:
break;
}
}
void JoyconPoller::ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status) {
// This mode is compatible with the active mode
ReadActiveMode(buffer, motion_status, {});
}
void JoyconPoller::UpdateColor(const Color& color) {
callbacks.on_color_data(color);
}
void JoyconPoller::updateAmiibo(const std::vector<u8>& amiibo_data) {
callbacks.on_amiibo_data(amiibo_data);
}
void JoyconPoller::UpdateRing(s16 value, const RingStatus& ring_status) {
float normalized_value = static_cast<float>(value - ring_status.default_value);
if (normalized_value > 0) {
normalized_value = normalized_value /
static_cast<float>(ring_status.max_value - ring_status.default_value);
}
if (normalized_value < 0) {
normalized_value = normalized_value /
static_cast<float>(ring_status.default_value - ring_status.min_value);
}
callbacks.on_ring_data(normalized_value);
}
void JoyconPoller::UpdateActiveLeftPadInput(const InputReportActive& input,
const MotionStatus& motion_status) {
static constexpr std::array<Joycon::PadButton, 11> left_buttons{
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
Joycon::PadButton::Left, Joycon::PadButton::LeftSL, Joycon::PadButton::LeftSR,
Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus,
Joycon::PadButton::Capture, Joycon::PadButton::StickL,
};
const u32 raw_button =
static_cast<u32>(input.button_input[2] | ((input.button_input[1] & 0b00101001) << 16));
for (std::size_t i = 0; i < left_buttons.size(); ++i) {
const bool button_status = (raw_button & static_cast<u32>(left_buttons[i])) != 0;
const int button = static_cast<int>(left_buttons[i]);
callbacks.on_button_data(button, button_status);
}
const u16 raw_left_axis_x =
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
const u16 raw_left_axis_y =
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
if (motion_status.is_enabled) {
auto left_motion = GetMotionInput(input, motion_status);
// Rotate motion axis to the correct direction
left_motion.accel_y = -left_motion.accel_y;
left_motion.accel_z = -left_motion.accel_z;
left_motion.gyro_x = -left_motion.gyro_x;
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), left_motion);
}
}
void JoyconPoller::UpdateActiveRightPadInput(const InputReportActive& input,
const MotionStatus& motion_status) {
static constexpr std::array<Joycon::PadButton, 11> right_buttons{
Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B,
Joycon::PadButton::A, Joycon::PadButton::RightSL, Joycon::PadButton::RightSR,
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
Joycon::PadButton::Home, Joycon::PadButton::StickR,
};
const u32 raw_button =
static_cast<u32>((input.button_input[0] << 8) | (input.button_input[1] << 16));
for (std::size_t i = 0; i < right_buttons.size(); ++i) {
const bool button_status = (raw_button & static_cast<u32>(right_buttons[i])) != 0;
const int button = static_cast<int>(right_buttons[i]);
callbacks.on_button_data(button, button_status);
}
const u16 raw_right_axis_x =
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
const u16 raw_right_axis_y =
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
if (motion_status.is_enabled) {
auto right_motion = GetMotionInput(input, motion_status);
// Rotate motion axis to the correct direction
right_motion.accel_x = -right_motion.accel_x;
right_motion.accel_y = -right_motion.accel_y;
right_motion.gyro_z = -right_motion.gyro_z;
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), right_motion);
}
}
void JoyconPoller::UpdateActiveProPadInput(const InputReportActive& input,
const MotionStatus& motion_status) {
static constexpr std::array<Joycon::PadButton, 18> pro_buttons{
Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
Joycon::PadButton::Left, Joycon::PadButton::L, Joycon::PadButton::ZL,
Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::Y,
Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A,
Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
Joycon::PadButton::Home, Joycon::PadButton::StickL, Joycon::PadButton::StickR,
};
const u32 raw_button = static_cast<u32>(input.button_input[2] | (input.button_input[0] << 8) |
(input.button_input[1] << 16));
for (std::size_t i = 0; i < pro_buttons.size(); ++i) {
const bool button_status = (raw_button & static_cast<u32>(pro_buttons[i])) != 0;
const int button = static_cast<int>(pro_buttons[i]);
callbacks.on_button_data(button, button_status);
}
const u16 raw_left_axis_x =
static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
const u16 raw_left_axis_y =
static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
const u16 raw_right_axis_x =
static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
const u16 raw_right_axis_y =
static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
if (motion_status.is_enabled) {
auto pro_motion = GetMotionInput(input, motion_status);
pro_motion.gyro_x = -pro_motion.gyro_x;
pro_motion.accel_y = -pro_motion.accel_y;
pro_motion.accel_z = -pro_motion.accel_z;
callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), pro_motion);
callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), pro_motion);
}
}
void JoyconPoller::UpdatePasiveLeftPadInput(const InputReportPassive& input) {
static constexpr std::array<Joycon::PasivePadButton, 11> left_buttons{
Joycon::PasivePadButton::Down_A, Joycon::PasivePadButton::Right_X,
Joycon::PasivePadButton::Left_B, Joycon::PasivePadButton::Up_Y,
Joycon::PasivePadButton::SL, Joycon::PasivePadButton::SR,
Joycon::PasivePadButton::L_R, Joycon::PasivePadButton::ZL_ZR,
Joycon::PasivePadButton::Minus, Joycon::PasivePadButton::Capture,
Joycon::PasivePadButton::StickL,
};
for (std::size_t i = 0; i < left_buttons.size(); ++i) {
const bool button_status = (input.button_input & static_cast<u32>(left_buttons[i])) != 0;
const int button = static_cast<int>(left_buttons[i]);
callbacks.on_button_data(button, button_status);
}
}
void JoyconPoller::UpdatePasiveRightPadInput(const InputReportPassive& input) {
static constexpr std::array<Joycon::PasivePadButton, 11> right_buttons{
Joycon::PasivePadButton::Down_A, Joycon::PasivePadButton::Right_X,
Joycon::PasivePadButton::Left_B, Joycon::PasivePadButton::Up_Y,
Joycon::PasivePadButton::SL, Joycon::PasivePadButton::SR,
Joycon::PasivePadButton::L_R, Joycon::PasivePadButton::ZL_ZR,
Joycon::PasivePadButton::Plus, Joycon::PasivePadButton::Home,
Joycon::PasivePadButton::StickR,
};
for (std::size_t i = 0; i < right_buttons.size(); ++i) {
const bool button_status = (input.button_input & static_cast<u32>(right_buttons[i])) != 0;
const int button = static_cast<int>(right_buttons[i]);
callbacks.on_button_data(button, button_status);
}
}
void JoyconPoller::UpdatePasiveProPadInput(const InputReportPassive& input) {
static constexpr std::array<Joycon::PasivePadButton, 14> pro_buttons{
Joycon::PasivePadButton::Down_A, Joycon::PasivePadButton::Right_X,
Joycon::PasivePadButton::Left_B, Joycon::PasivePadButton::Up_Y,
Joycon::PasivePadButton::SL, Joycon::PasivePadButton::SR,
Joycon::PasivePadButton::L_R, Joycon::PasivePadButton::ZL_ZR,
Joycon::PasivePadButton::Minus, Joycon::PasivePadButton::Plus,
Joycon::PasivePadButton::Capture, Joycon::PasivePadButton::Home,
Joycon::PasivePadButton::StickL, Joycon::PasivePadButton::StickR,
};
for (std::size_t i = 0; i < pro_buttons.size(); ++i) {
const bool button_status = (input.button_input & static_cast<u32>(pro_buttons[i])) != 0;
const int button = static_cast<int>(pro_buttons[i]);
callbacks.on_button_data(button, button_status);
}
}
f32 JoyconPoller::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const {
const f32 value = static_cast<f32>(raw_value - calibration.center);
if (value > 0.0f) {
return value / calibration.max;
}
return value / calibration.min;
}
f32 JoyconPoller::GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal,
AccelerometerSensitivity sensitivity) const {
const f32 value = raw * (1.0f / (cal.scale - cal.offset)) * 4;
switch (sensitivity) {
case Joycon::AccelerometerSensitivity::G2:
return value / 4.0f;
case Joycon::AccelerometerSensitivity::G4:
return value / 2.0f;
case Joycon::AccelerometerSensitivity::G8:
return value;
case Joycon::AccelerometerSensitivity::G16:
return value * 2.0f;
}
return value;
}
f32 JoyconPoller::GetGyroValue(s16 raw, const MotionSensorCalibration& cal,
GyroSensitivity sensitivity) const {
const f32 value = (raw - cal.offset) * (936.0f / (cal.scale - cal.offset)) / 360.0f;
switch (sensitivity) {
case Joycon::GyroSensitivity::DPS250:
return value / 8.0f;
case Joycon::GyroSensitivity::DPS500:
return value / 4.0f;
case Joycon::GyroSensitivity::DPS1000:
return value / 2.0f;
case Joycon::GyroSensitivity::DPS2000:
return value;
}
return value;
}
s16 JoyconPoller::GetRawIMUValues(std::size_t sensor, size_t axis,
const InputReportActive& input) const {
return input.motion_input[(sensor * 3) + axis];
}
MotionData JoyconPoller::GetMotionInput(const InputReportActive& input,
const MotionStatus& motion_status) const {
MotionData motion{};
const auto& accel_cal = motion_calibration.accelerometer;
const auto& gyro_cal = motion_calibration.gyro;
const s16 raw_accel_x = input.motion_input[1];
const s16 raw_accel_y = input.motion_input[0];
const s16 raw_accel_z = input.motion_input[2];
const s16 raw_gyro_x = input.motion_input[4];
const s16 raw_gyro_y = input.motion_input[3];
const s16 raw_gyro_z = input.motion_input[5];
motion.delta_timestamp = motion_status.delta_time;
motion.accel_x =
GetAccelerometerValue(raw_accel_x, accel_cal[1], motion_status.accelerometer_sensitivity);
motion.accel_y =
GetAccelerometerValue(raw_accel_y, accel_cal[0], motion_status.accelerometer_sensitivity);
motion.accel_z =
GetAccelerometerValue(raw_accel_z, accel_cal[2], motion_status.accelerometer_sensitivity);
motion.gyro_x = GetGyroValue(raw_gyro_x, gyro_cal[1], motion_status.gyro_sensitivity);
motion.gyro_y = GetGyroValue(raw_gyro_y, gyro_cal[0], motion_status.gyro_sensitivity);
motion.gyro_z = GetGyroValue(raw_gyro_z, gyro_cal[2], motion_status.gyro_sensitivity);
// TODO(German77): Return all three samples data
return motion;
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <functional>
#include <span>
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
// Handles input packages and triggers the corresponding input events
class JoyconPoller {
public:
JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
JoyStickCalibration right_stick_calibration_,
MotionCalibration motion_calibration_);
void SetCallbacks(const Joycon::JoyconCallbacks& callbacks_);
/// Handles data from passive packages
void ReadPassiveMode(std::span<u8> buffer);
/// Handles data from active packages
void ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
const RingStatus& ring_status);
/// Handles data from nfc or ir packages
void ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status);
void UpdateColor(const Color& color);
void UpdateRing(s16 value, const RingStatus& ring_status);
void updateAmiibo(const std::vector<u8>& amiibo_data);
private:
void UpdateActiveLeftPadInput(const InputReportActive& input,
const MotionStatus& motion_status);
void UpdateActiveRightPadInput(const InputReportActive& input,
const MotionStatus& motion_status);
void UpdateActiveProPadInput(const InputReportActive& input, const MotionStatus& motion_status);
void UpdatePasiveLeftPadInput(const InputReportPassive& buffer);
void UpdatePasiveRightPadInput(const InputReportPassive& buffer);
void UpdatePasiveProPadInput(const InputReportPassive& buffer);
/// Returns a calibrated joystick axis from raw axis data
f32 GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const;
/// Returns a calibrated accelerometer axis from raw motion data
f32 GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal,
AccelerometerSensitivity sensitivity) const;
/// Returns a calibrated gyro axis from raw motion data
f32 GetGyroValue(s16 raw_value, const MotionSensorCalibration& cal,
GyroSensitivity sensitivity) const;
/// Returns a raw motion value from a buffer
s16 GetRawIMUValues(size_t sensor, size_t axis, const InputReportActive& input) const;
/// Returns motion data from a buffer
MotionData GetMotionInput(const InputReportActive& input,
const MotionStatus& motion_status) const;
ControllerType device_type{};
// Device calibration
JoyStickCalibration left_stick_calibration{};
JoyStickCalibration right_stick_calibration{};
MotionCalibration motion_calibration{};
Joycon::JoyconCallbacks callbacks{};
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/ringcon.h"
namespace InputCommon::Joycon {
RingConProtocol::RingConProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult RingConProtocol::EnableRingCon() {
LOG_DEBUG(Input, "Enable Ringcon");
DriverResult result{DriverResult::Success};
SetBlocking();
if (result == DriverResult::Success) {
result = SetReportMode(ReportMode::STANDARD_FULL_60HZ);
}
if (result == DriverResult::Success) {
result = EnableMCU(true);
}
if (result == DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetDeviceMode,
.mode = MCUMode::Standby,
.crc = {},
};
result = ConfigureMCU(config);
}
SetNonBlocking();
return result;
}
DriverResult RingConProtocol::DisableRingCon() {
LOG_DEBUG(Input, "Disable RingCon");
DriverResult result{DriverResult::Success};
SetBlocking();
if (result == DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
SetNonBlocking();
return result;
}
DriverResult RingConProtocol::StartRingconPolling() {
LOG_DEBUG(Input, "Enable Ringcon");
bool is_connected = false;
DriverResult result{DriverResult::Success};
SetBlocking();
if (result == DriverResult::Success) {
result = IsRingConnected(is_connected);
}
if (result == DriverResult::Success && is_connected) {
LOG_INFO(Input, "Ringcon detected");
result = ConfigureRing();
}
if (result == DriverResult::Success) {
is_enabled = true;
}
SetNonBlocking();
return result;
}
DriverResult RingConProtocol::IsRingConnected(bool& is_connected) {
LOG_DEBUG(Input, "IsRingConnected");
constexpr std::size_t max_tries = 28;
std::vector<u8> output;
std::size_t tries = 0;
is_connected = false;
do {
std::array<u8, 1> empty_data{};
const auto result = SendSubCommand(SubCommand::UNKNOWN_RINGCON, empty_data, output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return DriverResult::NoDeviceDetected;
}
} while (output[14] != 0x59 || output[16] != 0x20);
is_connected = true;
return DriverResult::Success;
}
DriverResult RingConProtocol::ConfigureRing() {
LOG_DEBUG(Input, "ConfigureRing");
constexpr std::size_t max_tries = 28;
DriverResult result{DriverResult::Success};
std::vector<u8> output;
std::size_t tries = 0;
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};
do {
result = SendSubCommand(SubCommand::UNKNOWN_RINGCON3, ring_config, output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return DriverResult::NoDeviceDetected;
}
} while (output[14] != 0x5C);
static constexpr std::array<u8, 4> ringcon_data{0x04, 0x01, 0x01, 0x02};
result = SendSubCommand(SubCommand::UNKNOWN_RINGCON2, ringcon_data, output);
return result;
}
bool RingConProtocol::IsEnabled() const {
return is_enabled;
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
class RingConProtocol final : private JoyconCommonProtocol {
public:
explicit RingConProtocol(std::shared_ptr<JoyconHandle> handle);
DriverResult EnableRingCon();
DriverResult DisableRingCon();
DriverResult StartRingconPolling();
bool IsEnabled() const;
private:
DriverResult IsRingConnected(bool& is_connected);
DriverResult ConfigureRing();
bool is_enabled{};
};
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,302 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cmath>
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/rumble.h"
namespace InputCommon::Joycon {
RumbleProtocol::RumbleProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult RumbleProtocol::EnableRumble(bool is_enabled) {
LOG_DEBUG(Input, "Enable Rumble");
const std::array<u8, 1> buffer{static_cast<u8>(is_enabled ? 1 : 0)};
std::vector<u8> output;
SetBlocking();
const auto result = SendSubCommand(SubCommand::ENABLE_VIBRATION, buffer, output);
SetNonBlocking();
return result;
}
DriverResult RumbleProtocol::SendVibration(const VibrationValue& vibration) {
std::array<u8, sizeof(DefaultVibrationBuffer)> buffer{};
if (vibration.high_amplitude <= 0.0f && vibration.low_amplitude <= 0.0f) {
return SendVibrationReport(DefaultVibrationBuffer);
}
// Protect joycons from damage from strong vibrations
const f32 clamp_amplitude =
1.0f / std::max(1.0f, vibration.high_amplitude + vibration.low_amplitude);
const u16 encoded_high_frequency = EncodeHighFrequency(vibration.high_frequency);
const u8 encoded_high_amplitude =
EncodeHighAmplitude(vibration.high_amplitude * clamp_amplitude);
const u8 encoded_low_frequency = EncodeLowFrequency(vibration.low_frequency);
const u16 encoded_low_amplitude = EncodeLowAmplitude(vibration.low_amplitude * clamp_amplitude);
buffer[0] = static_cast<u8>(encoded_high_frequency & 0xFF);
buffer[1] = static_cast<u8>(encoded_high_amplitude | ((encoded_high_frequency >> 8) & 0x01));
buffer[2] = static_cast<u8>(encoded_low_frequency | ((encoded_low_amplitude >> 8) & 0x80));
buffer[3] = static_cast<u8>(encoded_low_amplitude & 0xFF);
// Duplicate rumble for now
buffer[4] = buffer[0];
buffer[5] = buffer[1];
buffer[6] = buffer[2];
buffer[7] = buffer[3];
return SendVibrationReport(buffer);
}
u16 RumbleProtocol::EncodeHighFrequency(f32 frequency) const {
const u8 new_frequency =
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f));
return static_cast<u16>((new_frequency - 0x60) * 4);
}
u8 RumbleProtocol::EncodeLowFrequency(f32 frequency) const {
const u8 new_frequency =
static_cast<u8>(std::clamp(std::log2(frequency / 10.0f) * 32.0f, 0.0f, 255.0f));
return static_cast<u8>(new_frequency - 0x40);
}
u8 RumbleProtocol::EncodeHighAmplitude(f32 amplitude) const {
/* More information about these values can be found here:
* https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
*/
static constexpr std::array<std::pair<f32, int>, 101> high_fequency_amplitude{
std::pair<f32, int>{0.0f, 0x0},
{0.01f, 0x2},
{0.012f, 0x4},
{0.014f, 0x6},
{0.017f, 0x8},
{0.02f, 0x0a},
{0.024f, 0x0c},
{0.028f, 0x0e},
{0.033f, 0x10},
{0.04f, 0x12},
{0.047f, 0x14},
{0.056f, 0x16},
{0.067f, 0x18},
{0.08f, 0x1a},
{0.095f, 0x1c},
{0.112f, 0x1e},
{0.117f, 0x20},
{0.123f, 0x22},
{0.128f, 0x24},
{0.134f, 0x26},
{0.14f, 0x28},
{0.146f, 0x2a},
{0.152f, 0x2c},
{0.159f, 0x2e},
{0.166f, 0x30},
{0.173f, 0x32},
{0.181f, 0x34},
{0.189f, 0x36},
{0.198f, 0x38},
{0.206f, 0x3a},
{0.215f, 0x3c},
{0.225f, 0x3e},
{0.23f, 0x40},
{0.235f, 0x42},
{0.24f, 0x44},
{0.245f, 0x46},
{0.251f, 0x48},
{0.256f, 0x4a},
{0.262f, 0x4c},
{0.268f, 0x4e},
{0.273f, 0x50},
{0.279f, 0x52},
{0.286f, 0x54},
{0.292f, 0x56},
{0.298f, 0x58},
{0.305f, 0x5a},
{0.311f, 0x5c},
{0.318f, 0x5e},
{0.325f, 0x60},
{0.332f, 0x62},
{0.34f, 0x64},
{0.347f, 0x66},
{0.355f, 0x68},
{0.362f, 0x6a},
{0.37f, 0x6c},
{0.378f, 0x6e},
{0.387f, 0x70},
{0.395f, 0x72},
{0.404f, 0x74},
{0.413f, 0x76},
{0.422f, 0x78},
{0.431f, 0x7a},
{0.44f, 0x7c},
{0.45f, 0x7e},
{0.46f, 0x80},
{0.47f, 0x82},
{0.48f, 0x84},
{0.491f, 0x86},
{0.501f, 0x88},
{0.512f, 0x8a},
{0.524f, 0x8c},
{0.535f, 0x8e},
{0.547f, 0x90},
{0.559f, 0x92},
{0.571f, 0x94},
{0.584f, 0x96},
{0.596f, 0x98},
{0.609f, 0x9a},
{0.623f, 0x9c},
{0.636f, 0x9e},
{0.65f, 0xa0},
{0.665f, 0xa2},
{0.679f, 0xa4},
{0.694f, 0xa6},
{0.709f, 0xa8},
{0.725f, 0xaa},
{0.741f, 0xac},
{0.757f, 0xae},
{0.773f, 0xb0},
{0.79f, 0xb2},
{0.808f, 0xb4},
{0.825f, 0xb6},
{0.843f, 0xb8},
{0.862f, 0xba},
{0.881f, 0xbc},
{0.9f, 0xbe},
{0.92f, 0xc0},
{0.94f, 0xc2},
{0.96f, 0xc4},
{0.981f, 0xc6},
{1.003f, 0xc8},
};
for (const auto& [amplitude_value, code] : high_fequency_amplitude) {
if (amplitude <= amplitude_value) {
return static_cast<u8>(code);
}
}
return static_cast<u8>(high_fequency_amplitude[high_fequency_amplitude.size() - 1].second);
}
u16 RumbleProtocol::EncodeLowAmplitude(f32 amplitude) const {
/* More information about these values can be found here:
* https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/rumble_data_table.md
*/
static constexpr std::array<std::pair<f32, int>, 101> high_fequency_amplitude{
std::pair<f32, int>{0.0f, 0x0040},
{0.01f, 0x8040},
{0.012f, 0x0041},
{0.014f, 0x8041},
{0.017f, 0x0042},
{0.02f, 0x8042},
{0.024f, 0x0043},
{0.028f, 0x8043},
{0.033f, 0x0044},
{0.04f, 0x8044},
{0.047f, 0x0045},
{0.056f, 0x8045},
{0.067f, 0x0046},
{0.08f, 0x8046},
{0.095f, 0x0047},
{0.112f, 0x8047},
{0.117f, 0x0048},
{0.123f, 0x8048},
{0.128f, 0x0049},
{0.134f, 0x8049},
{0.14f, 0x004a},
{0.146f, 0x804a},
{0.152f, 0x004b},
{0.159f, 0x804b},
{0.166f, 0x004c},
{0.173f, 0x804c},
{0.181f, 0x004d},
{0.189f, 0x804d},
{0.198f, 0x004e},
{0.206f, 0x804e},
{0.215f, 0x004f},
{0.225f, 0x804f},
{0.23f, 0x0050},
{0.235f, 0x8050},
{0.24f, 0x0051},
{0.245f, 0x8051},
{0.251f, 0x0052},
{0.256f, 0x8052},
{0.262f, 0x0053},
{0.268f, 0x8053},
{0.273f, 0x0054},
{0.279f, 0x8054},
{0.286f, 0x0055},
{0.292f, 0x8055},
{0.298f, 0x0056},
{0.305f, 0x8056},
{0.311f, 0x0057},
{0.318f, 0x8057},
{0.325f, 0x0058},
{0.332f, 0x8058},
{0.34f, 0x0059},
{0.347f, 0x8059},
{0.355f, 0x005a},
{0.362f, 0x805a},
{0.37f, 0x005b},
{0.378f, 0x805b},
{0.387f, 0x005c},
{0.395f, 0x805c},
{0.404f, 0x005d},
{0.413f, 0x805d},
{0.422f, 0x005e},
{0.431f, 0x805e},
{0.44f, 0x005f},
{0.45f, 0x805f},
{0.46f, 0x0060},
{0.47f, 0x8060},
{0.48f, 0x0061},
{0.491f, 0x8061},
{0.501f, 0x0062},
{0.512f, 0x8062},
{0.524f, 0x0063},
{0.535f, 0x8063},
{0.547f, 0x0064},
{0.559f, 0x8064},
{0.571f, 0x0065},
{0.584f, 0x8065},
{0.596f, 0x0066},
{0.609f, 0x8066},
{0.623f, 0x0067},
{0.636f, 0x8067},
{0.65f, 0x0068},
{0.665f, 0x8068},
{0.679f, 0x0069},
{0.694f, 0x8069},
{0.709f, 0x006a},
{0.725f, 0x806a},
{0.741f, 0x006b},
{0.757f, 0x806b},
{0.773f, 0x006c},
{0.79f, 0x806c},
{0.808f, 0x006d},
{0.825f, 0x806d},
{0.843f, 0x006e},
{0.862f, 0x806e},
{0.881f, 0x006f},
{0.9f, 0x806f},
{0.92f, 0x0070},
{0.94f, 0x8070},
{0.96f, 0x0071},
{0.981f, 0x8071},
{1.003f, 0x0072},
};
for (const auto& [amplitude_value, code] : high_fequency_amplitude) {
if (amplitude <= amplitude_value) {
return static_cast<u16>(code);
}
}
return static_cast<u16>(high_fequency_amplitude[high_fequency_amplitude.size() - 1].second);
}
} // namespace InputCommon::Joycon

View file

@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
class RumbleProtocol final : private JoyconCommonProtocol {
public:
explicit RumbleProtocol(std::shared_ptr<JoyconHandle> handle);
DriverResult EnableRumble(bool is_enabled);
DriverResult SendVibration(const VibrationValue& vibration);
private:
u16 EncodeHighFrequency(f32 frequency) const;
u8 EncodeLowFrequency(f32 frequency) const;
u8 EncodeHighAmplitude(f32 amplitude) const;
u16 EncodeLowAmplitude(f32 amplitude) const;
};
} // namespace InputCommon::Joycon

View file

@ -79,6 +79,17 @@ void InputEngine::SetBattery(const PadIdentifier& identifier, Common::Input::Bat
TriggerOnBatteryChange(identifier, 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) { void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value) {
{ {
std::scoped_lock lock{mutex}; std::scoped_lock lock{mutex};
@ -176,6 +187,18 @@ Common::Input::BatteryLevel InputEngine::GetBattery(const PadIdentifier& identif
return controller.battery; 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 { BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) const {
std::scoped_lock lock{mutex}; std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier); const auto controller_iter = controller_list.find(identifier);
@ -328,6 +351,20 @@ void InputEngine::TriggerOnBatteryChange(const PadIdentifier& identifier,
} }
} }
void InputEngine::TriggerOnColorChange(const PadIdentifier& identifier,
[[maybe_unused]] Common::Input::BodyColorStatus value) {
std::scoped_lock lock{mutex_callback};
for (const auto& poller_pair : callback_list) {
const InputIdentifier& poller = poller_pair.second;
if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Color, 0)) {
continue;
}
if (poller.callback.on_change) {
poller.callback.on_change();
}
}
}
void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion, void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value) { const BasicMotion& value) {
std::scoped_lock lock{mutex_callback}; std::scoped_lock lock{mutex_callback};

View file

@ -40,6 +40,7 @@ enum class EngineInputType {
Battery, Battery,
Button, Button,
Camera, Camera,
Color,
HatButton, HatButton,
Motion, Motion,
Nfc, Nfc,
@ -199,6 +200,7 @@ public:
bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const; bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const;
f32 GetAxis(const PadIdentifier& identifier, int axis) const; f32 GetAxis(const PadIdentifier& identifier, int axis) const;
Common::Input::BatteryLevel GetBattery(const PadIdentifier& identifier) 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; BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const;
Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const; Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const;
Common::Input::NfcStatus GetNfc(const PadIdentifier& identifier) const; Common::Input::NfcStatus GetNfc(const PadIdentifier& identifier) const;
@ -212,6 +214,7 @@ protected:
void SetHatButton(const PadIdentifier& identifier, int button, u8 value); void SetHatButton(const PadIdentifier& identifier, int button, u8 value);
void SetAxis(const PadIdentifier& identifier, int axis, f32 value); void SetAxis(const PadIdentifier& identifier, int axis, f32 value);
void SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel 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 SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value);
void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value); void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value);
void SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value); void SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value);
@ -227,6 +230,7 @@ private:
std::unordered_map<int, float> axes; std::unordered_map<int, float> axes;
std::unordered_map<int, BasicMotion> motions; std::unordered_map<int, BasicMotion> motions;
Common::Input::BatteryLevel battery{}; Common::Input::BatteryLevel battery{};
Common::Input::BodyColorStatus color{};
Common::Input::CameraStatus camera{}; Common::Input::CameraStatus camera{};
Common::Input::NfcStatus nfc{}; Common::Input::NfcStatus nfc{};
}; };
@ -235,6 +239,8 @@ private:
void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value); void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value);
void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value); void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value);
void TriggerOnBatteryChange(const PadIdentifier& identifier, Common::Input::BatteryLevel 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, void TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value); const BasicMotion& value);
void TriggerOnCameraChange(const PadIdentifier& identifier, void TriggerOnCameraChange(const PadIdentifier& identifier,

View file

@ -498,6 +498,58 @@ private:
InputEngine* input_engine; InputEngine* input_engine;
}; };
class InputFromColor final : public Common::Input::InputDevice {
public:
explicit InputFromColor(PadIdentifier identifier_, InputEngine* input_engine_)
: identifier(identifier_), input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
.type = EngineInputType::Color,
.index = 0,
.callback = engine_callback,
};
last_color_value = {};
callback_key = input_engine->SetCallback(input_identifier);
}
~InputFromColor() override {
input_engine->DeleteCallback(callback_key);
}
Common::Input::BodyColorStatus GetStatus() const {
return input_engine->GetColor(identifier);
}
void ForceUpdate() override {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Color,
.color_status = GetStatus(),
};
last_color_value = status.color_status;
TriggerOnChange(status);
}
void OnChange() {
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Color,
.color_status = GetStatus(),
};
if (status.color_status.body != last_color_value.body) {
last_color_value = status.color_status;
TriggerOnChange(status);
}
}
private:
const PadIdentifier identifier;
int callback_key;
Common::Input::BodyColorStatus last_color_value;
InputEngine* input_engine;
};
class InputFromMotion final : public Common::Input::InputDevice { class InputFromMotion final : public Common::Input::InputDevice {
public: public:
explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_, explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_,
@ -966,6 +1018,18 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateBatteryDevice(
return std::make_unique<InputFromBattery>(identifier, input_engine.get()); return std::make_unique<InputFromBattery>(identifier, input_engine.get());
} }
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateColorDevice(
const Common::ParamPackage& params) {
const PadIdentifier identifier = {
.guid = Common::UUID{params.Get("guid", "")},
.port = static_cast<std::size_t>(params.Get("port", 0)),
.pad = static_cast<std::size_t>(params.Get("pad", 0)),
};
input_engine->PreSetController(identifier);
return std::make_unique<InputFromColor>(identifier, input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateMotionDevice( std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateMotionDevice(
Common::ParamPackage params) { Common::ParamPackage params) {
const PadIdentifier identifier = { const PadIdentifier identifier = {
@ -1053,6 +1117,9 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::Create(
if (params.Has("battery")) { if (params.Has("battery")) {
return CreateBatteryDevice(params); return CreateBatteryDevice(params);
} }
if (params.Has("color")) {
return CreateColorDevice(params);
}
if (params.Has("camera")) { if (params.Has("camera")) {
return CreateCameraDevice(params); return CreateCameraDevice(params);
} }

View file

@ -190,6 +190,17 @@ private:
std::unique_ptr<Common::Input::InputDevice> CreateBatteryDevice( std::unique_ptr<Common::Input::InputDevice> CreateBatteryDevice(
const Common::ParamPackage& params); 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. * Creates a motion device from the parameters given.
* @param params contains parameters for creating the device: * @param params contains parameters for creating the device:

View file

@ -20,6 +20,7 @@
#include "input_common/input_poller.h" #include "input_common/input_poller.h"
#include "input_common/main.h" #include "input_common/main.h"
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
#include "input_common/drivers/joycon.h"
#include "input_common/drivers/sdl_driver.h" #include "input_common/drivers/sdl_driver.h"
#endif #endif
@ -53,6 +54,7 @@ struct InputSubsystem::Impl {
RegisterEngine("virtual_gamepad", virtual_gamepad); RegisterEngine("virtual_gamepad", virtual_gamepad);
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
RegisterEngine("sdl", sdl); RegisterEngine("sdl", sdl);
RegisterEngine("joycon", joycon);
#endif #endif
Common::Input::RegisterInputFactory("touch_from_button", Common::Input::RegisterInputFactory("touch_from_button",
@ -80,6 +82,7 @@ struct InputSubsystem::Impl {
UnregisterEngine(virtual_gamepad); UnregisterEngine(virtual_gamepad);
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
UnregisterEngine(sdl); UnregisterEngine(sdl);
UnregisterEngine(joycon);
#endif #endif
Common::Input::UnregisterInputFactory("touch_from_button"); Common::Input::UnregisterInputFactory("touch_from_button");
@ -100,6 +103,8 @@ struct InputSubsystem::Impl {
auto udp_devices = udp_client->GetInputDevices(); auto udp_devices = udp_client->GetInputDevices();
devices.insert(devices.end(), udp_devices.begin(), udp_devices.end()); devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
auto joycon_devices = joycon->GetInputDevices();
devices.insert(devices.end(), joycon_devices.begin(), joycon_devices.end());
auto sdl_devices = sdl->GetInputDevices(); auto sdl_devices = sdl->GetInputDevices();
devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end()); devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
#endif #endif
@ -129,6 +134,9 @@ struct InputSubsystem::Impl {
if (engine == sdl->GetEngineName()) { if (engine == sdl->GetEngineName()) {
return sdl; return sdl;
} }
if (engine == joycon->GetEngineName()) {
return joycon;
}
#endif #endif
return nullptr; return nullptr;
} }
@ -210,6 +218,9 @@ struct InputSubsystem::Impl {
if (engine == sdl->GetEngineName()) { if (engine == sdl->GetEngineName()) {
return true; return true;
} }
if (engine == joycon->GetEngineName()) {
return true;
}
#endif #endif
return false; return false;
} }
@ -221,6 +232,7 @@ struct InputSubsystem::Impl {
udp_client->BeginConfiguration(); udp_client->BeginConfiguration();
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
sdl->BeginConfiguration(); sdl->BeginConfiguration();
joycon->BeginConfiguration();
#endif #endif
} }
@ -231,6 +243,7 @@ struct InputSubsystem::Impl {
udp_client->EndConfiguration(); udp_client->EndConfiguration();
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
sdl->EndConfiguration(); sdl->EndConfiguration();
joycon->EndConfiguration();
#endif #endif
} }
@ -258,6 +271,7 @@ struct InputSubsystem::Impl {
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
std::shared_ptr<SDLDriver> sdl; std::shared_ptr<SDLDriver> sdl;
std::shared_ptr<Joycons> joycon;
#endif #endif
}; };

View file

@ -440,6 +440,7 @@ void Config::ReadControlValues() {
ReadBasicSetting(Settings::values.emulate_analog_keyboard); ReadBasicSetting(Settings::values.emulate_analog_keyboard);
Settings::values.mouse_panning = false; Settings::values.mouse_panning = false;
ReadBasicSetting(Settings::values.mouse_panning_sensitivity); ReadBasicSetting(Settings::values.mouse_panning_sensitivity);
ReadBasicSetting(Settings::values.enable_joycon_driver);
ReadBasicSetting(Settings::values.tas_enable); ReadBasicSetting(Settings::values.tas_enable);
ReadBasicSetting(Settings::values.tas_loop); ReadBasicSetting(Settings::values.tas_loop);
@ -1138,6 +1139,7 @@ void Config::SaveControlValues() {
WriteGlobalSetting(Settings::values.enable_accurate_vibrations); WriteGlobalSetting(Settings::values.enable_accurate_vibrations);
WriteGlobalSetting(Settings::values.motion_enabled); WriteGlobalSetting(Settings::values.motion_enabled);
WriteBasicSetting(Settings::values.enable_raw_input); WriteBasicSetting(Settings::values.enable_raw_input);
WriteBasicSetting(Settings::values.enable_joycon_driver);
WriteBasicSetting(Settings::values.keyboard_enabled); WriteBasicSetting(Settings::values.keyboard_enabled);
WriteBasicSetting(Settings::values.emulate_analog_keyboard); WriteBasicSetting(Settings::values.emulate_analog_keyboard);
WriteBasicSetting(Settings::values.mouse_panning_sensitivity); WriteBasicSetting(Settings::values.mouse_panning_sensitivity);

View file

@ -138,6 +138,7 @@ void ConfigureInputAdvanced::ApplyConfiguration() {
Settings::values.controller_navigation = ui->controller_navigation->isChecked(); Settings::values.controller_navigation = ui->controller_navigation->isChecked();
Settings::values.enable_ring_controller = ui->enable_ring_controller->isChecked(); Settings::values.enable_ring_controller = ui->enable_ring_controller->isChecked();
Settings::values.enable_ir_sensor = ui->enable_ir_sensor->isChecked(); Settings::values.enable_ir_sensor = ui->enable_ir_sensor->isChecked();
Settings::values.enable_joycon_driver = ui->enable_joycon_driver->isChecked();
} }
void ConfigureInputAdvanced::LoadConfiguration() { void ConfigureInputAdvanced::LoadConfiguration() {
@ -172,6 +173,7 @@ void ConfigureInputAdvanced::LoadConfiguration() {
ui->controller_navigation->setChecked(Settings::values.controller_navigation.GetValue()); ui->controller_navigation->setChecked(Settings::values.controller_navigation.GetValue());
ui->enable_ring_controller->setChecked(Settings::values.enable_ring_controller.GetValue()); ui->enable_ring_controller->setChecked(Settings::values.enable_ring_controller.GetValue());
ui->enable_ir_sensor->setChecked(Settings::values.enable_ir_sensor.GetValue()); ui->enable_ir_sensor->setChecked(Settings::values.enable_ir_sensor.GetValue());
ui->enable_joycon_driver->setChecked(Settings::values.enable_joycon_driver.GetValue());
UpdateUIEnabled(); UpdateUIEnabled();
} }

View file

@ -2696,6 +2696,22 @@
</widget> </widget>
</item> </item>
<item row="5" column="0"> <item row="5" column="0">
<widget class="QCheckBox" name="enable_joycon_driver">
<property name="toolTip">
<string>Requires restarting yuzu</string>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>23</height>
</size>
</property>
<property name="text">
<string>Enable direct JoyCon driver</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QCheckBox" name="mouse_panning"> <widget class="QCheckBox" name="mouse_panning">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@ -2708,7 +2724,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="2"> <item row="7" column="2">
<widget class="QSpinBox" name="mouse_panning_sensitivity"> <widget class="QSpinBox" name="mouse_panning_sensitivity">
<property name="toolTip"> <property name="toolTip">
<string>Mouse sensitivity</string> <string>Mouse sensitivity</string>
@ -2730,14 +2746,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0"> <item row="7" column="0">
<widget class="QLabel" name="motion_touch"> <widget class="QLabel" name="motion_touch">
<property name="text"> <property name="text">
<string>Motion / Touch</string> <string>Motion / Touch</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="2"> <item row="7" column="2">
<widget class="QPushButton" name="buttonMotionTouch"> <widget class="QPushButton" name="buttonMotionTouch">
<property name="text"> <property name="text">
<string>Configure</string> <string>Configure</string>

View file

@ -66,6 +66,18 @@ QString GetButtonName(Common::Input::ButtonNames button_name) {
return QObject::tr("R"); return QObject::tr("R");
case Common::Input::ButtonNames::TriggerL: case Common::Input::ButtonNames::TriggerL:
return QObject::tr("L"); return QObject::tr("L");
case Common::Input::ButtonNames::TriggerZR:
return QObject::tr("ZR");
case Common::Input::ButtonNames::TriggerZL:
return QObject::tr("ZL");
case Common::Input::ButtonNames::TriggerSR:
return QObject::tr("SR");
case Common::Input::ButtonNames::TriggerSL:
return QObject::tr("SL");
case Common::Input::ButtonNames::ButtonStickL:
return QObject::tr("Stick L");
case Common::Input::ButtonNames::ButtonStickR:
return QObject::tr("Stick R");
case Common::Input::ButtonNames::ButtonA: case Common::Input::ButtonNames::ButtonA:
return QObject::tr("A"); return QObject::tr("A");
case Common::Input::ButtonNames::ButtonB: case Common::Input::ButtonNames::ButtonB:
@ -76,6 +88,14 @@ QString GetButtonName(Common::Input::ButtonNames button_name) {
return QObject::tr("Y"); return QObject::tr("Y");
case Common::Input::ButtonNames::ButtonStart: case Common::Input::ButtonNames::ButtonStart:
return QObject::tr("Start"); return QObject::tr("Start");
case Common::Input::ButtonNames::ButtonPlus:
return QObject::tr("Plus");
case Common::Input::ButtonNames::ButtonMinus:
return QObject::tr("Minus");
case Common::Input::ButtonNames::ButtonHome:
return QObject::tr("Home");
case Common::Input::ButtonNames::ButtonCapture:
return QObject::tr("Capture");
case Common::Input::ButtonNames::L1: case Common::Input::ButtonNames::L1:
return QObject::tr("L1"); return QObject::tr("L1");
case Common::Input::ButtonNames::L2: case Common::Input::ButtonNames::L2:

View file

@ -103,9 +103,13 @@ void PlayerControlPreview::UpdateColors() {
colors.left = colors.primary; colors.left = colors.primary;
colors.right = colors.primary; colors.right = colors.primary;
// Possible alternative to set colors from settings
// colors.left = QColor(controller->GetColors().left.body); const auto color_left = controller->GetColorsValues()[0].body;
// colors.right = QColor(controller->GetColors().right.body); const auto color_right = controller->GetColorsValues()[1].body;
if (color_left != 0 && color_right != 0) {
colors.left = QColor(color_left);
colors.right = QColor(color_right);
}
} }
void PlayerControlPreview::ResetInputs() { void PlayerControlPreview::ResetInputs() {

View file

@ -6,7 +6,7 @@
#include <QMenu> #include <QMenu>
#include <QTimer> #include <QTimer>
#include "core/hid/emulated_devices.h" #include "core/hid/emulated_controller.h"
#include "core/hid/hid_core.h" #include "core/hid/hid_core.h"
#include "input_common/drivers/keyboard.h" #include "input_common/drivers/keyboard.h"
#include "input_common/drivers/mouse.h" #include "input_common/drivers/mouse.h"
@ -126,9 +126,9 @@ ConfigureRingController::ConfigureRingController(QWidget* parent,
ui->buttonRingAnalogPush, ui->buttonRingAnalogPush,
}; };
emulated_device = hid_core_.GetEmulatedDevices(); emulated_controller = hid_core_.GetEmulatedController(Core::HID::NpadIdType::Player1);
emulated_device->SaveCurrentConfig(); emulated_controller->SaveCurrentConfig();
emulated_device->EnableConfiguration(); emulated_controller->EnableConfiguration();
LoadConfiguration(); LoadConfiguration();
@ -143,9 +143,9 @@ ConfigureRingController::ConfigureRingController(QWidget* parent,
HandleClick( HandleClick(
analog_map_buttons[sub_button_id], analog_map_buttons[sub_button_id],
[=, this](const Common::ParamPackage& params) { [=, this](const Common::ParamPackage& params) {
Common::ParamPackage param = emulated_device->GetRingParam(); Common::ParamPackage param = emulated_controller->GetRingParam();
SetAnalogParam(params, param, analog_sub_buttons[sub_button_id]); SetAnalogParam(params, param, analog_sub_buttons[sub_button_id]);
emulated_device->SetRingParam(param); emulated_controller->SetRingParam(param);
}, },
InputCommon::Polling::InputType::Stick); InputCommon::Polling::InputType::Stick);
}); });
@ -155,16 +155,16 @@ ConfigureRingController::ConfigureRingController(QWidget* parent,
connect(analog_button, &QPushButton::customContextMenuRequested, connect(analog_button, &QPushButton::customContextMenuRequested,
[=, this](const QPoint& menu_location) { [=, this](const QPoint& menu_location) {
QMenu context_menu; QMenu context_menu;
Common::ParamPackage param = emulated_device->GetRingParam(); Common::ParamPackage param = emulated_controller->GetRingParam();
context_menu.addAction(tr("Clear"), [&] { context_menu.addAction(tr("Clear"), [&] {
emulated_device->SetRingParam({}); emulated_controller->SetRingParam(param);
analog_map_buttons[sub_button_id]->setText(tr("[not set]")); analog_map_buttons[sub_button_id]->setText(tr("[not set]"));
}); });
context_menu.addAction(tr("Invert axis"), [&] { context_menu.addAction(tr("Invert axis"), [&] {
const bool invert_value = param.Get("invert_x", "+") == "-"; const bool invert_value = param.Get("invert_x", "+") == "-";
const std::string invert_str = invert_value ? "+" : "-"; const std::string invert_str = invert_value ? "+" : "-";
param.Set("invert_x", invert_str); param.Set("invert_x", invert_str);
emulated_device->SetRingParam(param); emulated_controller->SetRingParam(param);
for (int sub_button_id2 = 0; sub_button_id2 < ANALOG_SUB_BUTTONS_NUM; for (int sub_button_id2 = 0; sub_button_id2 < ANALOG_SUB_BUTTONS_NUM;
++sub_button_id2) { ++sub_button_id2) {
analog_map_buttons[sub_button_id2]->setText( analog_map_buttons[sub_button_id2]->setText(
@ -177,11 +177,11 @@ ConfigureRingController::ConfigureRingController(QWidget* parent,
} }
connect(ui->sliderRingAnalogDeadzone, &QSlider::valueChanged, [=, this] { connect(ui->sliderRingAnalogDeadzone, &QSlider::valueChanged, [=, this] {
Common::ParamPackage param = emulated_device->GetRingParam(); Common::ParamPackage param = emulated_controller->GetRingParam();
const auto slider_value = ui->sliderRingAnalogDeadzone->value(); const auto slider_value = ui->sliderRingAnalogDeadzone->value();
ui->labelRingAnalogDeadzone->setText(tr("Deadzone: %1%").arg(slider_value)); ui->labelRingAnalogDeadzone->setText(tr("Deadzone: %1%").arg(slider_value));
param.Set("deadzone", slider_value / 100.0f); param.Set("deadzone", slider_value / 100.0f);
emulated_device->SetRingParam(param); emulated_controller->SetRingParam(param);
}); });
connect(ui->restore_defaults_button, &QPushButton::clicked, this, connect(ui->restore_defaults_button, &QPushButton::clicked, this,
@ -202,7 +202,7 @@ ConfigureRingController::ConfigureRingController(QWidget* parent,
} }
ConfigureRingController::~ConfigureRingController() { ConfigureRingController::~ConfigureRingController() {
emulated_device->DisableConfiguration(); emulated_controller->DisableConfiguration();
}; };
void ConfigureRingController::changeEvent(QEvent* event) { void ConfigureRingController::changeEvent(QEvent* event) {
@ -219,7 +219,7 @@ void ConfigureRingController::RetranslateUI() {
void ConfigureRingController::UpdateUI() { void ConfigureRingController::UpdateUI() {
RetranslateUI(); RetranslateUI();
const Common::ParamPackage param = emulated_device->GetRingParam(); const Common::ParamPackage param = emulated_controller->GetRingParam();
for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) { for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; ++sub_button_id) {
auto* const analog_button = analog_map_buttons[sub_button_id]; auto* const analog_button = analog_map_buttons[sub_button_id];
@ -240,9 +240,9 @@ void ConfigureRingController::UpdateUI() {
} }
void ConfigureRingController::ApplyConfiguration() { void ConfigureRingController::ApplyConfiguration() {
emulated_device->DisableConfiguration(); emulated_controller->DisableConfiguration();
emulated_device->SaveCurrentConfig(); emulated_controller->SaveCurrentConfig();
emulated_device->EnableConfiguration(); emulated_controller->EnableConfiguration();
} }
void ConfigureRingController::LoadConfiguration() { void ConfigureRingController::LoadConfiguration() {
@ -252,7 +252,7 @@ void ConfigureRingController::LoadConfiguration() {
void ConfigureRingController::RestoreDefaults() { void ConfigureRingController::RestoreDefaults() {
const std::string default_ring_string = InputCommon::GenerateAnalogParamFromKeys( const std::string default_ring_string = InputCommon::GenerateAnalogParamFromKeys(
0, 0, Config::default_ringcon_analogs[0], Config::default_ringcon_analogs[1], 0, 0.05f); 0, 0, Config::default_ringcon_analogs[0], Config::default_ringcon_analogs[1], 0, 0.05f);
emulated_device->SetRingParam(Common::ParamPackage(default_ring_string)); emulated_controller->SetRingParam(Common::ParamPackage(default_ring_string));
UpdateUI(); UpdateUI();
} }

View file

@ -13,7 +13,7 @@ class InputSubsystem;
namespace Core::HID { namespace Core::HID {
class HIDCore; class HIDCore;
class EmulatedDevices; class EmulatedController;
} // namespace Core::HID } // namespace Core::HID
namespace Ui { namespace Ui {
@ -78,7 +78,7 @@ private:
std::optional<std::function<void(const Common::ParamPackage&)>> input_setter; std::optional<std::function<void(const Common::ParamPackage&)>> input_setter;
InputCommon::InputSubsystem* input_subsystem; InputCommon::InputSubsystem* input_subsystem;
Core::HID::EmulatedDevices* emulated_device; Core::HID::EmulatedController* emulated_controller;
std::unique_ptr<Ui::ConfigureRingController> ui; std::unique_ptr<Ui::ConfigureRingController> ui;
}; };

View file

@ -390,7 +390,7 @@ private:
GameList* game_list; GameList* game_list;
LoadingScreen* loading_screen; LoadingScreen* loading_screen;
QTimer shutdown_timer; QTimer shutdown_timer;
OverlayDialog* shutdown_dialog; OverlayDialog* shutdown_dialog{};
GameListPlaceholder* game_list_placeholder; GameListPlaceholder* game_list_placeholder;