Merge pull request #9492 from german77/joycon_release

Input_common: Implement custom joycon driver v2
master
liamwhite 2023-01-24 09:29:37 +07:00 committed by GitHub
commit a68af583ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 5824 additions and 420 deletions

@ -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 {
@ -62,21 +64,22 @@ enum class CameraFormat {
None, None,
}; };
// Vibration reply from the controller // Different results that can happen from a device request
enum class VibrationError { enum class DriverResult {
None, Success,
WrongReply,
Timeout,
UnsupportedControllerType,
HandleInUse,
ErrorReadingData,
ErrorWritingData,
NoDeviceDetected,
InvalidHandle,
NotSupported, NotSupported,
Disabled, Disabled,
Unknown, Unknown,
}; };
// Polling mode reply from the controller
enum class PollingError {
None,
NotSupported,
Unknown,
};
// Nfc reply from the controller // Nfc reply from the controller
enum class NfcState { enum class NfcState {
Success, Success,
@ -90,13 +93,6 @@ enum class NfcState {
Unknown, Unknown,
}; };
// Ir camera reply from the controller
enum class CameraError {
None,
NotSupported,
Unknown,
};
// Hint for amplification curve to be used // Hint for amplification curve to be used
enum class VibrationAmplificationType { enum class VibrationAmplificationType {
Linear, Linear,
@ -190,6 +186,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 +226,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
@ -316,22 +328,24 @@ class OutputDevice {
public: public:
virtual ~OutputDevice() = default; virtual ~OutputDevice() = default;
virtual void SetLED([[maybe_unused]] const LedStatus& led_status) {} virtual DriverResult SetLED([[maybe_unused]] const LedStatus& led_status) {
return DriverResult::NotSupported;
}
virtual VibrationError SetVibration([[maybe_unused]] const VibrationStatus& vibration_status) { virtual DriverResult SetVibration([[maybe_unused]] const VibrationStatus& vibration_status) {
return VibrationError::NotSupported; return DriverResult::NotSupported;
} }
virtual bool IsVibrationEnabled() { virtual bool IsVibrationEnabled() {
return false; return false;
} }
virtual PollingError SetPollingMode([[maybe_unused]] PollingMode polling_mode) { virtual DriverResult SetPollingMode([[maybe_unused]] PollingMode polling_mode) {
return PollingError::NotSupported; return DriverResult::NotSupported;
} }
virtual CameraError SetCameraFormat([[maybe_unused]] CameraFormat camera_format) { virtual DriverResult SetCameraFormat([[maybe_unused]] CameraFormat camera_format) {
return CameraError::NotSupported; return DriverResult::NotSupported;
} }
virtual NfcState SupportsNfc() const { virtual NfcState SupportsNfc() const {

@ -483,6 +483,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{true, "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"};

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm> #include <algorithm>
#include <common/scope_exit.h>
#include "common/polyfill_ranges.h" #include "common/polyfill_ranges.h"
#include "common/thread.h" #include "common/thread.h"
@ -94,6 +95,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),
@ -107,6 +109,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));
@ -133,18 +137,28 @@ 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[0] = right_joycon;
nfc_params = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"}; camera_params[0].Set("camera", true);
camera_params[1] = Common::ParamPackage{"engine:camera,camera:1"};
ring_params[1] = Common::ParamPackage{"engine:joycon,axis_x:100,axis_y:101"};
nfc_params[0] = Common::ParamPackage{"engine:virtual_amiibo,nfc:1"};
nfc_params[1] = right_joycon;
nfc_params[1].Set("nfc", true);
output_params[LeftIndex] = left_joycon; output_params[LeftIndex] = left_joycon;
output_params[RightIndex] = right_joycon; output_params[RightIndex] = right_joycon;
output_params[2] = camera_params; output_params[2] = camera_params[1];
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);
@ -160,8 +174,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);
camera_devices = Common::Input::CreateInputDevice(camera_params); std::ranges::transform(color_params, color_devices.begin(), Common::Input::CreateInputDevice);
nfc_devices = Common::Input::CreateInputDevice(nfc_params); std::ranges::transform(camera_params, camera_devices.begin(), Common::Input::CreateInputDevice);
std::ranges::transform(ring_params, ring_analog_devices.begin(),
Common::Input::CreateInputDevice);
std::ranges::transform(nfc_params, nfc_devices.begin(), Common::Input::CreateInputDevice);
std::ranges::transform(output_params, output_devices.begin(), std::ranges::transform(output_params, output_devices.begin(),
Common::Input::CreateOutputDevice); Common::Input::CreateOutputDevice);
@ -323,6 +340,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;
@ -336,22 +366,37 @@ void EmulatedController::ReloadInput() {
motion_devices[index]->ForceUpdate(); motion_devices[index]->ForceUpdate();
} }
if (camera_devices) { for (std::size_t index = 0; index < camera_devices.size(); ++index) {
camera_devices->SetCallback({ if (!camera_devices[index]) {
continue;
}
camera_devices[index]->SetCallback({
.on_change = .on_change =
[this](const Common::Input::CallbackStatus& callback) { SetCamera(callback); }, [this](const Common::Input::CallbackStatus& callback) { SetCamera(callback); },
}); });
camera_devices->ForceUpdate(); camera_devices[index]->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;
}
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 = .on_change =
[this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); }, [this](const Common::Input::CallbackStatus& callback) { SetNfc(callback); },
}); });
nfc_devices->ForceUpdate(); nfc_devices[index]->ForceUpdate();
}
} }
// Register TAS devices. No need to force update // Register TAS devices. No need to force update
@ -421,6 +466,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();
} }
@ -436,8 +484,15 @@ void EmulatedController::UnloadInput() {
for (auto& stick : virtual_stick_devices) { for (auto& stick : virtual_stick_devices) {
stick.reset(); stick.reset();
} }
camera_devices.reset(); for (auto& camera : camera_devices) {
nfc_devices.reset(); camera.reset();
}
for (auto& ring : ring_analog_devices) {
ring.reset();
}
for (auto& nfc : nfc_devices) {
nfc.reset();
}
} }
void EmulatedController::EnableConfiguration() { void EmulatedController::EnableConfiguration() {
@ -449,6 +504,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) {
@ -502,6 +562,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() {
@ -773,17 +836,21 @@ void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback,
if (index >= controller.stick_values.size()) { if (index >= controller.stick_values.size()) {
return; return;
} }
std::unique_lock lock{mutex}; auto trigger_guard =
SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Stick, !is_configuring); });
std::scoped_lock lock{mutex};
const auto stick_value = TransformToStick(callback); const auto stick_value = TransformToStick(callback);
// Only read stick values that have the same uuid or are over the threshold to avoid flapping // Only read stick values that have the same uuid or are over the threshold to avoid flapping
if (controller.stick_values[index].uuid != uuid) { if (controller.stick_values[index].uuid != uuid) {
const bool is_tas = uuid == TAS_UUID; const bool is_tas = uuid == TAS_UUID;
if (is_tas && stick_value.x.value == 0 && stick_value.y.value == 0) { if (is_tas && stick_value.x.value == 0 && stick_value.y.value == 0) {
trigger_guard.Cancel();
return; return;
} }
if (!is_tas && !stick_value.down && !stick_value.up && !stick_value.left && if (!is_tas && !stick_value.down && !stick_value.up && !stick_value.left &&
!stick_value.right) { !stick_value.right) {
trigger_guard.Cancel();
return; return;
} }
} }
@ -794,8 +861,6 @@ void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback,
if (is_configuring) { if (is_configuring) {
controller.analog_stick_state.left = {}; controller.analog_stick_state.left = {};
controller.analog_stick_state.right = {}; controller.analog_stick_state.right = {};
lock.unlock();
TriggerOnChange(ControllerTriggerType::Stick, false);
return; return;
} }
@ -827,9 +892,6 @@ void EmulatedController::SetStick(const Common::Input::CallbackStatus& callback,
controller.npad_button_state.stick_r_down.Assign(controller.stick_values[index].down); controller.npad_button_state.stick_r_down.Assign(controller.stick_values[index].down);
break; break;
} }
lock.unlock();
TriggerOnChange(ControllerTriggerType::Stick, true);
} }
void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callback, void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callback,
@ -837,7 +899,9 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac
if (index >= controller.trigger_values.size()) { if (index >= controller.trigger_values.size()) {
return; return;
} }
std::unique_lock lock{mutex}; auto trigger_guard =
SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Trigger, !is_configuring); });
std::scoped_lock lock{mutex};
const auto trigger_value = TransformToTrigger(callback); const auto trigger_value = TransformToTrigger(callback);
// Only read trigger values that have the same uuid or are pressed once // Only read trigger values that have the same uuid or are pressed once
@ -853,13 +917,12 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac
if (is_configuring) { if (is_configuring) {
controller.gc_trigger_state.left = 0; controller.gc_trigger_state.left = 0;
controller.gc_trigger_state.right = 0; controller.gc_trigger_state.right = 0;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Trigger, false);
return; return;
} }
// Only GC controllers have analog triggers // Only GC controllers have analog triggers
if (npad_type != NpadStyleIndex::GameCube) { if (npad_type != NpadStyleIndex::GameCube) {
trigger_guard.Cancel();
return; return;
} }
@ -876,9 +939,6 @@ void EmulatedController::SetTrigger(const Common::Input::CallbackStatus& callbac
controller.npad_button_state.zr.Assign(trigger.pressed.value); controller.npad_button_state.zr.Assign(trigger.pressed.value);
break; break;
} }
lock.unlock();
TriggerOnChange(ControllerTriggerType::Trigger, true);
} }
void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback, void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback,
@ -886,7 +946,8 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback
if (index >= controller.motion_values.size()) { if (index >= controller.motion_values.size()) {
return; return;
} }
std::unique_lock lock{mutex}; SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Motion, !is_configuring); });
std::scoped_lock lock{mutex};
auto& raw_status = controller.motion_values[index].raw_status; auto& raw_status = controller.motion_values[index].raw_status;
auto& emulated = controller.motion_values[index].emulated; auto& emulated = controller.motion_values[index].emulated;
@ -907,8 +968,6 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback
force_update_motion = raw_status.force_update; force_update_motion = raw_status.force_update;
if (is_configuring) { if (is_configuring) {
lock.unlock();
TriggerOnChange(ControllerTriggerType::Motion, false);
return; return;
} }
@ -918,9 +977,56 @@ void EmulatedController::SetMotion(const Common::Input::CallbackStatus& callback
motion.rotation = emulated.GetRotations(); motion.rotation = emulated.GetRotations();
motion.orientation = emulated.GetOrientation(); motion.orientation = emulated.GetOrientation();
motion.is_at_rest = !emulated.IsMoving(motion_sensitivity); motion.is_at_rest = !emulated.IsMoving(motion_sensitivity);
}
lock.unlock(); void EmulatedController::SetColors(const Common::Input::CallbackStatus& callback,
TriggerOnChange(ControllerTriggerType::Motion, true); std::size_t index) {
if (index >= controller.color_values.size()) {
return;
}
auto trigger_guard =
SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Color, !is_configuring); });
std::scoped_lock lock{mutex};
controller.color_values[index] = TransformToColor(callback);
if (is_configuring) {
return;
}
if (controller.color_values[index].body == 0) {
trigger_guard.Cancel();
return;
}
controller.colors_state.fullkey = {
.body = GetNpadColor(controller.color_values[index].body),
.button = GetNpadColor(controller.color_values[index].buttons),
};
if (npad_type == NpadStyleIndex::ProController) {
controller.colors_state.left = {
.body = GetNpadColor(controller.color_values[index].left_grip),
.button = GetNpadColor(controller.color_values[index].buttons),
};
controller.colors_state.right = {
.body = GetNpadColor(controller.color_values[index].right_grip),
.button = GetNpadColor(controller.color_values[index].buttons),
};
} else {
switch (index) {
case LeftIndex:
controller.colors_state.left = {
.body = GetNpadColor(controller.color_values[index].body),
.button = GetNpadColor(controller.color_values[index].buttons),
};
break;
case RightIndex:
controller.colors_state.right = {
.body = GetNpadColor(controller.color_values[index].body),
.button = GetNpadColor(controller.color_values[index].buttons),
};
break;
}
}
} }
void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback, void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callback,
@ -928,12 +1034,11 @@ void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callbac
if (index >= controller.battery_values.size()) { if (index >= controller.battery_values.size()) {
return; return;
} }
std::unique_lock lock{mutex}; SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Battery, !is_configuring); });
std::scoped_lock lock{mutex};
controller.battery_values[index] = TransformToBattery(callback); controller.battery_values[index] = TransformToBattery(callback);
if (is_configuring) { if (is_configuring) {
lock.unlock();
TriggerOnChange(ControllerTriggerType::Battery, false);
return; return;
} }
@ -989,18 +1094,14 @@ void EmulatedController::SetBattery(const Common::Input::CallbackStatus& callbac
}; };
break; break;
} }
lock.unlock();
TriggerOnChange(ControllerTriggerType::Battery, true);
} }
void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback) { void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback) {
std::unique_lock lock{mutex}; SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::IrSensor, !is_configuring); });
std::scoped_lock lock{mutex};
controller.camera_values = TransformToCamera(callback); controller.camera_values = TransformToCamera(callback);
if (is_configuring) { if (is_configuring) {
lock.unlock();
TriggerOnChange(ControllerTriggerType::IrSensor, false);
return; return;
} }
@ -1008,18 +1109,28 @@ void EmulatedController::SetCamera(const Common::Input::CallbackStatus& callback
controller.camera_state.format = controller.camera_state.format =
static_cast<Core::IrSensor::ImageTransferProcessorFormat>(controller.camera_values.format); static_cast<Core::IrSensor::ImageTransferProcessorFormat>(controller.camera_values.format);
controller.camera_state.data = controller.camera_values.data; controller.camera_state.data = controller.camera_values.data;
}
lock.unlock(); void EmulatedController::SetRingAnalog(const Common::Input::CallbackStatus& callback) {
TriggerOnChange(ControllerTriggerType::IrSensor, true); SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::RingController, !is_configuring); });
std::scoped_lock lock{mutex};
const auto force_value = TransformToStick(callback);
controller.ring_analog_value = force_value.x;
if (is_configuring) {
return;
}
controller.ring_analog_state.force = force_value.x.value;
} }
void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) { void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) {
std::unique_lock lock{mutex}; SCOPE_EXIT({ TriggerOnChange(ControllerTriggerType::Nfc, !is_configuring); });
std::scoped_lock lock{mutex};
controller.nfc_values = TransformToNfc(callback); controller.nfc_values = TransformToNfc(callback);
if (is_configuring) { if (is_configuring) {
lock.unlock();
TriggerOnChange(ControllerTriggerType::Nfc, false);
return; return;
} }
@ -1027,9 +1138,6 @@ void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) {
controller.nfc_values.state, controller.nfc_values.state,
controller.nfc_values.data, controller.nfc_values.data,
}; };
lock.unlock();
TriggerOnChange(ControllerTriggerType::Nfc, true);
} }
bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue vibration) { bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue vibration) {
@ -1061,7 +1169,7 @@ bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue v
.type = type, .type = type,
}; };
return output_devices[device_index]->SetVibration(status) == return output_devices[device_index]->SetVibration(status) ==
Common::Input::VibrationError::None; Common::Input::DriverResult::Success;
} }
bool EmulatedController::IsVibrationEnabled(std::size_t device_index) { bool EmulatedController::IsVibrationEnabled(std::size_t device_index) {
@ -1083,16 +1191,32 @@ bool EmulatedController::IsVibrationEnabled(std::size_t device_index) {
return output_devices[device_index]->IsVibrationEnabled(); return output_devices[device_index]->IsVibrationEnabled();
} }
bool EmulatedController::SetPollingMode(Common::Input::PollingMode polling_mode) { Common::Input::DriverResult EmulatedController::SetPollingMode(
LOG_INFO(Service_HID, "Set polling mode {}", polling_mode); EmulatedDeviceIndex device_index, Common::Input::PollingMode polling_mode) {
auto& output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)]; LOG_INFO(Service_HID, "Set polling mode {}, device_index={}", polling_mode, device_index);
auto& left_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Left)];
auto& right_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_output_device = output_devices[3]; auto& nfc_output_device = output_devices[3];
const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode); if (device_index == EmulatedDeviceIndex::LeftIndex) {
const auto mapped_nfc_result = output_device->SetPollingMode(polling_mode); return left_output_device->SetPollingMode(polling_mode);
}
return virtual_nfc_result == Common::Input::PollingError::None || if (device_index == EmulatedDeviceIndex::RightIndex) {
mapped_nfc_result == Common::Input::PollingError::None; const auto virtual_nfc_result = nfc_output_device->SetPollingMode(polling_mode);
const auto mapped_nfc_result = right_output_device->SetPollingMode(polling_mode);
if (virtual_nfc_result == Common::Input::DriverResult::Success) {
return virtual_nfc_result;
}
return mapped_nfc_result;
}
left_output_device->SetPollingMode(polling_mode);
right_output_device->SetPollingMode(polling_mode);
nfc_output_device->SetPollingMode(polling_mode);
return Common::Input::DriverResult::Success;
} }
bool EmulatedController::SetCameraFormat( bool EmulatedController::SetCameraFormat(
@ -1103,13 +1227,22 @@ bool EmulatedController::SetCameraFormat(
auto& camera_output_device = output_devices[2]; auto& camera_output_device = output_devices[2];
if (right_output_device->SetCameraFormat(static_cast<Common::Input::CameraFormat>( if (right_output_device->SetCameraFormat(static_cast<Common::Input::CameraFormat>(
camera_format)) == Common::Input::CameraError::None) { camera_format)) == Common::Input::DriverResult::Success) {
return true; return true;
} }
// Fallback to Qt camera if native device doesn't have support // Fallback to Qt camera if native device doesn't have support
return camera_output_device->SetCameraFormat(static_cast<Common::Input::CameraFormat>( return camera_output_device->SetCameraFormat(static_cast<Common::Input::CameraFormat>(
camera_format)) == Common::Input::CameraError::None; camera_format)) == Common::Input::DriverResult::Success;
}
Common::ParamPackage EmulatedController::GetRingParam() const {
return ring_params[0];
}
void EmulatedController::SetRingParam(Common::ParamPackage param) {
ring_params[0] = std::move(param);
ReloadInput();
} }
bool EmulatedController::HasNfc() const { bool EmulatedController::HasNfc() const {
@ -1263,39 +1396,35 @@ void EmulatedController::Connect(bool use_temporary_value) {
return; return;
} }
std::unique_lock lock{mutex}; auto trigger_guard =
SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Connected, !is_configuring); });
std::scoped_lock lock{mutex};
if (is_configuring) { if (is_configuring) {
tmp_is_connected = true; tmp_is_connected = true;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Connected, false);
return; return;
} }
if (is_connected) { if (is_connected) {
trigger_guard.Cancel();
return; return;
} }
is_connected = true; is_connected = true;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Connected, true);
} }
void EmulatedController::Disconnect() { void EmulatedController::Disconnect() {
std::unique_lock lock{mutex}; auto trigger_guard =
SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Disconnected, !is_configuring); });
std::scoped_lock lock{mutex};
if (is_configuring) { if (is_configuring) {
tmp_is_connected = false; tmp_is_connected = false;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Disconnected, false);
return; return;
} }
if (!is_connected) { if (!is_connected) {
trigger_guard.Cancel();
return; return;
} }
is_connected = false; is_connected = false;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Disconnected, true);
} }
bool EmulatedController::IsConnected(bool get_temporary_value) const { bool EmulatedController::IsConnected(bool get_temporary_value) const {
@ -1320,19 +1449,21 @@ NpadStyleIndex EmulatedController::GetNpadStyleIndex(bool get_temporary_value) c
} }
void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) { void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) {
std::unique_lock lock{mutex}; auto trigger_guard =
SCOPE_GUARD({ TriggerOnChange(ControllerTriggerType::Type, !is_configuring); });
std::scoped_lock lock{mutex};
if (is_configuring) { if (is_configuring) {
if (tmp_npad_type == npad_type_) { if (tmp_npad_type == npad_type_) {
trigger_guard.Cancel();
return; return;
} }
tmp_npad_type = npad_type_; tmp_npad_type = npad_type_;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Type, false);
return; return;
} }
if (npad_type == npad_type_) { if (npad_type == npad_type_) {
trigger_guard.Cancel();
return; return;
} }
if (is_connected) { if (is_connected) {
@ -1340,9 +1471,6 @@ void EmulatedController::SetNpadStyleIndex(NpadStyleIndex npad_type_) {
NpadIdTypeToIndex(npad_id_type)); NpadIdTypeToIndex(npad_id_type));
} }
npad_type = npad_type_; npad_type = npad_type_;
lock.unlock();
TriggerOnChange(ControllerTriggerType::Type, true);
} }
LedPattern EmulatedController::GetLedPattern() const { LedPattern EmulatedController::GetLedPattern() const {
@ -1403,6 +1531,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) {
@ -1436,7 +1568,7 @@ DebugPadButton EmulatedController::GetDebugPadButtons() const {
} }
AnalogSticks EmulatedController::GetSticks() const { AnalogSticks EmulatedController::GetSticks() const {
std::unique_lock lock{mutex}; std::scoped_lock lock{mutex};
if (is_configuring) { if (is_configuring) {
return {}; return {};
@ -1486,6 +1618,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;

@ -35,19 +35,27 @@ 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 =
using NfcDevices = std::unique_ptr<Common::Input::InputDevice>; std::array<std::unique_ptr<Common::Input::InputDevice>, max_emulated_controllers>;
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 = std::array<Common::ParamPackage, max_emulated_controllers>;
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 +66,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 +93,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 +129,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 +143,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 +156,7 @@ enum class ControllerTriggerType {
Battery, Battery,
Vibration, Vibration,
IrSensor, IrSensor,
RingController,
Nfc, Nfc,
Connected, Connected,
Disconnected, Disconnected,
@ -294,6 +310,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 +343,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;
@ -341,10 +363,12 @@ public:
/** /**
* Sets the desired data to be polled from a controller * Sets the desired data to be polled from a controller
* @param device_index index of the controller to set the polling mode
* @param polling_mode type of input desired buttons, gyro, nfc, ir, etc. * @param polling_mode type of input desired buttons, gyro, nfc, ir, etc.
* @return true if SetPollingMode was successfull * @return driver result from this command
*/ */
bool SetPollingMode(Common::Input::PollingMode polling_mode); Common::Input::DriverResult SetPollingMode(EmulatedDeviceIndex device_index,
Common::Input::PollingMode polling_mode);
/** /**
* Sets the desired camera format to be polled from a controller * Sets the desired camera format to be polled from a controller
@ -353,6 +377,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 +465,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 +485,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 +530,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 +541,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;

@ -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) {

@ -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;

@ -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;

@ -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

@ -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,20 @@ 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();
if (controller_type == Core::HID::NpadStyleIndex::JoyconDual) {
if (controller.is_dual_left_connected) {
controller.device->SetPollingMode(Core::HID::EmulatedDeviceIndex::LeftIndex,
Common::Input::PollingMode::Active);
}
if (controller.is_dual_right_connected) {
controller.device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
}
} else {
controller.device->SetPollingMode(Core::HID::EmulatedDeviceIndex::AllDevices,
Common::Input::PollingMode::Active);
}
SignalStyleSetChangedEvent(npad_id); SignalStyleSetChangedEvent(npad_id);
WriteEmptyEntry(controller.shared_memory); WriteEmptyEntry(controller.shared_memory);
} }

@ -297,7 +297,7 @@ 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,
@ -326,7 +326,7 @@ 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,

@ -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,20 @@ 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(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Ring);
return; return;
} }
void RingController::OnRelease() { void RingController::OnRelease() {
input->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
return; return;
}; };

@ -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

@ -108,6 +108,8 @@ void IRS::StopImageProcessor(Kernel::HLERequestContext& ctx) {
auto result = IsIrCameraHandleValid(parameters.camera_handle); auto result = IsIrCameraHandleValid(parameters.camera_handle);
if (result.IsSuccess()) { if (result.IsSuccess()) {
// TODO: Stop Image processor // TODO: Stop Image processor
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
result = ResultSuccess; result = ResultSuccess;
} }
@ -139,6 +141,8 @@ void IRS::RunMomentProcessor(Kernel::HLERequestContext& ctx) {
MakeProcessor<MomentProcessor>(parameters.camera_handle, device); MakeProcessor<MomentProcessor>(parameters.camera_handle, device);
auto& image_transfer_processor = GetProcessor<MomentProcessor>(parameters.camera_handle); auto& image_transfer_processor = GetProcessor<MomentProcessor>(parameters.camera_handle);
image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetConfig(parameters.processor_config);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -170,6 +174,8 @@ void IRS::RunClusteringProcessor(Kernel::HLERequestContext& ctx) {
auto& image_transfer_processor = auto& image_transfer_processor =
GetProcessor<ClusteringProcessor>(parameters.camera_handle); GetProcessor<ClusteringProcessor>(parameters.camera_handle);
image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetConfig(parameters.processor_config);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -219,6 +225,8 @@ void IRS::RunImageTransferProcessor(Kernel::HLERequestContext& ctx) {
GetProcessor<ImageTransferProcessor>(parameters.camera_handle); GetProcessor<ImageTransferProcessor>(parameters.camera_handle);
image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetConfig(parameters.processor_config);
image_transfer_processor.SetTransferMemoryPointer(transfer_memory); image_transfer_processor.SetTransferMemoryPointer(transfer_memory);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -294,6 +302,8 @@ void IRS::RunTeraPluginProcessor(Kernel::HLERequestContext& ctx) {
auto& image_transfer_processor = auto& image_transfer_processor =
GetProcessor<TeraPluginProcessor>(parameters.camera_handle); GetProcessor<TeraPluginProcessor>(parameters.camera_handle);
image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetConfig(parameters.processor_config);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -343,6 +353,8 @@ void IRS::RunPointingProcessor(Kernel::HLERequestContext& ctx) {
MakeProcessor<PointingProcessor>(camera_handle, device); MakeProcessor<PointingProcessor>(camera_handle, device);
auto& image_transfer_processor = GetProcessor<PointingProcessor>(camera_handle); auto& image_transfer_processor = GetProcessor<PointingProcessor>(camera_handle);
image_transfer_processor.SetConfig(processor_config); image_transfer_processor.SetConfig(processor_config);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -453,6 +465,8 @@ void IRS::RunImageTransferExProcessor(Kernel::HLERequestContext& ctx) {
GetProcessor<ImageTransferProcessor>(parameters.camera_handle); GetProcessor<ImageTransferProcessor>(parameters.camera_handle);
image_transfer_processor.SetConfig(parameters.processor_config); image_transfer_processor.SetConfig(parameters.processor_config);
image_transfer_processor.SetTransferMemoryPointer(transfer_memory); image_transfer_processor.SetTransferMemoryPointer(transfer_memory);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -479,6 +493,8 @@ void IRS::RunIrLedProcessor(Kernel::HLERequestContext& ctx) {
MakeProcessor<IrLedProcessor>(camera_handle, device); MakeProcessor<IrLedProcessor>(camera_handle, device);
auto& image_transfer_processor = GetProcessor<IrLedProcessor>(camera_handle); auto& image_transfer_processor = GetProcessor<IrLedProcessor>(camera_handle);
image_transfer_processor.SetConfig(processor_config); image_transfer_processor.SetConfig(processor_config);
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::IR);
} }
IPC::ResponseBuilder rb{ctx, 2}; IPC::ResponseBuilder rb{ctx, 2};
@ -504,6 +520,8 @@ void IRS::StopImageProcessorAsync(Kernel::HLERequestContext& ctx) {
auto result = IsIrCameraHandleValid(parameters.camera_handle); auto result = IsIrCameraHandleValid(parameters.camera_handle);
if (result.IsSuccess()) { if (result.IsSuccess()) {
// TODO: Stop image processor async // TODO: Stop image processor async
npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
result = ResultSuccess; result = ResultSuccess;
} }

@ -130,7 +130,9 @@ Result NfcDevice::StartDetection(NFP::TagProtocol allowed_protocol) {
return WrongDeviceState; return WrongDeviceState;
} }
if (!npad_device->SetPollingMode(Common::Input::PollingMode::NFC)) { if (npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::NFC) !=
Common::Input::DriverResult::Success) {
LOG_ERROR(Service_NFC, "Nfc not supported"); LOG_ERROR(Service_NFC, "Nfc not supported");
return NfcDisabled; return NfcDisabled;
} }
@ -141,7 +143,8 @@ Result NfcDevice::StartDetection(NFP::TagProtocol allowed_protocol) {
} }
Result NfcDevice::StopDetection() { Result NfcDevice::StopDetection() {
npad_device->SetPollingMode(Common::Input::PollingMode::Active); npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
if (device_state == NFP::DeviceState::Initialized) { if (device_state == NFP::DeviceState::Initialized) {
return ResultSuccess; return ResultSuccess;

@ -152,7 +152,9 @@ Result NfpDevice::StartDetection(TagProtocol allowed_protocol) {
return WrongDeviceState; return WrongDeviceState;
} }
if (!npad_device->SetPollingMode(Common::Input::PollingMode::NFC)) { if (npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::NFC) !=
Common::Input::DriverResult::Success) {
LOG_ERROR(Service_NFP, "Nfc not supported"); LOG_ERROR(Service_NFP, "Nfc not supported");
return NfcDisabled; return NfcDisabled;
} }
@ -163,7 +165,8 @@ Result NfpDevice::StartDetection(TagProtocol allowed_protocol) {
} }
Result NfpDevice::StopDetection() { Result NfpDevice::StopDetection() {
npad_device->SetPollingMode(Common::Input::PollingMode::Active); npad_device->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
if (device_state == DeviceState::Initialized) { if (device_state == DeviceState::Initialized) {
return ResultSuccess; return ResultSuccess;

@ -51,8 +51,29 @@ 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/irs.cpp
helpers/joycon_protocol/irs.h
helpers/joycon_protocol/nfc.cpp
helpers/joycon_protocol/nfc.h
helpers/joycon_protocol/poller.cpp
helpers/joycon_protocol/poller.h
helpers/joycon_protocol/ringcon.cpp
helpers/joycon_protocol/ringcon.h
helpers/joycon_protocol/rumble.cpp
helpers/joycon_protocol/rumble.h
) )
target_link_libraries(input_common PRIVATE SDL2::SDL2) target_link_libraries(input_common PRIVATE SDL2::SDL2)
target_compile_definitions(input_common PRIVATE HAVE_SDL2) target_compile_definitions(input_common PRIVATE HAVE_SDL2)

@ -72,11 +72,11 @@ std::size_t Camera::getImageHeight() const {
} }
} }
Common::Input::CameraError Camera::SetCameraFormat( Common::Input::DriverResult Camera::SetCameraFormat(
[[maybe_unused]] const PadIdentifier& identifier_, [[maybe_unused]] const PadIdentifier& identifier_,
const Common::Input::CameraFormat camera_format) { const Common::Input::CameraFormat camera_format) {
status.format = camera_format; status.format = camera_format;
return Common::Input::CameraError::None; return Common::Input::DriverResult::Success;
} }
} // namespace InputCommon } // namespace InputCommon

@ -22,7 +22,7 @@ public:
std::size_t getImageWidth() const; std::size_t getImageWidth() const;
std::size_t getImageHeight() const; std::size_t getImageHeight() const;
Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_, Common::Input::DriverResult SetCameraFormat(const PadIdentifier& identifier_,
Common::Input::CameraFormat camera_format) override; Common::Input::CameraFormat camera_format) override;
private: private:

@ -324,7 +324,7 @@ bool GCAdapter::GetGCEndpoint(libusb_device* device) {
return true; return true;
} }
Common::Input::VibrationError GCAdapter::SetVibration( Common::Input::DriverResult GCAdapter::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) { const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f; const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f;
const auto processed_amplitude = const auto processed_amplitude =
@ -333,9 +333,9 @@ Common::Input::VibrationError GCAdapter::SetVibration(
pads[identifier.port].rumble_amplitude = processed_amplitude; pads[identifier.port].rumble_amplitude = processed_amplitude;
if (!rumble_enabled) { if (!rumble_enabled) {
return Common::Input::VibrationError::Disabled; return Common::Input::DriverResult::Disabled;
} }
return Common::Input::VibrationError::None; return Common::Input::DriverResult::Success;
} }
bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) { bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {

@ -25,7 +25,7 @@ public:
explicit GCAdapter(std::string input_engine_); explicit GCAdapter(std::string input_engine_);
~GCAdapter() override; ~GCAdapter() override;
Common::Input::VibrationError SetVibration( Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override; bool IsVibrationEnabled(const PadIdentifier& identifier) override;

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

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

@ -334,6 +334,15 @@ 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 &&
(guid.uuid[8] == 0x06 || guid.uuid[8] == 0x07)) {
LOG_WARNING(Input, "Preferring joycon driver for device index {}", joystick_index);
SDL_JoystickClose(sdl_joystick);
return;
}
}
std::scoped_lock lock{joystick_map_mutex}; 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);
@ -456,9 +465,13 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); SDL_SetHint(SDL_HINT_JOYSTICK_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, "0");
} else {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1"); 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.
@ -548,7 +561,7 @@ std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
return devices; return devices;
} }
Common::Input::VibrationError SDLDriver::SetVibration( Common::Input::DriverResult SDLDriver::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) { const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto joystick = const auto joystick =
GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast<int>(identifier.port)); GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast<int>(identifier.port));
@ -582,7 +595,7 @@ Common::Input::VibrationError SDLDriver::SetVibration(
.vibration = new_vibration, .vibration = new_vibration,
}); });
return Common::Input::VibrationError::None; return Common::Input::DriverResult::Success;
} }
bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) { bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) {

@ -63,7 +63,7 @@ public:
bool IsStickInverted(const Common::ParamPackage& params) override; bool IsStickInverted(const Common::ParamPackage& params) override;
Common::Input::VibrationError SetVibration( Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override; const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override; bool IsVibrationEnabled(const PadIdentifier& identifier) override;

@ -22,22 +22,23 @@ VirtualAmiibo::VirtualAmiibo(std::string input_engine_) : InputEngine(std::move(
VirtualAmiibo::~VirtualAmiibo() = default; VirtualAmiibo::~VirtualAmiibo() = default;
Common::Input::PollingError VirtualAmiibo::SetPollingMode( Common::Input::DriverResult VirtualAmiibo::SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier_, [[maybe_unused]] const PadIdentifier& identifier_,
const Common::Input::PollingMode polling_mode_) { const Common::Input::PollingMode polling_mode_) {
polling_mode = polling_mode_; polling_mode = polling_mode_;
if (polling_mode == Common::Input::PollingMode::NFC) { switch (polling_mode) {
case Common::Input::PollingMode::NFC:
if (state == State::Initialized) { if (state == State::Initialized) {
state = State::WaitingForAmiibo; state = State::WaitingForAmiibo;
} }
} else { return Common::Input::DriverResult::Success;
default:
if (state == State::AmiiboIsOpen) { if (state == State::AmiiboIsOpen) {
CloseAmiibo(); CloseAmiibo();
} }
return Common::Input::DriverResult::NotSupported;
} }
return Common::Input::PollingError::None;
} }
Common::Input::NfcState VirtualAmiibo::SupportsNfc( Common::Input::NfcState VirtualAmiibo::SupportsNfc(

@ -36,7 +36,7 @@ public:
~VirtualAmiibo() override; ~VirtualAmiibo() override;
// Sets polling mode to a controller // Sets polling mode to a controller
Common::Input::PollingError SetPollingMode( Common::Input::DriverResult SetPollingMode(
const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) override; const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) override;
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override; Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override;

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

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

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

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

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

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

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

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

@ -0,0 +1,298 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/irs.h"
namespace InputCommon::Joycon {
IrsProtocol::IrsProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult IrsProtocol::EnableIrs() {
LOG_INFO(Input, "Enable IRS");
ScopedSetBlocking sb(this);
DriverResult result{DriverResult::Success};
if (result == DriverResult::Success) {
result = SetReportMode(ReportMode::NFC_IR_MODE_60HZ);
}
if (result == DriverResult::Success) {
result = EnableMCU(true);
}
if (result == DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::Standby);
}
if (result == DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetMCUMode,
.mode = MCUMode::IR,
.crc = {},
};
result = ConfigureMCU(config);
}
if (result == DriverResult::Success) {
result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::IR);
}
if (result == DriverResult::Success) {
result = ConfigureIrs();
}
if (result == DriverResult::Success) {
result = WriteRegistersStep1();
}
if (result == DriverResult::Success) {
result = WriteRegistersStep2();
}
is_enabled = true;
return result;
}
DriverResult IrsProtocol::DisableIrs() {
LOG_DEBUG(Input, "Disable IRS");
ScopedSetBlocking sb(this);
DriverResult result{DriverResult::Success};
if (result == DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
return result;
}
DriverResult IrsProtocol::SetIrsConfig(IrsMode mode, IrsResolution format) {
irs_mode = mode;
switch (format) {
case IrsResolution::Size320x240:
resolution_code = IrsResolutionCode::Size320x240;
fragments = IrsFragments::Size320x240;
resolution = IrsResolution::Size320x240;
break;
case IrsResolution::Size160x120:
resolution_code = IrsResolutionCode::Size160x120;
fragments = IrsFragments::Size160x120;
resolution = IrsResolution::Size160x120;
break;
case IrsResolution::Size80x60:
resolution_code = IrsResolutionCode::Size80x60;
fragments = IrsFragments::Size80x60;
resolution = IrsResolution::Size80x60;
break;
case IrsResolution::Size20x15:
resolution_code = IrsResolutionCode::Size20x15;
fragments = IrsFragments::Size20x15;
resolution = IrsResolution::Size20x15;
break;
case IrsResolution::Size40x30:
default:
resolution_code = IrsResolutionCode::Size40x30;
fragments = IrsFragments::Size40x30;
resolution = IrsResolution::Size40x30;
break;
}
// Restart feature
if (is_enabled) {
DisableIrs();
return EnableIrs();
}
return DriverResult::Success;
}
DriverResult IrsProtocol::RequestImage(std::span<u8> buffer) {
const u8 next_packet_fragment =
static_cast<u8>((packet_fragment + 1) % (static_cast<u8>(fragments) + 1));
if (buffer[0] == 0x31 && buffer[49] == 0x03) {
u8 new_packet_fragment = buffer[52];
if (new_packet_fragment == next_packet_fragment) {
packet_fragment = next_packet_fragment;
memcpy(buf_image.data() + (300 * packet_fragment), buffer.data() + 59, 300);
return RequestFrame(packet_fragment);
}
if (new_packet_fragment == packet_fragment) {
return RequestFrame(packet_fragment);
}
return ResendFrame(next_packet_fragment);
}
return RequestFrame(packet_fragment);
}
DriverResult IrsProtocol::ConfigureIrs() {
LOG_DEBUG(Input, "Configure IRS");
constexpr std::size_t max_tries = 28;
std::vector<u8> output;
std::size_t tries = 0;
const IrsConfigure irs_configuration{
.command = MCUCommand::ConfigureIR,
.sub_command = MCUSubCommand::SetDeviceMode,
.irs_mode = IrsMode::ImageTransfer,
.number_of_fragments = fragments,
.mcu_major_version = 0x0500,
.mcu_minor_version = 0x1800,
.crc = {},
};
buf_image.resize((static_cast<u8>(fragments) + 1) * 300);
std::array<u8, sizeof(IrsConfigure)> request_data{};
memcpy(request_data.data(), &irs_configuration, sizeof(IrsConfigure));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
do {
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return DriverResult::WrongReply;
}
} while (output[15] != 0x0b);
return DriverResult::Success;
}
DriverResult IrsProtocol::WriteRegistersStep1() {
LOG_DEBUG(Input, "WriteRegistersStep1");
DriverResult result{DriverResult::Success};
constexpr std::size_t max_tries = 28;
std::vector<u8> output;
std::size_t tries = 0;
const IrsWriteRegisters irs_registers{
.command = MCUCommand::ConfigureIR,
.sub_command = MCUSubCommand::WriteDeviceRegisters,
.number_of_registers = 0x9,
.registers =
{
IrsRegister{IrRegistersAddress::Resolution, static_cast<u8>(resolution_code)},
{IrRegistersAddress::ExposureLSB, static_cast<u8>(exposure & 0xff)},
{IrRegistersAddress::ExposureMSB, static_cast<u8>(exposure >> 8)},
{IrRegistersAddress::ExposureTime, 0x00},
{IrRegistersAddress::Leds, static_cast<u8>(leds)},
{IrRegistersAddress::DigitalGainLSB, static_cast<u8>((digital_gain & 0x0f) << 4)},
{IrRegistersAddress::DigitalGainMSB, static_cast<u8>((digital_gain & 0xf0) >> 4)},
{IrRegistersAddress::LedFilter, static_cast<u8>(led_filter)},
{IrRegistersAddress::WhitePixelThreshold, 0xc8},
},
.crc = {},
};
std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
std::array<u8, 38> mcu_request{0x02};
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
mcu_request[37] = 0xFF;
if (result != DriverResult::Success) {
return result;
}
do {
result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
// First time we need to set the report mode
if (result == DriverResult::Success && tries == 0) {
result = SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
}
if (result == DriverResult::Success && tries == 0) {
GetSubCommandResponse(SubCommand::SET_MCU_CONFIG, output);
}
if (result != DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return DriverResult::WrongReply;
}
} while (!(output[15] == 0x13 && output[17] == 0x07) && output[15] != 0x23);
return DriverResult::Success;
}
DriverResult IrsProtocol::WriteRegistersStep2() {
LOG_DEBUG(Input, "WriteRegistersStep2");
constexpr std::size_t max_tries = 28;
std::vector<u8> output;
std::size_t tries = 0;
const IrsWriteRegisters irs_registers{
.command = MCUCommand::ConfigureIR,
.sub_command = MCUSubCommand::WriteDeviceRegisters,
.number_of_registers = 0x8,
.registers =
{
IrsRegister{IrRegistersAddress::LedIntensitiyMSB,
static_cast<u8>(led_intensity >> 8)},
{IrRegistersAddress::LedIntensitiyLSB, static_cast<u8>(led_intensity & 0xff)},
{IrRegistersAddress::ImageFlip, static_cast<u8>(image_flip)},
{IrRegistersAddress::DenoiseSmoothing, static_cast<u8>((denoise >> 16) & 0xff)},
{IrRegistersAddress::DenoiseEdge, static_cast<u8>((denoise >> 8) & 0xff)},
{IrRegistersAddress::DenoiseColor, static_cast<u8>(denoise & 0xff)},
{IrRegistersAddress::UpdateTime, 0x2d},
{IrRegistersAddress::FinalizeConfig, 0x01},
},
.crc = {},
};
std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
do {
const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return DriverResult::WrongReply;
}
} while (output[15] != 0x13 && output[15] != 0x23);
return DriverResult::Success;
}
DriverResult IrsProtocol::RequestFrame(u8 frame) {
std::array<u8, 38> mcu_request{};
mcu_request[3] = frame;
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
mcu_request[37] = 0xFF;
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
}
DriverResult IrsProtocol::ResendFrame(u8 frame) {
std::array<u8, 38> mcu_request{};
mcu_request[1] = 0x1;
mcu_request[2] = frame;
mcu_request[3] = 0x0;
mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
mcu_request[37] = 0xFF;
return SendMCUCommand(SubCommand::SET_REPORT_MODE, mcu_request);
}
std::vector<u8> IrsProtocol::GetImage() const {
return buf_image;
}
IrsResolution IrsProtocol::GetIrsFormat() const {
return resolution;
}
bool IrsProtocol::IsEnabled() const {
return is_enabled;
}
} // namespace InputCommon::Joycon

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
// https://github.com/CTCaer/jc_toolkit
// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
#pragma once
#include <vector>
#include "input_common/helpers/joycon_protocol/common_protocol.h"
#include "input_common/helpers/joycon_protocol/joycon_types.h"
namespace InputCommon::Joycon {
class IrsProtocol final : private JoyconCommonProtocol {
public:
explicit IrsProtocol(std::shared_ptr<JoyconHandle> handle);
DriverResult EnableIrs();
DriverResult DisableIrs();
DriverResult SetIrsConfig(IrsMode mode, IrsResolution format);
DriverResult RequestImage(std::span<u8> buffer);
std::vector<u8> GetImage() const;
IrsResolution GetIrsFormat() const;
bool IsEnabled() const;
private:
DriverResult ConfigureIrs();
DriverResult WriteRegistersStep1();
DriverResult WriteRegistersStep2();
DriverResult RequestFrame(u8 frame);
DriverResult ResendFrame(u8 frame);
IrsMode irs_mode{IrsMode::ImageTransfer};
IrsResolution resolution{IrsResolution::Size40x30};
IrsResolutionCode resolution_code{IrsResolutionCode::Size40x30};
IrsFragments fragments{IrsFragments::Size40x30};
IrLeds leds{IrLeds::BrightAndDim};
IrExLedFilter led_filter{IrExLedFilter::Enabled};
IrImageFlip image_flip{IrImageFlip::Normal};
u8 digital_gain{0x01};
u16 exposure{0x2490};
u16 led_intensity{0x0f10};
u32 denoise{0x012344};
u8 packet_fragment{};
std::vector<u8> buf_image; // 8bpp greyscale image.
bool is_enabled{};
};
} // namespace InputCommon::Joycon

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

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

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

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

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

@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "common/logging/log.h"
#include "input_common/helpers/joycon_protocol/ringcon.h"
namespace InputCommon::Joycon {
RingConProtocol::RingConProtocol(std::shared_ptr<JoyconHandle> handle)
: JoyconCommonProtocol(std::move(handle)) {}
DriverResult RingConProtocol::EnableRingCon() {
LOG_DEBUG(Input, "Enable Ringcon");
ScopedSetBlocking sb(this);
DriverResult result{DriverResult::Success};
if (result == DriverResult::Success) {
result = SetReportMode(ReportMode::STANDARD_FULL_60HZ);
}
if (result == DriverResult::Success) {
result = EnableMCU(true);
}
if (result == DriverResult::Success) {
const MCUConfig config{
.command = MCUCommand::ConfigureMCU,
.sub_command = MCUSubCommand::SetDeviceMode,
.mode = MCUMode::Standby,
.crc = {},
};
result = ConfigureMCU(config);
}
return result;
}
DriverResult RingConProtocol::DisableRingCon() {
LOG_DEBUG(Input, "Disable RingCon");
ScopedSetBlocking sb(this);
DriverResult result{DriverResult::Success};
if (result == DriverResult::Success) {
result = EnableMCU(false);
}
is_enabled = false;
return result;
}
DriverResult RingConProtocol::StartRingconPolling() {
LOG_DEBUG(Input, "Enable Ringcon");
ScopedSetBlocking sb(this);
DriverResult result{DriverResult::Success};
bool is_connected = false;
if (result == DriverResult::Success) {
result = IsRingConnected(is_connected);
}
if (result == DriverResult::Success && is_connected) {
LOG_INFO(Input, "Ringcon detected");
result = ConfigureRing();
}
if (result == DriverResult::Success) {
is_enabled = true;
}
return result;
}
DriverResult RingConProtocol::IsRingConnected(bool& is_connected) {
LOG_DEBUG(Input, "IsRingConnected");
constexpr std::size_t max_tries = 28;
constexpr u8 ring_controller_id = 0x20;
std::vector<u8> output;
std::size_t tries = 0;
is_connected = false;
do {
std::array<u8, 1> empty_data{};
const auto result = SendSubCommand(SubCommand::UNKNOWN_RINGCON, empty_data, output);
if (result != DriverResult::Success) {
return result;
}
if (tries++ >= max_tries) {
return DriverResult::NoDeviceDetected;
}
} while (output[16] != ring_controller_id);
is_connected = true;
return DriverResult::Success;
}
DriverResult RingConProtocol::ConfigureRing() {
LOG_DEBUG(Input, "ConfigureRing");
static constexpr std::array<u8, 37> ring_config{
0x06, 0x03, 0x25, 0x06, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x16, 0xED, 0x34, 0x36,
0x00, 0x00, 0x00, 0x0A, 0x64, 0x0B, 0xE6, 0xA9, 0x22, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0xA8, 0xE1, 0x34, 0x36};
const DriverResult result = SendSubCommand(SubCommand::UNKNOWN_RINGCON3, ring_config);
if (result != DriverResult::Success) {
return result;
}
static constexpr std::array<u8, 4> ringcon_data{0x04, 0x01, 0x01, 0x02};
return SendSubCommand(SubCommand::UNKNOWN_RINGCON2, ringcon_data);
}
bool RingConProtocol::IsEnabled() const {
return is_enabled;
}
} // namespace InputCommon::Joycon

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

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

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

@ -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};

@ -40,6 +40,7 @@ enum class EngineInputType {
Battery, Battery,
Button, Button,
Camera, Camera,
Color,
HatButton, HatButton,
Motion, Motion,
Nfc, Nfc,
@ -104,14 +105,17 @@ public:
void EndConfiguration(); void EndConfiguration();
// Sets a led pattern for a controller // Sets a led pattern for a controller
virtual void SetLeds([[maybe_unused]] const PadIdentifier& identifier, virtual Common::Input::DriverResult SetLeds(
[[maybe_unused]] const Common::Input::LedStatus& led_status) {} [[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::LedStatus& led_status) {
return Common::Input::DriverResult::NotSupported;
}
// Sets rumble to a controller // Sets rumble to a controller
virtual Common::Input::VibrationError SetVibration( virtual Common::Input::DriverResult SetVibration(
[[maybe_unused]] const PadIdentifier& identifier, [[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) { [[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
return Common::Input::VibrationError::NotSupported; return Common::Input::DriverResult::NotSupported;
} }
// Returns true if device supports vibrations // Returns true if device supports vibrations
@ -120,17 +124,17 @@ public:
} }
// Sets polling mode to a controller // Sets polling mode to a controller
virtual Common::Input::PollingError SetPollingMode( virtual Common::Input::DriverResult SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier, [[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::PollingMode polling_mode) { [[maybe_unused]] const Common::Input::PollingMode polling_mode) {
return Common::Input::PollingError::NotSupported; return Common::Input::DriverResult::NotSupported;
} }
// Sets camera format to a controller // Sets camera format to a controller
virtual Common::Input::CameraError SetCameraFormat( virtual Common::Input::DriverResult SetCameraFormat(
[[maybe_unused]] const PadIdentifier& identifier, [[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] Common::Input::CameraFormat camera_format) { [[maybe_unused]] Common::Input::CameraFormat camera_format) {
return Common::Input::CameraError::NotSupported; return Common::Input::DriverResult::NotSupported;
} }
// Returns success if nfc is supported // Returns success if nfc is supported
@ -199,6 +203,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 +217,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 +233,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 +242,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,

@ -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_,
@ -754,11 +806,11 @@ public:
explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_) explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_)
: identifier(identifier_), input_engine(input_engine_) {} : identifier(identifier_), input_engine(input_engine_) {}
void SetLED(const Common::Input::LedStatus& led_status) override { Common::Input::DriverResult SetLED(const Common::Input::LedStatus& led_status) override {
input_engine->SetLeds(identifier, led_status); return input_engine->SetLeds(identifier, led_status);
} }
Common::Input::VibrationError SetVibration( Common::Input::DriverResult SetVibration(
const Common::Input::VibrationStatus& vibration_status) override { const Common::Input::VibrationStatus& vibration_status) override {
return input_engine->SetVibration(identifier, vibration_status); return input_engine->SetVibration(identifier, vibration_status);
} }
@ -767,11 +819,12 @@ public:
return input_engine->IsVibrationEnabled(identifier); return input_engine->IsVibrationEnabled(identifier);
} }
Common::Input::PollingError SetPollingMode(Common::Input::PollingMode polling_mode) override { Common::Input::DriverResult SetPollingMode(Common::Input::PollingMode polling_mode) override {
return input_engine->SetPollingMode(identifier, polling_mode); return input_engine->SetPollingMode(identifier, polling_mode);
} }
Common::Input::CameraError SetCameraFormat(Common::Input::CameraFormat camera_format) override { Common::Input::DriverResult SetCameraFormat(
Common::Input::CameraFormat camera_format) override {
return input_engine->SetCameraFormat(identifier, camera_format); return input_engine->SetCameraFormat(identifier, camera_format);
} }
@ -966,6 +1019,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 +1118,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);
} }

@ -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:

@ -23,6 +23,7 @@
#include "input_common/drivers/gc_adapter.h" #include "input_common/drivers/gc_adapter.h"
#endif #endif
#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
@ -81,6 +82,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",
@ -111,6 +113,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");
@ -133,6 +136,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
@ -164,6 +169,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;
} }
@ -247,6 +255,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;
} }
@ -260,6 +271,7 @@ struct InputSubsystem::Impl {
udp_client->BeginConfiguration(); udp_client->BeginConfiguration();
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
sdl->BeginConfiguration(); sdl->BeginConfiguration();
joycon->BeginConfiguration();
#endif #endif
} }
@ -272,6 +284,7 @@ struct InputSubsystem::Impl {
udp_client->EndConfiguration(); udp_client->EndConfiguration();
#ifdef HAVE_SDL2 #ifdef HAVE_SDL2
sdl->EndConfiguration(); sdl->EndConfiguration();
joycon->EndConfiguration();
#endif #endif
} }
@ -304,6 +317,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
}; };

@ -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);
@ -1139,6 +1140,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);

@ -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();
} }

@ -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="6" 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>

@ -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:

@ -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() {

@ -4,9 +4,11 @@
#include <memory> #include <memory>
#include <QKeyEvent> #include <QKeyEvent>
#include <QMenu> #include <QMenu>
#include <QMessageBox>
#include <QTimer> #include <QTimer>
#include <fmt/format.h>
#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 +128,16 @@ 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();
Core::HID::ControllerUpdateCallback engine_callback{
.on_change = [this](Core::HID::ControllerTriggerType type) { ControllerUpdate(type); },
.is_npad_service = false,
};
callback_key = emulated_controller->SetCallback(engine_callback);
is_controller_set = true;
LoadConfiguration(); LoadConfiguration();
@ -143,9 +152,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 +164,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,16 +186,19 @@ 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,
&ConfigureRingController::RestoreDefaults); &ConfigureRingController::RestoreDefaults);
connect(ui->enable_ring_controller_button, &QPushButton::clicked, this,
&ConfigureRingController::EnableRingController);
timeout_timer->setSingleShot(true); timeout_timer->setSingleShot(true);
connect(timeout_timer.get(), &QTimer::timeout, [this] { SetPollingResult({}, true); }); connect(timeout_timer.get(), &QTimer::timeout, [this] { SetPollingResult({}, true); });
@ -202,7 +214,14 @@ ConfigureRingController::ConfigureRingController(QWidget* parent,
} }
ConfigureRingController::~ConfigureRingController() { ConfigureRingController::~ConfigureRingController() {
emulated_device->DisableConfiguration(); emulated_controller->SetPollingMode(Core::HID::EmulatedDeviceIndex::RightIndex,
Common::Input::PollingMode::Active);
emulated_controller->DisableConfiguration();
if (is_controller_set) {
emulated_controller->DeleteCallback(callback_key);
is_controller_set = false;
}
}; };
void ConfigureRingController::changeEvent(QEvent* event) { void ConfigureRingController::changeEvent(QEvent* event) {
@ -219,7 +238,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 +259,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,10 +271,62 @@ 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();
} }
void ConfigureRingController::EnableRingController() {
const auto dialog_title = tr("Error enabling ring input");
is_ring_enabled = false;
ui->ring_controller_sensor_value->setText(tr("Not connected"));
if (!Settings::values.enable_joycon_driver) {
QMessageBox::warning(this, dialog_title, tr("Direct Joycon driver is not enabled"));
return;
}
ui->enable_ring_controller_button->setEnabled(false);
ui->enable_ring_controller_button->setText(tr("Configuring"));
// SetPollingMode is blocking. Allow to update the button status before calling the command
repaint();
const auto result = emulated_controller->SetPollingMode(
Core::HID::EmulatedDeviceIndex::RightIndex, Common::Input::PollingMode::Ring);
switch (result) {
case Common::Input::DriverResult::Success:
is_ring_enabled = true;
break;
case Common::Input::DriverResult::NotSupported:
QMessageBox::warning(this, dialog_title,
tr("The current mapped device doesn't support the ring controller"));
break;
case Common::Input::DriverResult::NoDeviceDetected:
QMessageBox::warning(this, dialog_title,
tr("The current mapped device doesn't have a ring attached"));
break;
default:
QMessageBox::warning(this, dialog_title,
tr("Unexpected driver result %1").arg(static_cast<int>(result)));
break;
}
ui->enable_ring_controller_button->setEnabled(true);
ui->enable_ring_controller_button->setText(tr("Enable"));
}
void ConfigureRingController::ControllerUpdate(Core::HID::ControllerTriggerType type) {
if (!is_ring_enabled) {
return;
}
if (type != Core::HID::ControllerTriggerType::RingController) {
return;
}
const auto value = emulated_controller->GetRingSensorValues();
const auto tex_value = QString::fromStdString(fmt::format("{:.3f}", value.raw_value));
ui->ring_controller_sensor_value->setText(tex_value);
}
void ConfigureRingController::HandleClick( void ConfigureRingController::HandleClick(
QPushButton* button, std::function<void(const Common::ParamPackage&)> new_input_setter, QPushButton* button, std::function<void(const Common::ParamPackage&)> new_input_setter,
InputCommon::Polling::InputType type) { InputCommon::Polling::InputType type) {

@ -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 {
@ -42,6 +42,12 @@ private:
/// Restore all buttons to their default values. /// Restore all buttons to their default values.
void RestoreDefaults(); void RestoreDefaults();
/// Sets current polling mode to ring input
void EnableRingController();
// Handles emulated controller events
void ControllerUpdate(Core::HID::ControllerTriggerType type);
/// Called when the button was pressed. /// Called when the button was pressed.
void HandleClick(QPushButton* button, void HandleClick(QPushButton* button,
std::function<void(const Common::ParamPackage&)> new_input_setter, std::function<void(const Common::ParamPackage&)> new_input_setter,
@ -78,7 +84,11 @@ 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;
bool is_ring_enabled{};
bool is_controller_set{};
int callback_key;
std::unique_ptr<Ui::ConfigureRingController> ui; std::unique_ptr<Ui::ConfigureRingController> ui;
}; };

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>298</width> <width>315</width>
<height>339</height> <height>400</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -49,12 +49,12 @@
<item> <item>
<widget class="QGroupBox" name="RingAnalog"> <widget class="QGroupBox" name="RingAnalog">
<property name="title"> <property name="title">
<string>Ring Sensor Parameters</string> <string>Virtual Ring Sensor Parameters</string>
</property> </property>
<property name="alignment"> <property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_3"> <layout class="QVBoxLayout" name="verticalLayout_1">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </property>
@ -106,7 +106,7 @@
<widget class="QPushButton" name="buttonRingAnalogPull"> <widget class="QPushButton" name="buttonRingAnalogPull">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>68</width> <width>70</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
@ -155,7 +155,7 @@
<widget class="QPushButton" name="buttonRingAnalogPush"> <widget class="QPushButton" name="buttonRingAnalogPush">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>68</width> <width>70</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
@ -227,6 +227,102 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="RingDriver">
<property name="title">
<string>Direct Joycon Driver</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<property name="verticalSpacing">
<number>10</number>
</property>
<item row="0" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>76</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="enable_ring_controller_label">
<property name="text">
<string>Enable Ring Input</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="enable_ring_controller_button">
<property name="text">
<string>Enable</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="ring_controller_sensor_label">
<property name="text">
<string>Ring Sensor Value</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="ring_controller_sensor_value">
<property name="text">
<string>Not connected</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">