diff --git a/src/common/input.h b/src/common/input.h
index d27b1d7728..1e5ba038d8 100644
--- a/src/common/input.h
+++ b/src/common/input.h
@@ -51,6 +51,8 @@ enum class PollingMode {
     NFC,
     // Enable infrared camera polling
     IR,
+    // Enable ring controller polling
+    Ring,
 };
 
 enum class CameraFormat {
@@ -67,6 +69,7 @@ enum class VibrationError {
     None,
     NotSupported,
     Disabled,
+    InvalidHandle,
     Unknown,
 };
 
@@ -74,6 +77,7 @@ enum class VibrationError {
 enum class PollingError {
     None,
     NotSupported,
+    InvalidHandle,
     Unknown,
 };
 
@@ -190,6 +194,8 @@ struct TouchStatus {
 struct BodyColorStatus {
     u32 body{};
     u32 buttons{};
+    u32 left_grip{};
+    u32 right_grip{};
 };
 
 // HD rumble data
@@ -228,17 +234,31 @@ enum class ButtonNames {
     Engine,
     // This will display the button by value instead of the button name
     Value,
+
+    // Joycon button names
     ButtonLeft,
     ButtonRight,
     ButtonDown,
     ButtonUp,
-    TriggerZ,
-    TriggerR,
-    TriggerL,
     ButtonA,
     ButtonB,
     ButtonX,
     ButtonY,
+    ButtonPlus,
+    ButtonMinus,
+    ButtonHome,
+    ButtonCapture,
+    ButtonStickL,
+    ButtonStickR,
+    TriggerL,
+    TriggerZL,
+    TriggerSL,
+    TriggerR,
+    TriggerZR,
+    TriggerSR,
+
+    // GC button names
+    TriggerZ,
     ButtonStart,
 
     // DS4 button names
diff --git a/src/input_common/CMakeLists.txt b/src/input_common/CMakeLists.txt
index cef2c4d52f..41885d0d26 100644
--- a/src/input_common/CMakeLists.txt
+++ b/src/input_common/CMakeLists.txt
@@ -51,8 +51,13 @@ 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/joycon_types.h
     )
     target_link_libraries(input_common PRIVATE SDL2::SDL2)
     target_compile_definitions(input_common PRIVATE HAVE_SDL2)
diff --git a/src/input_common/drivers/joycon.cpp b/src/input_common/drivers/joycon.cpp
new file mode 100644
index 0000000000..eab10d11c0
--- /dev/null
+++ b/src/input_common/drivers/joycon.cpp
@@ -0,0 +1,615 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <fmt/format.h>
+
+#include "common/param_package.h"
+#include "common/settings.h"
+#include "common/thread.h"
+#include "input_common/drivers/joycon.h"
+#include "input_common/helpers/joycon_driver.h"
+#include "input_common/helpers/joycon_protocol/joycon_types.h"
+
+namespace InputCommon {
+
+Joycons::Joycons(const std::string& input_engine_) : InputEngine(input_engine_) {
+    LOG_INFO(Input, "Joycon driver Initialization started");
+    const int init_res = SDL_hid_init();
+    if (init_res == 0) {
+        Setup();
+    } else {
+        LOG_ERROR(Input, "Hidapi could not be initialized. failed with error = {}", init_res);
+    }
+}
+
+Joycons::~Joycons() {
+    Reset();
+}
+
+void Joycons::Reset() {
+    scan_thread = {};
+    for (const auto& device : left_joycons) {
+        if (!device) {
+            continue;
+        }
+        device->Stop();
+    }
+    for (const auto& device : right_joycons) {
+        if (!device) {
+            continue;
+        }
+        device->Stop();
+    }
+    for (const auto& device : pro_joycons) {
+        if (!device) {
+            continue;
+        }
+        device->Stop();
+    }
+    SDL_hid_exit();
+}
+
+void Joycons::Setup() {
+    u32 port = 0;
+    for (auto& device : left_joycons) {
+        PreSetController(GetIdentifier(port, Joycon::ControllerType::Left));
+        device = std::make_shared<Joycon::JoyconDriver>(port++);
+    }
+    for (auto& device : right_joycons) {
+        PreSetController(GetIdentifier(port, Joycon::ControllerType::Right));
+        device = std::make_shared<Joycon::JoyconDriver>(port++);
+    }
+    for (auto& device : pro_joycons) {
+        PreSetController(GetIdentifier(port, Joycon::ControllerType::Pro));
+        device = std::make_shared<Joycon::JoyconDriver>(port++);
+    }
+
+    if (!scan_thread_running) {
+        scan_thread = std::jthread([this](std::stop_token stop_token) { ScanThread(stop_token); });
+    }
+}
+
+void Joycons::ScanThread(std::stop_token stop_token) {
+    constexpr u16 nintendo_vendor_id = 0x057e;
+    Common::SetCurrentThreadName("yuzu:input:JoyconScanThread");
+    scan_thread_running = true;
+    while (!stop_token.stop_requested()) {
+        SDL_hid_device_info* devs = SDL_hid_enumerate(nintendo_vendor_id, 0x0);
+        SDL_hid_device_info* cur_dev = devs;
+
+        while (cur_dev) {
+            if (IsDeviceNew(cur_dev)) {
+                LOG_DEBUG(Input, "Device Found,type : {:04X} {:04X}", cur_dev->vendor_id,
+                          cur_dev->product_id);
+                RegisterNewDevice(cur_dev);
+            }
+            cur_dev = cur_dev->next;
+        }
+
+        std::this_thread::sleep_for(std::chrono::seconds(5));
+    }
+    scan_thread_running = false;
+}
+
+bool Joycons::IsDeviceNew(SDL_hid_device_info* device_info) const {
+    Joycon::ControllerType type{};
+    Joycon::SerialNumber serial_number{};
+
+    const auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
+    if (result != Joycon::DriverResult::Success) {
+        return false;
+    }
+
+    const auto result2 = Joycon::JoyconDriver::GetSerialNumber(device_info, serial_number);
+    if (result2 != Joycon::DriverResult::Success) {
+        return false;
+    }
+
+    auto is_handle_identical = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
+        if (!device) {
+            return false;
+        }
+        if (!device->IsConnected()) {
+            return false;
+        }
+        if (device->GetHandleSerialNumber() != serial_number) {
+            return false;
+        }
+        return true;
+    };
+
+    // Check if device already exist
+    switch (type) {
+    case Joycon::ControllerType::Left:
+        for (const auto& device : left_joycons) {
+            if (is_handle_identical(device)) {
+                return false;
+            }
+        }
+        break;
+    case Joycon::ControllerType::Right:
+        for (const auto& device : right_joycons) {
+            if (is_handle_identical(device)) {
+                return false;
+            }
+        }
+        break;
+    case Joycon::ControllerType::Pro:
+    case Joycon::ControllerType::Grip:
+        for (const auto& device : pro_joycons) {
+            if (is_handle_identical(device)) {
+                return false;
+            }
+        }
+        break;
+    default:
+        return false;
+    }
+
+    return true;
+}
+
+void Joycons::RegisterNewDevice(SDL_hid_device_info* device_info) {
+    Joycon::ControllerType type{};
+    auto result = Joycon::JoyconDriver::GetDeviceType(device_info, type);
+    auto handle = GetNextFreeHandle(type);
+    if (handle == nullptr) {
+        LOG_WARNING(Input, "No free handles available");
+        return;
+    }
+    if (result == Joycon::DriverResult::Success) {
+        result = handle->RequestDeviceAccess(device_info);
+    }
+    if (result == Joycon::DriverResult::Success) {
+        LOG_WARNING(Input, "Initialize device");
+
+        std::function<void(Joycon::Battery)> on_battery_data;
+        std::function<void(Joycon::Color)> on_button_data;
+        std::function<void(int, f32)> on_stick_data;
+        std::function<void(int, std::array<u8, 6>)> on_motion_data;
+        std::function<void(s16)> on_ring_data;
+        std::function<void(const std::vector<u8>&)> on_amiibo_data;
+
+        const std::size_t port = handle->GetDevicePort();
+        handle->on_battery_data = {
+            [this, port, type](Joycon::Battery value) { OnBatteryUpdate(port, type, value); }};
+        handle->on_color_data = {
+            [this, port, type](Joycon::Color value) { OnColorUpdate(port, type, value); }};
+        handle->on_button_data = {
+            [this, port, type](int id, bool value) { OnButtonUpdate(port, type, id, value); }};
+        handle->on_stick_data = {
+            [this, port, type](int id, f32 value) { OnStickUpdate(port, type, id, value); }};
+        handle->on_motion_data = {[this, port, type](int id, Joycon::MotionData value) {
+            OnMotionUpdate(port, type, id, value);
+        }};
+        handle->on_ring_data = {[this](f32 ring_data) { OnRingConUpdate(ring_data); }};
+        handle->on_amiibo_data = {[this, port](const std::vector<u8>& amiibo_data) {
+            OnAmiiboUpdate(port, amiibo_data);
+        }};
+        handle->InitializeDevice();
+    }
+}
+
+std::shared_ptr<Joycon::JoyconDriver> Joycons::GetNextFreeHandle(
+    Joycon::ControllerType type) const {
+
+    if (type == Joycon::ControllerType::Left) {
+        for (const auto& device : left_joycons) {
+            if (!device->IsConnected()) {
+                return device;
+            }
+        }
+    }
+    if (type == Joycon::ControllerType::Right) {
+        for (const auto& device : right_joycons) {
+            if (!device->IsConnected()) {
+                return device;
+            }
+        }
+    }
+    if (type == Joycon::ControllerType::Pro || type == Joycon::ControllerType::Grip) {
+        for (const auto& device : pro_joycons) {
+            if (!device->IsConnected()) {
+                return device;
+            }
+        }
+    }
+    return nullptr;
+}
+
+bool Joycons::IsVibrationEnabled(const PadIdentifier& identifier) {
+    const auto handle = GetHandle(identifier);
+    if (handle == nullptr) {
+        return false;
+    }
+    return handle->IsVibrationEnabled();
+}
+
+Common::Input::VibrationError Joycons::SetVibration(
+    const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) {
+    const Joycon::VibrationValue native_vibration{
+        .low_amplitude = vibration.low_amplitude,
+        .low_frequency = vibration.low_frequency,
+        .high_amplitude = vibration.high_amplitude,
+        .high_frequency = vibration.high_amplitude,
+    };
+    auto handle = GetHandle(identifier);
+    if (handle == nullptr) {
+        return Common::Input::VibrationError::InvalidHandle;
+    }
+
+    handle->SetVibration(native_vibration);
+    return Common::Input::VibrationError::None;
+}
+
+void Joycons::SetLeds(const PadIdentifier& identifier, const Common::Input::LedStatus& led_status) {
+    auto handle = GetHandle(identifier);
+    if (handle == nullptr) {
+        return;
+    }
+    int led_config = led_status.led_1 ? 1 : 0;
+    led_config += led_status.led_2 ? 2 : 0;
+    led_config += led_status.led_3 ? 4 : 0;
+    led_config += led_status.led_4 ? 8 : 0;
+
+    const auto result = handle->SetLedConfig(static_cast<u8>(led_config));
+    if (result != Joycon::DriverResult::Success) {
+        LOG_ERROR(Input, "Failed to set led config");
+    }
+}
+
+Common::Input::CameraError Joycons::SetCameraFormat(const PadIdentifier& identifier_,
+                                                    Common::Input::CameraFormat camera_format) {
+    return Common::Input::CameraError::NotSupported;
+};
+
+Common::Input::NfcState Joycons::SupportsNfc(const PadIdentifier& identifier_) const {
+    return Common::Input::NfcState::Success;
+};
+
+Common::Input::NfcState Joycons::WriteNfcData(const PadIdentifier& identifier_,
+                                              const std::vector<u8>& data) {
+    return Common::Input::NfcState::NotSupported;
+};
+
+Common::Input::PollingError Joycons::SetPollingMode(const PadIdentifier& identifier,
+                                                    const Common::Input::PollingMode polling_mode) {
+    auto handle = GetHandle(identifier);
+    if (handle == nullptr) {
+        LOG_ERROR(Input, "Invalid handle {}", identifier.port);
+        return Common::Input::PollingError::InvalidHandle;
+    }
+
+    switch (polling_mode) {
+    case Common::Input::PollingMode::NFC:
+        handle->SetNfcMode();
+        break;
+    case Common::Input::PollingMode::Active:
+        handle->SetActiveMode();
+        break;
+    case Common::Input::PollingMode::Pasive:
+        handle->SetPasiveMode();
+        break;
+    case Common::Input::PollingMode::Ring:
+        handle->SetRingConMode();
+        break;
+    default:
+        return Common::Input::PollingError::NotSupported;
+    }
+
+    return Common::Input::PollingError::None;
+}
+
+void Joycons::OnBatteryUpdate(std::size_t port, Joycon::ControllerType type,
+                              Joycon::Battery value) {
+    const auto identifier = GetIdentifier(port, type);
+    if (value.charging != 0) {
+        SetBattery(identifier, Common::Input::BatteryLevel::Charging);
+        return;
+    }
+
+    Common::Input::BatteryLevel battery{value.status.Value()};
+    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) {}
+
+void Joycons::OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value) {
+    const auto identifier = GetIdentifier(port, type);
+    SetButton(identifier, id, value);
+}
+
+void Joycons::OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value) {
+    const auto identifier = GetIdentifier(port, type);
+    SetAxis(identifier, id, value);
+}
+
+void Joycons::OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
+                             const Joycon::MotionData& value) {
+    const auto identifier = GetIdentifier(port, type);
+    BasicMotion motion_data{
+        .gyro_x = value.gyro_x,
+        .gyro_y = value.gyro_y,
+        .gyro_z = value.gyro_z,
+        .accel_x = value.accel_x,
+        .accel_y = value.accel_y,
+        .accel_z = value.accel_z,
+        .delta_timestamp = 15000,
+    };
+    SetMotion(identifier, id, motion_data);
+}
+
+void Joycons::OnRingConUpdate(f32 ring_data) {
+    // To simplify ring detection it will always be mapped to an empty identifier for all
+    // controllers
+    constexpr PadIdentifier identifier = {
+        .guid = Common::UUID{},
+        .port = 0,
+        .pad = 0,
+    };
+    SetAxis(identifier, 100, ring_data);
+}
+
+void Joycons::OnAmiiboUpdate(std::size_t port, const std::vector<u8>& amiibo_data) {
+    const auto identifier = GetIdentifier(port, Joycon::ControllerType::Right);
+    SetNfc(identifier, {Common::Input::NfcState::NewAmiibo, amiibo_data});
+}
+
+std::shared_ptr<Joycon::JoyconDriver> Joycons::GetHandle(PadIdentifier identifier) const {
+    auto is_handle_active = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
+        if (!device) {
+            return false;
+        }
+        if (!device->IsConnected()) {
+            return false;
+        }
+        if (device->GetDevicePort() == identifier.port) {
+            return true;
+        }
+        return false;
+    };
+    const auto type = static_cast<Joycon::ControllerType>(identifier.pad);
+    if (type == Joycon::ControllerType::Left) {
+        for (const auto& device : left_joycons) {
+            if (is_handle_active(device)) {
+                return device;
+            }
+        }
+    }
+    if (type == Joycon::ControllerType::Right) {
+        for (const auto& device : right_joycons) {
+            if (is_handle_active(device)) {
+                return device;
+            }
+        }
+    }
+    if (type == Joycon::ControllerType::Pro || type == Joycon::ControllerType::Grip) {
+        for (const auto& device : pro_joycons) {
+            if (is_handle_active(device)) {
+                return device;
+            }
+        }
+    }
+    return nullptr;
+}
+
+PadIdentifier Joycons::GetIdentifier(std::size_t port, Joycon::ControllerType type) const {
+    return {
+        .guid = Common::UUID{Common::InvalidUUID},
+        .port = port,
+        .pad = static_cast<std::size_t>(type),
+    };
+}
+
+std::vector<Common::ParamPackage> Joycons::GetInputDevices() const {
+    std::vector<Common::ParamPackage> devices{};
+
+    auto add_entry = [&](std::shared_ptr<Joycon::JoyconDriver> device) {
+        if (!device) {
+            return;
+        }
+        if (!device->IsConnected()) {
+            return;
+        }
+        std::string name = fmt::format("{} {}", JoyconName(device->GetHandleDeviceType()),
+                                       device->GetDevicePort());
+        devices.emplace_back(Common::ParamPackage{
+            {"engine", GetEngineName()},
+            {"display", std::move(name)},
+            {"port", std::to_string(device->GetDevicePort())},
+            {"pad", std::to_string(static_cast<std::size_t>(device->GetHandleDeviceType()))},
+        });
+    };
+
+    for (const auto& controller : left_joycons) {
+        add_entry(controller);
+    }
+    for (const auto& controller : right_joycons) {
+        add_entry(controller);
+    }
+    for (const auto& controller : pro_joycons) {
+        add_entry(controller);
+    }
+
+    return devices;
+}
+
+ButtonMapping Joycons::GetButtonMappingForDevice(const Common::ParamPackage& params) {
+    static constexpr std::array<std::pair<Settings::NativeButton::Values, Joycon::PadButton>, 20>
+        switch_to_joycon_button = {
+            std::pair{Settings::NativeButton::A, Joycon::PadButton::A},
+            {Settings::NativeButton::B, Joycon::PadButton::B},
+            {Settings::NativeButton::X, Joycon::PadButton::X},
+            {Settings::NativeButton::Y, Joycon::PadButton::Y},
+            {Settings::NativeButton::DLeft, Joycon::PadButton::Left},
+            {Settings::NativeButton::DUp, Joycon::PadButton::Up},
+            {Settings::NativeButton::DRight, Joycon::PadButton::Right},
+            {Settings::NativeButton::DDown, Joycon::PadButton::Down},
+            {Settings::NativeButton::SL, Joycon::PadButton::LeftSL},
+            {Settings::NativeButton::SR, Joycon::PadButton::LeftSR},
+            {Settings::NativeButton::L, Joycon::PadButton::L},
+            {Settings::NativeButton::R, Joycon::PadButton::R},
+            {Settings::NativeButton::ZL, Joycon::PadButton::ZL},
+            {Settings::NativeButton::ZR, Joycon::PadButton::ZR},
+            {Settings::NativeButton::Plus, Joycon::PadButton::Plus},
+            {Settings::NativeButton::Minus, Joycon::PadButton::Minus},
+            {Settings::NativeButton::Home, Joycon::PadButton::Home},
+            {Settings::NativeButton::Screenshot, Joycon::PadButton::Capture},
+            {Settings::NativeButton::LStick, Joycon::PadButton::StickL},
+            {Settings::NativeButton::RStick, Joycon::PadButton::StickR},
+        };
+
+    if (!params.Has("port")) {
+        return {};
+    }
+
+    ButtonMapping mapping{};
+    for (const auto& [switch_button, joycon_button] : switch_to_joycon_button) {
+        Common::ParamPackage button_params{};
+        button_params.Set("engine", GetEngineName());
+        button_params.Set("port", params.Get("port", 0));
+        button_params.Set("button", static_cast<int>(joycon_button));
+        mapping.insert_or_assign(switch_button, std::move(button_params));
+    }
+
+    return mapping;
+}
+
+AnalogMapping Joycons::GetAnalogMappingForDevice(const Common::ParamPackage& params) {
+    if (!params.Has("port")) {
+        return {};
+    }
+
+    AnalogMapping mapping = {};
+    Common::ParamPackage left_analog_params;
+    left_analog_params.Set("engine", GetEngineName());
+    left_analog_params.Set("port", params.Get("port", 0));
+    left_analog_params.Set("axis_x", static_cast<int>(Joycon::PadAxes::LeftStickX));
+    left_analog_params.Set("axis_y", static_cast<int>(Joycon::PadAxes::LeftStickY));
+    mapping.insert_or_assign(Settings::NativeAnalog::LStick, std::move(left_analog_params));
+    Common::ParamPackage right_analog_params;
+    right_analog_params.Set("engine", GetEngineName());
+    right_analog_params.Set("port", params.Get("port", 0));
+    right_analog_params.Set("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 {};
+    }
+
+    MotionMapping mapping = {};
+    Common::ParamPackage left_motion_params;
+    left_motion_params.Set("engine", GetEngineName());
+    left_motion_params.Set("port", params.Get("port", 0));
+    left_motion_params.Set("motion", 0);
+    mapping.insert_or_assign(Settings::NativeMotion::MotionLeft, std::move(left_motion_params));
+    Common::ParamPackage right_Motion_params;
+    right_Motion_params.Set("engine", GetEngineName());
+    right_Motion_params.Set("port", params.Get("port", 0));
+    right_Motion_params.Set("motion", 1);
+    mapping.insert_or_assign(Settings::NativeMotion::MotionRight, std::move(right_Motion_params));
+    return mapping;
+}
+
+Common::Input::ButtonNames Joycons::GetUIButtonName(const Common::ParamPackage& params) const {
+    const auto button = static_cast<Joycon::PadButton>(params.Get("button", 0));
+    switch (button) {
+    case Joycon::PadButton::Left:
+        return Common::Input::ButtonNames::ButtonLeft;
+    case Joycon::PadButton::Right:
+        return Common::Input::ButtonNames::ButtonRight;
+    case Joycon::PadButton::Down:
+        return Common::Input::ButtonNames::ButtonDown;
+    case Joycon::PadButton::Up:
+        return Common::Input::ButtonNames::ButtonUp;
+    case Joycon::PadButton::LeftSL:
+    case Joycon::PadButton::RightSL:
+        return Common::Input::ButtonNames::TriggerSL;
+    case Joycon::PadButton::LeftSR:
+    case Joycon::PadButton::RightSR:
+        return Common::Input::ButtonNames::TriggerSR;
+    case Joycon::PadButton::L:
+        return Common::Input::ButtonNames::TriggerL;
+    case Joycon::PadButton::R:
+        return Common::Input::ButtonNames::TriggerR;
+    case Joycon::PadButton::ZL:
+        return Common::Input::ButtonNames::TriggerZL;
+    case Joycon::PadButton::ZR:
+        return Common::Input::ButtonNames::TriggerZR;
+    case Joycon::PadButton::A:
+        return Common::Input::ButtonNames::ButtonA;
+    case Joycon::PadButton::B:
+        return Common::Input::ButtonNames::ButtonB;
+    case Joycon::PadButton::X:
+        return Common::Input::ButtonNames::ButtonX;
+    case Joycon::PadButton::Y:
+        return Common::Input::ButtonNames::ButtonY;
+    case Joycon::PadButton::Plus:
+        return Common::Input::ButtonNames::ButtonPlus;
+    case Joycon::PadButton::Minus:
+        return Common::Input::ButtonNames::ButtonMinus;
+    case Joycon::PadButton::Home:
+        return Common::Input::ButtonNames::ButtonHome;
+    case Joycon::PadButton::Capture:
+        return Common::Input::ButtonNames::ButtonCapture;
+    case Joycon::PadButton::StickL:
+        return Common::Input::ButtonNames::ButtonStickL;
+    case Joycon::PadButton::StickR:
+        return Common::Input::ButtonNames::ButtonStickR;
+    default:
+        return Common::Input::ButtonNames::Undefined;
+    }
+}
+
+Common::Input::ButtonNames Joycons::GetUIName(const Common::ParamPackage& params) const {
+    if (params.Has("button")) {
+        return GetUIButtonName(params);
+    }
+    if (params.Has("axis")) {
+        return Common::Input::ButtonNames::Value;
+    }
+    if (params.Has("motion")) {
+        return Common::Input::ButtonNames::Engine;
+    }
+
+    return Common::Input::ButtonNames::Invalid;
+}
+
+std::string Joycons::JoyconName(Joycon::ControllerType type) const {
+    switch (type) {
+    case Joycon::ControllerType::Left:
+        return "Left Joycon";
+    case Joycon::ControllerType::Right:
+        return "Right Joycon";
+    case Joycon::ControllerType::Pro:
+        return "Pro Controller";
+    case Joycon::ControllerType::Grip:
+        return "Grip Controller";
+    default:
+        return "Unknow Joycon";
+    }
+}
+} // namespace InputCommon
diff --git a/src/input_common/drivers/joycon.h b/src/input_common/drivers/joycon.h
new file mode 100644
index 0000000000..56c1172701
--- /dev/null
+++ b/src/input_common/drivers/joycon.h
@@ -0,0 +1,107 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <span>
+#include <thread>
+#include <SDL_hidapi.h>
+
+#include "input_common/input_engine.h"
+
+namespace InputCommon::Joycon {
+using SerialNumber = std::array<u8, 15>;
+struct Battery;
+struct Color;
+struct MotionData;
+enum class ControllerType;
+enum class DriverResult;
+class JoyconDriver;
+} // namespace InputCommon::Joycon
+
+namespace InputCommon {
+
+class Joycons final : public InputCommon::InputEngine {
+public:
+    explicit Joycons(const std::string& input_engine_);
+
+    ~Joycons();
+
+    bool IsVibrationEnabled(const PadIdentifier& identifier) override;
+    Common::Input::VibrationError SetVibration(
+        const PadIdentifier& identifier, const Common::Input::VibrationStatus& vibration) override;
+
+    void SetLeds(const PadIdentifier& identifier,
+                 const Common::Input::LedStatus& led_status) override;
+
+    Common::Input::CameraError SetCameraFormat(const PadIdentifier& identifier_,
+                                               Common::Input::CameraFormat camera_format) override;
+
+    Common::Input::NfcState SupportsNfc(const PadIdentifier& identifier_) const override;
+    Common::Input::NfcState WriteNfcData(const PadIdentifier& identifier_,
+                                         const std::vector<u8>& data) override;
+
+    Common::Input::PollingError SetPollingMode(
+        const PadIdentifier& identifier, const Common::Input::PollingMode polling_mode) override;
+
+    /// Used for automapping features
+    std::vector<Common::ParamPackage> GetInputDevices() const override;
+    ButtonMapping GetButtonMappingForDevice(const Common::ParamPackage& params) override;
+    AnalogMapping GetAnalogMappingForDevice(const Common::ParamPackage& params) override;
+    MotionMapping GetMotionMappingForDevice(const Common::ParamPackage& params) override;
+    Common::Input::ButtonNames GetUIName(const Common::ParamPackage& params) const override;
+
+private:
+    static constexpr std::size_t MaxSupportedControllers = 8;
+
+    /// For shutting down, clear all data, join all threads, release usb devices
+    void Reset();
+
+    /// Registers controllers, clears all data and starts the scan thread
+    void Setup();
+
+    /// Actively searchs for new devices
+    void ScanThread(std::stop_token stop_token);
+
+    /// Returns true if device is valid and not registered
+    bool IsDeviceNew(SDL_hid_device_info* device_info) const;
+
+    /// Tries to connect to the new device
+    void RegisterNewDevice(SDL_hid_device_info* device_info);
+
+    /// Returns the next free handle
+    std::shared_ptr<Joycon::JoyconDriver> GetNextFreeHandle(Joycon::ControllerType type) const;
+
+    void OnBatteryUpdate(std::size_t port, Joycon::ControllerType type, Joycon::Battery value);
+    void OnColorUpdate(std::size_t port, Joycon::ControllerType type, const Joycon::Color& value);
+    void OnButtonUpdate(std::size_t port, Joycon::ControllerType type, int id, bool value);
+    void OnStickUpdate(std::size_t port, Joycon::ControllerType type, int id, f32 value);
+    void OnMotionUpdate(std::size_t port, Joycon::ControllerType type, int id,
+                        const Joycon::MotionData& value);
+    void OnRingConUpdate(f32 ring_data);
+    void OnAmiiboUpdate(std::size_t port, const std::vector<u8>& amiibo_data);
+
+    /// Returns a JoyconHandle corresponding to a PadIdentifier
+    std::shared_ptr<Joycon::JoyconDriver> GetHandle(PadIdentifier identifier) const;
+
+    /// Returns a PadIdentifier corresponding to the port number
+    PadIdentifier GetIdentifier(std::size_t port, Joycon::ControllerType type) const;
+
+    std::string JoyconName(std::size_t port) const;
+
+    Common::Input::ButtonNames GetUIButtonName(const Common::ParamPackage& params) const;
+
+    /// Returns the name of the device in text format
+    std::string JoyconName(Joycon::ControllerType type) const;
+
+    std::jthread scan_thread;
+    bool scan_thread_running{};
+
+    // Joycon types are split by type to ease supporting dualjoycon configurations
+    std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> left_joycons{};
+    std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> right_joycons{};
+    std::array<std::shared_ptr<Joycon::JoyconDriver>, MaxSupportedControllers> pro_joycons{};
+};
+
+} // namespace InputCommon
diff --git a/src/input_common/helpers/joycon_driver.cpp b/src/input_common/helpers/joycon_driver.cpp
new file mode 100644
index 0000000000..a0a2a180b5
--- /dev/null
+++ b/src/input_common/helpers/joycon_driver.cpp
@@ -0,0 +1,382 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "common/logging/log.h"
+#include "common/swap.h"
+#include "common/thread.h"
+#include "input_common/helpers/joycon_driver.h"
+
+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;
+
+    // Set HW default configuration
+    vibration_enabled = true;
+    motion_enabled = true;
+    hidbus_enabled = false;
+    nfc_enabled = false;
+    passive_enabled = false;
+    gyro_sensitivity = Joycon::GyroSensitivity::DPS2000;
+    gyro_performance = Joycon::GyroPerformance::HZ833;
+    accelerometer_sensitivity = Joycon::AccelerometerSensitivity::G8;
+    accelerometer_performance = Joycon::AccelerometerPerformance::HZ100;
+
+    // Initialize HW Protocols
+
+    // Get fixed joycon info
+    supported_features = GetSupportedFeatures();
+
+    // Get Calibration data
+
+    // Set led status
+
+    // Apply HW configuration
+    SetPollingMode();
+
+    // Start pooling for data
+    is_connected = true;
+    if (!input_thread_running) {
+        input_thread =
+            std::jthread([this](std::stop_token stop_token) { InputThread(stop_token); });
+    }
+
+    disable_input_thread = false;
+    return DriverResult::Success;
+}
+
+void JoyconDriver::InputThread(std::stop_token stop_token) {
+    LOG_INFO(Input, "JC Adapter input thread started");
+    Common::SetCurrentThreadName("JoyconInput");
+    input_thread_running = true;
+
+    // Max update rate is 5ms, ensure we are always able to read a bit faster
+    constexpr int ThreadDelay = 2;
+    std::vector<u8> buffer(MaxBufferSize);
+
+    while (!stop_token.stop_requested()) {
+        int status = 0;
+
+        if (!IsInputThreadValid()) {
+            input_thread.request_stop();
+            continue;
+        }
+
+        // By disabling the input thread we can ensure custom commands will succeed as no package is
+        // skipped
+        if (!disable_input_thread) {
+            status = SDL_hid_read_timeout(hidapi_handle->handle, buffer.data(), buffer.size(),
+                                          ThreadDelay);
+        } else {
+            std::this_thread::sleep_for(std::chrono::milliseconds(ThreadDelay));
+        }
+
+        if (IsPayloadCorrect(status, buffer)) {
+            OnNewData(buffer);
+        }
+
+        std::this_thread::yield();
+    }
+
+    is_connected = false;
+    input_thread_running = false;
+    LOG_INFO(Input, "JC Adapter input thread stopped");
+}
+
+void JoyconDriver::OnNewData(std::span<u8> buffer) {
+    const auto report_mode = static_cast<InputReport>(buffer[0]);
+
+    switch (report_mode) {
+    case InputReport::STANDARD_FULL_60HZ:
+        ReadActiveMode(buffer);
+        break;
+    case InputReport::NFC_IR_MODE_60HZ:
+        ReadNfcIRMode(buffer);
+        break;
+    case InputReport::SIMPLE_HID_MODE:
+        ReadPassiveMode(buffer);
+        break;
+    default:
+        LOG_ERROR(Input, "Report mode not Implemented {}", report_mode);
+        break;
+    }
+}
+
+void JoyconDriver::SetPollingMode() {
+    disable_input_thread = true;
+    disable_input_thread = false;
+}
+
+JoyconDriver::SupportedFeatures JoyconDriver::GetSupportedFeatures() {
+    SupportedFeatures features{
+        .passive = true,
+        .motion = true,
+        .vibration = true,
+    };
+
+    if (device_type == ControllerType::Right) {
+        features.nfc = true;
+        features.irs = true;
+        features.hidbus = true;
+    }
+
+    if (device_type == ControllerType::Pro) {
+        features.nfc = true;
+    }
+    return features;
+}
+
+void JoyconDriver::ReadActiveMode(std::span<u8> buffer) {
+    InputReportActive data{};
+    memcpy(&data, buffer.data(), sizeof(InputReportActive));
+
+    // Packages can be a litte bit inconsistent. Average the delta time to provide a smoother motion
+    // experience
+    const auto now = std::chrono::steady_clock::now();
+    const auto new_delta_time =
+        std::chrono::duration_cast<std::chrono::microseconds>(now - last_update).count();
+    delta_time = static_cast<u64>((delta_time * 0.8f) + (new_delta_time * 0.2));
+    last_update = now;
+
+    switch (device_type) {
+    case Joycon::ControllerType::Left:
+        break;
+    case Joycon::ControllerType::Right:
+        break;
+    case Joycon::ControllerType::Pro:
+        break;
+    case Joycon::ControllerType::Grip:
+    case Joycon::ControllerType::Dual:
+    case Joycon::ControllerType::None:
+        break;
+    }
+
+    on_battery_data(data.battery_status);
+    on_color_data(color);
+}
+
+void JoyconDriver::ReadPassiveMode(std::span<u8> buffer) {
+    InputReportPassive data{};
+    memcpy(&data, buffer.data(), sizeof(InputReportPassive));
+
+    switch (device_type) {
+    case Joycon::ControllerType::Left:
+        break;
+    case Joycon::ControllerType::Right:
+        break;
+    case Joycon::ControllerType::Pro:
+        break;
+    case Joycon::ControllerType::Grip:
+    case Joycon::ControllerType::Dual:
+    case Joycon::ControllerType::None:
+        break;
+    }
+}
+
+void JoyconDriver::ReadNfcIRMode(std::span<u8> buffer) {
+    // This mode is compatible with the active mode
+    ReadActiveMode(buffer);
+
+    if (!nfc_enabled) {
+        return;
+    }
+}
+
+bool JoyconDriver::IsInputThreadValid() const {
+    if (!is_connected) {
+        return false;
+    }
+    if (hidapi_handle->handle == nullptr) {
+        return false;
+    }
+    // Controller is not responding. Terminate connection
+    if (error_counter > MaxErrorCount) {
+        return false;
+    }
+    return true;
+}
+
+bool JoyconDriver::IsPayloadCorrect(int status, std::span<const u8> buffer) {
+    if (status <= -1) {
+        error_counter++;
+        return false;
+    }
+    // There's no new data
+    if (status == 0) {
+        return false;
+    }
+    // No reply ever starts with zero
+    if (buffer[0] == 0x00) {
+        error_counter++;
+        return false;
+    }
+    error_counter = 0;
+    return true;
+}
+
+DriverResult JoyconDriver::SetVibration(const VibrationValue& vibration) {
+    std::scoped_lock lock{mutex};
+    return DriverResult::NotSupported;
+}
+
+DriverResult JoyconDriver::SetLedConfig(u8 led_pattern) {
+    std::scoped_lock lock{mutex};
+    return DriverResult::NotSupported;
+}
+
+DriverResult JoyconDriver::SetPasiveMode() {
+    motion_enabled = false;
+    hidbus_enabled = false;
+    nfc_enabled = false;
+    passive_enabled = true;
+    SetPollingMode();
+    return DriverResult::Success;
+}
+
+DriverResult JoyconDriver::SetActiveMode() {
+    motion_enabled = false;
+    hidbus_enabled = false;
+    nfc_enabled = false;
+    passive_enabled = false;
+    SetPollingMode();
+    return DriverResult::Success;
+}
+
+DriverResult JoyconDriver::SetNfcMode() {
+    motion_enabled = false;
+    hidbus_enabled = false;
+    nfc_enabled = true;
+    passive_enabled = false;
+    SetPollingMode();
+    return DriverResult::Success;
+}
+
+DriverResult JoyconDriver::SetRingConMode() {
+    motion_enabled = true;
+    hidbus_enabled = true;
+    nfc_enabled = false;
+    passive_enabled = false;
+    SetPollingMode();
+    return DriverResult::Success;
+}
+
+bool JoyconDriver::IsConnected() const {
+    std::scoped_lock lock{mutex};
+    return is_connected;
+}
+
+bool JoyconDriver::IsVibrationEnabled() const {
+    std::scoped_lock lock{mutex};
+    return vibration_enabled;
+}
+
+FirmwareVersion JoyconDriver::GetDeviceVersion() const {
+    std::scoped_lock lock{mutex};
+    return version;
+}
+
+Color JoyconDriver::GetDeviceColor() const {
+    std::scoped_lock lock{mutex};
+    return color;
+}
+
+std::size_t JoyconDriver::GetDevicePort() const {
+    std::scoped_lock lock{mutex};
+    return port;
+}
+
+ControllerType JoyconDriver::GetDeviceType() const {
+    std::scoped_lock lock{mutex};
+    return handle_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;
+}
+
+Joycon::DriverResult JoyconDriver::GetDeviceType(SDL_hid_device_info* device_info,
+                                                 ControllerType& controller_type) {
+    std::array<std::pair<u32, Joycon::ControllerType>, 4> supported_devices{
+        std::pair<u32, Joycon::ControllerType>{0x2006, Joycon::ControllerType::Left},
+        {0x2007, Joycon::ControllerType::Right},
+        {0x2009, Joycon::ControllerType::Pro},
+        {0x200E, Joycon::ControllerType::Grip},
+    };
+    constexpr u16 nintendo_vendor_id = 0x057e;
+
+    controller_type = Joycon::ControllerType::None;
+    if (device_info->vendor_id != nintendo_vendor_id) {
+        return Joycon::DriverResult::UnsupportedControllerType;
+    }
+
+    for (const auto& [product_id, type] : supported_devices) {
+        if (device_info->product_id == static_cast<u16>(product_id)) {
+            controller_type = type;
+            return Joycon::DriverResult::Success;
+        }
+    }
+    return Joycon::DriverResult::UnsupportedControllerType;
+}
+
+Joycon::DriverResult JoyconDriver::GetSerialNumber(SDL_hid_device_info* device_info,
+                                                   Joycon::SerialNumber& serial_number) {
+    if (device_info->serial_number == nullptr) {
+        return Joycon::DriverResult::Unknown;
+    }
+    std::memcpy(&serial_number, device_info->serial_number, 15);
+    return Joycon::DriverResult::Success;
+}
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/helpers/joycon_driver.h b/src/input_common/helpers/joycon_driver.h
new file mode 100644
index 0000000000..be3053a7b4
--- /dev/null
+++ b/src/input_common/helpers/joycon_driver.h
@@ -0,0 +1,146 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <functional>
+#include <mutex>
+#include <span>
+#include <thread>
+
+#include "input_common/helpers/joycon_protocol/joycon_types.h"
+
+namespace InputCommon::Joycon {
+
+class JoyconDriver final {
+public:
+    explicit JoyconDriver(std::size_t port_);
+
+    ~JoyconDriver();
+
+    DriverResult RequestDeviceAccess(SDL_hid_device_info* device_info);
+    DriverResult InitializeDevice();
+    void Stop();
+
+    bool IsConnected() const;
+    bool IsVibrationEnabled() const;
+
+    FirmwareVersion GetDeviceVersion() const;
+    Color GetDeviceColor() const;
+    std::size_t GetDevicePort() const;
+    ControllerType GetDeviceType() const;
+    ControllerType GetHandleDeviceType() const;
+    SerialNumber GetSerialNumber() const;
+    SerialNumber GetHandleSerialNumber() const;
+
+    DriverResult SetVibration(const VibrationValue& vibration);
+    DriverResult SetLedConfig(u8 led_pattern);
+    DriverResult SetPasiveMode();
+    DriverResult SetActiveMode();
+    DriverResult SetNfcMode();
+    DriverResult SetRingConMode();
+
+    // Returns device type from hidapi handle
+    static Joycon::DriverResult GetDeviceType(SDL_hid_device_info* device_info,
+                                              Joycon::ControllerType& controller_type);
+
+    // Returns serial number from hidapi handle
+    static Joycon::DriverResult GetSerialNumber(SDL_hid_device_info* device_info,
+                                                Joycon::SerialNumber& serial_number);
+
+    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, MotionData)> on_motion_data;
+    std::function<void(f32)> on_ring_data;
+    std::function<void(const std::vector<u8>&)> on_amiibo_data;
+
+private:
+    struct SupportedFeatures {
+        bool passive{};
+        bool hidbus{};
+        bool irs{};
+        bool motion{};
+        bool nfc{};
+        bool vibration{};
+    };
+
+    /// Main thread, actively request new data from the handle
+    void InputThread(std::stop_token stop_token);
+
+    /// Called everytime a valid package arrives
+    void OnNewData(std::span<u8> buffer);
+
+    /// Updates device configuration to enable or disable features
+    void SetPollingMode();
+
+    /// Returns true if input thread is valid and doesn't need to be stopped
+    bool IsInputThreadValid() const;
+
+    /// Returns true if the data should be interpreted. Otherwise the error counter is incremented
+    bool IsPayloadCorrect(int status, std::span<const u8> buffer);
+
+    /// Returns a list of supported features that can be enabled on this device
+    SupportedFeatures GetSupportedFeatures();
+
+    /// Handles data from passive packages
+    void ReadPassiveMode(std::span<u8> buffer);
+
+    /// Handles data from active packages
+    void ReadActiveMode(std::span<u8> buffer);
+
+    /// Handles data from nfc or ir packages
+    void ReadNfcIRMode(std::span<u8> buffer);
+
+    // Protocol Features
+
+    // Connection status
+    bool is_connected{};
+    u64 delta_time;
+    std::size_t error_counter{};
+    std::shared_ptr<JoyconHandle> hidapi_handle = nullptr;
+    std::chrono::time_point<std::chrono::steady_clock> last_update;
+
+    // External device status
+    bool starlink_connected{};
+    bool ring_connected{};
+    bool amiibo_detected{};
+
+    // Harware configuration
+    u8 leds{};
+    ReportMode mode{};
+    bool passive_enabled{};   // Low power mode, Ideal for multiple controllers at the same time
+    bool hidbus_enabled{};    // External device support
+    bool irs_enabled{};       // Infrared camera input
+    bool motion_enabled{};    // Enables motion input
+    bool nfc_enabled{};       // Enables Amiibo detection
+    bool vibration_enabled{}; // Allows vibrations
+
+    // Calibration data
+    GyroSensitivity gyro_sensitivity{};
+    GyroPerformance gyro_performance{};
+    AccelerometerSensitivity accelerometer_sensitivity{};
+    AccelerometerPerformance accelerometer_performance{};
+    JoyStickCalibration left_stick_calibration{};
+    JoyStickCalibration right_stick_calibration{};
+    MotionCalibration motion_calibration{};
+
+    // 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/joycon_types.h b/src/input_common/helpers/joycon_protocol/joycon_types.h
new file mode 100644
index 0000000000..de512fe639
--- /dev/null
+++ b/src/input_common/helpers/joycon_protocol/joycon_types.h
@@ -0,0 +1,494 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+// Based on dkms-hid-nintendo implementation, CTCaer joycon toolkit and dekuNukem reverse
+// engineering https://github.com/nicman23/dkms-hid-nintendo/blob/master/src/hid-nintendo.c
+// https://github.com/CTCaer/jc_toolkit
+// https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering
+
+#pragma once
+
+#include <array>
+#include <functional>
+#include <SDL_hidapi.h>
+
+#include "common/bit_field.h"
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+
+namespace InputCommon::Joycon {
+constexpr u32 MaxErrorCount = 50;
+constexpr u32 MaxBufferSize = 60;
+constexpr u32 MaxResponseSize = 49;
+constexpr u32 MaxSubCommandResponseSize = 64;
+constexpr std::array<u8, 8> DefaultVibrationBuffer{0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40};
+
+using MacAddress = std::array<u8, 6>;
+using SerialNumber = std::array<u8, 15>;
+
+enum class ControllerType {
+    None,
+    Left,
+    Right,
+    Pro,
+    Grip,
+    Dual,
+};
+
+enum class PadAxes {
+    LeftStickX,
+    LeftStickY,
+    RightStickX,
+    RightStickY,
+    Undefined,
+};
+
+enum class PadMotion {
+    LeftMotion,
+    RightMotion,
+    Undefined,
+};
+
+enum class PadButton : u32 {
+    Down = 0x000001,
+    Up = 0x000002,
+    Right = 0x000004,
+    Left = 0x000008,
+    LeftSR = 0x000010,
+    LeftSL = 0x000020,
+    L = 0x000040,
+    ZL = 0x000080,
+    Y = 0x000100,
+    X = 0x000200,
+    B = 0x000400,
+    A = 0x000800,
+    RightSR = 0x001000,
+    RightSL = 0x002000,
+    R = 0x004000,
+    ZR = 0x008000,
+    Minus = 0x010000,
+    Plus = 0x020000,
+    StickR = 0x040000,
+    StickL = 0x080000,
+    Home = 0x100000,
+    Capture = 0x200000,
+};
+
+enum class PasivePadButton : u32 {
+    Down_A = 0x0001,
+    Right_X = 0x0002,
+    Left_B = 0x0004,
+    Up_Y = 0x0008,
+    SL = 0x0010,
+    SR = 0x0020,
+    Minus = 0x0100,
+    Plus = 0x0200,
+    StickL = 0x0400,
+    StickR = 0x0800,
+    Home = 0x1000,
+    Capture = 0x2000,
+    L_R = 0x4000,
+    ZL_ZR = 0x8000,
+};
+
+enum class OutputReport : u8 {
+    RUMBLE_AND_SUBCMD = 0x01,
+    FW_UPDATE_PKT = 0x03,
+    RUMBLE_ONLY = 0x10,
+    MCU_DATA = 0x11,
+    USB_CMD = 0x80,
+};
+
+enum class InputReport : u8 {
+    SUBCMD_REPLY = 0x21,
+    STANDARD_FULL_60HZ = 0x30,
+    NFC_IR_MODE_60HZ = 0x31,
+    SIMPLE_HID_MODE = 0x3F,
+    INPUT_USB_RESPONSE = 0x81,
+};
+
+enum class FeatureReport : u8 {
+    Last_SUBCMD = 0x02,
+    OTA_GW_UPGRADE = 0x70,
+    SETUP_MEM_READ = 0x71,
+    MEM_READ = 0x72,
+    ERASE_MEM_SECTOR = 0x73,
+    MEM_WRITE = 0x74,
+    LAUNCH = 0x75,
+};
+
+enum class SubCommand : u8 {
+    STATE = 0x00,
+    MANUAL_BT_PAIRING = 0x01,
+    REQ_DEV_INFO = 0x02,
+    SET_REPORT_MODE = 0x03,
+    TRIGGERS_ELAPSED = 0x04,
+    GET_PAGE_LIST_STATE = 0x05,
+    SET_HCI_STATE = 0x06,
+    RESET_PAIRING_INFO = 0x07,
+    LOW_POWER_MODE = 0x08,
+    SPI_FLASH_READ = 0x10,
+    SPI_FLASH_WRITE = 0x11,
+    RESET_MCU = 0x20,
+    SET_MCU_CONFIG = 0x21,
+    SET_MCU_STATE = 0x22,
+    SET_PLAYER_LIGHTS = 0x30,
+    GET_PLAYER_LIGHTS = 0x31,
+    SET_HOME_LIGHT = 0x38,
+    ENABLE_IMU = 0x40,
+    SET_IMU_SENSITIVITY = 0x41,
+    WRITE_IMU_REG = 0x42,
+    READ_IMU_REG = 0x43,
+    ENABLE_VIBRATION = 0x48,
+    GET_REGULATED_VOLTAGE = 0x50,
+    SET_EXTERNAL_CONFIG = 0x58,
+    UNKNOWN_RINGCON = 0x59,
+    UNKNOWN_RINGCON2 = 0x5A,
+    UNKNOWN_RINGCON3 = 0x5C,
+};
+
+enum class UsbSubCommand : u8 {
+    CONN_STATUS = 0x01,
+    HADSHAKE = 0x02,
+    BAUDRATE_3M = 0x03,
+    NO_TIMEOUT = 0x04,
+    EN_TIMEOUT = 0x05,
+    RESET = 0x06,
+    PRE_HANDSHAKE = 0x91,
+    SEND_UART = 0x92,
+};
+
+enum class CalMagic : u8 {
+    USR_MAGIC_0 = 0xB2,
+    USR_MAGIC_1 = 0xA1,
+    USRR_MAGI_SIZE = 2,
+};
+
+enum class CalAddr {
+    SERIAL_NUMBER = 0X6000,
+    DEVICE_TYPE = 0X6012,
+    COLOR_EXIST = 0X601B,
+    FACT_LEFT_DATA = 0X603d,
+    FACT_RIGHT_DATA = 0X6046,
+    COLOR_DATA = 0X6050,
+    FACT_IMU_DATA = 0X6020,
+    USER_LEFT_MAGIC = 0X8010,
+    USER_LEFT_DATA = 0X8012,
+    USER_RIGHT_MAGIC = 0X801B,
+    USER_RIGHT_DATA = 0X801D,
+    USER_IMU_MAGIC = 0X8026,
+    USER_IMU_DATA = 0X8028,
+};
+
+enum class ReportMode : u8 {
+    ACTIVE_POLLING_NFC_IR_CAMERA_DATA = 0x00,
+    ACTIVE_POLLING_NFC_IR_CAMERA_CONFIGURATION = 0x01,
+    ACTIVE_POLLING_NFC_IR_CAMERA_DATA_CONFIGURATION = 0x02,
+    ACTIVE_POLLING_IR_CAMERA_DATA = 0x03,
+    MCU_UPDATE_STATE = 0x23,
+    STANDARD_FULL_60HZ = 0x30,
+    NFC_IR_MODE_60HZ = 0x31,
+    SIMPLE_HID_MODE = 0x3F,
+};
+
+enum class GyroSensitivity : u8 {
+    DPS250,
+    DPS500,
+    DPS1000,
+    DPS2000, // Default
+};
+
+enum class AccelerometerSensitivity : u8 {
+    G8, // Default
+    G4,
+    G2,
+    G16,
+};
+
+enum class GyroPerformance : u8 {
+    HZ833,
+    HZ208, // Default
+};
+
+enum class AccelerometerPerformance : u8 {
+    HZ200,
+    HZ100, // Default
+};
+
+enum class MCUCommand : u8 {
+    ConfigureMCU = 0x21,
+    ConfigureIR = 0x23,
+};
+
+enum class MCUSubCommand : u8 {
+    SetMCUMode = 0x0,
+    SetDeviceMode = 0x1,
+    ReadDeviceMode = 0x02,
+    WriteDeviceRegisters = 0x4,
+};
+
+enum class MCUMode : u8 {
+    Suspend = 0,
+    Standby = 1,
+    Ringcon = 3,
+    NFC = 4,
+    IR = 5,
+    MaybeFWUpdate = 6,
+};
+
+enum class MCURequest : u8 {
+    GetMCUStatus = 1,
+    GetNFCData = 2,
+    GetIRData = 3,
+};
+
+enum class MCUReport : u8 {
+    Empty = 0x00,
+    StateReport = 0x01,
+    IRData = 0x03,
+    BusyInitializing = 0x0b,
+    IRStatus = 0x13,
+    IRRegisters = 0x1b,
+    NFCState = 0x2a,
+    NFCReadData = 0x3a,
+    EmptyAwaitingCmd = 0xff,
+};
+
+enum class MCUPacketFlag : u8 {
+    MorePacketsRemaining = 0x00,
+    LastCommandPacket = 0x08,
+};
+
+enum class NFCReadCommand : u8 {
+    CancelAll = 0x00,
+    StartPolling = 0x01,
+    StopPolling = 0x02,
+    StartWaitingRecieve = 0x04,
+    Ntag = 0x06,
+    Mifare = 0x0F,
+};
+
+enum class NFCTagType : u8 {
+    AllTags = 0x00,
+    Ntag215 = 0x01,
+};
+
+enum class DriverResult {
+    Success,
+    WrongReply,
+    Timeout,
+    UnsupportedControllerType,
+    HandleInUse,
+    ErrorReadingData,
+    ErrorWritingData,
+    NoDeviceDetected,
+    InvalidHandle,
+    NotSupported,
+    Unknown,
+};
+
+struct MotionSensorCalibration {
+    s16 offset;
+    s16 scale;
+};
+
+struct MotionCalibration {
+    std::array<MotionSensorCalibration, 3> accelerometer;
+    std::array<MotionSensorCalibration, 3> gyro;
+};
+
+// Basic motion data containing data from the sensors and a timestamp in microseconds
+struct MotionData {
+    float gyro_x{};
+    float gyro_y{};
+    float gyro_z{};
+    float accel_x{};
+    float accel_y{};
+    float accel_z{};
+    u64 delta_timestamp{};
+};
+
+struct JoyStickAxisCalibration {
+    u16 max{1};
+    u16 min{1};
+    u16 center{0};
+};
+
+struct JoyStickCalibration {
+    JoyStickAxisCalibration x;
+    JoyStickAxisCalibration y;
+};
+
+struct RingCalibration {
+    s16 default_value;
+    s16 max_value;
+    s16 min_value;
+};
+
+struct Color {
+    u32 body;
+    u32 buttons;
+    u32 left_grip;
+    u32 right_grip;
+};
+
+struct Battery {
+    union {
+        u8 raw{};
+
+        BitField<0, 4, u8> unknown;
+        BitField<4, 1, u8> charging;
+        BitField<5, 3, u8> status;
+    };
+};
+
+struct VibrationValue {
+    f32 low_amplitude;
+    f32 low_frequency;
+    f32 high_amplitude;
+    f32 high_frequency;
+};
+
+struct JoyconHandle {
+    SDL_hid_device* handle = nullptr;
+    u8 packet_counter{};
+};
+
+struct MCUConfig {
+    MCUCommand command;
+    MCUSubCommand sub_command;
+    MCUMode mode;
+    INSERT_PADDING_BYTES(0x22);
+    u8 crc;
+};
+static_assert(sizeof(MCUConfig) == 0x26, "MCUConfig is an invalid size");
+
+#pragma pack(push, 1)
+struct InputReportPassive {
+    InputReport report_mode;
+    u16 button_input;
+    u8 stick_state;
+    std::array<u8, 10> unknown_data;
+};
+static_assert(sizeof(InputReportPassive) == 0xE, "InputReportPassive is an invalid size");
+
+struct InputReportActive {
+    InputReport report_mode;
+    u8 packet_id;
+    Battery battery_status;
+    std::array<u8, 3> button_input;
+    std::array<u8, 3> left_stick_state;
+    std::array<u8, 3> right_stick_state;
+    u8 vibration_code;
+    std::array<s16, 6 * 2> motion_input;
+    INSERT_PADDING_BYTES(0x2);
+    s16 ring_input;
+};
+static_assert(sizeof(InputReportActive) == 0x29, "InputReportActive is an invalid size");
+
+struct InputReportNfcIr {
+    InputReport report_mode;
+    u8 packet_id;
+    Battery battery_status;
+    std::array<u8, 3> button_input;
+    std::array<u8, 3> left_stick_state;
+    std::array<u8, 3> right_stick_state;
+    u8 vibration_code;
+    std::array<s16, 6 * 2> motion_input;
+    INSERT_PADDING_BYTES(0x4);
+};
+static_assert(sizeof(InputReportNfcIr) == 0x29, "InputReportNfcIr is an invalid size");
+#pragma pack(pop)
+
+struct IMUCalibration {
+    std::array<s16, 3> accelerometer_offset;
+    std::array<s16, 3> accelerometer_scale;
+    std::array<s16, 3> gyroscope_offset;
+    std::array<s16, 3> gyroscope_scale;
+};
+static_assert(sizeof(IMUCalibration) == 0x18, "IMUCalibration is an invalid size");
+
+struct NFCReadBlock {
+    u8 start;
+    u8 end;
+};
+static_assert(sizeof(NFCReadBlock) == 0x2, "NFCReadBlock is an invalid size");
+
+struct NFCReadBlockCommand {
+    u8 block_count{};
+    std::array<NFCReadBlock, 4> blocks{};
+};
+static_assert(sizeof(NFCReadBlockCommand) == 0x9, "NFCReadBlockCommand is an invalid size");
+
+struct NFCReadCommandData {
+    u8 unknown;
+    u8 uuid_length;
+    u8 unknown_2;
+    std::array<u8, 6> uid;
+    NFCTagType tag_type;
+    NFCReadBlockCommand read_block;
+};
+static_assert(sizeof(NFCReadCommandData) == 0x13, "NFCReadCommandData is an invalid size");
+
+struct NFCPollingCommandData {
+    u8 enable_mifare;
+    u8 unknown_1;
+    u8 unknown_2;
+    u8 unknown_3;
+    u8 unknown_4;
+};
+static_assert(sizeof(NFCPollingCommandData) == 0x05, "NFCPollingCommandData is an invalid size");
+
+struct NFCRequestState {
+    MCUSubCommand sub_command;
+    NFCReadCommand command_argument;
+    u8 packet_id;
+    INSERT_PADDING_BYTES(0x1);
+    MCUPacketFlag packet_flag;
+    u8 data_length;
+    union {
+        std::array<u8, 0x1F> raw_data;
+        NFCReadCommandData nfc_read;
+        NFCPollingCommandData nfc_polling;
+    };
+    u8 crc;
+};
+static_assert(sizeof(NFCRequestState) == 0x26, "NFCRequestState is an invalid size");
+
+struct FirmwareVersion {
+    u8 major;
+    u8 minor;
+};
+static_assert(sizeof(FirmwareVersion) == 0x2, "FirmwareVersion is an invalid size");
+
+struct DeviceInfo {
+    FirmwareVersion firmware;
+    MacAddress mac_address;
+};
+static_assert(sizeof(DeviceInfo) == 0x8, "DeviceInfo is an invalid size");
+
+struct MotionStatus {
+    bool is_enabled;
+    u64 delta_time;
+    GyroSensitivity gyro_sensitivity;
+    AccelerometerSensitivity accelerometer_sensitivity;
+};
+
+struct RingStatus {
+    bool is_enabled;
+    s16 default_value;
+    s16 max_value;
+    s16 min_value;
+};
+
+struct JoyconCallbacks {
+    std::function<void(Battery)> on_battery_data;
+    std::function<void(Color)> on_color_data;
+    std::function<void(int, bool)> on_button_data;
+    std::function<void(int, f32)> on_stick_data;
+    std::function<void(int, const MotionData&)> on_motion_data;
+    std::function<void(f32)> on_ring_data;
+    std::function<void(const std::vector<u8>&)> on_amiibo_data;
+};
+
+} // namespace InputCommon::Joycon
diff --git a/src/input_common/main.cpp b/src/input_common/main.cpp
index e0b2131ed8..c77fc04ee6 100644
--- a/src/input_common/main.cpp
+++ b/src/input_common/main.cpp
@@ -23,6 +23,7 @@
 #include "input_common/drivers/gc_adapter.h"
 #endif
 #ifdef HAVE_SDL2
+#include "input_common/drivers/joycon.h"
 #include "input_common/drivers/sdl_driver.h"
 #endif
 
@@ -81,6 +82,7 @@ struct InputSubsystem::Impl {
         RegisterEngine("virtual_gamepad", virtual_gamepad);
 #ifdef HAVE_SDL2
         RegisterEngine("sdl", sdl);
+        RegisterEngine("joycon", joycon);
 #endif
 
         Common::Input::RegisterInputFactory("touch_from_button",
@@ -111,6 +113,7 @@ struct InputSubsystem::Impl {
         UnregisterEngine(virtual_gamepad);
 #ifdef HAVE_SDL2
         UnregisterEngine(sdl);
+        UnregisterEngine(joycon);
 #endif
 
         Common::Input::UnregisterInputFactory("touch_from_button");
@@ -133,6 +136,8 @@ struct InputSubsystem::Impl {
         auto udp_devices = udp_client->GetInputDevices();
         devices.insert(devices.end(), udp_devices.begin(), udp_devices.end());
 #ifdef HAVE_SDL2
+        auto joycon_devices = joycon->GetInputDevices();
+        devices.insert(devices.end(), joycon_devices.begin(), joycon_devices.end());
         auto sdl_devices = sdl->GetInputDevices();
         devices.insert(devices.end(), sdl_devices.begin(), sdl_devices.end());
 #endif
@@ -164,6 +169,9 @@ struct InputSubsystem::Impl {
         if (engine == sdl->GetEngineName()) {
             return sdl;
         }
+        if (engine == joycon->GetEngineName()) {
+            return joycon;
+        }
 #endif
         return nullptr;
     }
@@ -247,6 +255,9 @@ struct InputSubsystem::Impl {
         if (engine == sdl->GetEngineName()) {
             return true;
         }
+        if (engine == joycon->GetEngineName()) {
+            return true;
+        }
 #endif
         return false;
     }
@@ -260,6 +271,7 @@ struct InputSubsystem::Impl {
         udp_client->BeginConfiguration();
 #ifdef HAVE_SDL2
         sdl->BeginConfiguration();
+        joycon->BeginConfiguration();
 #endif
     }
 
@@ -272,6 +284,7 @@ struct InputSubsystem::Impl {
         udp_client->EndConfiguration();
 #ifdef HAVE_SDL2
         sdl->EndConfiguration();
+        joycon->EndConfiguration();
 #endif
     }
 
@@ -304,6 +317,7 @@ struct InputSubsystem::Impl {
 
 #ifdef HAVE_SDL2
     std::shared_ptr<SDLDriver> sdl;
+    std::shared_ptr<Joycons> joycon;
 #endif
 };