diff options
Diffstat (limited to '')
21 files changed, 5163 insertions, 33 deletions
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) { |