summaryrefslogtreecommitdiff
path: root/src/input_common
diff options
context:
space:
mode:
Diffstat (limited to 'src/input_common')
-rw-r--r--src/input_common/CMakeLists.txt23
-rw-r--r--src/input_common/drivers/camera.cpp4
-rw-r--r--src/input_common/drivers/camera.h4
-rw-r--r--src/input_common/drivers/gc_adapter.cpp12
-rw-r--r--src/input_common/drivers/gc_adapter.h2
-rw-r--r--src/input_common/drivers/joycon.cpp844
-rw-r--r--src/input_common/drivers/joycon.h125
-rw-r--r--src/input_common/drivers/keyboard.cpp2
-rw-r--r--src/input_common/drivers/mouse.cpp231
-rw-r--r--src/input_common/drivers/mouse.h43
-rw-r--r--src/input_common/drivers/sdl_driver.cpp129
-rw-r--r--src/input_common/drivers/sdl_driver.h2
-rw-r--r--src/input_common/drivers/virtual_amiibo.cpp179
-rw-r--r--src/input_common/drivers/virtual_amiibo.h23
-rw-r--r--src/input_common/drivers/virtual_gamepad.cpp16
-rw-r--r--src/input_common/drivers/virtual_gamepad.h12
-rw-r--r--src/input_common/helpers/joycon_driver.cpp710
-rw-r--r--src/input_common/helpers/joycon_driver.h158
-rw-r--r--src/input_common/helpers/joycon_protocol/calibration.cpp218
-rw-r--r--src/input_common/helpers/joycon_protocol/calibration.h82
-rw-r--r--src/input_common/helpers/joycon_protocol/common_protocol.cpp313
-rw-r--r--src/input_common/helpers/joycon_protocol/common_protocol.h201
-rw-r--r--src/input_common/helpers/joycon_protocol/generic_functions.cpp136
-rw-r--r--src/input_common/helpers/joycon_protocol/generic_functions.h114
-rw-r--r--src/input_common/helpers/joycon_protocol/irs.cpp299
-rw-r--r--src/input_common/helpers/joycon_protocol/irs.h63
-rw-r--r--src/input_common/helpers/joycon_protocol/joycon_types.h808
-rw-r--r--src/input_common/helpers/joycon_protocol/nfc.cpp985
-rw-r--r--src/input_common/helpers/joycon_protocol/nfc.h114
-rw-r--r--src/input_common/helpers/joycon_protocol/poller.cpp374
-rw-r--r--src/input_common/helpers/joycon_protocol/poller.h84
-rw-r--r--src/input_common/helpers/joycon_protocol/ringcon.cpp115
-rw-r--r--src/input_common/helpers/joycon_protocol/ringcon.h38
-rw-r--r--src/input_common/helpers/joycon_protocol/rumble.cpp299
-rw-r--r--src/input_common/helpers/joycon_protocol/rumble.h33
-rw-r--r--src/input_common/helpers/stick_from_buttons.cpp50
-rw-r--r--src/input_common/helpers/udp_protocol.cpp2
-rw-r--r--src/input_common/input_engine.cpp48
-rw-r--r--src/input_common/input_engine.h59
-rw-r--r--src/input_common/input_mapping.cpp18
-rw-r--r--src/input_common/input_poller.cpp135
-rw-r--r--src/input_common/input_poller.h11
-rw-r--r--src/input_common/main.cpp14
-rw-r--r--src/input_common/main.h2
44 files changed, 6919 insertions, 215 deletions
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt
index cef2c4d52..322c29065 100644
--- a/src/input_common/CMakeLists.txt
+++ b/src/input_common/CMakeLists.txt
@@ -51,8 +51,29 @@ endif()
if (ENABLE_SDL2)
target_sources(input_common PRIVATE
+ drivers/joycon.cpp
+ drivers/joycon.h
drivers/sdl_driver.cpp
drivers/sdl_driver.h
+ helpers/joycon_driver.cpp
+ helpers/joycon_driver.h
+ helpers/joycon_protocol/calibration.cpp
+ helpers/joycon_protocol/calibration.h
+ helpers/joycon_protocol/common_protocol.cpp
+ helpers/joycon_protocol/common_protocol.h
+ helpers/joycon_protocol/generic_functions.cpp
+ helpers/joycon_protocol/generic_functions.h
+ helpers/joycon_protocol/joycon_types.h
+ helpers/joycon_protocol/irs.cpp
+ helpers/joycon_protocol/irs.h
+ helpers/joycon_protocol/nfc.cpp
+ helpers/joycon_protocol/nfc.h
+ helpers/joycon_protocol/poller.cpp
+ helpers/joycon_protocol/poller.h
+ helpers/joycon_protocol/ringcon.cpp
+ helpers/joycon_protocol/ringcon.h
+ helpers/joycon_protocol/rumble.cpp
+ helpers/joycon_protocol/rumble.h
)
target_link_libraries(input_common PRIVATE SDL2::SDL2)
target_compile_definitions(input_common PRIVATE HAVE_SDL2)
@@ -68,7 +89,7 @@ if (ENABLE_LIBUSB)
endif()
create_target_directory_groups(input_common)
-target_link_libraries(input_common PUBLIC core PRIVATE common Boost::boost)
+target_link_libraries(input_common PUBLIC core PRIVATE common Boost::headers)
if (YUZU_USE_PRECOMPILED_HEADERS)
target_precompile_headers(input_common PRIVATE precompiled_headers.h)
diff --git a/src/input_common/drivers/camera.cpp b/src/input_common/drivers/camera.cpp
index fad9177dc..04970f635 100644
--- a/src/input_common/drivers/camera.cpp
+++ b/src/input_common/drivers/camera.cpp
@@ -72,11 +72,11 @@ std::size_t Camera::getImageHeight() const {
}
}
-Common::Input::CameraError Camera::SetCameraFormat(
+Common::Input::DriverResult Camera::SetCameraFormat(
[[maybe_unused]] const PadIdentifier& identifier_,
const Common::Input::CameraFormat camera_format) {
status.format = camera_format;
- return Common::Input::CameraError::None;
+ return Common::Input::DriverResult::Success;
}
} // namespace InputCommon
diff --git a/src/input_common/drivers/camera.h b/src/input_common/drivers/camera.h
index ead3e0fde..24b27e325 100644
--- a/src/input_common/drivers/camera.h
+++ b/src/input_common/drivers/camera.h
@@ -22,8 +22,8 @@ public:
std::size_t getImageWidth() const;
std::size_t getImageHeight() const;
- Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_,
- Common::Input::CameraFormat camera_format) override;
+ Common::Input::DriverResult SetCameraFormat(const PadIdentifier& identifier_,
+ Common::Input::CameraFormat camera_format) override;
private:
Common::Input::CameraStatus status{};
diff --git a/src/input_common/drivers/gc_adapter.cpp b/src/input_common/drivers/gc_adapter.cpp
index 826fa2109..3ad34884d 100644
--- a/src/input_common/drivers/gc_adapter.cpp
+++ b/src/input_common/drivers/gc_adapter.cpp
@@ -6,6 +6,7 @@
#include "common/logging/log.h"
#include "common/param_package.h"
+#include "common/polyfill_thread.h"
#include "common/settings_input.h"
#include "common/thread.h"
#include "input_common/drivers/gc_adapter.h"
@@ -217,8 +218,7 @@ void GCAdapter::AdapterScanThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("ScanGCAdapter");
usb_adapter_handle = nullptr;
pads = {};
- while (!stop_token.stop_requested() && !Setup()) {
- std::this_thread::sleep_for(std::chrono::seconds(2));
+ while (!Setup() && Common::StoppableTimedWait(stop_token, std::chrono::seconds{2})) {
}
}
@@ -324,7 +324,7 @@ bool GCAdapter::GetGCEndpoint(libusb_device* device) {
return true;
}
-Common::Input::VibrationError GCAdapter::SetVibration(
+Common::Input::DriverResult GCAdapter::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto mean_amplitude = (vibration.low_amplitude + vibration.high_amplitude) * 0.5f;
const auto processed_amplitude =
@@ -333,9 +333,9 @@ Common::Input::VibrationError GCAdapter::SetVibration(
pads[identifier.port].rumble_amplitude = processed_amplitude;
if (!rumble_enabled) {
- return Common::Input::VibrationError::Disabled;
+ return Common::Input::DriverResult::Disabled;
}
- return Common::Input::VibrationError::None;
+ return Common::Input::DriverResult::Success;
}
bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identifier) {
@@ -344,7 +344,7 @@ bool GCAdapter::IsVibrationEnabled([[maybe_unused]] const PadIdentifier& identif
void GCAdapter::UpdateVibrations() {
// Use 8 states to keep the switching between on/off fast enough for
- // a human to feel different vibration strenght
+ // a human to feel different vibration strength
// More states == more rumble strengths == slower update time
constexpr u8 vibration_states = 8;
diff --git a/src/input_common/drivers/gc_adapter.h b/src/input_common/drivers/gc_adapter.h
index b5270fd0b..3c2eb376d 100644
--- a/src/input_common/drivers/gc_adapter.h
+++ b/src/input_common/drivers/gc_adapter.h
@@ -25,7 +25,7 @@ public:
explicit GCAdapter(std::string input_engine_);
~GCAdapter() override;
- Common::Input::VibrationError SetVibration(
+ Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp
new file mode 100644
index 000000000..52494e0d9
--- /dev/null
+++ b/src/input_common/drivers/joycon.cpp
@@ -0,0 +1,844 @@
+// 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/polyfill_ranges.h"
+#include "common/polyfill_thread.h"
+#include "common/settings.h"
+#include "common/thread.h"
+#include "input_common/drivers/joycon.h"
+#include "input_common/helpers/joycon_driver.h"
+#include "input_common/helpers/joycon_protocol/joycon_types.h"
+
+namespace InputCommon {
+
+Joycons::Joycons(const std::string& input_engine_) : InputEngine(input_engine_) {
+ // Avoid conflicting with SDL driver
+ if (!Settings::values.enable_joycon_driver && !Settings::values.enable_procon_driver) {
+ return;
+ }
+ LOG_INFO(Input, "Joycon driver Initialization started");
+ const int init_res = SDL_hid_init();
+ if (init_res == 0) {
+ Setup();
+ } else {
+ LOG_ERROR(Input, "Hidapi could not be initialized. failed with error = {}", init_res);
+ }
+}
+
+Joycons::~Joycons() {
+ Reset();
+}
+
+void Joycons::Reset() {
+ scan_thread = {};
+ for (const auto& device : left_joycons) {
+ if (!device) {
+ continue;
+ }
+ device->Stop();
+ }
+ for (const auto& device : right_joycons) {
+ if (!device) {
+ continue;
+ }
+ device->Stop();
+ }
+ for (const auto& device : pro_controller) {
+ if (!device) {
+ continue;
+ }
+ device->Stop();
+ }
+ SDL_hid_exit();
+}
+
+void Joycons::Setup() {
+ u32 port = 0;
+ PreSetController(GetIdentifier(0, Joycon::ControllerType::None));
+ for (auto& device : left_joycons) {
+ PreSetController(GetIdentifier(port, Joycon::ControllerType::Left));
+ device = std::make_shared<Joycon::JoyconDriver>(port++);
+ }
+ port = 0;
+ for (auto& device : right_joycons) {
+ PreSetController(GetIdentifier(port, Joycon::ControllerType::Right));
+ device = std::make_shared<Joycon::JoyconDriver>(port++);
+ }
+ port = 0;
+ for (auto& device : pro_controller) {
+ PreSetController(GetIdentifier(port, Joycon::ControllerType::Pro));
+ device = std::make_shared<Joycon::JoyconDriver>(port++);
+ }
+
+ scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); });
+}
+
+void Joycons::ScanThread(std::stop_token stop_token) {
+ constexpr u16 nintendo_vendor_id = 0x057e;
+ Common::SetCurrentThreadName("JoyconScanThread");
+
+ do {
+ SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0);
+ SDL_hid_device_info* cur_dev = devs;
+
+ while (cur_dev) {
+ if (IsDeviceNew(cur_dev)) {
+ LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id,
+ cur_dev->product_id);
+ RegisterNewDevice(cur_dev);
+ }
+ cur_dev = cur_dev->next;
+ }
+
+ SDL_hid_free_enumeration(devs);
+ } while (Common::StoppableTimedWait(stop_token, std::chrono::seconds{5}));
+}
+
+bool Joycons::IsDeviceNew(SDL_hid_device_info* device_info) const {
+ Joycon::ControllerType type{};
+ Joycon::SerialNumber serial_number{};
+
+ const auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
+ if (result != 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:
+ if (!Settings::values.enable_joycon_driver) {
+ return false;
+ }
+ for (const auto& device : left_joycons) {
+ if (is_handle_identical(device)) {
+ return false;
+ }
+ }
+ break;
+ case Joycon::ControllerType::Right:
+ if (!Settings::values.enable_joycon_driver) {
+ return false;
+ }
+ for (const auto& device : right_joycons) {
+ if (is_handle_identical(device)) {
+ return false;
+ }
+ }
+ break;
+ case Joycon::ControllerType::Pro:
+ if (!Settings::values.enable_procon_driver) {
+ return false;
+ }
+ for (const auto& device : pro_controller) {
+ if (is_handle_identical(device)) {
+ return false;
+ }
+ }
+ break;
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+void Joycons::RegisterNewDevice(SDL_hid_device_info* device_info) {
+ Joycon::ControllerType type{};
+ auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
+ auto handle = GetNextFreeHandle(type);
+ if (handle == nullptr) {
+ LOG_WARNING(Input, "No free handles available");
+ return;
+ }
+ if (result == 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, type](const Joycon::TagInfo& tag_info) {
+ OnAmiiboUpdate(port, type, tag_info);
+ }},
+ .on_camera_data = {[this, port](const std::vector<u8>& camera_data,
+ Joycon::IrsResolution format) {
+ OnCameraUpdate(port, camera_data, format);
+ }},
+ };
+
+ handle->InitializeDevice();
+ handle->SetCallbacks(callbacks);
+ }
+}
+
+std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle(
+ Joycon::ControllerType type) const {
+ if (type == Joycon::ControllerType::Left) {
+ const auto unconnected_device =
+ std::ranges::find_if(left_joycons, [](auto& device) { return !device->IsConnected(); });
+ if (unconnected_device != left_joycons.end()) {
+ return *unconnected_device;
+ }
+ }
+ if (type == Joycon::ControllerType::Right) {
+ const auto unconnected_device = std::ranges::find_if(
+ right_joycons, [](auto& device) { return !device->IsConnected(); });
+
+ if (unconnected_device != right_joycons.end()) {
+ return *unconnected_device;
+ }
+ }
+ if (type == Joycon::ControllerType::Pro) {
+ const auto unconnected_device = std::ranges::find_if(
+ pro_controller, [](auto& device) { return !device->IsConnected(); });
+
+ if (unconnected_device != pro_controller.end()) {
+ return *unconnected_device;
+ }
+ }
+ return nullptr;
+}
+
+bool Joycons::IsVibrationEnabled(const PadIdentifier& identifier) {
+ const auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return false;
+ }
+ return handle->IsVibrationEnabled();
+}
+
+Common::Input::DriverResult Joycons::SetVibration(const PadIdentifier& identifier,
+ const Common::Input::VibrationStatus& vibration) {
+ const Joycon::VibrationValue native_vibration{
+ .low_amplitude = vibration.low_amplitude,
+ .low_frequency = vibration.low_frequency,
+ .high_amplitude = vibration.high_amplitude,
+ .high_frequency = vibration.high_frequency,
+ };
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::DriverResult::InvalidHandle;
+ }
+
+ handle->SetVibration(native_vibration);
+ return Common::Input::DriverResult::Success;
+}
+
+Common::Input::DriverResult Joycons::SetLeds(const PadIdentifier& identifier,
+ const Common::Input::LedStatus& led_status) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::DriverResult::InvalidHandle;
+ }
+ int led_config = led_status.led_1 ? 1 : 0;
+ led_config += led_status.led_2 ? 2 : 0;
+ led_config += led_status.led_3 ? 4 : 0;
+ led_config += led_status.led_4 ? 8 : 0;
+
+ return 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::StartNfcPolling(const PadIdentifier& identifier) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::NfcState::Unknown;
+ }
+ return TranslateDriverResult(handle->StartNfcPolling());
+};
+
+Common::Input::NfcState Joycons::StopNfcPolling(const PadIdentifier& identifier) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::NfcState::Unknown;
+ }
+ return TranslateDriverResult(handle->StopNfcPolling());
+};
+
+Common::Input::NfcState Joycons::ReadAmiiboData(const PadIdentifier& identifier,
+ std::vector<u8>& out_data) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::NfcState::Unknown;
+ }
+ return TranslateDriverResult(handle->ReadAmiiboData(out_data));
+}
+
+Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier,
+ const std::vector<u8>& data) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::NfcState::Unknown;
+ }
+ return TranslateDriverResult(handle->WriteNfcData(data));
+};
+
+Common::Input::NfcState Joycons::ReadMifareData(const PadIdentifier& identifier,
+ const Common::Input::MifareRequest& request,
+ Common::Input::MifareRequest& data) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::NfcState::Unknown;
+ }
+
+ const auto command = static_cast<Joycon::MifareCmd>(request.data[0].command);
+ std::vector<Joycon::MifareReadChunk> read_request{};
+ for (const auto& request_data : request.data) {
+ if (request_data.command == 0) {
+ continue;
+ }
+ Joycon::MifareReadChunk chunk = {
+ .command = command,
+ .sector_key = {},
+ .sector = request_data.sector,
+ };
+ memcpy(chunk.sector_key.data(), request_data.key.data(),
+ sizeof(Joycon::MifareReadChunk::sector_key));
+ read_request.emplace_back(chunk);
+ }
+
+ std::vector<Joycon::MifareReadData> read_data(read_request.size());
+ const auto result = handle->ReadMifareData(read_request, read_data);
+ if (result == Joycon::DriverResult::Success) {
+ for (std::size_t i = 0; i < read_request.size(); i++) {
+ data.data[i] = {
+ .command = static_cast<u8>(command),
+ .sector = read_data[i].sector,
+ .key = {},
+ .data = read_data[i].data,
+ };
+ }
+ }
+ return TranslateDriverResult(result);
+};
+
+Common::Input::NfcState Joycons::WriteMifareData(const PadIdentifier& identifier,
+ const Common::Input::MifareRequest& request) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ return Common::Input::NfcState::Unknown;
+ }
+
+ const auto command = static_cast<Joycon::MifareCmd>(request.data[0].command);
+ std::vector<Joycon::MifareWriteChunk> write_request{};
+ for (const auto& request_data : request.data) {
+ if (request_data.command == 0) {
+ continue;
+ }
+ Joycon::MifareWriteChunk chunk = {
+ .command = command,
+ .sector_key = {},
+ .sector = request_data.sector,
+ .data = {},
+ };
+ memcpy(chunk.sector_key.data(), request_data.key.data(),
+ sizeof(Joycon::MifareReadChunk::sector_key));
+ memcpy(chunk.data.data(), request_data.data.data(), sizeof(Joycon::MifareWriteChunk::data));
+ write_request.emplace_back(chunk);
+ }
+
+ return TranslateDriverResult(handle->WriteMifareData(write_request));
+};
+
+Common::Input::DriverResult Joycons::SetPollingMode(const PadIdentifier& identifier,
+ const Common::Input::PollingMode polling_mode) {
+ auto handle = GetHandle(identifier);
+ if (handle == nullptr) {
+ LOG_ERROR(Input, "Invalid handle {}", identifier.port);
+ return Common::Input::DriverResult::InvalidHandle;
+ }
+
+ switch (polling_mode) {
+ case Common::Input::PollingMode::Active:
+ return static_cast<Common::Input::DriverResult>(handle->SetActiveMode());
+ case Common::Input::PollingMode::Passive:
+ return static_cast<Common::Input::DriverResult>(handle->SetPassiveMode());
+ 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
+ static constexpr PadIdentifier identifier = {
+ .guid = Common::UUID{},
+ .port = 0,
+ .pad = 0,
+ };
+ SetAxis(identifier, 100, ring_data);
+}
+
+void Joycons::OnAmiiboUpdate(std::size_t port, Joycon::ControllerType type,
+ const Joycon::TagInfo& tag_info) {
+ const auto identifier = GetIdentifier(port, type);
+ const auto nfc_state = tag_info.uuid_length == 0 ? Common::Input::NfcState::AmiiboRemoved
+ : Common::Input::NfcState::NewAmiibo;
+
+ const Common::Input::NfcStatus nfc_status{
+ .state = nfc_state,
+ .uuid_length = tag_info.uuid_length,
+ .protocol = tag_info.protocol,
+ .tag_type = tag_info.tag_type,
+ .uuid = tag_info.uuid,
+ };
+
+ SetNfc(identifier, nfc_status);
+}
+
+void Joycons::OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data,
+ Joycon::IrsResolution format) {
+ const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right);
+ SetCamera(identifier, {static_cast<Common::Input::CameraFormat>(format), camera_data});
+}
+
+std::shared_ptr<Joycon::JoyconDriver> Joycons::GetHandle(PadIdentifier identifier) const {
+ auto is_handle_active = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
+ if (!device) {
+ return false;
+ }
+ if (!device->IsConnected()) {
+ return false;
+ }
+ if (device->GetDevicePort() == identifier.port) {
+ return true;
+ }
+ return false;
+ };
+ const auto type = static_cast<Joycon::ControllerType>(identifier.pad);
+
+ if (type == Joycon::ControllerType::Left) {
+ const auto matching_device = std::ranges::find_if(
+ left_joycons, [is_handle_active](auto& device) { return is_handle_active(device); });
+
+ if (matching_device != left_joycons.end()) {
+ return *matching_device;
+ }
+ }
+
+ if (type == Joycon::ControllerType::Right) {
+ const auto matching_device = std::ranges::find_if(
+ right_joycons, [is_handle_active](auto& device) { return is_handle_active(device); });
+
+ if (matching_device != right_joycons.end()) {
+ return *matching_device;
+ }
+ }
+
+ if (type == Joycon::ControllerType::Pro) {
+ const auto matching_device = std::ranges::find_if(
+ pro_controller, [is_handle_active](auto& device) { return is_handle_active(device); });
+
+ if (matching_device != pro_controller.end()) {
+ return *matching_device;
+ }
+ }
+
+ return nullptr;
+}
+
+PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const {
+ const std::array<u8, 16> guid{0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, static_cast<u8>(type)};
+ return {
+ .guid = Common::UUID{guid},
+ .port = port,
+ .pad = static_cast<std::size_t>(type),
+ };
+}
+
+Common::ParamPackage Joycons::GetParamPackage(std::size_t port, Joycon::ControllerType type) const {
+ const auto identifier = GetIdentifier(port, type);
+ return {
+ {"engine", GetEngineName()},
+ {"guid", identifier.guid.RawString()},
+ {"port", std::to_string(identifier.port)},
+ {"pad", std::to_string(identifier.pad)},
+ };
+}
+
+std::vector<Common::ParamPackage> Joycons::GetInputDevices() const {
+ std::vector<Common::ParamPackage> devices{};
+
+ auto add_entry = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
+ if (!device) {
+ return;
+ }
+ if (!device->IsConnected()) {
+ return;
+ }
+ auto param = GetParamPackage(device->GetDevicePort(), device->GetHandleDeviceType());
+ std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()),
+ device->GetDevicePort() + 1);
+ param.Set("display", std::move(name));
+ devices.emplace_back(param);
+ };
+
+ for (const auto& controller : left_joycons) {
+ add_entry(controller);
+ }
+ for (const auto& controller : right_joycons) {
+ add_entry(controller);
+ }
+ for (const auto& controller : pro_controller) {
+ add_entry(controller);
+ }
+
+ // List dual joycon pairs
+ for (std::size_t i = 0; i < MaxSupportedControllers; i++) {
+ if (!left_joycons[i] || !right_joycons[i]) {
+ continue;
+ }
+ if (!left_joycons[i]->IsConnected() || !right_joycons[i]->IsConnected()) {
+ continue;
+ }
+ auto main_param = GetParamPackage(i, left_joycons[i]->GetHandleDeviceType());
+ const auto second_param = GetParamPackage(i, right_joycons[i]->GetHandleDeviceType());
+ const auto type = Joycon::ControllerType::Dual;
+ std::string name = fmt::format("{} {}", JoyconName(type), i + 1);
+
+ main_param.Set("display", std::move(name));
+ main_param.Set("guid2", second_param.Get("guid", ""));
+ main_param.Set("pad", std::to_string(static_cast<size_t>(type)));
+ devices.emplace_back(main_param);
+ }
+
+ return devices;
+}
+
+ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) {
+ static constexpr std::array<std::tuple<Settings::NativeButton::Values, Joycon::PadButton, bool>,
+ 18>
+ switch_to_joycon_button = {
+ std::tuple{Settings::NativeButton::A, Joycon::PadButton::A, true},
+ {Settings::NativeButton::B, Joycon::PadButton::B, true},
+ {Settings::NativeButton::X, Joycon::PadButton::X, true},
+ {Settings::NativeButton::Y, Joycon::PadButton::Y, true},
+ {Settings::NativeButton::DLeft, Joycon::PadButton::Left, false},
+ {Settings::NativeButton::DUp, Joycon::PadButton::Up, false},
+ {Settings::NativeButton::DRight, Joycon::PadButton::Right, false},
+ {Settings::NativeButton::DDown, Joycon::PadButton::Down, false},
+ {Settings::NativeButton::L, Joycon::PadButton::L, false},
+ {Settings::NativeButton::R, Joycon::PadButton::R, true},
+ {Settings::NativeButton::ZL, Joycon::PadButton::ZL, false},
+ {Settings::NativeButton::ZR, Joycon::PadButton::ZR, true},
+ {Settings::NativeButton::Plus, Joycon::PadButton::Plus, true},
+ {Settings::NativeButton::Minus, Joycon::PadButton::Minus, false},
+ {Settings::NativeButton::Home, Joycon::PadButton::Home, true},
+ {Settings::NativeButton::Screenshot, Joycon::PadButton::Capture, false},
+ {Settings::NativeButton::LStick, Joycon::PadButton::StickL, false},
+ {Settings::NativeButton::RStick, Joycon::PadButton::StickR, true},
+ };
+
+ if (!params.Has("port")) {
+ return {};
+ }
+
+ ButtonMapping mapping{};
+ for (const auto& [switch_button, joycon_button, side] : switch_to_joycon_button) {
+ const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
+ auto pad = static_cast<Joycon::ControllerType>(params.Get("pad", 0));
+ if (pad == Joycon::ControllerType::Dual) {
+ pad = side ? Joycon::ControllerType::Right : Joycon::ControllerType::Left;
+ }
+
+ Common::ParamPackage button_params = GetParamPackage(port, pad);
+ button_params.Set("button", static_cast<int>(joycon_button));
+ mapping.insert_or_assign(switch_button, std::move(button_params));
+ }
+
+ // Map SL and SR buttons for left joycons
+ if (params.Get("pad", 0) == static_cast<int>(Joycon::ControllerType::Left)) {
+ const std::size_t port = static_cast<std::size_t>(params.Get("port", 0));
+ Common::ParamPackage button_params = GetParamPackage(port, Joycon::ControllerType::Left);
+
+ Common::ParamPackage sl_button_params = button_params;
+ Common::ParamPackage sr_button_params = button_params;
+ sl_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSL));
+ sr_button_params.Set("button", static_cast<int>(Joycon::PadButton::LeftSR));
+ mapping.insert_or_assign(Settings::NativeButton::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::Dual:
+ return "Dual Joycon";
+ default:
+ return "Unknown Switch Controller";
+ }
+}
+
+Common::Input::NfcState Joycons::TranslateDriverResult(Joycon::DriverResult result) const {
+ switch (result) {
+ case Joycon::DriverResult::Success:
+ return Common::Input::NfcState::Success;
+ case Joycon::DriverResult::Disabled:
+ return Common::Input::NfcState::WrongDeviceState;
+ case Joycon::DriverResult::NotSupported:
+ return Common::Input::NfcState::NotSupported;
+ default:
+ return Common::Input::NfcState::Unknown;
+ }
+}
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/joycon.h b/src/input_common/drivers/joycon.h
new file mode 100644
index 000000000..4c323d7d6
--- /dev/null
+++ b/src/input_common/drivers/joycon.h
@@ -0,0 +1,125 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <span>
+#include <thread>
+#include <SDL_hidapi.h>
+
+#include "input_common/input_engine.h"
+
+namespace InputCommon::Joycon {
+using SerialNumber = std::array<u8, 15>;
+struct Battery;
+struct Color;
+struct MotionData;
+struct TagInfo;
+enum class ControllerType : u8;
+enum class 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 StartNfcPolling(const PadIdentifier& identifier) override;
+ Common::Input::NfcState StopNfcPolling(const PadIdentifier& identifier) override;
+ Common::Input::NfcState ReadAmiiboData(const PadIdentifier& identifier,
+ std::vector<u8>& out_data) override;
+ Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier,
+ const std::vector<u8>& data) override;
+ Common::Input::NfcState ReadMifareData(const PadIdentifier& identifier,
+ const Common::Input::MifareRequest& request,
+ Common::Input::MifareRequest& out_data) override;
+ Common::Input::NfcState WriteMifareData(const PadIdentifier& identifier,
+ const Common::Input::MifareRequest& request) override;
+
+ Common::Input::DriverResult SetPollingMode(
+ const PadIdentifier& identifier, const Common::Input::PollingMode polling_mode) override;
+
+ /// Used for automapping features
+ std::vector<Common::ParamPackage> GetInputDevices() const override;
+ ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
+ AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
+ MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
+ Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
+
+private:
+ static constexpr std::size_t MaxSupportedControllers = 8;
+
+ /// For shutting down, clear all data, join all threads, release usb devices
+ void Reset();
+
+ /// Registers controllers, clears all data and starts the scan thread
+ void Setup();
+
+ /// Actively searches for new devices
+ void ScanThread(std::stop_token stop_token);
+
+ /// Returns true if device is valid and not registered
+ bool IsDeviceNew(SDL_hid_device_info* device_info) const;
+
+ /// Tries to connect to the new device
+ void RegisterNewDevice(SDL_hid_device_info* device_info);
+
+ /// Returns the next free handle
+ std::shared_ptr<Joycon::JoyconDriver> GetNextFreeHandle(Joycon::ControllerType type) const;
+
+ void OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, Joycon::Battery value);
+ void OnColorUpdate(std::size_t port, Joycon::ControllerType type, const Joycon::Color& value);
+ void OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value);
+ void OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value);
+ void OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
+ const Joycon::MotionData& value);
+ void OnRingConUpdate(f32 ring_data);
+ void OnAmiiboUpdate(std::size_t port, Joycon::ControllerType type,
+ const Joycon::TagInfo& amiibo_data);
+ void OnCameraUpdate(std::size_t port, const std::vector<u8>& camera_data,
+ Joycon::IrsResolution format);
+
+ /// Returns a JoyconHandle corresponding to a PadIdentifier
+ std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const;
+
+ /// Returns a PadIdentifier corresponding to the port number and joycon type
+ PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const;
+
+ /// Returns a ParamPackage corresponding to the port number and joycon type
+ Common::ParamPackage GetParamPackage(std::size_t port, Joycon::ControllerType type) const;
+
+ std::string JoyconName(std::size_t port) const;
+
+ Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
+
+ /// Returns the name of the device in text format
+ std::string JoyconName(Joycon::ControllerType type) const;
+
+ Common::Input::NfcState TranslateDriverResult(Joycon::DriverResult result) const;
+
+ std::jthread scan_thread;
+
+ // Joycon types are split by type to ease supporting dualjoycon configurations
+ std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> left_joycons{};
+ std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> right_joycons{};
+ std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> pro_controller{};
+};
+
+} // namespace InputCommon
diff --git a/src/input_common/drivers/keyboard.cpp b/src/input_common/drivers/keyboard.cpp
index 71e612fbf..2567df9af 100644
--- a/src/input_common/drivers/keyboard.cpp
+++ b/src/input_common/drivers/keyboard.cpp
@@ -24,7 +24,7 @@ constexpr PadIdentifier keyboard_modifier_identifier = {
};
Keyboard::Keyboard(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
- // Keyboard is broken into 3 diferent sets:
+ // Keyboard is broken into 3 different sets:
// key: Unfiltered intended for controllers.
// keyboard_key: Allows only Settings::NativeKeyboard::Keys intended for keyboard emulation.
// keyboard_modifier: Allows only Settings::NativeKeyboard::Modifiers intended for keyboard
diff --git a/src/input_common/drivers/mouse.cpp b/src/input_common/drivers/mouse.cpp
index faf9cbdc3..f07cf8a0e 100644
--- a/src/input_common/drivers/mouse.cpp
+++ b/src/input_common/drivers/mouse.cpp
@@ -3,6 +3,7 @@
#include <thread>
#include <fmt/format.h>
+#include <math.h>
#include "common/param_package.h"
#include "common/settings.h"
@@ -10,135 +11,232 @@
#include "input_common/drivers/mouse.h"
namespace InputCommon {
+constexpr int update_time = 10;
+constexpr float default_stick_sensitivity = 0.0044f;
+constexpr float default_motion_sensitivity = 0.0003f;
+constexpr float maximum_rotation_speed = 2.0f;
constexpr int mouse_axis_x = 0;
constexpr int mouse_axis_y = 1;
constexpr int wheel_axis_x = 2;
constexpr int wheel_axis_y = 3;
-constexpr int motion_wheel_y = 4;
-constexpr int touch_axis_x = 10;
-constexpr int touch_axis_y = 11;
constexpr PadIdentifier identifier = {
.guid = Common::UUID{},
.port = 0,
.pad = 0,
};
+constexpr PadIdentifier motion_identifier = {
+ .guid = Common::UUID{},
+ .port = 0,
+ .pad = 1,
+};
+
+constexpr PadIdentifier real_mouse_identifier = {
+ .guid = Common::UUID{},
+ .port = 1,
+ .pad = 0,
+};
+
+constexpr PadIdentifier touch_identifier = {
+ .guid = Common::UUID{},
+ .port = 2,
+ .pad = 0,
+};
+
Mouse::Mouse(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
PreSetController(identifier);
+ PreSetController(real_mouse_identifier);
+ PreSetController(touch_identifier);
+ PreSetController(motion_identifier);
+
+ // Initialize all mouse axis
PreSetAxis(identifier, mouse_axis_x);
PreSetAxis(identifier, mouse_axis_y);
PreSetAxis(identifier, wheel_axis_x);
PreSetAxis(identifier, wheel_axis_y);
- PreSetAxis(identifier, motion_wheel_y);
- PreSetAxis(identifier, touch_axis_x);
- PreSetAxis(identifier, touch_axis_y);
+ PreSetAxis(real_mouse_identifier, mouse_axis_x);
+ PreSetAxis(real_mouse_identifier, mouse_axis_y);
+ PreSetAxis(touch_identifier, mouse_axis_x);
+ PreSetAxis(touch_identifier, mouse_axis_y);
+
+ // Initialize variables
+ mouse_origin = {};
+ last_mouse_position = {};
+ wheel_position = {};
+ last_mouse_change = {};
+ last_motion_change = {};
+
update_thread = std::jthread([this](std::stop_token stop_token) { UpdateThread(stop_token); });
}
void Mouse::UpdateThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("Mouse");
- constexpr int update_time = 10;
+
while (!stop_token.stop_requested()) {
- if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
- // Slow movement by 4%
- last_mouse_change *= 0.96f;
- const float sensitivity =
- Settings::values.mouse_panning_sensitivity.GetValue() * 0.022f;
- SetAxis(identifier, mouse_axis_x, last_mouse_change.x * sensitivity);
- SetAxis(identifier, mouse_axis_y, -last_mouse_change.y * sensitivity);
- }
-
- SetAxis(identifier, motion_wheel_y, 0.0f);
-
- if (mouse_panning_timout++ > 20) {
- StopPanning();
- }
+ UpdateStickInput();
+ UpdateMotionInput();
+
std::this_thread::sleep_for(std::chrono::milliseconds(update_time));
}
}
-void Mouse::MouseMove(int x, int y, f32 touch_x, f32 touch_y, int center_x, int center_y) {
- // If native mouse is enabled just set the screen coordinates
- if (Settings::values.mouse_enabled) {
- SetAxis(identifier, mouse_axis_x, touch_x);
- SetAxis(identifier, mouse_axis_y, touch_y);
+void Mouse::UpdateStickInput() {
+ if (!Settings::values.mouse_panning) {
return;
}
- SetAxis(identifier, touch_axis_x, touch_x);
- SetAxis(identifier, touch_axis_y, touch_y);
+ const float length = last_mouse_change.Length();
- if (Settings::values.mouse_panning) {
- auto mouse_change =
- (Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
- mouse_panning_timout = 0;
+ // Prevent input from exceeding the max range (1.0f) too much,
+ // but allow some room to make it easier to sustain
+ if (length > 1.2f) {
+ last_mouse_change /= length;
+ last_mouse_change *= 1.2f;
+ }
+
+ auto mouse_change = last_mouse_change;
+
+ // Bind the mouse change to [0 <= deadzone_counterweight <= 1,1]
+ if (length < 1.0f) {
+ const float deadzone_h_counterweight =
+ Settings::values.mouse_panning_deadzone_x_counterweight.GetValue();
+ const float deadzone_v_counterweight =
+ Settings::values.mouse_panning_deadzone_y_counterweight.GetValue();
+ mouse_change /= length;
+ mouse_change.x *= length + (1 - length) * deadzone_h_counterweight * 0.01f;
+ mouse_change.y *= length + (1 - length) * deadzone_v_counterweight * 0.01f;
+ }
+
+ SetAxis(identifier, mouse_axis_x, mouse_change.x);
+ SetAxis(identifier, mouse_axis_y, -mouse_change.y);
+
+ // Decay input over time
+ const float clamped_length = std::min(1.0f, length);
+ const float decay_strength = Settings::values.mouse_panning_decay_strength.GetValue();
+ const float decay = 1 - clamped_length * clamped_length * decay_strength * 0.01f;
+ const float min_decay = Settings::values.mouse_panning_min_decay.GetValue();
+ const float clamped_decay = std::min(1 - min_decay / 100.0f, decay);
+ last_mouse_change *= clamped_decay;
+}
- const auto move_distance = mouse_change.Length();
- if (move_distance == 0) {
- return;
- }
+void Mouse::UpdateMotionInput() {
+ // This may need its own sensitivity instead of using the average
+ const float sensitivity = (Settings::values.mouse_panning_x_sensitivity.GetValue() +
+ Settings::values.mouse_panning_y_sensitivity.GetValue()) /
+ 2.0f * default_motion_sensitivity;
- // Make slow movements at least 3 units on lenght
- if (move_distance < 3.0f) {
- // Normalize value
- mouse_change /= move_distance;
- mouse_change *= 3.0f;
- }
+ const float rotation_velocity = std::sqrt(last_motion_change.x * last_motion_change.x +
+ last_motion_change.y * last_motion_change.y);
- // Average mouse movements
- last_mouse_change = (last_mouse_change * 0.91f) + (mouse_change * 0.09f);
+ if (rotation_velocity > maximum_rotation_speed / sensitivity) {
+ const float multiplier = maximum_rotation_speed / rotation_velocity / sensitivity;
+ last_motion_change.x = last_motion_change.x * multiplier;
+ last_motion_change.y = last_motion_change.y * multiplier;
+ }
- const auto last_move_distance = last_mouse_change.Length();
+ const BasicMotion motion_data{
+ .gyro_x = last_motion_change.x * sensitivity,
+ .gyro_y = last_motion_change.y * sensitivity,
+ .gyro_z = last_motion_change.z * sensitivity,
+ .accel_x = 0,
+ .accel_y = 0,
+ .accel_z = 0,
+ .delta_timestamp = update_time * 1000,
+ };
- // Make fast movements clamp to 8 units on lenght
- if (last_move_distance > 8.0f) {
- // Normalize value
- last_mouse_change /= last_move_distance;
- last_mouse_change *= 8.0f;
- }
+ if (Settings::values.mouse_panning) {
+ last_motion_change.x = 0;
+ last_motion_change.y = 0;
+ }
+ last_motion_change.z = 0;
- // Ignore average if it's less than 1 unit and use current movement value
- if (last_move_distance < 1.0f) {
- last_mouse_change = mouse_change / mouse_change.Length();
- }
+ SetMotion(motion_identifier, 0, motion_data);
+}
+
+void Mouse::Move(int x, int y, int center_x, int center_y) {
+ if (Settings::values.mouse_panning) {
+ const auto mouse_change =
+ (Common::MakeVec(x, y) - Common::MakeVec(center_x, center_y)).Cast<float>();
+ const float x_sensitivity =
+ Settings::values.mouse_panning_x_sensitivity.GetValue() * default_stick_sensitivity;
+ const float y_sensitivity =
+ Settings::values.mouse_panning_y_sensitivity.GetValue() * default_stick_sensitivity;
+
+ last_motion_change += {-mouse_change.y, -mouse_change.x, 0};
+ last_mouse_change.x += mouse_change.x * x_sensitivity * 0.09f;
+ last_mouse_change.y += mouse_change.y * y_sensitivity * 0.09f;
return;
}
if (button_pressed) {
const auto mouse_move = Common::MakeVec<int>(x, y) - mouse_origin;
- const float sensitivity = Settings::values.mouse_panning_sensitivity.GetValue() * 0.0012f;
- SetAxis(identifier, mouse_axis_x, static_cast<float>(mouse_move.x) * sensitivity);
- SetAxis(identifier, mouse_axis_y, static_cast<float>(-mouse_move.y) * sensitivity);
+ const float x_sensitivity = Settings::values.mouse_panning_x_sensitivity.GetValue();
+ const float y_sensitivity = Settings::values.mouse_panning_y_sensitivity.GetValue();
+ SetAxis(identifier, mouse_axis_x,
+ static_cast<float>(mouse_move.x) * x_sensitivity * 0.0012f);
+ SetAxis(identifier, mouse_axis_y,
+ static_cast<float>(-mouse_move.y) * y_sensitivity * 0.0012f);
+
+ last_motion_change = {
+ static_cast<float>(-mouse_move.y) / 50.0f,
+ static_cast<float>(-mouse_move.x) / 50.0f,
+ last_motion_change.z,
+ };
}
}
-void Mouse::PressButton(int x, int y, f32 touch_x, f32 touch_y, MouseButton button) {
- SetAxis(identifier, touch_axis_x, touch_x);
- SetAxis(identifier, touch_axis_y, touch_y);
+void Mouse::MouseMove(f32 touch_x, f32 touch_y) {
+ SetAxis(real_mouse_identifier, mouse_axis_x, touch_x);
+ SetAxis(real_mouse_identifier, mouse_axis_y, touch_y);
+}
+
+void Mouse::TouchMove(f32 touch_x, f32 touch_y) {
+ SetAxis(touch_identifier, mouse_axis_x, touch_x);
+ SetAxis(touch_identifier, mouse_axis_y, touch_y);
+}
+
+void Mouse::PressButton(int x, int y, MouseButton button) {
SetButton(identifier, static_cast<int>(button), true);
+
// Set initial analog parameters
mouse_origin = {x, y};
last_mouse_position = {x, y};
button_pressed = true;
}
+void Mouse::PressMouseButton(MouseButton button) {
+ SetButton(real_mouse_identifier, static_cast<int>(button), true);
+}
+
+void Mouse::PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button) {
+ SetAxis(touch_identifier, mouse_axis_x, touch_x);
+ SetAxis(touch_identifier, mouse_axis_y, touch_y);
+ SetButton(touch_identifier, static_cast<int>(button), true);
+}
+
void Mouse::ReleaseButton(MouseButton button) {
SetButton(identifier, static_cast<int>(button), false);
+ SetButton(real_mouse_identifier, static_cast<int>(button), false);
+ SetButton(touch_identifier, static_cast<int>(button), false);
- if (!Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
+ if (!Settings::values.mouse_panning) {
SetAxis(identifier, mouse_axis_x, 0);
SetAxis(identifier, mouse_axis_y, 0);
}
+
+ last_motion_change.x = 0;
+ last_motion_change.y = 0;
+
button_pressed = false;
}
void Mouse::MouseWheelChange(int x, int y) {
wheel_position.x += x;
wheel_position.y += y;
+ last_motion_change.z += static_cast<f32>(y) / 100.0f;
SetAxis(identifier, wheel_axis_x, static_cast<f32>(wheel_position.x));
SetAxis(identifier, wheel_axis_y, static_cast<f32>(wheel_position.y));
- SetAxis(identifier, motion_wheel_y, static_cast<f32>(y) / 100.0f);
}
void Mouse::ReleaseAllButtons() {
@@ -146,10 +244,6 @@ void Mouse::ReleaseAllButtons() {
button_pressed = false;
}
-void Mouse::StopPanning() {
- last_mouse_change = {};
-}
-
std::vector<Common::ParamPackage> Mouse::GetInputDevices() const {
std::vector<Common::ParamPackage> devices;
devices.emplace_back(Common::ParamPackage{
@@ -207,6 +301,9 @@ Common::Input::ButtonNames Mouse::GetUIName(const Common::ParamPackage& params)
if (params.Has("axis_x") && params.Has("axis_y") && params.Has("axis_z")) {
return Common::Input::ButtonNames::Engine;
}
+ if (params.Has("motion")) {
+ return Common::Input::ButtonNames::Engine;
+ }
return Common::Input::ButtonNames::Invalid;
}
diff --git a/src/input_common/drivers/mouse.h b/src/input_common/drivers/mouse.h
index 72073cc23..0e8edcce1 100644
--- a/src/input_common/drivers/mouse.h
+++ b/src/input_common/drivers/mouse.h
@@ -37,13 +37,43 @@ public:
* @param center_x the x-coordinate of the middle of the screen
* @param center_y the y-coordinate of the middle of the screen
*/
- void MouseMove(int x, int y, f32 touch_x, f32 touch_y, int center_x, int center_y);
+ void Move(int x, int y, int center_x, int center_y);
/**
- * Sets the status of all buttons bound with the key to pressed
- * @param key_code the code of the key to press
+ * Signals that real mouse has moved.
+ * @param x the absolute position on the touchscreen of the cursor
+ * @param y the absolute position on the touchscreen of the cursor
*/
- void PressButton(int x, int y, f32 touch_x, f32 touch_y, MouseButton button);
+ void MouseMove(f32 touch_x, f32 touch_y);
+
+ /**
+ * Signals that touch finger has moved.
+ * @param x the absolute position on the touchscreen of the cursor
+ * @param y the absolute position on the touchscreen of the cursor
+ */
+ void TouchMove(f32 touch_x, f32 touch_y);
+
+ /**
+ * Sets the status of a button to pressed
+ * @param x the x-coordinate of the cursor
+ * @param y the y-coordinate of the cursor
+ * @param button the id of the button to press
+ */
+ void PressButton(int x, int y, MouseButton button);
+
+ /**
+ * Sets the status of a mouse button to pressed
+ * @param button the id of the button to press
+ */
+ void PressMouseButton(MouseButton button);
+
+ /**
+ * Sets the status of touch finger to pressed
+ * @param x the absolute position on the touchscreen of the cursor
+ * @param y the absolute position on the touchscreen of the cursor
+ * @param button the id of the button to press
+ */
+ void PressTouchButton(f32 touch_x, f32 touch_y, MouseButton button);
/**
* Sets the status of all buttons bound with the key to released
@@ -66,16 +96,17 @@ public:
private:
void UpdateThread(std::stop_token stop_token);
- void StopPanning();
+ void UpdateStickInput();
+ void UpdateMotionInput();
Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
Common::Vec2<int> mouse_origin;
Common::Vec2<int> last_mouse_position;
Common::Vec2<float> last_mouse_change;
+ Common::Vec3<float> last_motion_change;
Common::Vec2<int> wheel_position;
bool button_pressed;
- int mouse_panning_timout{};
std::jthread update_thread;
};
diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp
index 9835d99d2..9f26392b1 100644
--- a/src/input_common/drivers/sdl_driver.cpp
+++ b/src/input_common/drivers/sdl_driver.cpp
@@ -109,14 +109,37 @@ public:
}
bool RumblePlay(const Common::Input::VibrationStatus vibration) {
- constexpr u32 rumble_max_duration_ms = 1000;
+ constexpr u32 rumble_max_duration_ms = 2000;
+ constexpr f32 low_start_sensitivity_limit = 140.0;
+ constexpr f32 low_width_sensitivity_limit = 400.0;
+ constexpr f32 high_start_sensitivity_limit = 200.0;
+ constexpr f32 high_width_sensitivity_limit = 700.0;
+ // Try to provide some feeling of the frequency by reducing the amplitude depending on it.
+ f32 low_frequency_scale = 1.0;
+ if (vibration.low_frequency > low_start_sensitivity_limit) {
+ low_frequency_scale =
+ std::max(1.0f - (vibration.low_frequency - low_start_sensitivity_limit) /
+ low_width_sensitivity_limit,
+ 0.3f);
+ }
+ f32 low_amplitude = vibration.low_amplitude * low_frequency_scale;
+
+ f32 high_frequency_scale = 1.0;
+ if (vibration.high_frequency > high_start_sensitivity_limit) {
+ high_frequency_scale =
+ std::max(1.0f - (vibration.high_frequency - high_start_sensitivity_limit) /
+ high_width_sensitivity_limit,
+ 0.3f);
+ }
+ f32 high_amplitude = vibration.high_amplitude * high_frequency_scale;
+
if (sdl_controller) {
- return SDL_GameControllerRumble(
- sdl_controller.get(), static_cast<u16>(vibration.low_amplitude),
- static_cast<u16>(vibration.high_amplitude), rumble_max_duration_ms) != -1;
+ return SDL_GameControllerRumble(sdl_controller.get(), static_cast<u16>(low_amplitude),
+ static_cast<u16>(high_amplitude),
+ rumble_max_duration_ms) != -1;
} else if (sdl_joystick) {
- return SDL_JoystickRumble(sdl_joystick.get(), static_cast<u16>(vibration.low_amplitude),
- static_cast<u16>(vibration.high_amplitude),
+ return SDL_JoystickRumble(sdl_joystick.get(), static_cast<u16>(low_amplitude),
+ static_cast<u16>(high_amplitude),
rumble_max_duration_ms) != -1;
}
@@ -127,6 +150,8 @@ public:
if (sdl_controller) {
const auto type = SDL_GameControllerGetType(sdl_controller.get());
return (type == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) ||
+ (type == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT) ||
+ (type == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT) ||
(type == SDL_CONTROLLER_TYPE_PS5);
}
return false;
@@ -205,9 +230,8 @@ public:
return false;
}
- Common::Input::BatteryLevel GetBatteryLevel() {
- const auto level = SDL_JoystickCurrentPowerLevel(sdl_joystick.get());
- switch (level) {
+ Common::Input::BatteryLevel GetBatteryLevel(SDL_JoystickPowerLevel battery_level) {
+ switch (battery_level) {
case SDL_JOYSTICK_POWER_EMPTY:
return Common::Input::BatteryLevel::Empty;
case SDL_JOYSTICK_POWER_LOW:
@@ -334,11 +358,27 @@ void SDLDriver::InitJoystick(int joystick_index) {
const auto guid = GetGUID(sdl_joystick);
+ if (Settings::values.enable_joycon_driver) {
+ if (guid.uuid[5] == 0x05 && guid.uuid[4] == 0x7e &&
+ (guid.uuid[8] == 0x06 || guid.uuid[8] == 0x07)) {
+ LOG_WARNING(Input, "Preferring joycon driver for device index {}", joystick_index);
+ SDL_JoystickClose(sdl_joystick);
+ return;
+ }
+ }
+
+ if (Settings::values.enable_procon_driver) {
+ if (guid.uuid[5] == 0x05 && guid.uuid[4] == 0x7e && guid.uuid[8] == 0x09) {
+ LOG_WARNING(Input, "Preferring joycon driver for device index {}", joystick_index);
+ SDL_JoystickClose(sdl_joystick);
+ return;
+ }
+ }
+
std::scoped_lock lock{joystick_map_mutex};
if (joystick_map.find(guid) == joystick_map.end()) {
auto joystick = std::make_shared<SDLJoystick>(guid, 0, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
- SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
joystick->EnableMotion();
joystick_map[guid].emplace_back(std::move(joystick));
return;
@@ -358,7 +398,6 @@ void SDLDriver::InitJoystick(int joystick_index) {
const int port = static_cast<int>(joystick_guid_list.size());
auto joystick = std::make_shared<SDLJoystick>(guid, port, sdl_joystick, sdl_gamecontroller);
PreSetController(joystick->GetPadIdentifier());
- SetBattery(joystick->GetPadIdentifier(), joystick->GetBatteryLevel());
joystick->EnableMotion();
joystick_guid_list.emplace_back(std::move(joystick));
}
@@ -398,8 +437,6 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
if (const auto joystick = GetSDLJoystickBySDLID(event.jbutton.which)) {
const PadIdentifier identifier = joystick->GetPadIdentifier();
SetButton(identifier, event.jbutton.button, true);
- // Battery doesn't trigger an event so just update every button press
- SetBattery(identifier, joystick->GetBatteryLevel());
}
break;
}
@@ -426,6 +463,13 @@ void SDLDriver::HandleGameControllerEvent(const SDL_Event& event) {
}
break;
}
+ case SDL_JOYBATTERYUPDATED: {
+ if (auto joystick = GetSDLJoystickBySDLID(event.jbattery.which)) {
+ const PadIdentifier identifier = joystick->GetPadIdentifier();
+ SetBattery(identifier, joystick->GetBatteryLevel(event.jbattery.level));
+ }
+ break;
+ }
case SDL_JOYDEVICEREMOVED:
LOG_DEBUG(Input, "Controller removed with Instance_ID {}", event.jdevice.which);
CloseJoystick(SDL_JoystickFromInstanceID(event.jdevice.which));
@@ -443,6 +487,10 @@ void SDLDriver::CloseJoysticks() {
}
SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_engine_)) {
+ // Set our application name. Currently passed to DBus by SDL and visible to the user through
+ // their desktop environment.
+ SDL_SetHint(SDL_HINT_APP_NAME, "yuzu");
+
if (!Settings::values.enable_raw_input) {
// Disable raw input. When enabled this setting causes SDL to die when a web applet opens
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
@@ -456,9 +504,25 @@ SDLDriver::SDLDriver(std::string input_engine_) : InputEngine(std::move(input_en
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
- // Use hidapi driver for joycons. This will allow joycons to be detected as a GameController and
- // not a generic one
- SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
+ // Disable hidapi drivers for joycon controllers when the custom joycon driver is enabled
+ if (Settings::values.enable_joycon_driver) {
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "0");
+ } else {
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOY_CONS, "1");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_JOYCON_HOME_LED, "0");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_VERTICAL_JOY_CONS, "1");
+ }
+
+ // Disable hidapi drivers for pro controllers when the custom joycon driver is enabled
+ if (Settings::values.enable_procon_driver) {
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "0");
+ } else {
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH, "1");
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0");
+ }
+
+ SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_PLAYER_LED, "1");
// Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native
// driver on Linux.
@@ -548,7 +612,7 @@ std::vector<Common::ParamPackage> SDLDriver::GetInputDevices() const {
return devices;
}
-Common::Input::VibrationError SDLDriver::SetVibration(
+Common::Input::DriverResult SDLDriver::SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
const auto joystick =
GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast<int>(identifier.port));
@@ -582,14 +646,14 @@ Common::Input::VibrationError SDLDriver::SetVibration(
.vibration = new_vibration,
});
- return Common::Input::VibrationError::None;
+ return Common::Input::DriverResult::Success;
}
bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) {
const auto joystick =
GetSDLJoystickByGUID(identifier.guid.RawString(), static_cast<int>(identifier.port));
- constexpr Common::Input::VibrationStatus test_vibration{
+ static constexpr Common::Input::VibrationStatus test_vibration{
.low_amplitude = 1,
.low_frequency = 160.0f,
.high_amplitude = 1,
@@ -597,7 +661,7 @@ bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) {
.type = Common::Input::VibrationAmplificationType::Exponential,
};
- constexpr Common::Input::VibrationStatus zero_vibration{
+ static constexpr Common::Input::VibrationStatus zero_vibration{
.low_amplitude = 0,
.low_frequency = 160.0f,
.high_amplitude = 0,
@@ -625,12 +689,27 @@ bool SDLDriver::IsVibrationEnabled(const PadIdentifier& identifier) {
}
void SDLDriver::SendVibrations() {
+ std::vector<VibrationRequest> filtered_vibrations{};
while (!vibration_queue.Empty()) {
VibrationRequest request;
vibration_queue.Pop(request);
const auto joystick = GetSDLJoystickByGUID(request.identifier.guid.RawString(),
static_cast<int>(request.identifier.port));
- joystick->RumblePlay(request.vibration);
+ const auto it = std::find_if(filtered_vibrations.begin(), filtered_vibrations.end(),
+ [request](VibrationRequest vibration) {
+ return vibration.identifier == request.identifier;
+ });
+ if (it == filtered_vibrations.end()) {
+ filtered_vibrations.push_back(std::move(request));
+ continue;
+ }
+ *it = request;
+ }
+
+ for (const auto& vibration : filtered_vibrations) {
+ const auto joystick = GetSDLJoystickByGUID(vibration.identifier.guid.RawString(),
+ static_cast<int>(vibration.identifier.port));
+ joystick->RumblePlay(vibration.vibration);
}
}
@@ -721,10 +800,12 @@ ButtonMapping SDLDriver::GetButtonMappingForDevice(const Common::ParamPackage& p
// This list is missing ZL/ZR since those are not considered buttons in SDL GameController.
// We will add those afterwards
- // This list also excludes Screenshot since theres not really a mapping for that
+ // This list also excludes Screenshot since there's not really a mapping for that
ButtonBindings switch_to_sdl_button;
- if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO) {
+ if (SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO ||
+ SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT ||
+ SDL_GameControllerGetType(controller) == SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT) {
switch_to_sdl_button = GetNintendoButtonBinding(joystick);
} else {
switch_to_sdl_button = GetDefaultButtonBinding();
@@ -980,7 +1061,7 @@ MotionMapping SDLDriver::GetMotionMappingForDevice(const Common::ParamPackage& p
Common::Input::ButtonNames SDLDriver::GetUIName(const Common::ParamPackage& params) const {
if (params.Has("button")) {
- // TODO(German77): Find how to substitue the values for real button names
+ // TODO(German77): Find how to substitute the values for real button names
return Common::Input::ButtonNames::Value;
}
if (params.Has("hat")) {
diff --git a/src/input_common/drivers/sdl_driver.h b/src/input_common/drivers/sdl_driver.h
index 366bcc496..ffde169b3 100644
--- a/src/input_common/drivers/sdl_driver.h
+++ b/src/input_common/drivers/sdl_driver.h
@@ -63,7 +63,7 @@ public:
bool IsStickInverted(const Common::ParamPackage& params) override;
- Common::Input::VibrationError SetVibration(
+ Common::Input::DriverResult SetVibration(
const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
bool IsVibrationEnabled(const PadIdentifier& identifier) override;
diff --git a/src/input_common/drivers/virtual_amiibo.cpp b/src/input_common/drivers/virtual_amiibo.cpp
index 63ffaca67..180eb53ef 100644
--- a/src/input_common/drivers/virtual_amiibo.cpp
+++ b/src/input_common/drivers/virtual_amiibo.cpp
@@ -22,28 +22,61 @@ VirtualAmiibo::VirtualAmiibo(std::string input_engine_) : InputEngine(std::move(
VirtualAmiibo::~VirtualAmiibo() = default;
-Common::Input::PollingError VirtualAmiibo::SetPollingMode(
+Common::Input::DriverResult VirtualAmiibo::SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier_,
const Common::Input::PollingMode polling_mode_) {
polling_mode = polling_mode_;
- if (polling_mode == Common::Input::PollingMode::NFC) {
- if (state == State::Initialized) {
- state = State::WaitingForAmiibo;
- }
- } else {
- if (state == State::AmiiboIsOpen) {
+ switch (polling_mode) {
+ case Common::Input::PollingMode::NFC:
+ state = State::Initialized;
+ return Common::Input::DriverResult::Success;
+ default:
+ if (state == State::TagNearby) {
CloseAmiibo();
}
+ state = State::Disabled;
+ return Common::Input::DriverResult::NotSupported;
}
-
- return Common::Input::PollingError::None;
}
Common::Input::NfcState VirtualAmiibo::SupportsNfc(
[[maybe_unused]] const PadIdentifier& identifier_) const {
return Common::Input::NfcState::Success;
}
+Common::Input::NfcState VirtualAmiibo::StartNfcPolling(const PadIdentifier& identifier_) {
+ if (state != State::Initialized) {
+ return Common::Input::NfcState::WrongDeviceState;
+ }
+ state = State::WaitingForAmiibo;
+ return Common::Input::NfcState::Success;
+}
+
+Common::Input::NfcState VirtualAmiibo::StopNfcPolling(const PadIdentifier& identifier_) {
+ if (state == State::Disabled) {
+ return Common::Input::NfcState::WrongDeviceState;
+ }
+ if (state == State::TagNearby) {
+ CloseAmiibo();
+ }
+ state = State::Initialized;
+ return Common::Input::NfcState::Success;
+}
+
+Common::Input::NfcState VirtualAmiibo::ReadAmiiboData(const PadIdentifier& identifier_,
+ std::vector<u8>& out_data) {
+ if (state != State::TagNearby) {
+ return Common::Input::NfcState::WrongDeviceState;
+ }
+
+ if (status.tag_type != 1U << 1) {
+ return Common::Input::NfcState::InvalidTagType;
+ }
+
+ out_data.resize(nfc_data.size());
+ memcpy(out_data.data(), nfc_data.data(), nfc_data.size());
+ return Common::Input::NfcState::Success;
+}
Common::Input::NfcState VirtualAmiibo::WriteNfcData(
[[maybe_unused]] const PadIdentifier& identifier_, const std::vector<u8>& data) {
@@ -56,7 +89,7 @@ Common::Input::NfcState VirtualAmiibo::WriteNfcData(
}
if (!nfc_file.Write(data)) {
- LOG_ERROR(Service_NFP, "Error writting to file");
+ LOG_ERROR(Service_NFP, "Error writing to file");
return Common::Input::NfcState::WriteFailed;
}
@@ -65,6 +98,69 @@ Common::Input::NfcState VirtualAmiibo::WriteNfcData(
return Common::Input::NfcState::Success;
}
+Common::Input::NfcState VirtualAmiibo::ReadMifareData(const PadIdentifier& identifier_,
+ const Common::Input::MifareRequest& request,
+ Common::Input::MifareRequest& out_data) {
+ if (state != State::TagNearby) {
+ return Common::Input::NfcState::WrongDeviceState;
+ }
+
+ if (status.tag_type != 1U << 6) {
+ return Common::Input::NfcState::InvalidTagType;
+ }
+
+ for (std::size_t i = 0; i < request.data.size(); i++) {
+ if (request.data[i].command == 0) {
+ continue;
+ }
+ out_data.data[i].command = request.data[i].command;
+ out_data.data[i].sector = request.data[i].sector;
+
+ const std::size_t sector_index =
+ request.data[i].sector * sizeof(Common::Input::MifareData::data);
+
+ if (nfc_data.size() < sector_index + sizeof(Common::Input::MifareData::data)) {
+ return Common::Input::NfcState::WriteFailed;
+ }
+
+ // Ignore the sector key as we don't support it
+ memcpy(out_data.data[i].data.data(), nfc_data.data() + sector_index,
+ sizeof(Common::Input::MifareData::data));
+ }
+
+ return Common::Input::NfcState::Success;
+}
+
+Common::Input::NfcState VirtualAmiibo::WriteMifareData(
+ const PadIdentifier& identifier_, const Common::Input::MifareRequest& request) {
+ if (state != State::TagNearby) {
+ return Common::Input::NfcState::WrongDeviceState;
+ }
+
+ if (status.tag_type != 1U << 6) {
+ return Common::Input::NfcState::InvalidTagType;
+ }
+
+ for (std::size_t i = 0; i < request.data.size(); i++) {
+ if (request.data[i].command == 0) {
+ continue;
+ }
+
+ const std::size_t sector_index =
+ request.data[i].sector * sizeof(Common::Input::MifareData::data);
+
+ if (nfc_data.size() < sector_index + sizeof(Common::Input::MifareData::data)) {
+ return Common::Input::NfcState::WriteFailed;
+ }
+
+ // Ignore the sector key as we don't support it
+ memcpy(nfc_data.data() + sector_index, request.data[i].data.data(),
+ sizeof(Common::Input::MifareData::data));
+ }
+
+ return Common::Input::NfcState::Success;
+}
+
VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const {
return state;
}
@@ -72,10 +168,7 @@ VirtualAmiibo::State VirtualAmiibo::GetCurrentState() const {
VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
const Common::FS::IOFile nfc_file{filename, Common::FS::FileAccessMode::Read,
Common::FS::FileType::BinaryFile};
-
- if (state != State::WaitingForAmiibo) {
- return Info::WrongDeviceState;
- }
+ std::vector<u8> data{};
if (!nfc_file.IsOpen()) {
return Info::UnableToLoad;
@@ -84,14 +177,15 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
switch (nfc_file.GetSize()) {
case AmiiboSize:
case AmiiboSizeWithoutPassword:
- nfc_data.resize(AmiiboSize);
- if (nfc_file.Read(nfc_data) < AmiiboSizeWithoutPassword) {
+ case AmiiboSizeWithSignature:
+ data.resize(AmiiboSize);
+ if (nfc_file.Read(data) < AmiiboSizeWithoutPassword) {
return Info::NotAnAmiibo;
}
break;
case MifareSize:
- nfc_data.resize(MifareSize);
- if (nfc_file.Read(nfc_data) < MifareSize) {
+ data.resize(MifareSize);
+ if (nfc_file.Read(data) < MifareSize) {
return Info::NotAnAmiibo;
}
break;
@@ -100,14 +194,44 @@ VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(const std::string& filename) {
}
file_path = filename;
- state = State::AmiiboIsOpen;
- SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data});
+ return LoadAmiibo(data);
+}
+
+VirtualAmiibo::Info VirtualAmiibo::LoadAmiibo(std::span<u8> data) {
+ if (state != State::WaitingForAmiibo) {
+ return Info::WrongDeviceState;
+ }
+
+ switch (data.size_bytes()) {
+ case AmiiboSize:
+ case AmiiboSizeWithoutPassword:
+ case AmiiboSizeWithSignature:
+ nfc_data.resize(AmiiboSize);
+ status.tag_type = 1U << 1;
+ status.uuid_length = 7;
+ break;
+ case MifareSize:
+ nfc_data.resize(MifareSize);
+ status.tag_type = 1U << 6;
+ status.uuid_length = 4;
+ break;
+ default:
+ return Info::NotAnAmiibo;
+ }
+
+ status.uuid = {};
+ status.protocol = 1;
+ state = State::TagNearby;
+ status.state = Common::Input::NfcState::NewAmiibo,
+ memcpy(nfc_data.data(), data.data(), data.size_bytes());
+ memcpy(status.uuid.data(), nfc_data.data(), status.uuid_length);
+ SetNfc(identifier, status);
return Info::Success;
}
VirtualAmiibo::Info VirtualAmiibo::ReloadAmiibo() {
- if (state == State::AmiiboIsOpen) {
- SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, nfc_data});
+ if (state == State::TagNearby) {
+ SetNfc(identifier, status);
return Info::Success;
}
@@ -115,9 +239,14 @@ VirtualAmiibo::Info VirtualAmiibo::ReloadAmiibo() {
}
VirtualAmiibo::Info VirtualAmiibo::CloseAmiibo() {
- state = polling_mode == Common::Input::PollingMode::NFC ? State::WaitingForAmiibo
- : State::Initialized;
- SetNfc(identifier, {Common::Input::NfcState::AmiiboRemoved, {}});
+ if (state != State::TagNearby) {
+ return Info::Success;
+ }
+
+ state = State::WaitingForAmiibo;
+ status.state = Common::Input::NfcState::AmiiboRemoved;
+ SetNfc(identifier, status);
+ status.tag_type = 0;
return Info::Success;
}
diff --git a/src/input_common/drivers/virtual_amiibo.h b/src/input_common/drivers/virtual_amiibo.h
index 0f9dad333..490f38e05 100644
--- a/src/input_common/drivers/virtual_amiibo.h
+++ b/src/input_common/drivers/virtual_amiibo.h
@@ -4,6 +4,7 @@
#pragma once
#include <array>
+#include <span>
#include <string>
#include <vector>
@@ -19,9 +20,10 @@ namespace InputCommon {
class VirtualAmiibo final : public InputEngine {
public:
enum class State {
+ Disabled,
Initialized,
WaitingForAmiibo,
- AmiiboIsOpen,
+ TagNearby,
};
enum class Info {
@@ -36,17 +38,26 @@ public:
~VirtualAmiibo() override;
// Sets polling mode to a controller
- Common::Input::PollingError SetPollingMode(
+ Common::Input::DriverResult SetPollingMode(
const PadIdentifier& identifier_, const Common::Input::PollingMode polling_mode_) override;
Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override;
-
+ Common::Input::NfcState StartNfcPolling(const PadIdentifier& identifier_) override;
+ Common::Input::NfcState StopNfcPolling(const PadIdentifier& identifier_) override;
+ Common::Input::NfcState ReadAmiiboData(const PadIdentifier& identifier_,
+ std::vector<u8>& out_data) override;
Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier_,
const std::vector<u8>& data) override;
+ Common::Input::NfcState ReadMifareData(const PadIdentifier& identifier_,
+ const Common::Input::MifareRequest& data,
+ Common::Input::MifareRequest& out_data) override;
+ Common::Input::NfcState WriteMifareData(const PadIdentifier& identifier_,
+ const Common::Input::MifareRequest& data) override;
State GetCurrentState() const;
Info LoadAmiibo(const std::string& amiibo_file);
+ Info LoadAmiibo(std::span<u8> data);
Info ReloadAmiibo();
Info CloseAmiibo();
@@ -55,11 +66,13 @@ public:
private:
static constexpr std::size_t AmiiboSize = 0x21C;
static constexpr std::size_t AmiiboSizeWithoutPassword = AmiiboSize - 0x8;
+ static constexpr std::size_t AmiiboSizeWithSignature = AmiiboSize + 0x20;
static constexpr std::size_t MifareSize = 0x400;
std::string file_path{};
- State state{State::Initialized};
+ State state{State::Disabled};
std::vector<u8> nfc_data;
- Common::Input::PollingMode polling_mode{Common::Input::PollingMode::Pasive};
+ Common::Input::NfcStatus status;
+ Common::Input::PollingMode polling_mode{Common::Input::PollingMode::Passive};
};
} // namespace InputCommon
diff --git a/src/input_common/drivers/virtual_gamepad.cpp b/src/input_common/drivers/virtual_gamepad.cpp
index 7db945aa6..c15cbbe58 100644
--- a/src/input_common/drivers/virtual_gamepad.cpp
+++ b/src/input_common/drivers/virtual_gamepad.cpp
@@ -39,6 +39,22 @@ void VirtualGamepad::SetStickPosition(std::size_t player_index, VirtualStick axi
SetStickPosition(player_index, static_cast<int>(axis_id), x_value, y_value);
}
+void VirtualGamepad::SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x,
+ float gyro_y, float gyro_z, float accel_x, float accel_y,
+ float accel_z) {
+ const auto identifier = GetIdentifier(player_index);
+ const BasicMotion motion_data{
+ .gyro_x = gyro_x,
+ .gyro_y = gyro_y,
+ .gyro_z = gyro_z,
+ .accel_x = accel_x,
+ .accel_y = accel_y,
+ .accel_z = accel_z,
+ .delta_timestamp = delta_timestamp,
+ };
+ SetMotion(identifier, 0, motion_data);
+}
+
void VirtualGamepad::ResetControllers() {
for (std::size_t i = 0; i < PlayerIndexCount; i++) {
SetStickPosition(i, VirtualStick::Left, 0.0f, 0.0f);
diff --git a/src/input_common/drivers/virtual_gamepad.h b/src/input_common/drivers/virtual_gamepad.h
index 3df91cc6f..dfbc45a28 100644
--- a/src/input_common/drivers/virtual_gamepad.h
+++ b/src/input_common/drivers/virtual_gamepad.h
@@ -52,7 +52,7 @@ public:
void SetButtonState(std::size_t player_index, VirtualButton button_id, bool value);
/**
- * Sets the status of all buttons bound with the key to released
+ * Sets the status of a stick to a specific player index
* @param player_index the player number that will take this action
* @param axis_id the id of the axis to move
* @param x_value the position of the stick in the x axis
@@ -62,6 +62,16 @@ public:
void SetStickPosition(std::size_t player_index, VirtualStick axis_id, float x_value,
float y_value);
+ /**
+ * Sets the status of the motion sensor to a specific player index
+ * @param player_index the player number that will take this action
+ * @param delta_timestamp time passed since last reading
+ * @param gyro_x,gyro_y,gyro_z the gyro sensor readings
+ * @param accel_x,accel_y,accel_z the acelerometer reading
+ */
+ void SetMotionState(std::size_t player_index, u64 delta_timestamp, float gyro_x, float gyro_y,
+ float gyro_z, float accel_x, float accel_y, float accel_z);
+
/// Restores all inputs into the neutral position
void ResetControllers();
diff --git a/src/input_common/helpers/joycon_driver.cpp b/src/input_common/helpers/joycon_driver.cpp
new file mode 100644
index 000000000..ec984a647
--- /dev/null
+++ b/src/input_common/helpers/joycon_driver.cpp
@@ -0,0 +1,710 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/logging/log.h"
+#include "common/scope_exit.h"
+#include "common/swap.h"
+#include "common/thread.h"
+#include "input_common/helpers/joycon_driver.h"
+#include "input_common/helpers/joycon_protocol/calibration.h"
+#include "input_common/helpers/joycon_protocol/generic_functions.h"
+#include "input_common/helpers/joycon_protocol/irs.h"
+#include "input_common/helpers/joycon_protocol/nfc.h"
+#include "input_common/helpers/joycon_protocol/poller.h"
+#include "input_common/helpers/joycon_protocol/ringcon.h"
+#include "input_common/helpers/joycon_protocol/rumble.h"
+
+namespace InputCommon::Joycon {
+JoyconDriver::JoyconDriver(std::size_t port_) : port{port_} {
+ hidapi_handle = std::make_shared<JoyconHandle>();
+}
+
+JoyconDriver::~JoyconDriver() {
+ Stop();
+}
+
+void JoyconDriver::Stop() {
+ is_connected = false;
+ input_thread = {};
+}
+
+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;
+ input_only_device = false;
+ gyro_sensitivity = Joycon::GyroSensitivity::DPS2000;
+ gyro_performance = Joycon::GyroPerformance::HZ833;
+ accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8;
+ accelerometer_performance = Joycon::AccelerometerPerformance::HZ100;
+
+ // Initialize HW Protocols
+ calibration_protocol = std::make_unique<CalibrationProtocol>(hidapi_handle);
+ generic_protocol = std::make_unique<GenericProtocol>(hidapi_handle);
+ irs_protocol = std::make_unique<IrsProtocol>(hidapi_handle);
+ nfc_protocol = std::make_unique<NfcProtocol>(hidapi_handle);
+ ring_protocol = std::make_unique<RingConProtocol>(hidapi_handle);
+ rumble_protocol = std::make_unique<RumbleProtocol>(hidapi_handle);
+
+ // Get fixed joycon info
+ if (generic_protocol->GetVersionNumber(version) != DriverResult::Success) {
+ // If this command fails the device doesn't accept configuration commands
+ input_only_device = true;
+ }
+
+ if (!input_only_device) {
+ generic_protocol->SetLowPowerMode(false);
+ generic_protocol->GetColor(color);
+ if (handle_device_type == ControllerType::Pro) {
+ // Some 3rd party controllers aren't pro controllers
+ generic_protocol->GetControllerType(device_type);
+ } else {
+ device_type = handle_device_type;
+ }
+ generic_protocol->GetSerialNumber(serial_number);
+ }
+
+ supported_features = GetSupportedFeatures();
+
+ // Get Calibration data
+ calibration_protocol->GetLeftJoyStickCalibration(left_stick_calibration);
+ calibration_protocol->GetRightJoyStickCalibration(right_stick_calibration);
+ calibration_protocol->GetImuCalibration(motion_calibration);
+
+ // Set led status
+ generic_protocol->SetLedBlinkPattern(static_cast<u8>(1 + port));
+
+ // Apply HW configuration
+ SetPollingMode();
+
+ // Initialize joycon poller
+ joycon_poller = std::make_unique<JoyconPoller>(device_type, left_stick_calibration,
+ right_stick_calibration, motion_calibration);
+
+ // Start polling for data
+ is_connected = true;
+ if (!input_thread_running) {
+ input_thread =
+ std::jthread([this](std::stop_token stop_token) { InputThread(stop_token); });
+ }
+
+ disable_input_thread = false;
+ return 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<ReportMode>(buffer[0]);
+
+ // Packages can be a little bit inconsistent. Average the delta time to provide a smoother
+ // motion experience
+ switch (report_mode) {
+ case ReportMode::STANDARD_FULL_60HZ:
+ case ReportMode::NFC_IR_MODE_60HZ:
+ case ReportMode::SIMPLE_HID_MODE: {
+ const auto now = std::chrono::steady_clock::now();
+ const auto new_delta_time = static_cast<u64>(
+ std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count());
+ delta_time = ((delta_time * 8) + (new_delta_time * 2)) / 10;
+ last_update = now;
+ joycon_poller->UpdateColor(color);
+ break;
+ }
+ default:
+ break;
+ }
+
+ const MotionStatus motion_status{
+ .is_enabled = motion_enabled,
+ .delta_time = delta_time,
+ .gyro_sensitivity = gyro_sensitivity,
+ .accelerometer_sensitivity = accelerometer_sensitivity,
+ };
+
+ // TODO: Remove this when calibration is properly loaded and not calculated
+ if (ring_connected && report_mode == ReportMode::STANDARD_FULL_60HZ) {
+ InputReportActive data{};
+ memcpy(&data, buffer.data(), sizeof(InputReportActive));
+ calibration_protocol->GetRingCalibration(ring_calibration, data.ring_input);
+ }
+
+ const RingStatus ring_status{
+ .is_enabled = ring_connected,
+ .default_value = ring_calibration.default_value,
+ .max_value = ring_calibration.max_value,
+ .min_value = ring_calibration.min_value,
+ };
+
+ if (irs_protocol->IsEnabled()) {
+ irs_protocol->RequestImage(buffer);
+ joycon_poller->UpdateCamera(irs_protocol->GetImage(), irs_protocol->GetIrsFormat());
+ }
+
+ if (nfc_protocol->IsPolling()) {
+ if (amiibo_detected) {
+ if (!nfc_protocol->HasAmiibo()) {
+ joycon_poller->UpdateAmiibo({});
+ amiibo_detected = false;
+ return;
+ }
+ }
+
+ if (!amiibo_detected) {
+ Joycon::TagInfo tag_info;
+ const auto result = nfc_protocol->GetTagInfo(tag_info);
+ if (result == DriverResult::Success) {
+ joycon_poller->UpdateAmiibo(tag_info);
+ amiibo_detected = true;
+ }
+ }
+ }
+
+ switch (report_mode) {
+ case ReportMode::STANDARD_FULL_60HZ:
+ joycon_poller->ReadActiveMode(buffer, motion_status, ring_status);
+ break;
+ case ReportMode::NFC_IR_MODE_60HZ:
+ joycon_poller->ReadNfcIRMode(buffer, motion_status);
+ break;
+ case ReportMode::SIMPLE_HID_MODE:
+ joycon_poller->ReadPassiveMode(buffer);
+ break;
+ case ReportMode::SUBCMD_REPLY:
+ LOG_DEBUG(Input, "Unhandled command reply");
+ break;
+ default:
+ LOG_ERROR(Input, "Report mode not Implemented {}", report_mode);
+ break;
+ }
+}
+
+DriverResult JoyconDriver::SetPollingMode() {
+ SCOPE_EXIT({ disable_input_thread = false; });
+ disable_input_thread = true;
+
+ rumble_protocol->EnableRumble(vibration_enabled && supported_features.vibration);
+
+ if (motion_enabled && supported_features.motion) {
+ generic_protocol->EnableImu(true);
+ generic_protocol->SetImuConfig(gyro_sensitivity, gyro_performance,
+ accelerometer_sensitivity, accelerometer_performance);
+ } else {
+ generic_protocol->EnableImu(false);
+ }
+
+ if (input_only_device) {
+ return DriverResult::NotSupported;
+ }
+
+ if (irs_protocol->IsEnabled()) {
+ irs_protocol->DisableIrs();
+ }
+
+ if (nfc_protocol->IsEnabled()) {
+ amiibo_detected = false;
+ nfc_protocol->DisableNfc();
+ }
+
+ if (ring_protocol->IsEnabled()) {
+ ring_connected = false;
+ ring_protocol->DisableRingCon();
+ }
+
+ if (irs_enabled && supported_features.irs) {
+ auto result = irs_protocol->EnableIrs();
+ if (result == DriverResult::Success) {
+ return result;
+ }
+ irs_protocol->DisableIrs();
+ LOG_ERROR(Input, "Error enabling IRS");
+ return result;
+ }
+
+ if (nfc_enabled && supported_features.nfc) {
+ auto result = nfc_protocol->EnableNfc();
+ if (result == DriverResult::Success) {
+ return result;
+ }
+ nfc_protocol->DisableNfc();
+ LOG_ERROR(Input, "Error enabling NFC");
+ return result;
+ }
+
+ if (hidbus_enabled && supported_features.hidbus) {
+ auto result = ring_protocol->EnableRingCon();
+ if (result == DriverResult::Success) {
+ result = ring_protocol->StartRingconPolling();
+ }
+ if (result == DriverResult::Success) {
+ ring_connected = true;
+ return result;
+ }
+ ring_connected = false;
+ ring_protocol->DisableRingCon();
+ LOG_ERROR(Input, "Error enabling Ringcon");
+ return result;
+ }
+
+ if (passive_enabled && supported_features.passive) {
+ const auto result = generic_protocol->EnablePassiveMode();
+ if (result == DriverResult::Success) {
+ 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");
+ }
+ // Switch calls this function after enabling active mode
+ generic_protocol->TriggersElapsed();
+
+ return result;
+}
+
+JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() {
+ SupportedFeatures features{
+ .passive = true,
+ .motion = true,
+ .vibration = true,
+ };
+
+ if (input_only_device) {
+ return features;
+ }
+
+ if (device_type == ControllerType::Right) {
+ features.nfc = true;
+ features.irs = true;
+ features.hidbus = true;
+ }
+
+ if (device_type == ControllerType::Pro) {
+ features.nfc = true;
+ }
+ return features;
+}
+
+bool JoyconDriver::IsInputThreadValid() const {
+ if (!is_connected.load()) {
+ return false;
+ }
+ if (hidapi_handle->handle == nullptr) {
+ return false;
+ }
+ // Controller is not responding. Terminate connection
+ if (error_counter > MaxErrorCount) {
+ return false;
+ }
+ return true;
+}
+
+bool JoyconDriver::IsPayloadCorrect(int status, std::span<const u8> buffer) {
+ if (status <= -1) {
+ error_counter++;
+ return false;
+ }
+ // There's no new data
+ if (status == 0) {
+ return false;
+ }
+ // No reply ever starts with zero
+ if (buffer[0] == 0x00) {
+ error_counter++;
+ return false;
+ }
+ error_counter = 0;
+ return true;
+}
+
+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::SetPassiveMode() {
+ 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;
+}
+
+DriverResult JoyconDriver::StartNfcPolling() {
+ std::scoped_lock lock{mutex};
+
+ if (!supported_features.nfc) {
+ return DriverResult::NotSupported;
+ }
+ if (!nfc_protocol->IsEnabled()) {
+ return DriverResult::Disabled;
+ }
+
+ disable_input_thread = true;
+ const auto result = nfc_protocol->StartNFCPollingMode();
+ disable_input_thread = false;
+
+ return result;
+}
+
+DriverResult JoyconDriver::StopNfcPolling() {
+ std::scoped_lock lock{mutex};
+
+ if (!supported_features.nfc) {
+ return DriverResult::NotSupported;
+ }
+ if (!nfc_protocol->IsEnabled()) {
+ return DriverResult::Disabled;
+ }
+
+ disable_input_thread = true;
+ const auto result = nfc_protocol->StopNFCPollingMode();
+ disable_input_thread = false;
+
+ if (amiibo_detected) {
+ amiibo_detected = false;
+ joycon_poller->UpdateAmiibo({});
+ }
+
+ return result;
+}
+
+DriverResult JoyconDriver::ReadAmiiboData(std::vector<u8>& out_data) {
+ std::scoped_lock lock{mutex};
+
+ if (!supported_features.nfc) {
+ return DriverResult::NotSupported;
+ }
+ if (!nfc_protocol->IsEnabled()) {
+ return DriverResult::Disabled;
+ }
+ if (!amiibo_detected) {
+ return DriverResult::ErrorWritingData;
+ }
+
+ out_data.resize(0x21C);
+ disable_input_thread = true;
+ const auto result = nfc_protocol->ReadAmiibo(out_data);
+ disable_input_thread = false;
+
+ return result;
+}
+
+DriverResult JoyconDriver::WriteNfcData(std::span<const u8> data) {
+ std::scoped_lock lock{mutex};
+
+ if (!supported_features.nfc) {
+ return DriverResult::NotSupported;
+ }
+ if (!nfc_protocol->IsEnabled()) {
+ return DriverResult::Disabled;
+ }
+ if (!amiibo_detected) {
+ return DriverResult::ErrorWritingData;
+ }
+
+ disable_input_thread = true;
+ const auto result = nfc_protocol->WriteAmiibo(data);
+ disable_input_thread = false;
+
+ return result;
+}
+
+DriverResult JoyconDriver::ReadMifareData(std::span<const MifareReadChunk> data,
+ std::span<MifareReadData> out_data) {
+ std::scoped_lock lock{mutex};
+
+ if (!supported_features.nfc) {
+ return DriverResult::NotSupported;
+ }
+ if (!nfc_protocol->IsEnabled()) {
+ return DriverResult::Disabled;
+ }
+ if (!amiibo_detected) {
+ return DriverResult::ErrorWritingData;
+ }
+
+ disable_input_thread = true;
+ const auto result = nfc_protocol->ReadMifare(data, out_data);
+ disable_input_thread = false;
+
+ return result;
+}
+
+DriverResult JoyconDriver::WriteMifareData(std::span<const MifareWriteChunk> data) {
+ std::scoped_lock lock{mutex};
+
+ if (!supported_features.nfc) {
+ return DriverResult::NotSupported;
+ }
+ if (!nfc_protocol->IsEnabled()) {
+ return DriverResult::Disabled;
+ }
+ if (!amiibo_detected) {
+ return DriverResult::ErrorWritingData;
+ }
+
+ disable_input_thread = true;
+ const auto result = nfc_protocol->WriteMifare(data);
+ disable_input_thread = false;
+
+ return result;
+}
+
+bool JoyconDriver::IsConnected() const {
+ std::scoped_lock lock{mutex};
+ return is_connected.load();
+}
+
+bool JoyconDriver::IsVibrationEnabled() const {
+ std::scoped_lock lock{mutex};
+ return vibration_enabled;
+}
+
+FirmwareVersion JoyconDriver::GetDeviceVersion() const {
+ std::scoped_lock lock{mutex};
+ return version;
+}
+
+Color JoyconDriver::GetDeviceColor() const {
+ std::scoped_lock lock{mutex};
+ return color;
+}
+
+std::size_t JoyconDriver::GetDevicePort() const {
+ std::scoped_lock lock{mutex};
+ return port;
+}
+
+ControllerType JoyconDriver::GetDeviceType() const {
+ std::scoped_lock lock{mutex};
+ return device_type;
+}
+
+ControllerType JoyconDriver::GetHandleDeviceType() const {
+ std::scoped_lock lock{mutex};
+ return handle_device_type;
+}
+
+SerialNumber JoyconDriver::GetSerialNumber() const {
+ std::scoped_lock lock{mutex};
+ return serial_number;
+}
+
+SerialNumber JoyconDriver::GetHandleSerialNumber() const {
+ std::scoped_lock lock{mutex};
+ return handle_serial_number;
+}
+
+void JoyconDriver::SetCallbacks(const JoyconCallbacks& callbacks) {
+ joycon_poller->SetCallbacks(callbacks);
+}
+
+DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info,
+ ControllerType& controller_type) {
+ static constexpr std::array<std::pair<u32, ControllerType>, 6> supported_devices{
+ std::pair<u32, ControllerType>{0x2006, ControllerType::Left},
+ {0x2007, ControllerType::Right},
+ {0x2009, ControllerType::Pro},
+ };
+ constexpr u16 nintendo_vendor_id = 0x057e;
+
+ controller_type = ControllerType::None;
+ if (device_info->vendor_id != nintendo_vendor_id) {
+ return 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
diff --git a/src/input_common/helpers/joycon_driver.h b/src/input_common/helpers/joycon_driver.h
new file mode 100644
index 000000000..45b32d2f8
--- /dev/null
+++ b/src/input_common/helpers/joycon_driver.h
@@ -0,0 +1,158 @@
+// 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 SetPassiveMode();
+ DriverResult SetActiveMode();
+ DriverResult SetIrMode();
+ DriverResult SetNfcMode();
+ DriverResult SetRingConMode();
+ DriverResult StartNfcPolling();
+ DriverResult StopNfcPolling();
+ DriverResult ReadAmiiboData(std::vector<u8>& out_data);
+ DriverResult WriteNfcData(std::span<const u8> data);
+ DriverResult ReadMifareData(std::span<const MifareReadChunk> request,
+ std::span<MifareReadData> out_data);
+ DriverResult WriteMifareData(std::span<const MifareWriteChunk> request);
+
+ 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 every time 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{};
+
+ // Hardware configuration
+ u8 leds{};
+ ReportMode mode{};
+ bool input_only_device{};
+ bool passive_enabled{}; // Low power mode, Ideal for multiple controllers at the same time
+ bool hidbus_enabled{}; // External device support
+ bool irs_enabled{}; // Infrared camera input
+ bool motion_enabled{}; // Enables motion input
+ bool nfc_enabled{}; // Enables Amiibo detection
+ bool vibration_enabled{}; // Allows vibrations
+
+ // Calibration data
+ GyroSensitivity gyro_sensitivity{};
+ GyroPerformance gyro_performance{};
+ AccelerometerSensitivity accelerometer_sensitivity{};
+ AccelerometerPerformance accelerometer_performance{};
+ JoyStickCalibration left_stick_calibration{};
+ JoyStickCalibration right_stick_calibration{};
+ MotionCalibration motion_calibration{};
+ RingCalibration ring_calibration{};
+
+ // Fixed joycon info
+ FirmwareVersion version{};
+ Color color{};
+ std::size_t port{};
+ ControllerType device_type{}; // Device type reported by controller
+ ControllerType handle_device_type{}; // Device type reported by hidapi
+ SerialNumber serial_number{}; // Serial number reported by controller
+ SerialNumber handle_serial_number{}; // Serial number type reported by hidapi
+ SupportedFeatures supported_features{};
+
+ // Thread related
+ mutable std::mutex mutex;
+ std::jthread input_thread;
+ bool input_thread_running{};
+ bool disable_input_thread{};
+};
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/calibration.cpp b/src/input_common/helpers/joycon_protocol/calibration.cpp
new file mode 100644
index 000000000..d8f040f75
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/calibration.cpp
@@ -0,0 +1,218 @@
+// 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);
+ DriverResult result{DriverResult::Success};
+ JoystickLeftSpiCalibration spi_calibration{};
+ bool has_user_calibration = false;
+ calibration = {};
+
+ if (result == DriverResult::Success) {
+ result = HasUserCalibration(SpiAddress::USER_LEFT_MAGIC, has_user_calibration);
+ }
+
+ // Read User defined calibration
+ if (result == DriverResult::Success && has_user_calibration) {
+ result = ReadSPI(SpiAddress::USER_LEFT_DATA, spi_calibration);
+ }
+
+ // Read Factory calibration
+ if (result == DriverResult::Success && !has_user_calibration) {
+ result = ReadSPI(SpiAddress::FACT_LEFT_DATA, spi_calibration);
+ }
+
+ if (result == DriverResult::Success) {
+ calibration.x.center = GetXAxisCalibrationValue(spi_calibration.center);
+ calibration.y.center = GetYAxisCalibrationValue(spi_calibration.center);
+ calibration.x.min = GetXAxisCalibrationValue(spi_calibration.min);
+ calibration.y.min = GetYAxisCalibrationValue(spi_calibration.min);
+ calibration.x.max = GetXAxisCalibrationValue(spi_calibration.max);
+ calibration.y.max = GetYAxisCalibrationValue(spi_calibration.max);
+ }
+
+ // Set a valid default calibration if data is missing
+ ValidateCalibration(calibration);
+
+ return result;
+}
+
+DriverResult CalibrationProtocol::GetRightJoyStickCalibration(JoyStickCalibration& calibration) {
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ JoystickRightSpiCalibration spi_calibration{};
+ bool has_user_calibration = false;
+ calibration = {};
+
+ if (result == DriverResult::Success) {
+ result = HasUserCalibration(SpiAddress::USER_RIGHT_MAGIC, has_user_calibration);
+ }
+
+ // Read User defined calibration
+ if (result == DriverResult::Success && has_user_calibration) {
+ result = ReadSPI(SpiAddress::USER_RIGHT_DATA, spi_calibration);
+ }
+
+ // Read Factory calibration
+ if (result == DriverResult::Success && !has_user_calibration) {
+ result = ReadSPI(SpiAddress::FACT_RIGHT_DATA, spi_calibration);
+ }
+
+ if (result == DriverResult::Success) {
+ calibration.x.center = GetXAxisCalibrationValue(spi_calibration.center);
+ calibration.y.center = GetYAxisCalibrationValue(spi_calibration.center);
+ calibration.x.min = GetXAxisCalibrationValue(spi_calibration.min);
+ calibration.y.min = GetYAxisCalibrationValue(spi_calibration.min);
+ calibration.x.max = GetXAxisCalibrationValue(spi_calibration.max);
+ calibration.y.max = GetYAxisCalibrationValue(spi_calibration.max);
+ }
+
+ // Set a valid default calibration if data is missing
+ ValidateCalibration(calibration);
+
+ return result;
+}
+
+DriverResult CalibrationProtocol::GetImuCalibration(MotionCalibration& calibration) {
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ ImuSpiCalibration spi_calibration{};
+ bool has_user_calibration = false;
+ calibration = {};
+
+ if (result == DriverResult::Success) {
+ result = HasUserCalibration(SpiAddress::USER_IMU_MAGIC, has_user_calibration);
+ }
+
+ // Read User defined calibration
+ if (result == DriverResult::Success && has_user_calibration) {
+ result = ReadSPI(SpiAddress::USER_IMU_DATA, spi_calibration);
+ }
+
+ // Read Factory calibration
+ if (result == DriverResult::Success && !has_user_calibration) {
+ result = ReadSPI(SpiAddress::FACT_IMU_DATA, spi_calibration);
+ }
+
+ if (result == DriverResult::Success) {
+ calibration.accelerometer[0].offset = spi_calibration.accelerometer_offset[0];
+ calibration.accelerometer[1].offset = spi_calibration.accelerometer_offset[1];
+ calibration.accelerometer[2].offset = spi_calibration.accelerometer_offset[2];
+
+ calibration.accelerometer[0].scale = spi_calibration.accelerometer_scale[0];
+ calibration.accelerometer[1].scale = spi_calibration.accelerometer_scale[1];
+ calibration.accelerometer[2].scale = spi_calibration.accelerometer_scale[2];
+
+ calibration.gyro[0].offset = spi_calibration.gyroscope_offset[0];
+ calibration.gyro[1].offset = spi_calibration.gyroscope_offset[1];
+ calibration.gyro[2].offset = spi_calibration.gyroscope_offset[2];
+
+ calibration.gyro[0].scale = spi_calibration.gyroscope_scale[0];
+ calibration.gyro[1].scale = spi_calibration.gyroscope_scale[1];
+ calibration.gyro[2].scale = spi_calibration.gyroscope_scale[2];
+ }
+
+ ValidateCalibration(calibration);
+
+ return result;
+}
+
+DriverResult CalibrationProtocol::GetRingCalibration(RingCalibration& calibration,
+ s16 current_value) {
+ constexpr s16 DefaultRingRange{800};
+
+ // TODO: Get default calibration form ring itself
+ if (ring_data_max == 0 && ring_data_min == 0) {
+ ring_data_max = current_value + DefaultRingRange;
+ ring_data_min = current_value - DefaultRingRange;
+ ring_data_default = current_value;
+ }
+ ring_data_max = std::max(ring_data_max, current_value);
+ ring_data_min = std::min(ring_data_min, current_value);
+ calibration = {
+ .default_value = ring_data_default,
+ .max_value = ring_data_max,
+ .min_value = ring_data_min,
+ };
+ return DriverResult::Success;
+}
+
+DriverResult CalibrationProtocol::HasUserCalibration(SpiAddress address,
+ bool& has_user_calibration) {
+ MagicSpiCalibration spi_magic{};
+ const DriverResult result{ReadSPI(address, spi_magic)};
+ has_user_calibration = false;
+ if (result == DriverResult::Success) {
+ has_user_calibration = spi_magic.first == CalibrationMagic::USR_MAGIC_0 &&
+ spi_magic.second == CalibrationMagic::USR_MAGIC_1;
+ }
+ return result;
+}
+
+u16 CalibrationProtocol::GetXAxisCalibrationValue(std::span<u8> block) const {
+ return static_cast<u16>(((block[1] & 0x0F) << 8) | block[0]);
+}
+
+u16 CalibrationProtocol::GetYAxisCalibrationValue(std::span<u8> block) const {
+ return static_cast<u16>((block[2] << 4) | (block[1] >> 4));
+}
+
+void CalibrationProtocol::ValidateCalibration(JoyStickCalibration& calibration) {
+ constexpr u16 DefaultStickCenter{0x800};
+ constexpr u16 DefaultStickRange{0x6cc};
+
+ calibration.x.center = ValidateValue(calibration.x.center, DefaultStickCenter);
+ calibration.x.max = ValidateValue(calibration.x.max, DefaultStickRange);
+ calibration.x.min = ValidateValue(calibration.x.min, DefaultStickRange);
+
+ calibration.y.center = ValidateValue(calibration.y.center, DefaultStickCenter);
+ calibration.y.max = ValidateValue(calibration.y.max, DefaultStickRange);
+ calibration.y.min = ValidateValue(calibration.y.min, DefaultStickRange);
+}
+
+void CalibrationProtocol::ValidateCalibration(MotionCalibration& calibration) {
+ constexpr s16 DefaultAccelerometerScale{0x4000};
+ constexpr s16 DefaultGyroScale{0x3be7};
+ constexpr s16 DefaultOffset{0};
+
+ for (auto& sensor : calibration.accelerometer) {
+ sensor.scale = ValidateValue(sensor.scale, DefaultAccelerometerScale);
+ sensor.offset = ValidateValue(sensor.offset, DefaultOffset);
+ }
+ for (auto& sensor : calibration.gyro) {
+ sensor.scale = ValidateValue(sensor.scale, DefaultGyroScale);
+ sensor.offset = ValidateValue(sensor.offset, DefaultOffset);
+ }
+}
+
+u16 CalibrationProtocol::ValidateValue(u16 value, u16 default_value) const {
+ if (value == 0) {
+ return default_value;
+ }
+ if (value == 0xFFF) {
+ return default_value;
+ }
+ return value;
+}
+
+s16 CalibrationProtocol::ValidateValue(s16 value, s16 default_value) const {
+ if (value == 0) {
+ return default_value;
+ }
+ if (value == 0xFFF) {
+ return default_value;
+ }
+ return value;
+}
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/calibration.h b/src/input_common/helpers/joycon_protocol/calibration.h
new file mode 100644
index 000000000..c6fd0f729
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/calibration.h
@@ -0,0 +1,82 @@
+// 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:
+ /// Returns true if the specified address corresponds to the magic value of user calibration
+ DriverResult HasUserCalibration(SpiAddress address, bool& has_user_calibration);
+
+ /// Converts a raw calibration block to an u16 value containing the x axis value
+ u16 GetXAxisCalibrationValue(std::span<u8> block) const;
+
+ /// Converts a raw calibration block to an u16 value containing the y axis value
+ u16 GetYAxisCalibrationValue(std::span<u8> block) const;
+
+ /// Ensures that all joystick calibration values are set
+ void ValidateCalibration(JoyStickCalibration& calibration);
+
+ /// Ensures that all motion calibration values are set
+ void ValidateCalibration(MotionCalibration& calibration);
+
+ /// Returns the default value if the value is either zero or 0xFFF
+ u16 ValidateValue(u16 value, u16 default_value) const;
+
+ /// Returns the default value if the value is either zero or 0xFFF
+ s16 ValidateValue(s16 value, s16 default_value) const;
+
+ s16 ring_data_max = 0;
+ s16 ring_data_default = 0;
+ s16 ring_data_min = 0;
+};
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/common_protocol.cpp b/src/input_common/helpers/joycon_protocol/common_protocol.cpp
new file mode 100644
index 000000000..88f4cec1c
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/common_protocol.cpp
@@ -0,0 +1,313 @@
+// 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) {
+ const auto result = ReadSPI(SpiAddress::DEVICE_TYPE, controller_type);
+
+ if (result == DriverResult::Success) {
+ // 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::SendRawData(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,
+ SubCommandResponse& output) {
+ constexpr int timeout_mili = 66;
+ constexpr int MaxTries = 3;
+ int tries = 0;
+
+ do {
+ int result = SDL_hid_read_timeout(hidapi_handle->handle, reinterpret_cast<u8*>(&output),
+ sizeof(SubCommandResponse), timeout_mili);
+
+ if (result < 1) {
+ LOG_ERROR(Input, "No response from joycon");
+ }
+ if (tries++ > MaxTries) {
+ return DriverResult::Timeout;
+ }
+ } while (output.input_report.report_mode != ReportMode::SUBCMD_REPLY &&
+ output.sub_command != sc);
+
+ return DriverResult::Success;
+}
+
+DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const u8> buffer,
+ SubCommandResponse& output) {
+ SubCommandPacket packet{
+ .output_report = OutputReport::RUMBLE_AND_SUBCMD,
+ .packet_counter = GetCounter(),
+ .sub_command = sc,
+ .command_data = {},
+ };
+
+ if (buffer.size() > packet.command_data.size()) {
+ return DriverResult::InvalidParameters;
+ }
+
+ memcpy(packet.command_data.data(), buffer.data(), buffer.size());
+
+ auto result = SendData(packet);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ return GetSubCommandResponse(sc, output);
+}
+
+DriverResult JoyconCommonProtocol::SendSubCommand(SubCommand sc, std::span<const u8> buffer) {
+ SubCommandResponse output{};
+ return SendSubCommand(sc, buffer, output);
+}
+
+DriverResult JoyconCommonProtocol::SendMCUCommand(SubCommand sc, std::span<const u8> buffer) {
+ SubCommandPacket packet{
+ .output_report = OutputReport::MCU_DATA,
+ .packet_counter = GetCounter(),
+ .sub_command = sc,
+ .command_data = {},
+ };
+
+ if (buffer.size() > packet.command_data.size()) {
+ return DriverResult::InvalidParameters;
+ }
+
+ memcpy(packet.command_data.data(), buffer.data(), buffer.size());
+
+ return SendData(packet);
+}
+
+DriverResult JoyconCommonProtocol::SendVibrationReport(std::span<const u8> buffer) {
+ VibrationPacket packet{
+ .output_report = OutputReport::RUMBLE_ONLY,
+ .packet_counter = GetCounter(),
+ .vibration_data = {},
+ };
+
+ if (buffer.size() > packet.vibration_data.size()) {
+ return DriverResult::InvalidParameters;
+ }
+
+ memcpy(packet.vibration_data.data(), buffer.data(), buffer.size());
+
+ return SendData(packet);
+}
+
+DriverResult JoyconCommonProtocol::ReadRawSPI(SpiAddress addr, std::span<u8> output) {
+ constexpr std::size_t HeaderSize = 5;
+ constexpr std::size_t MaxTries = 5;
+ std::size_t tries = 0;
+ SubCommandResponse response{};
+ std::array<u8, sizeof(ReadSpiPacket)> buffer{};
+ const ReadSpiPacket packet_data{
+ .spi_address = addr,
+ .size = static_cast<u8>(output.size()),
+ };
+
+ memcpy(buffer.data(), &packet_data, sizeof(ReadSpiPacket));
+ do {
+ const auto result = SendSubCommand(SubCommand::SPI_FLASH_READ, buffer, response);
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if (tries++ > MaxTries) {
+ return DriverResult::Timeout;
+ }
+ } while (response.spi_address != addr);
+
+ if (response.command_data.size() < packet_data.size + HeaderSize) {
+ return DriverResult::WrongReply;
+ }
+
+ // Remove header from output
+ memcpy(output.data(), response.command_data.data() + HeaderSize, packet_data.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, "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, "Failed with error {}", result);
+ }
+
+ return result;
+}
+
+DriverResult JoyconCommonProtocol::GetMCUDataResponse(ReportMode report_mode,
+ MCUCommandResponse& output) {
+ constexpr int TimeoutMili = 200;
+ constexpr int MaxTries = 9;
+ int tries = 0;
+
+ do {
+ int result = SDL_hid_read_timeout(hidapi_handle->handle, reinterpret_cast<u8*>(&output),
+ sizeof(MCUCommandResponse), TimeoutMili);
+
+ if (result < 1) {
+ LOG_ERROR(Input, "No response from joycon attempt {}", tries);
+ }
+ if (tries++ > MaxTries) {
+ return DriverResult::Timeout;
+ }
+ } while (output.input_report.report_mode != report_mode ||
+ output.mcu_report == MCUReport::EmptyAwaitingCmd);
+
+ return DriverResult::Success;
+}
+
+DriverResult JoyconCommonProtocol::SendMCUData(ReportMode report_mode, MCUSubCommand sc,
+ std::span<const u8> buffer,
+ MCUCommandResponse& output) {
+ SubCommandPacket packet{
+ .output_report = OutputReport::MCU_DATA,
+ .packet_counter = GetCounter(),
+ .mcu_sub_command = sc,
+ .command_data = {},
+ };
+
+ if (buffer.size() > packet.command_data.size()) {
+ return DriverResult::InvalidParameters;
+ }
+
+ memcpy(packet.command_data.data(), buffer.data(), buffer.size());
+
+ auto result = SendData(packet);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ result = GetMCUDataResponse(report_mode, output);
+
+ return DriverResult::Success;
+}
+
+DriverResult JoyconCommonProtocol::WaitSetMCUMode(ReportMode report_mode, MCUMode mode) {
+ MCUCommandResponse output{};
+ constexpr std::size_t MaxTries{16};
+ std::size_t tries{};
+
+ do {
+ const auto result = SendMCUData(report_mode, MCUSubCommand::SetDeviceMode, {}, output);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if (tries++ > MaxTries) {
+ return DriverResult::WrongReply;
+ }
+ } while (output.mcu_report != MCUReport::StateReport ||
+ output.mcu_data[6] != 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
diff --git a/src/input_common/helpers/joycon_protocol/common_protocol.h b/src/input_common/helpers/joycon_protocol/common_protocol.h
new file mode 100644
index 000000000..411ec018a
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/common_protocol.h
@@ -0,0 +1,201 @@
+// 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 SendRawData(std::span<const u8> buffer);
+
+ template <typename Output>
+ requires std::is_trivially_copyable_v<Output>
+ DriverResult SendData(const Output& output) {
+ std::array<u8, sizeof(Output)> buffer;
+ std::memcpy(buffer.data(), &output, sizeof(Output));
+ return SendRawData(buffer);
+ }
+
+ /**
+ * Waits for incoming data of the joycon device that matches the subcommand
+ * @param sub_command type of data to be returned
+ * @returns a buffer containing the response
+ */
+ DriverResult GetSubCommandResponse(SubCommand sub_command, SubCommandResponse& output);
+
+ /**
+ * Sends a sub command to the device and waits for it's reply
+ * @param sc sub command to be send
+ * @param buffer data to be send
+ * @returns output buffer containing the response
+ */
+ DriverResult SendSubCommand(SubCommand sc, std::span<const u8> buffer,
+ SubCommandResponse& output);
+
+ /**
+ * Sends a sub command to the device and waits for it's reply and ignores the output
+ * @param sc sub command to be send
+ * @param buffer data to be send
+ */
+ 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
+ * @returns output buffer containing the response
+ */
+ DriverResult ReadRawSPI(SpiAddress addr, std::span<u8> output);
+
+ /**
+ * Reads the SPI memory stored on the joycon
+ * @param Initial address location
+ * @returns output object containing the response
+ */
+ template <typename Output>
+ requires std::is_trivially_copyable_v<Output>
+ DriverResult ReadSPI(SpiAddress addr, Output& output) {
+ std::array<u8, sizeof(Output)> buffer;
+ output = {};
+
+ const auto result = ReadRawSPI(addr, buffer);
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ std::memcpy(&output, buffer.data(), sizeof(Output));
+ return DriverResult::Success;
+ }
+
+ /**
+ * Enables MCU chip on the joycon
+ * @param enable if true the chip will be enabled
+ */
+ DriverResult EnableMCU(bool enable);
+
+ /**
+ * Configures the MCU to the corresponding 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 response
+ */
+ DriverResult GetMCUDataResponse(ReportMode report_mode_, MCUCommandResponse& output);
+
+ /**
+ * Sends data to the MCU chip and waits for it's reply
+ * @param report mode of the expected reply
+ * @param sub command to be send
+ * @param buffer data to be send
+ * @returns output buffer containing the response
+ */
+ DriverResult SendMCUData(ReportMode report_mode, MCUSubCommand sc, std::span<const u8> buffer,
+ MCUCommandResponse& output);
+
+ /**
+ * Wait's until the MCU chip is on the specified mode
+ * @param report mode of the expected reply
+ * @param MCUMode configuration
+ */
+ 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
diff --git a/src/input_common/helpers/joycon_protocol/generic_functions.cpp b/src/input_common/helpers/joycon_protocol/generic_functions.cpp
new file mode 100644
index 000000000..548a4b9e3
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/generic_functions.cpp
@@ -0,0 +1,136 @@
+// 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::SetLowPowerMode(bool enable) {
+ ScopedSetBlocking sb(this);
+ const std::array<u8, 1> buffer{static_cast<u8>(enable ? 1 : 0)};
+ return SendSubCommand(SubCommand::LOW_POWER_MODE, buffer);
+}
+
+DriverResult GenericProtocol::TriggersElapsed() {
+ ScopedSetBlocking sb(this);
+ return SendSubCommand(SubCommand::TRIGGERS_ELAPSED, {});
+}
+
+DriverResult GenericProtocol::GetDeviceInfo(DeviceInfo& device_info) {
+ ScopedSetBlocking sb(this);
+ SubCommandResponse output{};
+
+ const auto result = SendSubCommand(SubCommand::REQ_DEV_INFO, {}, output);
+
+ device_info = {};
+ if (result == DriverResult::Success) {
+ device_info = output.device_info;
+ }
+
+ 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::array<u8, 12> buffer{};
+ const auto result = ReadRawSPI(SpiAddress::COLOR_DATA, 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::array<u8, 16> buffer{};
+ const auto result = ReadRawSPI(SpiAddress::SERIAL_NUMBER, 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
diff --git a/src/input_common/helpers/joycon_protocol/generic_functions.h b/src/input_common/helpers/joycon_protocol/generic_functions.h
new file mode 100644
index 000000000..424831e81
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/generic_functions.h
@@ -0,0 +1,114 @@
+// 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();
+
+ /// Enables or disables the low power mode
+ DriverResult SetLowPowerMode(bool enable);
+
+ /// Unknown function used by the switch
+ DriverResult TriggersElapsed();
+
+ /**
+ * Sends a request to obtain the joycon firmware and mac from handle
+ * @returns controller device info
+ */
+ DriverResult GetDeviceInfo(DeviceInfo& controller_type);
+
+ /**
+ * Sends a request to obtain the joycon type from handle
+ * @returns controller type of the joycon
+ */
+ DriverResult GetControllerType(ControllerType& controller_type);
+
+ /**
+ * Enables motion input
+ * @param enable if true motion data will be enabled
+ */
+ DriverResult EnableImu(bool enable);
+
+ /**
+ * Configures the motion sensor with the specified parameters
+ * @param gsen gyroscope sensor sensitvity in degrees per second
+ * @param gfrec gyroscope sensor frequency in hertz
+ * @param asen accelerometer sensitivity in G force
+ * @param afrec accelerometer frequency in hertz
+ */
+ DriverResult SetImuConfig(GyroSensitivity gsen, GyroPerformance gfrec,
+ AccelerometerSensitivity asen, AccelerometerPerformance afrec);
+
+ /**
+ * Request battery level from the device
+ * @returns battery level
+ */
+ DriverResult GetBattery(u32& battery_level);
+
+ /**
+ * Request joycon colors from the device
+ * @returns colors of the body and buttons
+ */
+ DriverResult GetColor(Color& color);
+
+ /**
+ * Request joycon serial number from the device
+ * @returns 16 byte serial number
+ */
+ DriverResult GetSerialNumber(SerialNumber& serial_number);
+
+ /**
+ * Request joycon serial number from the device
+ * @returns 16 byte serial number
+ */
+ DriverResult GetTemperature(u32& temperature);
+
+ /**
+ * Request joycon serial number from the device
+ * @returns 16 byte serial number
+ */
+ DriverResult GetVersionNumber(FirmwareVersion& version);
+
+ /**
+ * Sets home led behaviour
+ */
+ DriverResult SetHomeLight();
+
+ /**
+ * Sets home led into a slow breathing state
+ */
+ DriverResult SetLedBusy();
+
+ /**
+ * Sets the 4 player leds on the joycon on a solid state
+ * @params bit flag containing the led state
+ */
+ DriverResult SetLedPattern(u8 leds);
+
+ /**
+ * Sets the 4 player leds on the joycon on a blinking state
+ * @returns bit flag containing the led state
+ */
+ DriverResult SetLedBlinkPattern(u8 leds);
+};
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/irs.cpp b/src/input_common/helpers/joycon_protocol/irs.cpp
new file mode 100644
index 000000000..731fd5981
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/irs.cpp
@@ -0,0 +1,299 @@
+// 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;
+ SubCommandResponse output{};
+ std::size_t tries = 0;
+
+ const IrsConfigure irs_configuration{
+ .command = MCUCommand::ConfigureIR,
+ .sub_command = MCUSubCommand::SetDeviceMode,
+ .irs_mode = IrsMode::ImageTransfer,
+ .number_of_fragments = fragments,
+ .mcu_major_version = 0x0500,
+ .mcu_minor_version = 0x1800,
+ .crc = {},
+ };
+ buf_image.resize((static_cast<u8>(fragments) + 1) * 300);
+
+ std::array<u8, sizeof(IrsConfigure)> request_data{};
+ memcpy(request_data.data(), &irs_configuration, sizeof(IrsConfigure));
+ request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
+ do {
+ const auto result = SendSubCommand(SubCommand::SET_MCU_CONFIG, request_data, output);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+ if (tries++ >= max_tries) {
+ return DriverResult::WrongReply;
+ }
+ } while (output.command_data[0] != 0x0b);
+
+ return DriverResult::Success;
+}
+
+DriverResult IrsProtocol::WriteRegistersStep1() {
+ LOG_DEBUG(Input, "WriteRegistersStep1");
+ DriverResult result{DriverResult::Success};
+ constexpr std::size_t max_tries = 28;
+ SubCommandResponse output{};
+ std::size_t tries = 0;
+
+ const IrsWriteRegisters irs_registers{
+ .command = MCUCommand::ConfigureIR,
+ .sub_command = MCUSubCommand::WriteDeviceRegisters,
+ .number_of_registers = 0x9,
+ .registers =
+ {
+ IrsRegister{IrRegistersAddress::Resolution, static_cast<u8>(resolution_code)},
+ {IrRegistersAddress::ExposureLSB, static_cast<u8>(exposure & 0xff)},
+ {IrRegistersAddress::ExposureMSB, static_cast<u8>(exposure >> 8)},
+ {IrRegistersAddress::ExposureTime, 0x00},
+ {IrRegistersAddress::Leds, static_cast<u8>(leds)},
+ {IrRegistersAddress::DigitalGainLSB, static_cast<u8>((digital_gain & 0x0f) << 4)},
+ {IrRegistersAddress::DigitalGainMSB, static_cast<u8>((digital_gain & 0xf0) >> 4)},
+ {IrRegistersAddress::LedFilter, static_cast<u8>(led_filter)},
+ {IrRegistersAddress::WhitePixelThreshold, 0xc8},
+ },
+ .crc = {},
+ };
+
+ std::array<u8, sizeof(IrsWriteRegisters)> request_data{};
+ memcpy(request_data.data(), &irs_registers, sizeof(IrsWriteRegisters));
+ request_data[37] = CalculateMCU_CRC8(request_data.data() + 1, 36);
+
+ std::array<u8, 38> mcu_request{0x02};
+ mcu_request[36] = CalculateMCU_CRC8(mcu_request.data(), 36);
+ mcu_request[37] = 0xFF;
+
+ if (result != 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.command_data[0] == 0x13 && output.command_data[2] == 0x07) &&
+ output.command_data[0] != 0x23);
+
+ return DriverResult::Success;
+}
+
+DriverResult IrsProtocol::WriteRegistersStep2() {
+ LOG_DEBUG(Input, "WriteRegistersStep2");
+ constexpr std::size_t max_tries = 28;
+ SubCommandResponse output{};
+ std::size_t tries = 0;
+
+ const IrsWriteRegisters irs_registers{
+ .command = MCUCommand::ConfigureIR,
+ .sub_command = MCUSubCommand::WriteDeviceRegisters,
+ .number_of_registers = 0x8,
+ .registers =
+ {
+ IrsRegister{IrRegistersAddress::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.command_data[0] != 0x13 && output.command_data[0] != 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
diff --git a/src/input_common/helpers/joycon_protocol/irs.h b/src/input_common/helpers/joycon_protocol/irs.h
new file mode 100644
index 000000000..76dfa02ea
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/irs.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
+// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
+// https://github.com/CTCaer/jc_toolkit
+// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
+
+#pragma once
+
+#include <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
diff --git a/src/input_common/helpers/joycon_protocol/joycon_types.h b/src/input_common/helpers/joycon_protocol/joycon_types.h
new file mode 100644
index 000000000..e0e431156
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/joycon_types.h
@@ -0,0 +1,808 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
+// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
+// https://github.com/CTCaer/jc_toolkit
+// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <SDL_hidapi.h>
+
+#include "common/bit_field.h"
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+
+namespace InputCommon::Joycon {
+constexpr u32 MaxErrorCount = 50;
+constexpr u32 MaxBufferSize = 368;
+constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40};
+
+using MacAddress = std::array<u8, 6>;
+using SerialNumber = std::array<u8, 15>;
+using TagUUID = std::array<u8, 7>;
+using MifareUUID = std::array<u8, 4>;
+
+enum class ControllerType : u8 {
+ None = 0x00,
+ Left = 0x01,
+ Right = 0x02,
+ Pro = 0x03,
+ Dual = 0x05, // TODO: Verify this id
+ LarkHvc1 = 0x07,
+ LarkHvc2 = 0x08,
+ LarkNesLeft = 0x09,
+ LarkNesRight = 0x0A,
+ Lucia = 0x0B,
+ Lagon = 0x0C,
+ Lager = 0x0D,
+};
+
+enum class PadAxes {
+ LeftStickX,
+ LeftStickY,
+ RightStickX,
+ RightStickY,
+ Undefined,
+};
+
+enum class PadMotion {
+ LeftMotion,
+ RightMotion,
+ Undefined,
+};
+
+enum class PadButton : u32 {
+ Down = 0x000001,
+ Up = 0x000002,
+ Right = 0x000004,
+ Left = 0x000008,
+ LeftSR = 0x000010,
+ LeftSL = 0x000020,
+ L = 0x000040,
+ ZL = 0x000080,
+ Y = 0x000100,
+ X = 0x000200,
+ B = 0x000400,
+ A = 0x000800,
+ RightSR = 0x001000,
+ RightSL = 0x002000,
+ R = 0x004000,
+ ZR = 0x008000,
+ Minus = 0x010000,
+ Plus = 0x020000,
+ StickR = 0x040000,
+ StickL = 0x080000,
+ Home = 0x100000,
+ Capture = 0x200000,
+};
+
+enum class PassivePadButton : u32 {
+ Down_A = 0x0001,
+ Right_X = 0x0002,
+ Left_B = 0x0004,
+ Up_Y = 0x0008,
+ SL = 0x0010,
+ SR = 0x0020,
+ Minus = 0x0100,
+ Plus = 0x0200,
+ StickL = 0x0400,
+ StickR = 0x0800,
+ Home = 0x1000,
+ Capture = 0x2000,
+ L_R = 0x4000,
+ ZL_ZR = 0x8000,
+};
+
+enum class PassivePadStick : u8 {
+ Right = 0x00,
+ RightDown = 0x01,
+ Down = 0x02,
+ DownLeft = 0x03,
+ Left = 0x04,
+ LeftUp = 0x05,
+ Up = 0x06,
+ UpRight = 0x07,
+ Neutral = 0x08,
+};
+
+enum class OutputReport : u8 {
+ RUMBLE_AND_SUBCMD = 0x01,
+ FW_UPDATE_PKT = 0x03,
+ RUMBLE_ONLY = 0x10,
+ MCU_DATA = 0x11,
+ USB_CMD = 0x80,
+};
+
+enum class FeatureReport : u8 {
+ Last_SUBCMD = 0x02,
+ OTA_GW_UPGRADE = 0x70,
+ SETUP_MEM_READ = 0x71,
+ MEM_READ = 0x72,
+ ERASE_MEM_SECTOR = 0x73,
+ MEM_WRITE = 0x74,
+ LAUNCH = 0x75,
+};
+
+enum class SubCommand : u8 {
+ STATE = 0x00,
+ MANUAL_BT_PAIRING = 0x01,
+ REQ_DEV_INFO = 0x02,
+ SET_REPORT_MODE = 0x03,
+ TRIGGERS_ELAPSED = 0x04,
+ GET_PAGE_LIST_STATE = 0x05,
+ SET_HCI_STATE = 0x06,
+ RESET_PAIRING_INFO = 0x07,
+ LOW_POWER_MODE = 0x08,
+ SPI_FLASH_READ = 0x10,
+ SPI_FLASH_WRITE = 0x11,
+ SPI_SECTOR_ERASE = 0x12,
+ RESET_MCU = 0x20,
+ SET_MCU_CONFIG = 0x21,
+ SET_MCU_STATE = 0x22,
+ SET_PLAYER_LIGHTS = 0x30,
+ GET_PLAYER_LIGHTS = 0x31,
+ SET_HOME_LIGHT = 0x38,
+ ENABLE_IMU = 0x40,
+ SET_IMU_SENSITIVITY = 0x41,
+ WRITE_IMU_REG = 0x42,
+ READ_IMU_REG = 0x43,
+ ENABLE_VIBRATION = 0x48,
+ GET_REGULATED_VOLTAGE = 0x50,
+ SET_EXTERNAL_CONFIG = 0x58,
+ GET_EXTERNAL_DEVICE_INFO = 0x59,
+ ENABLE_EXTERNAL_POLLING = 0x5A,
+ DISABLE_EXTERNAL_POLLING = 0x5B,
+ SET_EXTERNAL_FORMAT_CONFIG = 0x5C,
+};
+
+enum class UsbSubCommand : u8 {
+ CONN_STATUS = 0x01,
+ HADSHAKE = 0x02,
+ BAUDRATE_3M = 0x03,
+ NO_TIMEOUT = 0x04,
+ EN_TIMEOUT = 0x05,
+ RESET = 0x06,
+ PRE_HANDSHAKE = 0x91,
+ SEND_UART = 0x92,
+};
+
+enum class CalibrationMagic : u8 {
+ USR_MAGIC_0 = 0xB2,
+ USR_MAGIC_1 = 0xA1,
+};
+
+enum class SpiAddress : u16 {
+ MAGIC = 0x0000,
+ MAC_ADDRESS = 0x0015,
+ PAIRING_INFO = 0x2000,
+ SHIPMENT = 0x5000,
+ SERIAL_NUMBER = 0x6000,
+ DEVICE_TYPE = 0x6012,
+ FORMAT_VERSION = 0x601B,
+ FACT_IMU_DATA = 0x6020,
+ FACT_LEFT_DATA = 0x603d,
+ FACT_RIGHT_DATA = 0x6046,
+ COLOR_DATA = 0x6050,
+ DESIGN_VARIATION = 0x605C,
+ SENSOR_DATA = 0x6080,
+ USER_LEFT_MAGIC = 0x8010,
+ USER_LEFT_DATA = 0x8012,
+ USER_RIGHT_MAGIC = 0x801B,
+ USER_RIGHT_DATA = 0x801D,
+ USER_IMU_MAGIC = 0x8026,
+ USER_IMU_DATA = 0x8028,
+};
+
+enum class ReportMode : u8 {
+ ACTIVE_POLLING_NFC_IR_CAMERA_DATA = 0x00,
+ ACTIVE_POLLING_NFC_IR_CAMERA_CONFIGURATION = 0x01,
+ ACTIVE_POLLING_NFC_IR_CAMERA_DATA_CONFIGURATION = 0x02,
+ ACTIVE_POLLING_IR_CAMERA_DATA = 0x03,
+ SUBCMD_REPLY = 0x21,
+ MCU_UPDATE_STATE = 0x23,
+ STANDARD_FULL_60HZ = 0x30,
+ NFC_IR_MODE_60HZ = 0x31,
+ SIMPLE_HID_MODE = 0x3F,
+ INPUT_USB_RESPONSE = 0x81,
+};
+
+enum class GyroSensitivity : u8 {
+ DPS250,
+ DPS500,
+ DPS1000,
+ DPS2000, // Default
+};
+
+enum class AccelerometerSensitivity : u8 {
+ G8, // Default
+ G4,
+ G2,
+ G16,
+};
+
+enum class GyroPerformance : u8 {
+ HZ833,
+ HZ208, // Default
+};
+
+enum class AccelerometerPerformance : u8 {
+ HZ200,
+ HZ100, // Default
+};
+
+enum class MCUCommand : u8 {
+ ConfigureMCU = 0x21,
+ ConfigureIR = 0x23,
+};
+
+enum class MCUSubCommand : u8 {
+ SetMCUMode = 0x0,
+ SetDeviceMode = 0x1,
+ ReadDeviceMode = 0x02,
+ WriteDeviceRegisters = 0x4,
+};
+
+enum class MCUMode : u8 {
+ Suspend = 0,
+ Standby = 1,
+ Ringcon = 3,
+ NFC = 4,
+ IR = 5,
+ MaybeFWUpdate = 6,
+};
+
+enum class MCURequest : u8 {
+ GetMCUStatus = 1,
+ GetNFCData = 2,
+ GetIRData = 3,
+};
+
+enum class MCUReport : u8 {
+ Empty = 0x00,
+ StateReport = 0x01,
+ IRData = 0x03,
+ BusyInitializing = 0x0b,
+ IRStatus = 0x13,
+ IRRegisters = 0x1b,
+ NFCState = 0x2a,
+ NFCReadData = 0x3a,
+ EmptyAwaitingCmd = 0xff,
+};
+
+enum class MCUPacketFlag : u8 {
+ MorePacketsRemaining = 0x00,
+ LastCommandPacket = 0x08,
+};
+
+enum class NFCCommand : u8 {
+ CancelAll = 0x00,
+ StartPolling = 0x01,
+ StopPolling = 0x02,
+ StartWaitingRecieve = 0x04,
+ ReadNtag = 0x06,
+ WriteNtag = 0x08,
+ Mifare = 0x0F,
+};
+
+enum class NFCTagType : u8 {
+ AllTags = 0x00,
+ Ntag215 = 0x01,
+};
+
+enum class NFCPages {
+ Block0 = 0,
+ Block3 = 3,
+ Block45 = 45,
+ Block135 = 135,
+ Block231 = 231,
+};
+
+enum class NFCStatus : u8 {
+ Ready = 0x00,
+ Polling = 0x01,
+ LastPackage = 0x04,
+ WriteDone = 0x05,
+ TagLost = 0x07,
+ WriteReady = 0x09,
+ MifareDone = 0x10,
+};
+
+enum class MifareCmd : u8 {
+ None = 0x00,
+ Read = 0x30,
+ AuthA = 0x60,
+ AuthB = 0x61,
+ Write = 0xA0,
+ Transfer = 0xB0,
+ Decrement = 0xC0,
+ Increment = 0xC1,
+ Store = 0xC2
+};
+
+enum class IrsMode : u8 {
+ None = 0x02,
+ Moment = 0x03,
+ Dpd = 0x04,
+ Clustering = 0x06,
+ ImageTransfer = 0x07,
+ Silhouette = 0x08,
+ TeraImage = 0x09,
+ SilhouetteTeraImage = 0x0A,
+};
+
+enum class IrsResolution {
+ Size320x240,
+ Size160x120,
+ Size80x60,
+ Size40x30,
+ Size20x15,
+ None,
+};
+
+enum class IrsResolutionCode : u8 {
+ Size320x240 = 0x00, // Full pixel array
+ Size160x120 = 0x50, // Sensor Binning [2 X 2]
+ Size80x60 = 0x64, // Sensor Binning [4 x 2] and Skipping [1 x 2]
+ Size40x30 = 0x69, // Sensor Binning [4 x 2] and Skipping [2 x 4]
+ Size20x15 = 0x6A, // Sensor Binning [4 x 2] and Skipping [4 x 4]
+};
+
+// Size of image divided by 300
+enum class IrsFragments : u8 {
+ Size20x15 = 0x00,
+ Size40x30 = 0x03,
+ Size80x60 = 0x0f,
+ Size160x120 = 0x3f,
+ Size320x240 = 0xFF,
+};
+
+enum class IrLeds : u8 {
+ BrightAndDim = 0x00,
+ Bright = 0x20,
+ Dim = 0x10,
+ None = 0x30,
+};
+
+enum class IrExLedFilter : u8 {
+ Disabled = 0x00,
+ Enabled = 0x03,
+};
+
+enum class IrImageFlip : u8 {
+ Normal = 0x00,
+ Inverted = 0x02,
+};
+
+enum class IrRegistersAddress : u16 {
+ UpdateTime = 0x0400,
+ FinalizeConfig = 0x0700,
+ LedFilter = 0x0e00,
+ Leds = 0x1000,
+ 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 ExternalDeviceId : u16 {
+ RingController = 0x2000,
+ Starlink = 0x2800,
+};
+
+enum class DriverResult {
+ Success,
+ WrongReply,
+ Timeout,
+ InvalidParameters,
+ UnsupportedControllerType,
+ HandleInUse,
+ ErrorReadingData,
+ ErrorWritingData,
+ NoDeviceDetected,
+ InvalidHandle,
+ NotSupported,
+ Disabled,
+ Delayed,
+ 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{};
+};
+
+// Output from SPI read command containing user calibration magic
+struct MagicSpiCalibration {
+ CalibrationMagic first;
+ CalibrationMagic second;
+};
+static_assert(sizeof(MagicSpiCalibration) == 0x2, "MagicSpiCalibration is an invalid size");
+
+// Output from SPI read command containing left joystick calibration
+struct JoystickLeftSpiCalibration {
+ std::array<u8, 3> max;
+ std::array<u8, 3> center;
+ std::array<u8, 3> min;
+};
+static_assert(sizeof(JoystickLeftSpiCalibration) == 0x9,
+ "JoystickLeftSpiCalibration is an invalid size");
+
+// Output from SPI read command containing right joystick calibration
+struct JoystickRightSpiCalibration {
+ std::array<u8, 3> center;
+ std::array<u8, 3> min;
+ std::array<u8, 3> max;
+};
+static_assert(sizeof(JoystickRightSpiCalibration) == 0x9,
+ "JoystickRightSpiCalibration is an invalid size");
+
+struct JoyStickAxisCalibration {
+ u16 max;
+ u16 min;
+ u16 center;
+};
+
+struct JoyStickCalibration {
+ JoyStickAxisCalibration x;
+ JoyStickAxisCalibration y;
+};
+
+struct ImuSpiCalibration {
+ std::array<s16, 3> accelerometer_offset;
+ std::array<s16, 3> accelerometer_scale;
+ std::array<s16, 3> gyroscope_offset;
+ std::array<s16, 3> gyroscope_scale;
+};
+static_assert(sizeof(ImuSpiCalibration) == 0x18, "ImuSpiCalibration is an invalid size");
+
+struct RingCalibration {
+ s16 default_value;
+ s16 max_value;
+ s16 min_value;
+};
+
+struct Color {
+ u32 body;
+ u32 buttons;
+ u32 left_grip;
+ u32 right_grip;
+};
+
+struct Battery {
+ union {
+ u8 raw{};
+
+ BitField<0, 4, u8> unknown;
+ BitField<4, 1, u8> charging;
+ BitField<5, 3, u8> status;
+ };
+};
+
+struct VibrationValue {
+ f32 low_amplitude;
+ f32 low_frequency;
+ f32 high_amplitude;
+ f32 high_frequency;
+};
+
+struct JoyconHandle {
+ SDL_hid_device* handle = nullptr;
+ u8 packet_counter{};
+};
+
+struct MCUConfig {
+ MCUCommand command;
+ MCUSubCommand sub_command;
+ MCUMode mode;
+ INSERT_PADDING_BYTES(0x22);
+ u8 crc;
+};
+static_assert(sizeof(MCUConfig) == 0x26, "MCUConfig is an invalid size");
+
+#pragma pack(push, 1)
+struct InputReportPassive {
+ ReportMode report_mode;
+ u16 button_input;
+ u8 stick_state;
+ std::array<u8, 10> unknown_data;
+};
+static_assert(sizeof(InputReportPassive) == 0xE, "InputReportPassive is an invalid size");
+
+struct InputReportActive {
+ ReportMode report_mode;
+ u8 packet_id;
+ Battery battery_status;
+ std::array<u8, 3> button_input;
+ std::array<u8, 3> left_stick_state;
+ std::array<u8, 3> right_stick_state;
+ u8 vibration_code;
+ std::array<s16, 6 * 2> motion_input;
+ INSERT_PADDING_BYTES(0x2);
+ s16 ring_input;
+};
+static_assert(sizeof(InputReportActive) == 0x29, "InputReportActive is an invalid size");
+
+struct InputReportNfcIr {
+ ReportMode report_mode;
+ u8 packet_id;
+ Battery battery_status;
+ std::array<u8, 3> button_input;
+ std::array<u8, 3> left_stick_state;
+ std::array<u8, 3> right_stick_state;
+ u8 vibration_code;
+ std::array<s16, 6 * 2> motion_input;
+ INSERT_PADDING_BYTES(0x4);
+};
+static_assert(sizeof(InputReportNfcIr) == 0x29, "InputReportNfcIr is an invalid size");
+#pragma pack(pop)
+
+struct NFCReadBlock {
+ u8 start;
+ u8 end;
+};
+static_assert(sizeof(NFCReadBlock) == 0x2, "NFCReadBlock is an invalid size");
+
+struct NFCReadBlockCommand {
+ u8 block_count{};
+ std::array<NFCReadBlock, 4> blocks{};
+};
+static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an invalid size");
+
+struct NFCReadCommandData {
+ u8 unknown;
+ u8 uuid_length;
+ TagUUID uid;
+ NFCTagType tag_type;
+ NFCReadBlockCommand read_block;
+};
+static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size");
+
+#pragma pack(push, 1)
+struct NFCWriteCommandData {
+ u8 unknown;
+ u8 uuid_length;
+ TagUUID uid;
+ NFCTagType tag_type;
+ u8 unknown2;
+ u8 unknown3;
+ u8 unknown4;
+ u8 unknown5;
+ u8 unknown6;
+ u8 unknown7;
+ u8 unknown8;
+ u8 magic;
+ u16_be write_count;
+ u8 amiibo_version;
+};
+static_assert(sizeof(NFCWriteCommandData) == 0x15, "NFCWriteCommandData is an invalid size");
+#pragma pack(pop)
+
+struct MifareCommandData {
+ u8 unknown1;
+ u8 unknown2;
+ u8 number_of_short_bytes;
+ MifareUUID uid;
+};
+static_assert(sizeof(MifareCommandData) == 0x7, "MifareCommandData is an invalid size");
+
+struct NFCPollingCommandData {
+ u8 enable_mifare;
+ u8 unknown_1;
+ u8 unknown_2;
+ u8 unknown_3;
+ u8 unknown_4;
+};
+static_assert(sizeof(NFCPollingCommandData) == 0x05, "NFCPollingCommandData is an invalid size");
+
+struct NFCRequestState {
+ NFCCommand command_argument;
+ u8 block_id;
+ u8 packet_id;
+ MCUPacketFlag packet_flag;
+ u8 data_length;
+ union {
+ std::array<u8, 0x1F> raw_data;
+ NFCReadCommandData nfc_read;
+ NFCPollingCommandData nfc_polling;
+ };
+ u8 crc;
+ INSERT_PADDING_BYTES(0x1);
+};
+static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size");
+
+struct NFCDataChunk {
+ u8 nfc_page;
+ u8 data_size;
+ std::array<u8, 0xFF> data;
+};
+
+struct NFCWritePackage {
+ NFCWriteCommandData command_data;
+ u8 number_of_chunks;
+ std::array<NFCDataChunk, 4> data_chunks;
+};
+
+struct MifareReadChunk {
+ MifareCmd command;
+ std::array<u8, 0x6> sector_key;
+ u8 sector;
+};
+
+struct MifareWriteChunk {
+ MifareCmd command;
+ std::array<u8, 0x6> sector_key;
+ u8 sector;
+ std::array<u8, 0x10> data;
+};
+
+struct MifareReadData {
+ u8 sector;
+ std::array<u8, 0x10> data;
+};
+
+struct MifareReadPackage {
+ MifareCommandData command_data;
+ std::array<MifareReadChunk, 0x10> data_chunks;
+};
+
+struct MifareWritePackage {
+ MifareCommandData command_data;
+ std::array<MifareWriteChunk, 0x10> data_chunks;
+};
+
+struct TagInfo {
+ u8 uuid_length;
+ u8 protocol;
+ u8 tag_type;
+ std::array<u8, 10> uuid;
+};
+
+struct IrsConfigure {
+ MCUCommand command;
+ MCUSubCommand sub_command;
+ IrsMode irs_mode;
+ IrsFragments number_of_fragments;
+ u16 mcu_major_version;
+ u16 mcu_minor_version;
+ INSERT_PADDING_BYTES(0x1D);
+ u8 crc;
+};
+static_assert(sizeof(IrsConfigure) == 0x26, "IrsConfigure is an invalid size");
+
+#pragma pack(push, 1)
+struct IrsRegister {
+ IrRegistersAddress address;
+ u8 value;
+};
+static_assert(sizeof(IrsRegister) == 0x3, "IrsRegister is an invalid size");
+
+struct IrsWriteRegisters {
+ MCUCommand command;
+ MCUSubCommand sub_command;
+ u8 number_of_registers;
+ std::array<IrsRegister, 9> registers;
+ INSERT_PADDING_BYTES(0x7);
+ u8 crc;
+};
+static_assert(sizeof(IrsWriteRegisters) == 0x26, "IrsWriteRegisters is an invalid size");
+#pragma pack(pop)
+
+struct FirmwareVersion {
+ u8 major;
+ u8 minor;
+};
+static_assert(sizeof(FirmwareVersion) == 0x2, "FirmwareVersion is an invalid size");
+
+struct DeviceInfo {
+ FirmwareVersion firmware;
+ std::array<u8, 2> unknown_1;
+ MacAddress mac_address;
+ std::array<u8, 2> unknown_2;
+};
+static_assert(sizeof(DeviceInfo) == 0xC, "DeviceInfo is an invalid size");
+
+struct MotionStatus {
+ bool is_enabled;
+ u64 delta_time;
+ GyroSensitivity gyro_sensitivity;
+ AccelerometerSensitivity accelerometer_sensitivity;
+};
+
+struct RingStatus {
+ bool is_enabled;
+ s16 default_value;
+ s16 max_value;
+ s16 min_value;
+};
+
+struct VibrationPacket {
+ OutputReport output_report;
+ u8 packet_counter;
+ std::array<u8, 0x8> vibration_data;
+};
+static_assert(sizeof(VibrationPacket) == 0xA, "VibrationPacket is an invalid size");
+
+struct SubCommandPacket {
+ OutputReport output_report;
+ u8 packet_counter;
+ INSERT_PADDING_BYTES(0x8); // This contains vibration data
+ union {
+ SubCommand sub_command;
+ MCUSubCommand mcu_sub_command;
+ };
+ std::array<u8, 0x26> command_data;
+};
+static_assert(sizeof(SubCommandPacket) == 0x31, "SubCommandPacket is an invalid size");
+
+#pragma pack(push, 1)
+struct ReadSpiPacket {
+ SpiAddress spi_address;
+ INSERT_PADDING_BYTES(0x2);
+ u8 size;
+};
+static_assert(sizeof(ReadSpiPacket) == 0x5, "ReadSpiPacket is an invalid size");
+
+struct SubCommandResponse {
+ InputReportPassive input_report;
+ SubCommand sub_command;
+ union {
+ std::array<u8, 0x30> command_data;
+ SpiAddress spi_address; // Reply from SPI_FLASH_READ subcommand
+ ExternalDeviceId external_device_id; // Reply from GET_EXTERNAL_DEVICE_INFO subcommand
+ DeviceInfo device_info; // Reply from REQ_DEV_INFO subcommand
+ };
+ u8 crc; // This is never used
+};
+static_assert(sizeof(SubCommandResponse) == 0x40, "SubCommandResponse is an invalid size");
+#pragma pack(pop)
+
+struct MCUCommandResponse {
+ InputReportNfcIr input_report;
+ INSERT_PADDING_BYTES(0x8);
+ MCUReport mcu_report;
+ std::array<u8, 0x13D> mcu_data;
+ u8 crc;
+};
+static_assert(sizeof(MCUCommandResponse) == 0x170, "MCUCommandResponse is an invalid size");
+
+struct JoyconCallbacks {
+ std::function<void(Battery)> on_battery_data;
+ std::function<void(Color)> on_color_data;
+ std::function<void(int, bool)> on_button_data;
+ std::function<void(int, f32)> on_stick_data;
+ std::function<void(int, const MotionData&)> on_motion_data;
+ std::function<void(f32)> on_ring_data;
+ std::function<void(const Joycon::TagInfo&)> on_amiibo_data;
+ std::function<void(const std::vector<u8>&, IrsResolution)> on_camera_data;
+};
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/nfc.cpp b/src/input_common/helpers/joycon_protocol/nfc.cpp
new file mode 100644
index 000000000..261f46255
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/nfc.cpp
@@ -0,0 +1,985 @@
+// 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);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitSetMCUMode(ReportMode::NFC_IR_MODE_60HZ, MCUMode::NFC);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::Ready);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStopPollingRequest(output);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::Ready);
+ }
+ if (result == DriverResult::Success) {
+ is_enabled = true;
+ }
+
+ 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;
+ is_polling = false;
+
+ return result;
+}
+
+DriverResult NfcProtocol::StartNFCPollingMode() {
+ LOG_DEBUG(Input, "Start NFC polling Mode");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStartPollingRequest(output);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::Polling);
+ }
+ if (result == DriverResult::Success) {
+ is_polling = true;
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::StopNFCPollingMode() {
+ LOG_DEBUG(Input, "Stop NFC polling Mode");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStopPollingRequest(output);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::WriteReady);
+ }
+ if (result == DriverResult::Success) {
+ is_polling = false;
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::GetTagInfo(Joycon::TagInfo& tag_info) {
+ if (update_counter++ < AMIIBO_UPDATE_DELAY) {
+ return DriverResult::Delayed;
+ }
+ update_counter = 0;
+
+ LOG_DEBUG(Input, "Scan for amiibos");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ TagFoundData tag_data{};
+
+ if (result == DriverResult::Success) {
+ result = IsTagInRange(tag_data);
+ }
+
+ if (result == DriverResult::Success) {
+ tag_info = {
+ .uuid_length = tag_data.uuid_size,
+ .protocol = 1,
+ .tag_type = tag_data.type,
+ .uuid = {},
+ };
+
+ memcpy(tag_info.uuid.data(), tag_data.uuid.data(), tag_data.uuid_size);
+
+ // Investigate why mifare type is not correct
+ if (tag_info.tag_type == 144) {
+ tag_info.tag_type = 1U << 6;
+ }
+
+ std::string uuid_string;
+ for (auto& content : tag_data.uuid) {
+ uuid_string += fmt::format(" {:02x}", content);
+ }
+ LOG_INFO(Input, "Tag detected, type={}, uuid={}", tag_data.type, uuid_string);
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::ReadAmiibo(std::vector<u8>& data) {
+ LOG_DEBUG(Input, "Scan for amiibos");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ TagFoundData tag_data{};
+
+ if (result == DriverResult::Success) {
+ result = IsTagInRange(tag_data, 7);
+ }
+
+ if (result == DriverResult::Success) {
+ result = GetAmiiboData(data);
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::WriteAmiibo(std::span<const u8> data) {
+ LOG_DEBUG(Input, "Write amiibo");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ TagUUID tag_uuid = GetTagUUID(data);
+ TagFoundData tag_data{};
+
+ if (result == DriverResult::Success) {
+ result = IsTagInRange(tag_data, 7);
+ }
+ if (result == DriverResult::Success) {
+ if (tag_data.uuid != tag_uuid) {
+ result = DriverResult::InvalidParameters;
+ }
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStopPollingRequest(output);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::Ready);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStartPollingRequest(output, true);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::WriteReady);
+ }
+ if (result == DriverResult::Success) {
+ result = WriteAmiiboData(tag_uuid, data);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::WriteDone);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStopPollingRequest(output);
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::ReadMifare(std::span<const MifareReadChunk> read_request,
+ std::span<MifareReadData> out_data) {
+ LOG_DEBUG(Input, "Read mifare");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ TagFoundData tag_data{};
+ MifareUUID tag_uuid{};
+
+ if (result == DriverResult::Success) {
+ result = IsTagInRange(tag_data, 7);
+ }
+ if (result == DriverResult::Success) {
+ memcpy(tag_uuid.data(), tag_data.uuid.data(), sizeof(MifareUUID));
+ result = GetMifareData(tag_uuid, read_request, out_data);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStopPollingRequest(output);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::Ready);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStartPollingRequest(output, true);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::WriteReady);
+ }
+ return result;
+}
+
+DriverResult NfcProtocol::WriteMifare(std::span<const MifareWriteChunk> write_request) {
+ LOG_DEBUG(Input, "Write mifare");
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ TagFoundData tag_data{};
+ MifareUUID tag_uuid{};
+
+ if (result == DriverResult::Success) {
+ result = IsTagInRange(tag_data, 7);
+ }
+ if (result == DriverResult::Success) {
+ memcpy(tag_uuid.data(), tag_data.uuid.data(), sizeof(MifareUUID));
+ result = WriteMifareData(tag_uuid, write_request);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStopPollingRequest(output);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::Ready);
+ }
+ if (result == DriverResult::Success) {
+ MCUCommandResponse output{};
+ result = SendStartPollingRequest(output, true);
+ }
+ if (result == DriverResult::Success) {
+ result = WaitUntilNfcIs(NFCStatus::WriteReady);
+ }
+ return result;
+}
+
+bool NfcProtocol::HasAmiibo() {
+ if (update_counter++ < AMIIBO_UPDATE_DELAY) {
+ return true;
+ }
+ update_counter = 0;
+
+ ScopedSetBlocking sb(this);
+ DriverResult result{DriverResult::Success};
+ TagFoundData tag_data{};
+
+ if (result == DriverResult::Success) {
+ result = IsTagInRange(tag_data, 7);
+ }
+
+ return result == DriverResult::Success;
+}
+
+DriverResult NfcProtocol::WaitUntilNfcIs(NFCStatus status) {
+ constexpr std::size_t timeout_limit = 10;
+ MCUCommandResponse output{};
+ std::size_t tries = 0;
+
+ do {
+ auto result = SendNextPackageRequest(output, {});
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+ if (tries++ > timeout_limit) {
+ return DriverResult::Timeout;
+ }
+ } while (output.mcu_report != MCUReport::NFCState ||
+ (output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
+ output.mcu_data[5] != 0x31 || output.mcu_data[6] != static_cast<u8>(status));
+
+ return DriverResult::Success;
+}
+
+DriverResult NfcProtocol::IsTagInRange(TagFoundData& data, std::size_t timeout_limit) {
+ MCUCommandResponse output{};
+ std::size_t tries = 0;
+
+ do {
+ const auto result = SendNextPackageRequest(output, {});
+ if (result != DriverResult::Success) {
+ return result;
+ }
+ if (tries++ > timeout_limit) {
+ return DriverResult::Timeout;
+ }
+ } while (output.mcu_report != MCUReport::NFCState ||
+ (output.mcu_data[1] << 8) + output.mcu_data[0] != 0x0500 ||
+ (output.mcu_data[6] != 0x09 && output.mcu_data[6] != 0x04));
+
+ data.type = output.mcu_data[12];
+ data.uuid_size = std::min(output.mcu_data[14], static_cast<u8>(sizeof(TagUUID)));
+ memcpy(data.uuid.data(), output.mcu_data.data() + 15, data.uuid.size());
+
+ return DriverResult::Success;
+}
+
+DriverResult NfcProtocol::GetAmiiboData(std::vector<u8>& ntag_data) {
+ constexpr std::size_t timeout_limit = 60;
+ MCUCommandResponse output{};
+ std::size_t tries = 0;
+
+ u8 package_index = 0;
+ std::size_t ntag_buffer_pos = 0;
+ auto result = SendReadAmiiboRequest(output, NFCPages::Block135);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ // Read Tag data
+ while (tries++ < timeout_limit) {
+ result = SendNextPackageRequest(output, package_index);
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if ((output.mcu_report == MCUReport::NFCReadData ||
+ output.mcu_report == MCUReport::NFCState) &&
+ nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
+ std::size_t payload_size = (output.mcu_data[4] << 8 | output.mcu_data[5]) & 0x7FF;
+ if (output.mcu_data[2] == 0x01) {
+ memcpy(ntag_data.data() + ntag_buffer_pos, output.mcu_data.data() + 66,
+ payload_size - 60);
+ ntag_buffer_pos += payload_size - 60;
+ } else {
+ memcpy(ntag_data.data() + ntag_buffer_pos, output.mcu_data.data() + 6,
+ payload_size);
+ }
+ package_index++;
+ continue;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
+ LOG_INFO(Input, "Finished reading amiibo");
+ return DriverResult::Success;
+ }
+ }
+
+ return DriverResult::Timeout;
+}
+
+DriverResult NfcProtocol::WriteAmiiboData(const TagUUID& tag_uuid, std::span<const u8> data) {
+ constexpr std::size_t timeout_limit = 60;
+ const auto nfc_data = MakeAmiiboWritePackage(tag_uuid, data);
+ const std::vector<u8> nfc_buffer_data = SerializeWritePackage(nfc_data);
+ std::span<const u8> buffer(nfc_buffer_data);
+ MCUCommandResponse output{};
+ u8 block_id = 1;
+ u8 package_index = 0;
+ std::size_t tries = 0;
+ std::size_t current_position = 0;
+
+ LOG_INFO(Input, "Writing amiibo data");
+
+ auto result = SendWriteAmiiboRequest(output, tag_uuid);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ // Read Tag data but ignore the actual sent data
+ while (tries++ < timeout_limit) {
+ result = SendNextPackageRequest(output, package_index);
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if ((output.mcu_report == MCUReport::NFCReadData ||
+ output.mcu_report == MCUReport::NFCState) &&
+ nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ if (output.mcu_report == MCUReport::NFCReadData && output.mcu_data[1] == 0x07) {
+ package_index++;
+ continue;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::LastPackage) {
+ LOG_INFO(Input, "Finished reading amiibo");
+ break;
+ }
+ }
+
+ // Send Data. Nfc buffer size is 31, Send the data in smaller packages
+ while (current_position < buffer.size() && tries++ < timeout_limit) {
+ const std::size_t next_position =
+ std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
+ const std::size_t block_size = next_position - current_position;
+ const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
+
+ SendWriteDataAmiiboRequest(output, block_id, is_last_packet,
+ buffer.subspan(current_position, block_size));
+
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if ((output.mcu_report == MCUReport::NFCReadData ||
+ output.mcu_report == MCUReport::NFCState) &&
+ nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ // Increase position when data is confirmed by the joycon
+ if (output.mcu_report == MCUReport::NFCState &&
+ (output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
+ output.mcu_data[3] == block_id) {
+ block_id++;
+ current_position = next_position;
+ }
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::GetMifareData(const MifareUUID& tag_uuid,
+ std::span<const MifareReadChunk> read_request,
+ std::span<MifareReadData> out_data) {
+ constexpr std::size_t timeout_limit = 60;
+ const auto nfc_data = MakeMifareReadPackage(tag_uuid, read_request);
+ const std::vector<u8> nfc_buffer_data = SerializeMifareReadPackage(nfc_data);
+ std::span<const u8> buffer(nfc_buffer_data);
+ DriverResult result = DriverResult::Success;
+ MCUCommandResponse output{};
+ u8 block_id = 1;
+ u8 package_index = 0;
+ std::size_t tries = 0;
+ std::size_t current_position = 0;
+
+ LOG_INFO(Input, "Reading Mifare data");
+
+ // Send data request. Nfc buffer size is 31, Send the data in smaller packages
+ while (current_position < buffer.size() && tries++ < timeout_limit) {
+ const std::size_t next_position =
+ std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
+ const std::size_t block_size = next_position - current_position;
+ const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
+
+ SendReadDataMifareRequest(output, block_id, is_last_packet,
+ buffer.subspan(current_position, block_size));
+
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ // Increase position when data is confirmed by the joycon
+ if (output.mcu_report == MCUReport::NFCState &&
+ (output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
+ output.mcu_data[3] == block_id) {
+ block_id++;
+ current_position = next_position;
+ }
+ }
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ // Wait for reply and save the output data
+ while (tries++ < timeout_limit) {
+ result = SendNextPackageRequest(output, package_index);
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && output.mcu_data[1] == 0x10) {
+ constexpr std::size_t DATA_LENGHT = 0x10 + 1;
+ constexpr std::size_t DATA_START = 11;
+ const u8 number_of_elements = output.mcu_data[10];
+ for (std::size_t i = 0; i < number_of_elements; i++) {
+ out_data[i].sector = output.mcu_data[DATA_START + (i * DATA_LENGHT)];
+ memcpy(out_data[i].data.data(),
+ output.mcu_data.data() + DATA_START + 1 + (i * DATA_LENGHT),
+ sizeof(MifareReadData::data));
+ }
+ package_index++;
+ continue;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::MifareDone) {
+ LOG_INFO(Input, "Finished reading mifare");
+ break;
+ }
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::WriteMifareData(const MifareUUID& tag_uuid,
+ std::span<const MifareWriteChunk> write_request) {
+ constexpr std::size_t timeout_limit = 60;
+ const auto nfc_data = MakeMifareWritePackage(tag_uuid, write_request);
+ const std::vector<u8> nfc_buffer_data = SerializeMifareWritePackage(nfc_data);
+ std::span<const u8> buffer(nfc_buffer_data);
+ DriverResult result = DriverResult::Success;
+ MCUCommandResponse output{};
+ u8 block_id = 1;
+ u8 package_index = 0;
+ std::size_t tries = 0;
+ std::size_t current_position = 0;
+
+ LOG_INFO(Input, "Writing Mifare data");
+
+ // Send data request. Nfc buffer size is 31, Send the data in smaller packages
+ while (current_position < buffer.size() && tries++ < timeout_limit) {
+ const std::size_t next_position =
+ std::min(current_position + sizeof(NFCRequestState::raw_data), buffer.size());
+ const std::size_t block_size = next_position - current_position;
+ const bool is_last_packet = block_size < sizeof(NFCRequestState::raw_data);
+
+ SendReadDataMifareRequest(output, block_id, is_last_packet,
+ buffer.subspan(current_position, block_size));
+
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ // Increase position when data is confirmed by the joycon
+ if (output.mcu_report == MCUReport::NFCState &&
+ (output.mcu_data[1] << 8) + output.mcu_data[0] == 0x0500 &&
+ output.mcu_data[3] == block_id) {
+ block_id++;
+ current_position = next_position;
+ }
+ }
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ // Wait for reply and ignore the output data
+ while (tries++ < timeout_limit) {
+ result = SendNextPackageRequest(output, package_index);
+ const auto nfc_status = static_cast<NFCStatus>(output.mcu_data[6]);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::TagLost) {
+ return DriverResult::ErrorReadingData;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && output.mcu_data[1] == 0x10) {
+ package_index++;
+ continue;
+ }
+
+ if (output.mcu_report == MCUReport::NFCState && nfc_status == NFCStatus::MifareDone) {
+ LOG_INFO(Input, "Finished writing mifare");
+ break;
+ }
+ }
+
+ return result;
+}
+
+DriverResult NfcProtocol::SendStartPollingRequest(MCUCommandResponse& output,
+ bool is_second_attempt) {
+ NFCRequestState request{
+ .command_argument = NFCCommand::StartPolling,
+ .block_id = {},
+ .packet_id = {},
+ .packet_flag = MCUPacketFlag::LastCommandPacket,
+ .data_length = sizeof(NFCPollingCommandData),
+ .nfc_polling =
+ {
+ .enable_mifare = 0x00,
+ .unknown_1 = static_cast<u8>(is_second_attempt ? 0xe8 : 0x00),
+ .unknown_2 = static_cast<u8>(is_second_attempt ? 0x03 : 0x00),
+ .unknown_3 = 0x2c,
+ .unknown_4 = 0x01,
+ },
+ .crc = {},
+ };
+
+ std::array<u8, sizeof(NFCRequestState)> request_data{};
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+DriverResult NfcProtocol::SendStopPollingRequest(MCUCommandResponse& output) {
+ NFCRequestState request{
+ .command_argument = NFCCommand::StopPolling,
+ .block_id = {},
+ .packet_id = {},
+ .packet_flag = MCUPacketFlag::LastCommandPacket,
+ .data_length = {},
+ .raw_data = {},
+ .crc = {},
+ };
+
+ std::array<u8, sizeof(NFCRequestState)> request_data{};
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+DriverResult NfcProtocol::SendNextPackageRequest(MCUCommandResponse& output, u8 packet_id) {
+ NFCRequestState request{
+ .command_argument = NFCCommand::StartWaitingRecieve,
+ .block_id = {},
+ .packet_id = packet_id,
+ .packet_flag = MCUPacketFlag::LastCommandPacket,
+ .data_length = {},
+ .raw_data = {},
+ .crc = {},
+ };
+
+ std::vector<u8> request_data(sizeof(NFCRequestState));
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+DriverResult NfcProtocol::SendReadAmiiboRequest(MCUCommandResponse& output, NFCPages ntag_pages) {
+ NFCRequestState request{
+ .command_argument = NFCCommand::ReadNtag,
+ .block_id = {},
+ .packet_id = {},
+ .packet_flag = MCUPacketFlag::LastCommandPacket,
+ .data_length = sizeof(NFCReadCommandData),
+ .nfc_read =
+ {
+ .unknown = 0xd0,
+ .uuid_length = sizeof(NFCReadCommandData::uid),
+ .uid = {},
+ .tag_type = NFCTagType::Ntag215,
+ .read_block = GetReadBlockCommand(ntag_pages),
+ },
+ .crc = {},
+ };
+
+ std::array<u8, sizeof(NFCRequestState)> request_data{};
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+DriverResult NfcProtocol::SendWriteAmiiboRequest(MCUCommandResponse& output,
+ const TagUUID& tag_uuid) {
+ NFCRequestState request{
+ .command_argument = NFCCommand::ReadNtag,
+ .block_id = {},
+ .packet_id = {},
+ .packet_flag = MCUPacketFlag::LastCommandPacket,
+ .data_length = sizeof(NFCReadCommandData),
+ .nfc_read =
+ {
+ .unknown = 0xd0,
+ .uuid_length = sizeof(NFCReadCommandData::uid),
+ .uid = tag_uuid,
+ .tag_type = NFCTagType::Ntag215,
+ .read_block = GetReadBlockCommand(NFCPages::Block3),
+ },
+ .crc = {},
+ };
+
+ std::array<u8, sizeof(NFCRequestState)> request_data{};
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+DriverResult NfcProtocol::SendWriteDataAmiiboRequest(MCUCommandResponse& output, u8 block_id,
+ bool is_last_packet,
+ std::span<const u8> data) {
+ const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
+ NFCRequestState request{
+ .command_argument = NFCCommand::WriteNtag,
+ .block_id = block_id,
+ .packet_id = {},
+ .packet_flag =
+ is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
+ .data_length = static_cast<u8>(data_size),
+ .raw_data = {},
+ .crc = {},
+ };
+ memcpy(request.raw_data.data(), data.data(), data_size);
+
+ std::array<u8, sizeof(NFCRequestState)> request_data{};
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+DriverResult NfcProtocol::SendReadDataMifareRequest(MCUCommandResponse& output, u8 block_id,
+ bool is_last_packet, std::span<const u8> data) {
+ const auto data_size = std::min(data.size(), sizeof(NFCRequestState::raw_data));
+ NFCRequestState request{
+ .command_argument = NFCCommand::Mifare,
+ .block_id = block_id,
+ .packet_id = {},
+ .packet_flag =
+ is_last_packet ? MCUPacketFlag::LastCommandPacket : MCUPacketFlag::MorePacketsRemaining,
+ .data_length = static_cast<u8>(data_size),
+ .raw_data = {},
+ .crc = {},
+ };
+ memcpy(request.raw_data.data(), data.data(), data_size);
+
+ std::array<u8, sizeof(NFCRequestState)> request_data{};
+ memcpy(request_data.data(), &request, sizeof(NFCRequestState));
+ request_data[36] = CalculateMCU_CRC8(request_data.data(), 36);
+ return SendMCUData(ReportMode::NFC_IR_MODE_60HZ, MCUSubCommand::ReadDeviceMode, request_data,
+ output);
+}
+
+std::vector<u8> NfcProtocol::SerializeWritePackage(const NFCWritePackage& package) const {
+ const std::size_t header_size =
+ sizeof(NFCWriteCommandData) + sizeof(NFCWritePackage::number_of_chunks);
+ std::vector<u8> serialized_data(header_size);
+ std::size_t start_index = 0;
+
+ memcpy(serialized_data.data(), &package, header_size);
+ start_index += header_size;
+
+ for (const auto& data_chunk : package.data_chunks) {
+ const std::size_t chunk_size =
+ sizeof(NFCDataChunk::nfc_page) + sizeof(NFCDataChunk::data_size) + data_chunk.data_size;
+
+ serialized_data.resize(start_index + chunk_size);
+ memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
+ start_index += chunk_size;
+ }
+
+ return serialized_data;
+}
+
+std::vector<u8> NfcProtocol::SerializeMifareReadPackage(const MifareReadPackage& package) const {
+ const std::size_t header_size = sizeof(MifareCommandData);
+ std::vector<u8> serialized_data(header_size);
+ std::size_t start_index = 0;
+
+ memcpy(serialized_data.data(), &package, header_size);
+ start_index += header_size;
+
+ for (const auto& data_chunk : package.data_chunks) {
+ const std::size_t chunk_size = sizeof(MifareReadChunk);
+ if (data_chunk.command == MifareCmd::None) {
+ continue;
+ }
+ serialized_data.resize(start_index + chunk_size);
+ memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
+ start_index += chunk_size;
+ }
+
+ return serialized_data;
+}
+
+std::vector<u8> NfcProtocol::SerializeMifareWritePackage(const MifareWritePackage& package) const {
+ const std::size_t header_size = sizeof(MifareCommandData);
+ std::vector<u8> serialized_data(header_size);
+ std::size_t start_index = 0;
+
+ memcpy(serialized_data.data(), &package, header_size);
+ start_index += header_size;
+
+ for (const auto& data_chunk : package.data_chunks) {
+ const std::size_t chunk_size = sizeof(MifareWriteChunk);
+ if (data_chunk.command == MifareCmd::None) {
+ continue;
+ }
+ serialized_data.resize(start_index + chunk_size);
+ memcpy(serialized_data.data() + start_index, &data_chunk, chunk_size);
+ start_index += chunk_size;
+ }
+
+ return serialized_data;
+}
+
+NFCWritePackage NfcProtocol::MakeAmiiboWritePackage(const TagUUID& tag_uuid,
+ std::span<const u8> data) const {
+ return {
+ .command_data{
+ .unknown = 0xd0,
+ .uuid_length = sizeof(NFCReadCommandData::uid),
+ .uid = tag_uuid,
+ .tag_type = NFCTagType::Ntag215,
+ .unknown2 = 0x00,
+ .unknown3 = 0x01,
+ .unknown4 = 0x04,
+ .unknown5 = 0xff,
+ .unknown6 = 0xff,
+ .unknown7 = 0xff,
+ .unknown8 = 0xff,
+ .magic = data[16],
+ .write_count = static_cast<u16>((data[17] << 8) + data[18]),
+ .amiibo_version = data[19],
+ },
+ .number_of_chunks = 3,
+ .data_chunks =
+ {
+ MakeAmiiboChunk(0x05, 0x20, data),
+ MakeAmiiboChunk(0x20, 0xf0, data),
+ MakeAmiiboChunk(0x5c, 0x98, data),
+ },
+ };
+}
+
+MifareReadPackage NfcProtocol::MakeMifareReadPackage(
+ const MifareUUID& tag_uuid, std::span<const MifareReadChunk> read_request) const {
+ MifareReadPackage package{
+ .command_data{
+ .unknown1 = 0xd0,
+ .unknown2 = 0x07,
+ .number_of_short_bytes = static_cast<u8>(
+ ((read_request.size() * sizeof(MifareReadChunk)) + sizeof(MifareUUID)) / 2),
+ .uid = tag_uuid,
+ },
+ .data_chunks = {},
+ };
+
+ for (std::size_t i = 0; i < read_request.size() && i < package.data_chunks.size(); ++i) {
+ package.data_chunks[i] = read_request[i];
+ }
+
+ return package;
+}
+
+MifareWritePackage NfcProtocol::MakeMifareWritePackage(
+ const MifareUUID& tag_uuid, std::span<const MifareWriteChunk> read_request) const {
+ MifareWritePackage package{
+ .command_data{
+ .unknown1 = 0xd0,
+ .unknown2 = 0x07,
+ .number_of_short_bytes = static_cast<u8>(
+ ((read_request.size() * sizeof(MifareReadChunk)) + sizeof(MifareUUID) + 2) / 2),
+ .uid = tag_uuid,
+ },
+ .data_chunks = {},
+ };
+
+ for (std::size_t i = 0; i < read_request.size() && i < package.data_chunks.size(); ++i) {
+ package.data_chunks[i] = read_request[i];
+ }
+
+ return package;
+}
+
+NFCDataChunk NfcProtocol::MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const {
+ constexpr u8 NFC_PAGE_SIZE = 4;
+
+ if (static_cast<std::size_t>(page * NFC_PAGE_SIZE) + size >= data.size()) {
+ return {};
+ }
+
+ NFCDataChunk chunk{
+ .nfc_page = page,
+ .data_size = size,
+ .data = {},
+ };
+ std::memcpy(chunk.data.data(), data.data() + (page * NFC_PAGE_SIZE), size);
+ return chunk;
+}
+
+NFCReadBlockCommand NfcProtocol::GetReadBlockCommand(NFCPages pages) const {
+ switch (pages) {
+ case NFCPages::Block0:
+ return {
+ .block_count = 1,
+ };
+ case NFCPages::Block3:
+ return {
+ .block_count = 1,
+ .blocks =
+ {
+ NFCReadBlock{0x03, 0x03},
+ },
+ };
+ case NFCPages::Block45:
+ return {
+ .block_count = 1,
+ .blocks =
+ {
+ NFCReadBlock{0x00, 0x2C},
+ },
+ };
+ case NFCPages::Block135:
+ return {
+ .block_count = 3,
+ .blocks =
+ {
+ NFCReadBlock{0x00, 0x3b},
+ {0x3c, 0x77},
+ {0x78, 0x86},
+ },
+ };
+ case NFCPages::Block231:
+ return {
+ .block_count = 4,
+ .blocks =
+ {
+ NFCReadBlock{0x00, 0x3b},
+ {0x3c, 0x77},
+ {0x78, 0x83},
+ {0xb4, 0xe6},
+ },
+ };
+ default:
+ return {};
+ };
+}
+
+TagUUID NfcProtocol::GetTagUUID(std::span<const u8> data) const {
+ if (data.size() < 10) {
+ return {};
+ }
+
+ // crc byte 3 is omitted in this operation
+ return {
+ data[0], data[1], data[2], data[4], data[5], data[6], data[7],
+ };
+}
+
+bool NfcProtocol::IsEnabled() const {
+ return is_enabled;
+}
+
+bool NfcProtocol::IsPolling() const {
+ return is_polling;
+}
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/nfc.h b/src/input_common/helpers/joycon_protocol/nfc.h
new file mode 100644
index 000000000..0be95e40e
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/nfc.h
@@ -0,0 +1,114 @@
+// 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 StopNFCPollingMode();
+
+ DriverResult GetTagInfo(Joycon::TagInfo& tag_info);
+
+ DriverResult ReadAmiibo(std::vector<u8>& data);
+
+ DriverResult WriteAmiibo(std::span<const u8> data);
+
+ DriverResult ReadMifare(std::span<const MifareReadChunk> read_request,
+ std::span<MifareReadData> out_data);
+
+ DriverResult WriteMifare(std::span<const MifareWriteChunk> write_request);
+
+ bool HasAmiibo();
+
+ bool IsEnabled() const;
+
+ bool IsPolling() const;
+
+private:
+ // Number of times the function will be delayed until it outputs valid data
+ static constexpr std::size_t AMIIBO_UPDATE_DELAY = 15;
+
+ struct TagFoundData {
+ u8 type;
+ u8 uuid_size;
+ TagUUID uuid;
+ };
+
+ DriverResult WaitUntilNfcIs(NFCStatus status);
+
+ DriverResult IsTagInRange(TagFoundData& data, std::size_t timeout_limit = 1);
+
+ DriverResult GetAmiiboData(std::vector<u8>& data);
+
+ DriverResult WriteAmiiboData(const TagUUID& tag_uuid, std::span<const u8> data);
+
+ DriverResult GetMifareData(const MifareUUID& tag_uuid,
+ std::span<const MifareReadChunk> read_request,
+ std::span<MifareReadData> out_data);
+
+ DriverResult WriteMifareData(const MifareUUID& tag_uuid,
+ std::span<const MifareWriteChunk> write_request);
+
+ DriverResult SendStartPollingRequest(MCUCommandResponse& output,
+ bool is_second_attempt = false);
+
+ DriverResult SendStopPollingRequest(MCUCommandResponse& output);
+
+ DriverResult SendNextPackageRequest(MCUCommandResponse& output, u8 packet_id);
+
+ DriverResult SendReadAmiiboRequest(MCUCommandResponse& output, NFCPages ntag_pages);
+
+ DriverResult SendWriteAmiiboRequest(MCUCommandResponse& output, const TagUUID& tag_uuid);
+
+ DriverResult SendWriteDataAmiiboRequest(MCUCommandResponse& output, u8 block_id,
+ bool is_last_packet, std::span<const u8> data);
+
+ DriverResult SendReadDataMifareRequest(MCUCommandResponse& output, u8 block_id,
+ bool is_last_packet, std::span<const u8> data);
+
+ std::vector<u8> SerializeWritePackage(const NFCWritePackage& package) const;
+
+ std::vector<u8> SerializeMifareReadPackage(const MifareReadPackage& package) const;
+
+ std::vector<u8> SerializeMifareWritePackage(const MifareWritePackage& package) const;
+
+ NFCWritePackage MakeAmiiboWritePackage(const TagUUID& tag_uuid, std::span<const u8> data) const;
+
+ NFCDataChunk MakeAmiiboChunk(u8 page, u8 size, std::span<const u8> data) const;
+
+ MifareReadPackage MakeMifareReadPackage(const MifareUUID& tag_uuid,
+ std::span<const MifareReadChunk> read_request) const;
+
+ MifareWritePackage MakeMifareWritePackage(const MifareUUID& tag_uuid,
+ std::span<const MifareWriteChunk> read_request) const;
+
+ NFCReadBlockCommand GetReadBlockCommand(NFCPages pages) const;
+
+ TagUUID GetTagUUID(std::span<const u8> data) const;
+
+ bool is_enabled{};
+ bool is_polling{};
+ std::size_t update_counter{};
+};
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/poller.cpp b/src/input_common/helpers/joycon_protocol/poller.cpp
new file mode 100644
index 000000000..1aab9e12a
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/poller.cpp
@@ -0,0 +1,374 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/logging/log.h"
+#include "input_common/helpers/joycon_protocol/poller.h"
+
+namespace InputCommon::Joycon {
+
+JoyconPoller::JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
+ JoyStickCalibration right_stick_calibration_,
+ MotionCalibration motion_calibration_)
+ : device_type{device_type_}, left_stick_calibration{left_stick_calibration_},
+ right_stick_calibration{right_stick_calibration_}, motion_calibration{motion_calibration_} {}
+
+void JoyconPoller::SetCallbacks(const JoyconCallbacks& callbacks_) {
+ callbacks = std::move(callbacks_);
+}
+
+void JoyconPoller::ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
+ const RingStatus& ring_status) {
+ InputReportActive data{};
+ memcpy(&data, buffer.data(), sizeof(InputReportActive));
+
+ switch (device_type) {
+ case ControllerType::Left:
+ UpdateActiveLeftPadInput(data, motion_status);
+ break;
+ case ControllerType::Right:
+ UpdateActiveRightPadInput(data, motion_status);
+ break;
+ case ControllerType::Pro:
+ UpdateActiveProPadInput(data, motion_status);
+ break;
+ default:
+ break;
+ }
+
+ if (ring_status.is_enabled) {
+ UpdateRing(data.ring_input, ring_status);
+ }
+
+ callbacks.on_battery_data(data.battery_status);
+}
+
+void JoyconPoller::ReadPassiveMode(std::span<u8> buffer) {
+ InputReportPassive data{};
+ memcpy(&data, buffer.data(), sizeof(InputReportPassive));
+
+ switch (device_type) {
+ case ControllerType::Left:
+ UpdatePassiveLeftPadInput(data);
+ break;
+ case ControllerType::Right:
+ UpdatePassiveRightPadInput(data);
+ break;
+ case ControllerType::Pro:
+ UpdatePassiveProPadInput(data);
+ break;
+ default:
+ break;
+ }
+}
+
+void JoyconPoller::ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status) {
+ // This mode is compatible with the active mode
+ ReadActiveMode(buffer, motion_status, {});
+}
+
+void JoyconPoller::UpdateColor(const Color& color) {
+ callbacks.on_color_data(color);
+}
+
+void JoyconPoller::UpdateAmiibo(const Joycon::TagInfo& tag_info) {
+ callbacks.on_amiibo_data(tag_info);
+}
+
+void JoyconPoller::UpdateCamera(const std::vector<u8>& camera_data, IrsResolution format) {
+ callbacks.on_camera_data(camera_data, format);
+}
+
+void JoyconPoller::UpdateRing(s16 value, const RingStatus& ring_status) {
+ float normalized_value = static_cast<float>(value - ring_status.default_value);
+ if (normalized_value > 0) {
+ normalized_value = normalized_value /
+ static_cast<float>(ring_status.max_value - ring_status.default_value);
+ }
+ if (normalized_value < 0) {
+ normalized_value = normalized_value /
+ static_cast<float>(ring_status.default_value - ring_status.min_value);
+ }
+ callbacks.on_ring_data(normalized_value);
+}
+
+void JoyconPoller::UpdateActiveLeftPadInput(const InputReportActive& input,
+ const MotionStatus& motion_status) {
+ static constexpr std::array<Joycon::PadButton, 11> left_buttons{
+ Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
+ Joycon::PadButton::Left, Joycon::PadButton::LeftSL, Joycon::PadButton::LeftSR,
+ Joycon::PadButton::L, Joycon::PadButton::ZL, Joycon::PadButton::Minus,
+ Joycon::PadButton::Capture, Joycon::PadButton::StickL,
+ };
+
+ const u32 raw_button =
+ static_cast<u32>(input.button_input[2] | ((input.button_input[1] & 0b00101001) << 16));
+ for (std::size_t i = 0; i < left_buttons.size(); ++i) {
+ const bool button_status = (raw_button & static_cast<u32>(left_buttons[i])) != 0;
+ const int button = static_cast<int>(left_buttons[i]);
+ callbacks.on_button_data(button, button_status);
+ }
+
+ const u16 raw_left_axis_x =
+ static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
+ const u16 raw_left_axis_y =
+ static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
+ const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
+ const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
+
+ if (motion_status.is_enabled) {
+ auto left_motion = GetMotionInput(input, motion_status);
+ // Rotate motion axis to the correct direction
+ left_motion.accel_y = -left_motion.accel_y;
+ left_motion.accel_z = -left_motion.accel_z;
+ left_motion.gyro_x = -left_motion.gyro_x;
+ callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), left_motion);
+ }
+}
+
+void JoyconPoller::UpdateActiveRightPadInput(const InputReportActive& input,
+ const MotionStatus& motion_status) {
+ static constexpr std::array<Joycon::PadButton, 11> right_buttons{
+ Joycon::PadButton::Y, Joycon::PadButton::X, Joycon::PadButton::B,
+ Joycon::PadButton::A, Joycon::PadButton::RightSL, Joycon::PadButton::RightSR,
+ Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
+ Joycon::PadButton::Home, Joycon::PadButton::StickR,
+ };
+
+ const u32 raw_button =
+ static_cast<u32>((input.button_input[0] << 8) | (input.button_input[1] << 16));
+ for (std::size_t i = 0; i < right_buttons.size(); ++i) {
+ const bool button_status = (raw_button & static_cast<u32>(right_buttons[i])) != 0;
+ const int button = static_cast<int>(right_buttons[i]);
+ callbacks.on_button_data(button, button_status);
+ }
+
+ const u16 raw_right_axis_x =
+ static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
+ const u16 raw_right_axis_y =
+ static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
+ const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
+ const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
+
+ if (motion_status.is_enabled) {
+ auto right_motion = GetMotionInput(input, motion_status);
+ // Rotate motion axis to the correct direction
+ right_motion.accel_x = -right_motion.accel_x;
+ right_motion.accel_y = -right_motion.accel_y;
+ right_motion.gyro_z = -right_motion.gyro_z;
+ callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), right_motion);
+ }
+}
+
+void JoyconPoller::UpdateActiveProPadInput(const InputReportActive& input,
+ const MotionStatus& motion_status) {
+ static constexpr std::array<Joycon::PadButton, 18> pro_buttons{
+ Joycon::PadButton::Down, Joycon::PadButton::Up, Joycon::PadButton::Right,
+ Joycon::PadButton::Left, Joycon::PadButton::L, Joycon::PadButton::ZL,
+ Joycon::PadButton::Minus, Joycon::PadButton::Capture, Joycon::PadButton::Y,
+ Joycon::PadButton::X, Joycon::PadButton::B, Joycon::PadButton::A,
+ Joycon::PadButton::R, Joycon::PadButton::ZR, Joycon::PadButton::Plus,
+ Joycon::PadButton::Home, Joycon::PadButton::StickL, Joycon::PadButton::StickR,
+ };
+
+ const u32 raw_button = static_cast<u32>(input.button_input[2] | (input.button_input[0] << 8) |
+ (input.button_input[1] << 16));
+ for (std::size_t i = 0; i < pro_buttons.size(); ++i) {
+ const bool button_status = (raw_button & static_cast<u32>(pro_buttons[i])) != 0;
+ const int button = static_cast<int>(pro_buttons[i]);
+ callbacks.on_button_data(button, button_status);
+ }
+
+ const u16 raw_left_axis_x =
+ static_cast<u16>(input.left_stick_state[0] | ((input.left_stick_state[1] & 0xf) << 8));
+ const u16 raw_left_axis_y =
+ static_cast<u16>((input.left_stick_state[1] >> 4) | (input.left_stick_state[2] << 4));
+ const u16 raw_right_axis_x =
+ static_cast<u16>(input.right_stick_state[0] | ((input.right_stick_state[1] & 0xf) << 8));
+ const u16 raw_right_axis_y =
+ static_cast<u16>((input.right_stick_state[1] >> 4) | (input.right_stick_state[2] << 4));
+
+ const f32 left_axis_x = GetAxisValue(raw_left_axis_x, left_stick_calibration.x);
+ const f32 left_axis_y = GetAxisValue(raw_left_axis_y, left_stick_calibration.y);
+ const f32 right_axis_x = GetAxisValue(raw_right_axis_x, right_stick_calibration.x);
+ const f32 right_axis_y = GetAxisValue(raw_right_axis_y, right_stick_calibration.y);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
+
+ if (motion_status.is_enabled) {
+ auto pro_motion = GetMotionInput(input, motion_status);
+ pro_motion.gyro_x = -pro_motion.gyro_x;
+ pro_motion.accel_y = -pro_motion.accel_y;
+ pro_motion.accel_z = -pro_motion.accel_z;
+ callbacks.on_motion_data(static_cast<int>(PadMotion::LeftMotion), pro_motion);
+ callbacks.on_motion_data(static_cast<int>(PadMotion::RightMotion), pro_motion);
+ }
+}
+
+void JoyconPoller::UpdatePassiveLeftPadInput(const InputReportPassive& input) {
+ static constexpr std::array<PassivePadButton, 11> left_buttons{
+ PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
+ PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
+ PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Minus,
+ PassivePadButton::Capture, PassivePadButton::StickL,
+ };
+
+ for (auto left_button : left_buttons) {
+ const bool button_status = (input.button_input & static_cast<u32>(left_button)) != 0;
+ const int button = static_cast<int>(left_button);
+ callbacks.on_button_data(button, button_status);
+ }
+
+ const auto [left_axis_x, left_axis_y] =
+ GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state));
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
+}
+
+void JoyconPoller::UpdatePassiveRightPadInput(const InputReportPassive& input) {
+ static constexpr std::array<PassivePadButton, 11> right_buttons{
+ PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
+ PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
+ PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Plus,
+ PassivePadButton::Home, PassivePadButton::StickR,
+ };
+
+ for (auto right_button : right_buttons) {
+ const bool button_status = (input.button_input & static_cast<u32>(right_button)) != 0;
+ const int button = static_cast<int>(right_button);
+ callbacks.on_button_data(button, button_status);
+ }
+
+ const auto [right_axis_x, right_axis_y] =
+ GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state));
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
+}
+
+void JoyconPoller::UpdatePassiveProPadInput(const InputReportPassive& input) {
+ static constexpr std::array<PassivePadButton, 14> pro_buttons{
+ PassivePadButton::Down_A, PassivePadButton::Right_X, PassivePadButton::Left_B,
+ PassivePadButton::Up_Y, PassivePadButton::SL, PassivePadButton::SR,
+ PassivePadButton::L_R, PassivePadButton::ZL_ZR, PassivePadButton::Minus,
+ PassivePadButton::Plus, PassivePadButton::Capture, PassivePadButton::Home,
+ PassivePadButton::StickL, PassivePadButton::StickR,
+ };
+
+ for (auto pro_button : pro_buttons) {
+ const bool button_status = (input.button_input & static_cast<u32>(pro_button)) != 0;
+ const int button = static_cast<int>(pro_button);
+ callbacks.on_button_data(button, button_status);
+ }
+
+ const auto [left_axis_x, left_axis_y] =
+ GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state & 0xf));
+ const auto [right_axis_x, right_axis_y] =
+ GetPassiveAxisValue(static_cast<PassivePadStick>(input.stick_state >> 4));
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickX), left_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::LeftStickY), left_axis_y);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickX), right_axis_x);
+ callbacks.on_stick_data(static_cast<int>(PadAxes::RightStickY), right_axis_y);
+}
+
+f32 JoyconPoller::GetAxisValue(u16 raw_value, Joycon::JoyStickAxisCalibration calibration) const {
+ const f32 value = static_cast<f32>(raw_value - calibration.center);
+ if (value > 0.0f) {
+ return value / calibration.max;
+ }
+ return value / calibration.min;
+}
+
+std::pair<f32, f32> JoyconPoller::GetPassiveAxisValue(PassivePadStick raw_value) const {
+ switch (raw_value) {
+ case PassivePadStick::Right:
+ return {1.0f, 0.0f};
+ case PassivePadStick::RightDown:
+ return {1.0f, -1.0f};
+ case PassivePadStick::Down:
+ return {0.0f, -1.0f};
+ case PassivePadStick::DownLeft:
+ return {-1.0f, -1.0f};
+ case PassivePadStick::Left:
+ return {-1.0f, 0.0f};
+ case PassivePadStick::LeftUp:
+ return {-1.0f, 1.0f};
+ case PassivePadStick::Up:
+ return {0.0f, 1.0f};
+ case PassivePadStick::UpRight:
+ return {1.0f, 1.0f};
+ case PassivePadStick::Neutral:
+ default:
+ return {0.0f, 0.0f};
+ }
+}
+
+f32 JoyconPoller::GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal,
+ AccelerometerSensitivity sensitivity) const {
+ const f32 value = raw * (1.0f / (cal.scale - cal.offset)) * 4;
+ switch (sensitivity) {
+ case Joycon::AccelerometerSensitivity::G2:
+ return value / 4.0f;
+ case Joycon::AccelerometerSensitivity::G4:
+ return value / 2.0f;
+ case Joycon::AccelerometerSensitivity::G8:
+ return value;
+ case Joycon::AccelerometerSensitivity::G16:
+ return value * 2.0f;
+ }
+ return value;
+}
+
+f32 JoyconPoller::GetGyroValue(s16 raw, const MotionSensorCalibration& cal,
+ GyroSensitivity sensitivity) const {
+ const f32 value = (raw - cal.offset) * (936.0f / (cal.scale - cal.offset)) / 360.0f;
+ switch (sensitivity) {
+ case Joycon::GyroSensitivity::DPS250:
+ return value / 8.0f;
+ case Joycon::GyroSensitivity::DPS500:
+ return value / 4.0f;
+ case Joycon::GyroSensitivity::DPS1000:
+ return value / 2.0f;
+ case Joycon::GyroSensitivity::DPS2000:
+ return value;
+ }
+ return value;
+}
+
+s16 JoyconPoller::GetRawIMUValues(std::size_t sensor, size_t axis,
+ const InputReportActive& input) const {
+ return input.motion_input[(sensor * 3) + axis];
+}
+
+MotionData JoyconPoller::GetMotionInput(const InputReportActive& input,
+ const MotionStatus& motion_status) const {
+ MotionData motion{};
+ const auto& accel_cal = motion_calibration.accelerometer;
+ const auto& gyro_cal = motion_calibration.gyro;
+ const s16 raw_accel_x = input.motion_input[1];
+ const s16 raw_accel_y = input.motion_input[0];
+ const s16 raw_accel_z = input.motion_input[2];
+ const s16 raw_gyro_x = input.motion_input[4];
+ const s16 raw_gyro_y = input.motion_input[3];
+ const s16 raw_gyro_z = input.motion_input[5];
+
+ motion.delta_timestamp = motion_status.delta_time;
+ motion.accel_x =
+ GetAccelerometerValue(raw_accel_x, accel_cal[1], motion_status.accelerometer_sensitivity);
+ motion.accel_y =
+ GetAccelerometerValue(raw_accel_y, accel_cal[0], motion_status.accelerometer_sensitivity);
+ motion.accel_z =
+ GetAccelerometerValue(raw_accel_z, accel_cal[2], motion_status.accelerometer_sensitivity);
+ motion.gyro_x = GetGyroValue(raw_gyro_x, gyro_cal[1], motion_status.gyro_sensitivity);
+ motion.gyro_y = GetGyroValue(raw_gyro_y, gyro_cal[0], motion_status.gyro_sensitivity);
+ motion.gyro_z = GetGyroValue(raw_gyro_z, gyro_cal[2], motion_status.gyro_sensitivity);
+
+ // TODO(German77): Return all three samples data
+ return motion;
+}
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/poller.h b/src/input_common/helpers/joycon_protocol/poller.h
new file mode 100644
index 000000000..3746abe5d
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/poller.h
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
+// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
+// https://github.com/CTCaer/jc_toolkit
+// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
+
+#pragma once
+
+#include <functional>
+#include <span>
+
+#include "input_common/helpers/joycon_protocol/joycon_types.h"
+
+namespace InputCommon::Joycon {
+
+// Handles input packages and triggers the corresponding input events
+class JoyconPoller {
+public:
+ JoyconPoller(ControllerType device_type_, JoyStickCalibration left_stick_calibration_,
+ JoyStickCalibration right_stick_calibration_,
+ MotionCalibration motion_calibration_);
+
+ void SetCallbacks(const JoyconCallbacks& callbacks_);
+
+ /// Handles data from passive packages
+ void ReadPassiveMode(std::span<u8> buffer);
+
+ /// Handles data from active packages
+ void ReadActiveMode(std::span<u8> buffer, const MotionStatus& motion_status,
+ const RingStatus& ring_status);
+
+ /// Handles data from nfc or ir packages
+ void ReadNfcIRMode(std::span<u8> buffer, const MotionStatus& motion_status);
+
+ void UpdateColor(const Color& color);
+ void UpdateRing(s16 value, const RingStatus& ring_status);
+ void UpdateAmiibo(const Joycon::TagInfo& tag_info);
+ void UpdateCamera(const std::vector<u8>& camera_data, IrsResolution format);
+
+private:
+ void UpdateActiveLeftPadInput(const InputReportActive& input,
+ const MotionStatus& motion_status);
+ void UpdateActiveRightPadInput(const InputReportActive& input,
+ const MotionStatus& motion_status);
+ void UpdateActiveProPadInput(const InputReportActive& input, const MotionStatus& motion_status);
+
+ void UpdatePassiveLeftPadInput(const InputReportPassive& buffer);
+ void UpdatePassiveRightPadInput(const InputReportPassive& buffer);
+ void UpdatePassiveProPadInput(const InputReportPassive& buffer);
+
+ /// Returns a calibrated joystick axis from raw axis data
+ f32 GetAxisValue(u16 raw_value, JoyStickAxisCalibration calibration) const;
+
+ /// Returns a digital joystick axis from passive axis data
+ std::pair<f32, f32> GetPassiveAxisValue(PassivePadStick raw_value) const;
+
+ /// Returns a calibrated accelerometer axis from raw motion data
+ f32 GetAccelerometerValue(s16 raw, const MotionSensorCalibration& cal,
+ AccelerometerSensitivity sensitivity) const;
+
+ /// Returns a calibrated gyro axis from raw motion data
+ f32 GetGyroValue(s16 raw_value, const MotionSensorCalibration& cal,
+ GyroSensitivity sensitivity) const;
+
+ /// Returns a raw motion value from a buffer
+ s16 GetRawIMUValues(size_t sensor, size_t axis, const InputReportActive& input) const;
+
+ /// Returns motion data from a buffer
+ MotionData GetMotionInput(const InputReportActive& input,
+ const MotionStatus& motion_status) const;
+
+ ControllerType device_type{};
+
+ // Device calibration
+ JoyStickCalibration left_stick_calibration{};
+ JoyStickCalibration right_stick_calibration{};
+ MotionCalibration motion_calibration{};
+
+ JoyconCallbacks callbacks{};
+};
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/ringcon.cpp b/src/input_common/helpers/joycon_protocol/ringcon.cpp
new file mode 100644
index 000000000..190cef812
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/ringcon.cpp
@@ -0,0 +1,115 @@
+// 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;
+ SubCommandResponse output{};
+ std::size_t tries = 0;
+ is_connected = false;
+
+ do {
+ const auto result = SendSubCommand(SubCommand::GET_EXTERNAL_DEVICE_INFO, {}, output);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ if (tries++ >= max_tries) {
+ return DriverResult::NoDeviceDetected;
+ }
+ } while (output.external_device_id != ExternalDeviceId::RingController);
+
+ 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::SET_EXTERNAL_FORMAT_CONFIG, ring_config);
+
+ if (result != DriverResult::Success) {
+ return result;
+ }
+
+ static constexpr std::array<u8, 4> ringcon_data{0x04, 0x01, 0x01, 0x02};
+ return SendSubCommand(SubCommand::ENABLE_EXTERNAL_POLLING, ringcon_data);
+}
+
+bool RingConProtocol::IsEnabled() const {
+ return is_enabled;
+}
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_protocol/ringcon.h b/src/input_common/helpers/joycon_protocol/ringcon.h
new file mode 100644
index 000000000..6e858f3fc
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/ringcon.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
+// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
+// https://github.com/CTCaer/jc_toolkit
+// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
+
+#pragma once
+
+#include <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
diff --git a/src/input_common/helpers/joycon_protocol/rumble.cpp b/src/input_common/helpers/joycon_protocol/rumble.cpp
new file mode 100644
index 000000000..63b60c946
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/rumble.cpp
@@ -0,0 +1,299 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <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
diff --git a/src/input_common/helpers/joycon_protocol/rumble.h b/src/input_common/helpers/joycon_protocol/rumble.h
new file mode 100644
index 000000000..6c12b7925
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/rumble.h
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
+// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
+// https://github.com/CTCaer/jc_toolkit
+// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
+
+#pragma once
+
+#include <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
diff --git a/src/input_common/helpers/stick_from_buttons.cpp b/src/input_common/helpers/stick_from_buttons.cpp
index f3a0b3419..a6be6dac1 100644
--- a/src/input_common/helpers/stick_from_buttons.cpp
+++ b/src/input_common/helpers/stick_from_buttons.cpp
@@ -11,6 +11,14 @@ namespace InputCommon {
class Stick final : public Common::Input::InputDevice {
public:
+ // Some games such as EARTH DEFENSE FORCE: WORLD BROTHERS
+ // do not play nicely with the theoretical maximum range.
+ // Using a value one lower from the maximum emulates real stick behavior.
+ static constexpr float MAX_RANGE = 32766.0f / 32767.0f;
+ static constexpr float TAU = Common::PI * 2.0f;
+ // Use wider angle to ease the transition.
+ static constexpr float APERTURE = TAU * 0.15f;
+
using Button = std::unique_ptr<Common::Input::InputDevice>;
Stick(Button up_, Button down_, Button left_, Button right_, Button modifier_, Button updater_,
@@ -56,30 +64,23 @@ public:
}
bool IsAngleGreater(float old_angle, float new_angle) const {
- constexpr float TAU = Common::PI * 2.0f;
- // Use wider angle to ease the transition.
- constexpr float aperture = TAU * 0.15f;
- const float top_limit = new_angle + aperture;
+ const float top_limit = new_angle + APERTURE;
return (old_angle > new_angle && old_angle <= top_limit) ||
(old_angle + TAU > new_angle && old_angle + TAU <= top_limit);
}
bool IsAngleSmaller(float old_angle, float new_angle) const {
- constexpr float TAU = Common::PI * 2.0f;
- // Use wider angle to ease the transition.
- constexpr float aperture = TAU * 0.15f;
- const float bottom_limit = new_angle - aperture;
+ const float bottom_limit = new_angle - APERTURE;
return (old_angle >= bottom_limit && old_angle < new_angle) ||
(old_angle - TAU >= bottom_limit && old_angle - TAU < new_angle);
}
float GetAngle(std::chrono::time_point<std::chrono::steady_clock> now) const {
- constexpr float TAU = Common::PI * 2.0f;
float new_angle = angle;
auto time_difference = static_cast<float>(
- std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count());
- time_difference /= 1000.0f * 1000.0f;
+ std::chrono::duration_cast<std::chrono::milliseconds>(now - last_update).count());
+ time_difference /= 1000.0f;
if (time_difference > 0.5f) {
time_difference = 0.5f;
}
@@ -196,8 +197,6 @@ public:
}
void UpdateStatus() {
- const float coef = modifier_status.value ? modifier_scale : 1.0f;
-
bool r = right_status;
bool l = left_status;
bool u = up_status;
@@ -215,7 +214,7 @@ public:
// Move if a key is pressed
if (r || l || u || d) {
- amplitude = coef;
+ amplitude = modifier_status.value ? modifier_scale : MAX_RANGE;
} else {
amplitude = 0;
}
@@ -269,30 +268,17 @@ public:
Common::Input::StickStatus status{};
status.x.properties = properties;
status.y.properties = properties;
+
if (Settings::values.emulate_analog_keyboard) {
const auto now = std::chrono::steady_clock::now();
- float angle_ = GetAngle(now);
+ const float angle_ = GetAngle(now);
status.x.raw_value = std::cos(angle_) * amplitude;
status.y.raw_value = std::sin(angle_) * amplitude;
return status;
}
- constexpr float SQRT_HALF = 0.707106781f;
- int x = 0, y = 0;
- if (right_status) {
- ++x;
- }
- if (left_status) {
- --x;
- }
- if (up_status) {
- ++y;
- }
- if (down_status) {
- --y;
- }
- const float coef = modifier_status.value ? modifier_scale : 1.0f;
- status.x.raw_value = static_cast<float>(x) * coef * (y == 0 ? 1.0f : SQRT_HALF);
- status.y.raw_value = static_cast<float>(y) * coef * (x == 0 ? 1.0f : SQRT_HALF);
+
+ status.x.raw_value = std::cos(goal_angle) * amplitude;
+ status.y.raw_value = std::sin(goal_angle) * amplitude;
return status;
}
diff --git a/src/input_common/helpers/udp_protocol.cpp b/src/input_common/helpers/udp_protocol.cpp
index 994380d21..e54a8fce1 100644
--- a/src/input_common/helpers/udp_protocol.cpp
+++ b/src/input_common/helpers/udp_protocol.cpp
@@ -25,7 +25,7 @@ namespace Response {
/**
* Returns Type if the packet is valid, else none
*
- * Note: Modifies the buffer to zero out the crc (since thats the easiest way to check without
+ * Note: Modifies the buffer to zero out the crc (since that's the easiest way to check without
* copying the buffer)
*/
std::optional<Type> Validate(u8* data, std::size_t size) {
diff --git a/src/input_common/input_engine.cpp b/src/input_common/input_engine.cpp
index 61cfd0911..e4c5b5b3c 100644
--- a/src/input_common/input_engine.cpp
+++ b/src/input_common/input_engine.cpp
@@ -79,6 +79,17 @@ void InputEngine::SetBattery(const PadIdentifier& identifier, Common::Input::Bat
TriggerOnBatteryChange(identifier, value);
}
+void InputEngine::SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value) {
+ {
+ std::scoped_lock lock{mutex};
+ ControllerData& controller = controller_list.at(identifier);
+ if (!configuring) {
+ controller.color = value;
+ }
+ }
+ TriggerOnColorChange(identifier, value);
+}
+
void InputEngine::SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value) {
{
std::scoped_lock lock{mutex};
@@ -176,6 +187,18 @@ Common::Input::BatteryLevel InputEngine::GetBattery(const PadIdentifier& identif
return controller.battery;
}
+Common::Input::BodyColorStatus InputEngine::GetColor(const PadIdentifier& identifier) const {
+ std::scoped_lock lock{mutex};
+ const auto controller_iter = controller_list.find(identifier);
+ if (controller_iter == controller_list.cend()) {
+ LOG_ERROR(Input, "Invalid identifier guid={}, pad={}, port={}", identifier.guid.RawString(),
+ identifier.pad, identifier.port);
+ return {};
+ }
+ const ControllerData& controller = controller_iter->second;
+ return controller.color;
+}
+
BasicMotion InputEngine::GetMotion(const PadIdentifier& identifier, int motion) const {
std::scoped_lock lock{mutex};
const auto controller_iter = controller_list.find(identifier);
@@ -328,6 +351,20 @@ void InputEngine::TriggerOnBatteryChange(const PadIdentifier& identifier,
}
}
+void InputEngine::TriggerOnColorChange(const PadIdentifier& identifier,
+ [[maybe_unused]] Common::Input::BodyColorStatus value) {
+ std::scoped_lock lock{mutex_callback};
+ for (const auto& poller_pair : callback_list) {
+ const InputIdentifier& poller = poller_pair.second;
+ if (!IsInputIdentifierEqual(poller, identifier, EngineInputType::Color, 0)) {
+ continue;
+ }
+ if (poller.callback.on_change) {
+ poller.callback.on_change();
+ }
+ }
+}
+
void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value) {
std::scoped_lock lock{mutex_callback};
@@ -343,13 +380,16 @@ void InputEngine::TriggerOnMotionChange(const PadIdentifier& identifier, int mot
if (!configuring || !mapping_callback.on_data) {
return;
}
+ const auto old_value = GetMotion(identifier, motion);
bool is_active = false;
- if (std::abs(value.accel_x) > 1.5f || std::abs(value.accel_y) > 1.5f ||
- std::abs(value.accel_z) > 1.5f) {
+ if (std::abs(value.accel_x - old_value.accel_x) > 1.5f ||
+ std::abs(value.accel_y - old_value.accel_y) > 1.5f ||
+ std::abs(value.accel_z - old_value.accel_z) > 1.5f) {
is_active = true;
}
- if (std::abs(value.gyro_x) > 0.6f || std::abs(value.gyro_y) > 0.6f ||
- std::abs(value.gyro_z) > 0.6f) {
+ if (std::abs(value.gyro_x - old_value.gyro_x) > 0.6f ||
+ std::abs(value.gyro_y - old_value.gyro_y) > 0.6f ||
+ std::abs(value.gyro_z - old_value.gyro_z) > 0.6f) {
is_active = true;
}
if (!is_active) {
diff --git a/src/input_common/input_engine.h b/src/input_common/input_engine.h
index 6cbcf5207..c2d0cbb34 100644
--- a/src/input_common/input_engine.h
+++ b/src/input_common/input_engine.h
@@ -40,6 +40,7 @@ enum class EngineInputType {
Battery,
Button,
Camera,
+ Color,
HatButton,
Motion,
Nfc,
@@ -104,14 +105,17 @@ public:
void EndConfiguration();
// Sets a led pattern for a controller
- virtual void SetLeds([[maybe_unused]] const PadIdentifier& identifier,
- [[maybe_unused]] const Common::Input::LedStatus& led_status) {}
+ virtual Common::Input::DriverResult SetLeds(
+ [[maybe_unused]] const PadIdentifier& identifier,
+ [[maybe_unused]] const Common::Input::LedStatus& led_status) {
+ return Common::Input::DriverResult::NotSupported;
+ }
// Sets rumble to a controller
- virtual Common::Input::VibrationError SetVibration(
+ virtual Common::Input::DriverResult SetVibration(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::VibrationStatus& vibration) {
- return Common::Input::VibrationError::NotSupported;
+ return Common::Input::DriverResult::NotSupported;
}
// Returns true if device supports vibrations
@@ -120,17 +124,17 @@ public:
}
// Sets polling mode to a controller
- virtual Common::Input::PollingError SetPollingMode(
+ virtual Common::Input::DriverResult SetPollingMode(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const Common::Input::PollingMode polling_mode) {
- return Common::Input::PollingError::NotSupported;
+ return Common::Input::DriverResult::NotSupported;
}
// Sets camera format to a controller
- virtual Common::Input::CameraError SetCameraFormat(
+ virtual Common::Input::DriverResult SetCameraFormat(
[[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] Common::Input::CameraFormat camera_format) {
- return Common::Input::CameraError::NotSupported;
+ return Common::Input::DriverResult::NotSupported;
}
// Returns success if nfc is supported
@@ -139,12 +143,46 @@ public:
return Common::Input::NfcState::NotSupported;
}
+ // Start scanning for nfc tags
+ virtual Common::Input::NfcState StartNfcPolling(
+ [[maybe_unused]] const PadIdentifier& identifier_) {
+ return Common::Input::NfcState::NotSupported;
+ }
+
+ // Start scanning for nfc tags
+ virtual Common::Input::NfcState StopNfcPolling(
+ [[maybe_unused]] const PadIdentifier& identifier_) {
+ return Common::Input::NfcState::NotSupported;
+ }
+
+ // Reads data from amiibo tag
+ virtual Common::Input::NfcState ReadAmiiboData(
+ [[maybe_unused]] const PadIdentifier& identifier_,
+ [[maybe_unused]] std::vector<u8>& out_data) {
+ return Common::Input::NfcState::NotSupported;
+ }
+
// Writes data to an nfc tag
virtual Common::Input::NfcState WriteNfcData([[maybe_unused]] const PadIdentifier& identifier,
[[maybe_unused]] const std::vector<u8>& data) {
return Common::Input::NfcState::NotSupported;
}
+ // Reads data from mifare tag
+ virtual Common::Input::NfcState ReadMifareData(
+ [[maybe_unused]] const PadIdentifier& identifier_,
+ [[maybe_unused]] const Common::Input::MifareRequest& request,
+ [[maybe_unused]] Common::Input::MifareRequest& out_data) {
+ return Common::Input::NfcState::NotSupported;
+ }
+
+ // Write data to mifare tag
+ virtual Common::Input::NfcState WriteMifareData(
+ [[maybe_unused]] const PadIdentifier& identifier_,
+ [[maybe_unused]] const Common::Input::MifareRequest& request) {
+ return Common::Input::NfcState::NotSupported;
+ }
+
// Returns the engine name
[[nodiscard]] const std::string& GetEngineName() const;
@@ -199,6 +237,7 @@ public:
bool GetHatButton(const PadIdentifier& identifier, int button, u8 direction) const;
f32 GetAxis(const PadIdentifier& identifier, int axis) const;
Common::Input::BatteryLevel GetBattery(const PadIdentifier& identifier) const;
+ Common::Input::BodyColorStatus GetColor(const PadIdentifier& identifier) const;
BasicMotion GetMotion(const PadIdentifier& identifier, int motion) const;
Common::Input::CameraStatus GetCamera(const PadIdentifier& identifier) const;
Common::Input::NfcStatus GetNfc(const PadIdentifier& identifier) const;
@@ -212,6 +251,7 @@ protected:
void SetHatButton(const PadIdentifier& identifier, int button, u8 value);
void SetAxis(const PadIdentifier& identifier, int axis, f32 value);
void SetBattery(const PadIdentifier& identifier, Common::Input::BatteryLevel value);
+ void SetColor(const PadIdentifier& identifier, Common::Input::BodyColorStatus value);
void SetMotion(const PadIdentifier& identifier, int motion, const BasicMotion& value);
void SetCamera(const PadIdentifier& identifier, const Common::Input::CameraStatus& value);
void SetNfc(const PadIdentifier& identifier, const Common::Input::NfcStatus& value);
@@ -227,6 +267,7 @@ private:
std::unordered_map<int, float> axes;
std::unordered_map<int, BasicMotion> motions;
Common::Input::BatteryLevel battery{};
+ Common::Input::BodyColorStatus color{};
Common::Input::CameraStatus camera{};
Common::Input::NfcStatus nfc{};
};
@@ -235,6 +276,8 @@ private:
void TriggerOnHatButtonChange(const PadIdentifier& identifier, int button, u8 value);
void TriggerOnAxisChange(const PadIdentifier& identifier, int axis, f32 value);
void TriggerOnBatteryChange(const PadIdentifier& identifier, Common::Input::BatteryLevel value);
+ void TriggerOnColorChange(const PadIdentifier& identifier,
+ Common::Input::BodyColorStatus value);
void TriggerOnMotionChange(const PadIdentifier& identifier, int motion,
const BasicMotion& value);
void TriggerOnCameraChange(const PadIdentifier& identifier,
diff --git a/src/input_common/input_mapping.cpp b/src/input_common/input_mapping.cpp
index d6e49d2c5..8c2ee4eb3 100644
--- a/src/input_common/input_mapping.cpp
+++ b/src/input_common/input_mapping.cpp
@@ -82,6 +82,9 @@ void MappingFactory::RegisterButton(const MappingData& data) {
new_input.Set("axis", data.index);
new_input.Set("threshold", 0.5f);
break;
+ case EngineInputType::Motion:
+ new_input.Set("motion", data.index);
+ break;
default:
return;
}
@@ -142,14 +145,11 @@ void MappingFactory::RegisterMotion(const MappingData& data) {
new_input.Set("port", static_cast<int>(data.pad.port));
new_input.Set("pad", static_cast<int>(data.pad.pad));
- // If engine is mouse map the mouse position as 3 axis motion
+ // If engine is mouse map it automatically to mouse motion
if (data.engine == "mouse") {
- new_input.Set("axis_x", 1);
- new_input.Set("invert_x", "-");
- new_input.Set("axis_y", 0);
- new_input.Set("axis_z", 4);
- new_input.Set("range", 1.0f);
- new_input.Set("deadzone", 0.0f);
+ new_input.Set("motion", 0);
+ new_input.Set("pad", 1);
+ new_input.Set("threshold", 0.001f);
input_queue.Push(new_input);
return;
}
@@ -194,6 +194,10 @@ bool MappingFactory::IsDriverValid(const MappingData& data) const {
if (data.engine == "keyboard" && data.pad.port != 0) {
return false;
}
+ // Only port 0 can be mapped on the mouse
+ if (data.engine == "mouse" && data.pad.port != 0) {
+ return false;
+ }
// To prevent mapping with two devices we disable any UDP except motion
if (!Settings::values.enable_udp_controller && data.engine == "cemuhookudp" &&
data.type != EngineInputType::Motion) {
diff --git a/src/input_common/input_poller.cpp b/src/input_common/input_poller.cpp
index fb8be42e2..870e76ab0 100644
--- a/src/input_common/input_poller.cpp
+++ b/src/input_common/input_poller.cpp
@@ -16,10 +16,10 @@ public:
class InputFromButton final : public Common::Input::InputDevice {
public:
- explicit InputFromButton(PadIdentifier identifier_, int button_, bool toggle_, bool inverted_,
- InputEngine* input_engine_)
- : identifier(identifier_), button(button_), toggle(toggle_), inverted(inverted_),
- input_engine(input_engine_) {
+ explicit InputFromButton(PadIdentifier identifier_, int button_, bool turbo_, bool toggle_,
+ bool inverted_, InputEngine* input_engine_)
+ : identifier(identifier_), button(button_), turbo(turbo_), toggle(toggle_),
+ inverted(inverted_), input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
@@ -40,6 +40,7 @@ public:
.value = input_engine->GetButton(identifier, button),
.inverted = inverted,
.toggle = toggle,
+ .turbo = turbo,
};
}
@@ -68,6 +69,7 @@ public:
private:
const PadIdentifier identifier;
const int button;
+ const bool turbo;
const bool toggle;
const bool inverted;
int callback_key;
@@ -77,10 +79,10 @@ private:
class InputFromHatButton final : public Common::Input::InputDevice {
public:
- explicit InputFromHatButton(PadIdentifier identifier_, int button_, u8 direction_, bool toggle_,
- bool inverted_, InputEngine* input_engine_)
- : identifier(identifier_), button(button_), direction(direction_), toggle(toggle_),
- inverted(inverted_), input_engine(input_engine_) {
+ explicit InputFromHatButton(PadIdentifier identifier_, int button_, u8 direction_, bool turbo_,
+ bool toggle_, bool inverted_, InputEngine* input_engine_)
+ : identifier(identifier_), button(button_), direction(direction_), turbo(turbo_),
+ toggle(toggle_), inverted(inverted_), input_engine(input_engine_) {
UpdateCallback engine_callback{[this]() { OnChange(); }};
const InputIdentifier input_identifier{
.identifier = identifier,
@@ -101,6 +103,7 @@ public:
.value = input_engine->GetHatButton(identifier, button, direction),
.inverted = inverted,
.toggle = toggle,
+ .turbo = turbo,
};
}
@@ -130,6 +133,7 @@ private:
const PadIdentifier identifier;
const int button;
const u8 direction;
+ const bool turbo;
const bool toggle;
const bool inverted;
int callback_key;
@@ -498,6 +502,58 @@ private:
InputEngine* input_engine;
};
+class InputFromColor final : public Common::Input::InputDevice {
+public:
+ explicit InputFromColor(PadIdentifier identifier_, InputEngine* input_engine_)
+ : identifier(identifier_), input_engine(input_engine_) {
+ UpdateCallback engine_callback{[this]() { OnChange(); }};
+ const InputIdentifier input_identifier{
+ .identifier = identifier,
+ .type = EngineInputType::Color,
+ .index = 0,
+ .callback = engine_callback,
+ };
+ last_color_value = {};
+ callback_key = input_engine->SetCallback(input_identifier);
+ }
+
+ ~InputFromColor() override {
+ input_engine->DeleteCallback(callback_key);
+ }
+
+ Common::Input::BodyColorStatus GetStatus() const {
+ return input_engine->GetColor(identifier);
+ }
+
+ void ForceUpdate() override {
+ const Common::Input::CallbackStatus status{
+ .type = Common::Input::InputType::Color,
+ .color_status = GetStatus(),
+ };
+
+ last_color_value = status.color_status;
+ TriggerOnChange(status);
+ }
+
+ void OnChange() {
+ const Common::Input::CallbackStatus status{
+ .type = Common::Input::InputType::Color,
+ .color_status = GetStatus(),
+ };
+
+ if (status.color_status.body != last_color_value.body) {
+ last_color_value = status.color_status;
+ TriggerOnChange(status);
+ }
+ }
+
+private:
+ const PadIdentifier identifier;
+ int callback_key;
+ Common::Input::BodyColorStatus last_color_value;
+ InputEngine* input_engine;
+};
+
class InputFromMotion final : public Common::Input::InputDevice {
public:
explicit InputFromMotion(PadIdentifier identifier_, int motion_sensor_, float gyro_threshold_,
@@ -611,7 +667,7 @@ public:
.raw_value = input_engine->GetAxis(identifier, axis_z),
.properties = properties_z,
};
- status.delta_timestamp = 5000;
+ status.delta_timestamp = 1000;
status.force_update = true;
return status;
}
@@ -736,8 +792,7 @@ public:
const Common::Input::CallbackStatus status{
.type = Common::Input::InputType::Nfc,
- .nfc_status = nfc_status.state,
- .raw_data = nfc_status.data,
+ .nfc_status = nfc_status,
};
TriggerOnChange(status);
@@ -754,11 +809,11 @@ public:
explicit OutputFromIdentifier(PadIdentifier identifier_, InputEngine* input_engine_)
: identifier(identifier_), input_engine(input_engine_) {}
- void SetLED(const Common::Input::LedStatus& led_status) override {
- input_engine->SetLeds(identifier, led_status);
+ Common::Input::DriverResult SetLED(const Common::Input::LedStatus& led_status) override {
+ return input_engine->SetLeds(identifier, led_status);
}
- Common::Input::VibrationError SetVibration(
+ Common::Input::DriverResult SetVibration(
const Common::Input::VibrationStatus& vibration_status) override {
return input_engine->SetVibration(identifier, vibration_status);
}
@@ -767,11 +822,12 @@ public:
return input_engine->IsVibrationEnabled(identifier);
}
- Common::Input::PollingError SetPollingMode(Common::Input::PollingMode polling_mode) override {
+ Common::Input::DriverResult SetPollingMode(Common::Input::PollingMode polling_mode) override {
return input_engine->SetPollingMode(identifier, polling_mode);
}
- Common::Input::CameraError SetCameraFormat(Common::Input::CameraFormat camera_format) override {
+ Common::Input::DriverResult SetCameraFormat(
+ Common::Input::CameraFormat camera_format) override {
return input_engine->SetCameraFormat(identifier, camera_format);
}
@@ -779,10 +835,31 @@ public:
return input_engine->SupportsNfc(identifier);
}
+ Common::Input::NfcState StartNfcPolling() {
+ return input_engine->StartNfcPolling(identifier);
+ }
+
+ Common::Input::NfcState StopNfcPolling() {
+ return input_engine->StopNfcPolling(identifier);
+ }
+
+ Common::Input::NfcState ReadAmiiboData(std::vector<u8>& out_data) {
+ return input_engine->ReadAmiiboData(identifier, out_data);
+ }
+
Common::Input::NfcState WriteNfcData(const std::vector<u8>& data) override {
return input_engine->WriteNfcData(identifier, data);
}
+ Common::Input::NfcState ReadMifareData(const Common::Input::MifareRequest& request,
+ Common::Input::MifareRequest& out_data) {
+ return input_engine->ReadMifareData(identifier, request, out_data);
+ }
+
+ Common::Input::NfcState WriteMifareData(const Common::Input::MifareRequest& request) {
+ return input_engine->WriteMifareData(identifier, request);
+ }
+
private:
const PadIdentifier identifier;
InputEngine* input_engine;
@@ -800,14 +877,15 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateButtonDevice(
const auto keyboard_key = params.Get("code", 0);
const auto toggle = params.Get("toggle", false) != 0;
const auto inverted = params.Get("inverted", false) != 0;
+ const auto turbo = params.Get("turbo", false) != 0;
input_engine->PreSetController(identifier);
input_engine->PreSetButton(identifier, button_id);
input_engine->PreSetButton(identifier, keyboard_key);
if (keyboard_key != 0) {
- return std::make_unique<InputFromButton>(identifier, keyboard_key, toggle, inverted,
+ return std::make_unique<InputFromButton>(identifier, keyboard_key, turbo, toggle, inverted,
input_engine.get());
}
- return std::make_unique<InputFromButton>(identifier, button_id, toggle, inverted,
+ return std::make_unique<InputFromButton>(identifier, button_id, turbo, toggle, inverted,
input_engine.get());
}
@@ -823,11 +901,12 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateHatButtonDevice(
const auto direction = input_engine->GetHatButtonId(params.Get("direction", ""));
const auto toggle = params.Get("toggle", false) != 0;
const auto inverted = params.Get("inverted", false) != 0;
+ const auto turbo = params.Get("turbo", false) != 0;
input_engine->PreSetController(identifier);
input_engine->PreSetHatButton(identifier, button_id);
- return std::make_unique<InputFromHatButton>(identifier, button_id, direction, toggle, inverted,
- input_engine.get());
+ return std::make_unique<InputFromHatButton>(identifier, button_id, direction, turbo, toggle,
+ inverted, input_engine.get());
}
std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateStickDevice(
@@ -880,6 +959,7 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateAnalogDevice(
.threshold = std::clamp(params.Get("threshold", 0.5f), 0.0f, 1.0f),
.offset = std::clamp(params.Get("offset", 0.0f), -1.0f, 1.0f),
.inverted = params.Get("invert", "+") == "-",
+ .inverted_button = params.Get("inverted", false) != 0,
.toggle = params.Get("toggle", false) != 0,
};
input_engine->PreSetController(identifier);
@@ -966,6 +1046,18 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::CreateBatteryDevice(
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(
Common::ParamPackage params) {
const PadIdentifier identifier = {
@@ -1053,6 +1145,9 @@ std::unique_ptr<Common::Input::InputDevice> InputFactory::Create(
if (params.Has("battery")) {
return CreateBatteryDevice(params);
}
+ if (params.Has("color")) {
+ return CreateColorDevice(params);
+ }
if (params.Has("camera")) {
return CreateCameraDevice(params);
}
diff --git a/src/input_common/input_poller.h b/src/input_common/input_poller.h
index d7db13ce4..e097e254c 100644
--- a/src/input_common/input_poller.h
+++ b/src/input_common/input_poller.h
@@ -191,6 +191,17 @@ private:
const Common::ParamPackage& params);
/**
+ * Creates a color device from the parameters given.
+ * @param params contains parameters for creating the device:
+ * - "guid": text string for identifying controllers
+ * - "port": port of the connected device
+ * - "pad": slot of the connected controller
+ * @returns a unique input device with the parameters specified
+ */
+ std::unique_ptr<Common::Input::InputDevice> CreateColorDevice(
+ const Common::ParamPackage& params);
+
+ /**
* Creates a motion device from the parameters given.
* @param params contains parameters for creating the device:
* - "axis_x": the controller horizontal axis id to bind with the input
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index e0b2131ed..c77fc04ee 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -23,6 +23,7 @@
#include "input_common/drivers/gc_adapter.h"
#endif
#ifdef HAVE_SDL2
+#include "input_common/drivers/joycon.h"
#include "input_common/drivers/sdl_driver.h"
#endif
@@ -81,6 +82,7 @@ struct InputSubsystem::Impl {
RegisterEngine("virtual_gamepad", virtual_gamepad);
#ifdef HAVE_SDL2
RegisterEngine("sdl", sdl);
+ RegisterEngine("joycon", joycon);
#endif
Common::Input::RegisterInputFactory("touch_from_button",
@@ -111,6 +113,7 @@ struct InputSubsystem::Impl {
UnregisterEngine(virtual_gamepad);
#ifdef HAVE_SDL2
UnregisterEngine(sdl);
+ UnregisterEngine(joycon);
#endif
Common::Input::UnregisterInputFactory("touch_from_button");
@@ -133,6 +136,8 @@ struct InputSubsystem::Impl {
auto udp_devices = udp_client->GetInputDevices();
devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
#ifdef HAVE_SDL2
+ auto joycon_devices = joycon->GetInputDevices();
+ devices.insert(devices.end(), joycon_devices.begin(), joycon_devices.end());
auto sdl_devices = sdl->GetInputDevices();
devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
#endif
@@ -164,6 +169,9 @@ struct InputSubsystem::Impl {
if (engine == sdl->GetEngineName()) {
return sdl;
}
+ if (engine == joycon->GetEngineName()) {
+ return joycon;
+ }
#endif
return nullptr;
}
@@ -247,6 +255,9 @@ struct InputSubsystem::Impl {
if (engine == sdl->GetEngineName()) {
return true;
}
+ if (engine == joycon->GetEngineName()) {
+ return true;
+ }
#endif
return false;
}
@@ -260,6 +271,7 @@ struct InputSubsystem::Impl {
udp_client->BeginConfiguration();
#ifdef HAVE_SDL2
sdl->BeginConfiguration();
+ joycon->BeginConfiguration();
#endif
}
@@ -272,6 +284,7 @@ struct InputSubsystem::Impl {
udp_client->EndConfiguration();
#ifdef HAVE_SDL2
sdl->EndConfiguration();
+ joycon->EndConfiguration();
#endif
}
@@ -304,6 +317,7 @@ struct InputSubsystem::Impl {
#ifdef HAVE_SDL2
std::shared_ptr<SDLDriver> sdl;
+ std::shared_ptr<Joycons> joycon;
#endif
};
diff --git a/src/input_common/main.h b/src/input_common/main.h
index 1207d786c..d64a6cb4c 100644
--- a/src/input_common/main.h
+++ b/src/input_common/main.h
@@ -132,7 +132,7 @@ public:
/// Retrieves the motion mappings for the given device.
[[nodiscard]] MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& device) const;
- /// Returns an enum contaning the name to be displayed from the input engine.
+ /// Returns an enum containing the name to be displayed from the input engine.
[[nodiscard]] Common::Input::ButtonNames GetButtonName(
const Common::ParamPackage& params) const;