From ada65c268b6b67e2b3b90e1a233413fe25c7c151 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Sat, 23 Nov 2024 17:53:05 +1000 Subject: [PATCH] Controller: Add JogCon --- src/core/controller.cpp | 5 + src/core/core.vcxproj | 2 + src/core/core.vcxproj.filters | 2 + src/core/jogcon.cpp | 457 ++++++++++++++++++++++++++++++++++ src/core/jogcon.h | 116 +++++++++ src/core/types.h | 1 + 6 files changed, 583 insertions(+) create mode 100644 src/core/jogcon.cpp create mode 100644 src/core/jogcon.h diff --git a/src/core/controller.cpp b/src/core/controller.cpp index 1fdfbc47a..fb4f8a73c 100644 --- a/src/core/controller.cpp +++ b/src/core/controller.cpp @@ -8,6 +8,7 @@ #include "game_database.h" #include "guncon.h" #include "host.h" +#include "jogcon.h" #include "justifier.h" #include "negcon.h" #include "negcon_rumble.h" @@ -38,6 +39,7 @@ static const Controller::ControllerInfo* s_controller_info[] = { &Justifier::INFO, &DigitalController::INFO_POPN, &DigitalController::INFO_DDGO, + &JogCon::INFO, }; const std::array Controller::PortDisplayOrder = {{0, 2, 3, 4, 1, 5, 6, 7}}; @@ -140,6 +142,9 @@ std::unique_ptr Controller::Create(ControllerType type, u32 index) case ControllerType::NeGconRumble: return NeGconRumble::Create(index); + case ControllerType::JogCon: + return JogCon::Create(index); + case ControllerType::None: default: return {}; diff --git a/src/core/core.vcxproj b/src/core/core.vcxproj index 1de21f683..e3f9feaf0 100644 --- a/src/core/core.vcxproj +++ b/src/core/core.vcxproj @@ -51,6 +51,7 @@ + @@ -131,6 +132,7 @@ + diff --git a/src/core/core.vcxproj.filters b/src/core/core.vcxproj.filters index e1dc75c5c..1068324cc 100644 --- a/src/core/core.vcxproj.filters +++ b/src/core/core.vcxproj.filters @@ -65,6 +65,7 @@ + @@ -139,6 +140,7 @@ + diff --git a/src/core/jogcon.cpp b/src/core/jogcon.cpp new file mode 100644 index 000000000..24a369909 --- /dev/null +++ b/src/core/jogcon.cpp @@ -0,0 +1,457 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#include "jogcon.h" +#include "host.h" +#include "system.h" + +#include "util/state_wrapper.h" + +#include "IconsPromptFont.h" + +#include "common/assert.h" +#include "common/bitutils.h" +#include "common/log.h" + +LOG_CHANNEL(AnalogController); + +JogCon::JogCon(u32 index) : Controller(index) +{ +} + +JogCon::~JogCon() = default; + +ControllerType JogCon::GetType() const +{ + return ControllerType::JogCon; +} + +void JogCon::Reset() +{ + ResetTransferState(); + ResetMotorConfig(); +} + +bool JogCon::DoState(StateWrapper& sw, bool apply_input_state) +{ + if (!Controller::DoState(sw, apply_input_state)) + return false; + + u16 button_state = m_button_state; + s8 steering_state = m_steering_state; + sw.Do(&button_state); + sw.Do(&steering_state); + if (apply_input_state) + { + m_button_state = button_state; + m_steering_state = steering_state; + } + + sw.Do(&m_command); + sw.Do(&m_command_step); + sw.Do(&m_status_byte); + sw.Do(&m_last_steering_state); + sw.Do(&m_last_motor_command); + + sw.Do(&m_configuration_mode); + sw.Do(&m_jogcon_mode); + + sw.Do(&m_rx_buffer); + sw.Do(&m_tx_buffer); + sw.Do(&m_rumble_config); + + return true; +} + +float JogCon::GetBindState(u32 index) const +{ + if (index >= static_cast(Button::MaxCount)) + { + const u32 sub_index = index - static_cast(Button::MaxCount); + if (sub_index >= static_cast(m_half_axis_state.size())) + return 0.0f; + + return static_cast(m_half_axis_state[sub_index]) * (1.0f / 255.0f); + } + else if (index < static_cast(Button::Toggle)) + { + return static_cast(((m_button_state >> index) & 1u) ^ 1u); + } + else + { + return 0.0f; + } +} + +void JogCon::SetBindState(u32 index, float value) +{ + if (index == static_cast(Button::Toggle)) + { + // FIXME + return; + } + else if (index >= static_cast(Button::MaxCount)) + { + const u32 sub_index = index - static_cast(Button::MaxCount); + if (sub_index >= static_cast(m_half_axis_state.size())) + return; + + const u8 u8_value = static_cast(std::clamp(value * /*m_analog_sensitivity * */ 255.0f, 0.0f, 255.0f)); + if (u8_value == m_half_axis_state[sub_index]) + return; + + m_half_axis_state[sub_index] = u8_value; + System::SetRunaheadReplayFlag(); + + m_steering_state = + (m_half_axis_state[static_cast(HalfAxis::SteeringRight)] != 0) ? + static_cast((m_half_axis_state[static_cast(HalfAxis::SteeringRight)] / 2)) : + -static_cast((static_cast(m_half_axis_state[static_cast(HalfAxis::SteeringLeft)]) + 1) / 2); + } + + const u16 bit = u16(1) << static_cast(index); + + if (value >= 0.5f /*m_button_deadzone*/) + { + if (m_button_state & bit) + System::SetRunaheadReplayFlag(); + + m_button_state &= ~(bit); + } + else + { + if (!(m_button_state & bit)) + System::SetRunaheadReplayFlag(); + + m_button_state |= bit; + } +} + +u32 JogCon::GetButtonStateBits() const +{ + return m_button_state ^ 0xFFFF; +} + +void JogCon::ResetTransferState() +{ + m_command = Command::Idle; + m_command_step = 0; +} + +u8 JogCon::GetIDByte() const +{ + return Truncate8((GetModeID() << 4) | GetResponseNumHalfwords()); +} + +u8 JogCon::GetModeID() const +{ + if (m_configuration_mode) + return 0xF; + else if (m_jogcon_mode) + return 0xE; + else + return 0x4; +} + +u8 JogCon::GetResponseNumHalfwords() const +{ + return m_jogcon_mode ? 3 : 1; +} + +void JogCon::SetMotorState(u8 value) +{ + const u8 command = (value >> 6); + const u8 direction = ((value >> 4) & 0x03); + const u8 force = (value & 0x0F); + + //WARNING_LOG("0x{:2X} command=0x{:X} direction={} force={}", value, command, direction, force); + + if (command == 0) + { + if (direction == 0) + { + WARNING_LOG("Stop motor"); + } + else if (direction == 1) + { + WARNING_LOG("Turn wheel clockwise with {} force", force); + } + else if (direction == 2) + { + WARNING_LOG("Turn wheel COUNTER clockwise with {} force", force); + } + else // if (direction == 3) + { + WARNING_LOG("Hold wheel in current position {} with {} force", m_steering_state, force); + // compute new distance to return position + } + } + + m_last_motor_command = command; +} + +void JogCon::ResetMotorConfig() +{ + m_rumble_config.fill(0xFF); + SetMotorState(0); +} + +void JogCon::Poll() +{ + m_tx_buffer[2] = Truncate8(m_button_state); + m_tx_buffer[3] = Truncate8(m_button_state >> 8); + + m_tx_buffer[4] = Truncate8(m_steering_state); + m_tx_buffer[5] = Truncate8(m_steering_state >> 8); // 0xFF if negative, otherwise 0x00 + + u8 rotation_state = 0; + if (m_steering_state > m_last_steering_state) + rotation_state = 1; + else if (m_steering_state < m_last_steering_state) + rotation_state = 2; + + m_tx_buffer[6] = rotation_state | (m_last_motor_command << 4); + + m_last_steering_state = m_steering_state; +} + +bool JogCon::Transfer(const u8 data_in, u8* data_out) +{ + bool ack; + m_rx_buffer[m_command_step] = data_in; + + switch (m_command) + { + case Command::Idle: + { + *data_out = 0xFF; + + if (data_in == 0x01) + { + DEBUG_LOG("ACK controller access"); + m_command = Command::Ready; + m_tx_buffer.fill(0); + m_rx_buffer.fill(0); + return true; + } + + return false; + } + break; + + case Command::Ready: + { + Assert(m_command_step == 0); + + if (data_in == 0x42) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::ReadPad; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + Poll(); + } + else if (data_in == 0x43) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::SetMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + Poll(); + } + else if (m_configuration_mode && data_in == 0x45) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::GetAnalogMode; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x01, 0x02, BoolToUInt8(m_jogcon_mode), 0x01, 0x01, 0x00}; + } + else if (m_configuration_mode && data_in == 0x46) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command46; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else if (m_configuration_mode && data_in == 0x47) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command47; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00}; + } + else if (m_configuration_mode && data_in == 0x4C) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::Command4C; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else if (m_configuration_mode && data_in == 0x4D) + { + m_response_length = (GetResponseNumHalfwords() + 1) * 2; + m_command = Command::GetSetRumble; + m_tx_buffer = {GetIDByte(), m_status_byte, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + } + else + { + ERROR_LOG("Unimplemented command 0x{:02X}", data_in); + + *data_out = 0xFF; + return false; + } + } + break; + + case Command::ReadPad: + { + if (m_command_step >= 2 && m_command_step < 7 && m_rumble_config[m_command_step - 2] == 0x00) + SetMotorState(data_in); + } + break; + + case Command::GetAnalogMode: + { + // just send the byte, nothing special to do here + } + break; + + case Command::SetMode: + { + m_configuration_mode = (m_rx_buffer[2] == 1); + + if (m_configuration_mode) + { + m_jogcon_mode = true; + m_status_byte = 0x5A; + } + + DEV_LOG("0x{:02x}({}) config mode", m_rx_buffer[2], m_configuration_mode ? "enter" : "leave"); + } + break; + + case Command::GetSetRumble: + { + if (m_command_step >= 2 && m_command_step < 7) + { + const u8 index = m_command_step - 2; + if (index >= 0) + { + m_tx_buffer[m_command_step] = m_rumble_config[index]; + m_rumble_config[index] = data_in; + + if (data_in == 0x00) + WARNING_LOG("Motor mapped to byte index {}", index); + } + } + else + { + // reset motor value if we're no longer mapping it + if (std::find(m_rumble_config.begin(), m_rumble_config.end(), 0) == m_rumble_config.end()) + SetMotorState(0); + } + } + break; + + case Command::Command46: + { + if (m_command_step == 2) + { + if (data_in == 0x00) + { + m_tx_buffer[4] = 0x01; + m_tx_buffer[5] = 0x02; + m_tx_buffer[6] = 0x00; + m_tx_buffer[7] = 0x0A; + } + else if (data_in == 0x01) + { + m_tx_buffer[4] = 0x01; + m_tx_buffer[5] = 0x01; + m_tx_buffer[6] = 0x01; + m_tx_buffer[7] = 0x14; + } + } + } + break; + + case Command::Command47: + { + if (m_command_step == 2 && data_in != 0x00) + { + m_tx_buffer[4] = 0x00; + m_tx_buffer[5] = 0x00; + m_tx_buffer[6] = 0x00; + m_tx_buffer[7] = 0x00; + } + } + break; + + case Command::Command4C: + { + if (m_command_step == 2) + { + if (data_in == 0x00) + m_tx_buffer[5] = 0x04; + else if (data_in == 0x01) + m_tx_buffer[4] = 0x03; + } + } + break; + + DefaultCaseIsUnreachable(); + } + + *data_out = m_tx_buffer[m_command_step]; + + m_command_step = (m_command_step + 1) % m_response_length; + ack = (m_command_step != 0); + + if (m_command_step == 0) + { + m_command = Command::Idle; + + DEBUG_LOG("Rx: {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", m_rx_buffer[0], m_rx_buffer[1], + m_rx_buffer[2], m_rx_buffer[3], m_rx_buffer[4], m_rx_buffer[5], m_rx_buffer[6], m_rx_buffer[7]); + DEBUG_LOG("Tx: {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}", m_tx_buffer[0], m_tx_buffer[1], + m_tx_buffer[2], m_tx_buffer[3], m_tx_buffer[4], m_tx_buffer[5], m_tx_buffer[6], m_tx_buffer[7]); + } + + return ack; +} + +std::unique_ptr JogCon::Create(u32 index) +{ + return std::make_unique(index); +} + +static const Controller::ControllerBindingInfo s_binding_info[] = { +#define BUTTON(name, display_name, icon_name, button, genb) \ + {name, display_name, icon_name, static_cast(button), InputBindingInfo::Type::Button, genb} +#define AXIS(name, display_name, icon_name, halfaxis, genb) \ + {name, display_name, icon_name, static_cast(JogCon::Button::MaxCount) + static_cast(halfaxis), \ + InputBindingInfo::Type::HalfAxis} + + // clang-format off + BUTTON("Up", TRANSLATE_NOOP("JogCon", "D-Pad Up"), ICON_PF_DPAD_UP, JogCon::Button::Up, GenericInputBinding::DPadUp), + BUTTON("Right", TRANSLATE_NOOP("JogCon", "D-Pad Right"), ICON_PF_DPAD_RIGHT, JogCon::Button::Right, GenericInputBinding::DPadRight), + BUTTON("Down", TRANSLATE_NOOP("JogCon", "D-Pad Down"), ICON_PF_DPAD_DOWN, JogCon::Button::Down, GenericInputBinding::DPadDown), + BUTTON("Left", TRANSLATE_NOOP("JogCon", "D-Pad Left"), ICON_PF_DPAD_LEFT, JogCon::Button::Left, GenericInputBinding::DPadLeft), + BUTTON("Triangle", TRANSLATE_NOOP("JogCon", "Triangle"), ICON_PF_BUTTON_TRIANGLE, JogCon::Button::Triangle, GenericInputBinding::Triangle), + BUTTON("Circle", TRANSLATE_NOOP("JogCon", "Circle"), ICON_PF_BUTTON_CIRCLE, JogCon::Button::Circle, GenericInputBinding::Circle), + BUTTON("Cross", TRANSLATE_NOOP("JogCon", "Cross"), ICON_PF_BUTTON_CROSS, JogCon::Button::Cross, GenericInputBinding::Cross), + BUTTON("Square", TRANSLATE_NOOP("JogCon", "Square"), ICON_PF_BUTTON_SQUARE, JogCon::Button::Square, GenericInputBinding::Square), + BUTTON("Select", TRANSLATE_NOOP("JogCon", "Select"), ICON_PF_SELECT_SHARE, JogCon::Button::Select, GenericInputBinding::Select), + BUTTON("Start", TRANSLATE_NOOP("JogCon", "Start"), ICON_PF_START, JogCon::Button::Start, GenericInputBinding::Start), + BUTTON("L1", TRANSLATE_NOOP("JogCon", "L1"), ICON_PF_LEFT_SHOULDER_L1, JogCon::Button::L1, GenericInputBinding::L1), + BUTTON("R1", TRANSLATE_NOOP("JogCon", "R1"), ICON_PF_RIGHT_SHOULDER_R1, JogCon::Button::R1, GenericInputBinding::R1), + BUTTON("L2", TRANSLATE_NOOP("JogCon", "L2"), ICON_PF_LEFT_TRIGGER_L2, JogCon::Button::L2, GenericInputBinding::L2), + BUTTON("R2", TRANSLATE_NOOP("JogCon", "R2"), ICON_PF_RIGHT_TRIGGER_R2, JogCon::Button::R2, GenericInputBinding::R2), + + AXIS("SteeringLeft", TRANSLATE_NOOP("JogCon", "Steering Left"), ICON_PF_ANALOG_LEFT, JogCon::HalfAxis::SteeringLeft, GenericInputBinding::LeftStickLeft), + AXIS("SteeringRight", TRANSLATE_NOOP("JogCon", "Steering Right"), ICON_PF_ANALOG_RIGHT, JogCon::HalfAxis::SteeringRight, GenericInputBinding::LeftStickRight), + +// clang-format on + +#undef BUTTON +#undef AXIS + +}; + +const Controller::ControllerInfo JogCon::INFO = { + ControllerType::JogCon, "JogCon", TRANSLATE_NOOP("ControllerType", "JogCon"), ICON_PF_STEERING_WHEEL, + s_binding_info, {}, Controller::VibrationCapabilities::NoVibration}; diff --git a/src/core/jogcon.h b/src/core/jogcon.h new file mode 100644 index 000000000..fbd17e43b --- /dev/null +++ b/src/core/jogcon.h @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: CC-BY-NC-ND-4.0 + +#pragma once + +#include "controller.h" + +#include + +class JogCon final : public Controller +{ +public: + enum class Button : u8 + { + Select = 0, + L3 = 1, + R3 = 2, + Start = 3, + Up = 4, + Right = 5, + Down = 6, + Left = 7, + L2 = 8, + R2 = 9, + L1 = 10, + R1 = 11, + Triangle = 12, + Circle = 13, + Cross = 14, + Square = 15, + Toggle = 16, + MaxCount + }; + + enum class HalfAxis : u8 + { + SteeringLeft, + SteeringRight, + MaxCount, + }; + + static const Controller::ControllerInfo INFO; + + JogCon(u32 index); + ~JogCon() override; + + static std::unique_ptr Create(u32 index); + + ControllerType GetType() const override; + + void Reset() override; + bool DoState(StateWrapper& sw, bool apply_input_state) override; + + float GetBindState(u32 index) const override; + void SetBindState(u32 index, float value) override; + u32 GetButtonStateBits() const override; + + void ResetTransferState() override; + bool Transfer(const u8 data_in, u8* data_out) override; + +private: + enum class Command : u8 + { + Idle, + Ready, + ReadPad, + SetMode, + GetAnalogMode, + GetSetRumble, + Command46, + Command47, + Command4C, + }; + + enum : u8 + { + LargeMotor = 0, + SmallMotor = 1 + }; + + u8 GetIDByte() const; + u8 GetModeID() const; + + // Get number of response halfwords (excluding the initial controller info halfword) + u8 GetResponseNumHalfwords() const; + + void Poll(); + + void SetMotorState(u8 value); + void ResetMotorConfig(); + + // buttons are active low + u16 m_button_state = UINT16_C(0xFFFF); + s8 m_steering_state = 0; + + // both directions of axis state, merged to m_steering_state + std::array(HalfAxis::MaxCount)> m_half_axis_state{}; + + Command m_command = Command::Idle; + u8 m_command_step = 0; + u8 m_response_length = 0; + u8 m_status_byte = 0x5A; + + s8 m_last_steering_state = 0; + u8 m_last_motor_command = 0; + + bool m_configuration_mode = false; + bool m_jogcon_mode = false; + + std::array m_rumble_config{}; + + // Transmit and receive buffers, not including the first Hi-Z/ack response byte + static constexpr u32 MAX_RESPONSE_LENGTH = 8; + std::array m_rx_buffer; + std::array m_tx_buffer; +}; diff --git a/src/core/types.h b/src/core/types.h index fae40cffa..c4ef594a9 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -222,6 +222,7 @@ enum class ControllerType : u8 Justifier, PopnController, DDGoController, + JogCon, Count };