From 12654e351ceed244311a82213b41e35ce21b4800 Mon Sep 17 00:00:00 2001 From: wiredopposite Date: Mon, 20 Jan 2025 10:11:14 -0700 Subject: [PATCH] add deadzone settings --- .gitmodules | 3 + Firmware/ESP32/CMakeLists.txt | 5 +- .../components/libfixmath/CMakeLists.txt | 27 + Firmware/ESP32/main/BLEServer/BLEServer.cpp | 304 +++++++----- Firmware/ESP32/main/BLEServer/BLEServer.h | 2 + Firmware/ESP32/main/BTManager/BTManager.cpp | 20 +- Firmware/ESP32/main/BTManager/BTManager.h | 3 +- .../ESP32/main/BTManager/BTManager_BP32.cpp | 15 +- Firmware/ESP32/main/Board/ogxm_log.h | 11 + Firmware/ESP32/main/CMakeLists.txt | 5 +- Firmware/ESP32/main/Gamepad.h | 364 -------------- Firmware/ESP32/main/Gamepad/Gamepad.h | 450 +++++++++++++++++ .../src => ESP32/main/Gamepad}/Range.h | 74 +-- Firmware/ESP32/main/Gamepad/fix16ext.h | 106 ++++ Firmware/ESP32/main/I2CDriver/I2CDriver.cpp | 2 +- .../main/UserSettings/JoystickSettings.cpp | 63 +++ .../main/UserSettings/JoystickSettings.h | 68 +++ Firmware/ESP32/main/UserSettings/NVSHelper.h | 2 +- .../main/UserSettings/TriggerSettings.cpp | 29 ++ .../ESP32/main/UserSettings/TriggerSettings.h | 40 ++ .../ESP32/main/UserSettings/UserProfile.cpp | 32 +- .../ESP32/main/UserSettings/UserProfile.h | 17 +- .../ESP32/main/UserSettings/UserSettings.cpp | 18 +- .../ESP32/main/UserSettings/UserSettings.h | 6 +- .../{1btstack_config.h => btstack_config.h} | 4 +- Firmware/ESP32/sdkconfig | 17 +- Firmware/RP2040/.vscode/settings.json | 2 +- Firmware/RP2040/CMakeLists.txt | 55 ++- Firmware/RP2040/src/BLEServer/BLEServer.cpp | 296 ++++++----- Firmware/RP2040/src/BLEServer/BLEServer.h | 4 +- .../src/BLEServer/att_delayed_response.gatt | 19 +- Firmware/RP2040/src/Bluepad32/Bluepad32.cpp | 13 +- Firmware/RP2040/src/Bluepad32/Bluepad32.h | 4 +- Firmware/RP2040/src/Board/board_api.cpp | 5 +- Firmware/RP2040/src/{ => Gamepad}/Gamepad.h | 465 ++++++++++-------- .../main => RP2040/src/Gamepad}/Range.h | 132 ++--- Firmware/RP2040/src/Gamepad/fix16ext.h | 106 ++++ .../src/I2CDriver/4Channel/I2CManager.h | 2 +- .../RP2040/src/I2CDriver/4Channel/I2CMaster.h | 2 +- .../RP2040/src/I2CDriver/4Channel/I2CSlave.h | 2 +- .../RP2040/src/I2CDriver/ESP32/I2CDriver.cpp | 3 - .../RP2040/src/I2CDriver/ESP32/I2CDriver.h | 2 +- Firmware/RP2040/src/OGXMini/OGXMini_ESP32.cpp | 26 +- Firmware/RP2040/src/OGXMini/OGXMini_PicoW.cpp | 4 +- .../RP2040/src/OGXMini/OGXMini_Standard.cpp | 21 +- .../src/USBDevice/DeviceDriver/DeviceDriver.h | 2 +- .../DeviceDriver/PSClassic/PSClassic.h | 1 - .../UARTBridge/uart_bridge/uart_bridge.c | 39 +- .../UARTBridge/uart_bridge/uart_bridge.h | 3 - .../USBDevice/DeviceDriver/WebApp/WebApp.cpp | 270 ++++++++-- .../USBDevice/DeviceDriver/WebApp/WebApp.h | 56 ++- .../USBDevice/DeviceDriver/XboxOG/XboxOG_SB.h | 1 - Firmware/RP2040/src/USBDevice/DeviceManager.h | 1 - .../src/USBHost/HostDriver/DInput/DInput.cpp | 6 +- .../HostDriver/HIDGeneric/HIDGeneric.cpp | 6 +- .../src/USBHost/HostDriver/HostDriver.h | 2 +- .../RP2040/src/USBHost/HostDriver/N64/N64.cpp | 7 +- .../RP2040/src/USBHost/HostDriver/PS3/PS3.cpp | 6 +- .../RP2040/src/USBHost/HostDriver/PS4/PS4.cpp | 6 +- .../RP2040/src/USBHost/HostDriver/PS5/PS5.cpp | 6 +- .../HostDriver/SwitchPro/SwitchPro.cpp | 9 +- .../HostDriver/SwitchWired/SwitchWired.cpp | 6 +- .../src/USBHost/HostDriver/XInput/Xbox360.cpp | 6 +- .../USBHost/HostDriver/XInput/Xbox360W.cpp | 6 +- .../src/USBHost/HostDriver/XInput/XboxOG.cpp | 6 +- .../src/USBHost/HostDriver/XInput/XboxOne.cpp | 6 +- Firmware/RP2040/src/USBHost/HostManager.h | 2 - .../src/UserSettings/JoystickSettings.cpp | 41 ++ .../src/UserSettings/JoystickSettings.h | 66 +++ Firmware/RP2040/src/UserSettings/NVSTool.h | 39 +- .../src/UserSettings/TriggerSettings.cpp | 19 + .../RP2040/src/UserSettings/TriggerSettings.h | 38 ++ .../RP2040/src/UserSettings/UserProfile.cpp | 10 +- .../RP2040/src/UserSettings/UserProfile.h | 17 +- .../RP2040/src/UserSettings/UserSettings.cpp | 50 +- .../RP2040/src/UserSettings/UserSettings.h | 12 +- Firmware/cmake/init_submodules.cmake | 28 +- Firmware/external/libfixmath | 1 + README.md | 10 +- WebApp | 2 +- images/WebAppPreview.png | Bin 0 -> 91606 bytes 81 files changed, 2398 insertions(+), 1238 deletions(-) create mode 100644 Firmware/ESP32/components/libfixmath/CMakeLists.txt delete mode 100644 Firmware/ESP32/main/Gamepad.h create mode 100644 Firmware/ESP32/main/Gamepad/Gamepad.h rename Firmware/{RP2040/src => ESP32/main/Gamepad}/Range.h (65%) create mode 100644 Firmware/ESP32/main/Gamepad/fix16ext.h create mode 100644 Firmware/ESP32/main/UserSettings/JoystickSettings.cpp create mode 100644 Firmware/ESP32/main/UserSettings/JoystickSettings.h create mode 100644 Firmware/ESP32/main/UserSettings/TriggerSettings.cpp create mode 100644 Firmware/ESP32/main/UserSettings/TriggerSettings.h rename Firmware/ESP32/main/{1btstack_config.h => btstack_config.h} (98%) rename Firmware/RP2040/src/{ => Gamepad}/Gamepad.h (50%) rename Firmware/{ESP32/main => RP2040/src/Gamepad}/Range.h (57%) create mode 100644 Firmware/RP2040/src/Gamepad/fix16ext.h create mode 100644 Firmware/RP2040/src/UserSettings/JoystickSettings.cpp create mode 100644 Firmware/RP2040/src/UserSettings/JoystickSettings.h create mode 100644 Firmware/RP2040/src/UserSettings/TriggerSettings.cpp create mode 100644 Firmware/RP2040/src/UserSettings/TriggerSettings.h create mode 160000 Firmware/external/libfixmath create mode 100644 images/WebAppPreview.png diff --git a/.gitmodules b/.gitmodules index 1b7381f..ea43512 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "WebApp"] path = WebApp url = https://github.com/wiredopposite/OGX-Mini-WebApp.git +[submodule "Firmware/external/libfixmath"] + path = Firmware/external/libfixmath + url = https://github.com/PetteriAimonen/libfixmath.git diff --git a/Firmware/ESP32/CMakeLists.txt b/Firmware/ESP32/CMakeLists.txt index f4aa783..e62b842 100644 --- a/Firmware/ESP32/CMakeLists.txt +++ b/Firmware/ESP32/CMakeLists.txt @@ -13,7 +13,10 @@ include(${EXTERNAL_CMAKE_DIR}/patch_libs.cmake) include(${EXTERNAL_CMAKE_DIR}/generate_gatt_header.cmake) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/integrate_btstack.cmake) -init_git_submodules(${EXTERNAL_DIR}) +init_git_submodules(${EXTERNAL_DIR} + ${EXTERNAL_DIR}/bluepad32 + ${EXTERNAL_DIR}/libfixmath +) apply_lib_patches(${EXTERNAL_DIR}) integrate_btstack(${EXTERNAL_DIR}) generate_gatt_header( diff --git a/Firmware/ESP32/components/libfixmath/CMakeLists.txt b/Firmware/ESP32/components/libfixmath/CMakeLists.txt new file mode 100644 index 0000000..dd31ca8 --- /dev/null +++ b/Firmware/ESP32/components/libfixmath/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.5) + +set(LIBFIXMATH_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../external/libfixmath) + +if (NOT EXISTS ${LIBFIXMATH_ROOT}) + message(FATAL_ERROR "External directory not found: ${LIBFIXMATH_ROOT}") +else() + message(STATUS "Found libfixmath at ${LIBFIXMATH_ROOT}") +endif() + +file(GLOB SRCS ${LIBFIXMATH_ROOT}/libfixmath/*.c) + +idf_component_register( + SRCS + ${SRCS} + INCLUDE_DIRS + ${LIBFIXMATH_ROOT} + ${LIBFIXMATH_ROOT}/libfixmath +) + +target_compile_definitions(${COMPONENT_LIB} PRIVATE + FIXMATH_FAST_SIN + FIXMATH_NO_64BIT + FIXMATH_NO_CACHE + FIXMATH_NO_HARD_DIVISION + FIXMATH_NO_OVERFLOW +) \ No newline at end of file diff --git a/Firmware/ESP32/main/BLEServer/BLEServer.cpp b/Firmware/ESP32/main/BLEServer/BLEServer.cpp index 4d8b64d..8ba99db 100644 --- a/Firmware/ESP32/main/BLEServer/BLEServer.cpp +++ b/Firmware/ESP32/main/BLEServer/BLEServer.cpp @@ -6,7 +6,8 @@ #include "att_server.h" #include "btstack.h" -#include "Gamepad.h" +#include "BTManager/BTManager.h" +#include "Gamepad/Gamepad.h" #include "BLEServer/BLEServer.h" #include "BLEServer/att_delayed_response.h" #include "UserSettings/UserProfile.h" @@ -14,33 +15,21 @@ namespace BLEServer { -static constexpr uint16_t PACKET_LEN_MAX = 18; - -#pragma pack(push, 1) -struct SetupPacket -{ - uint8_t max_gamepads{1}; - uint8_t index{0}; - uint8_t device_type{0}; - uint8_t profile_id{1}; -}; -static_assert(sizeof(SetupPacket) == 4, "BLEServer::SetupPacket struct size mismatch"); -#pragma pack(pop) - -SetupPacket setup_packet_; +constexpr uint16_t PACKET_LEN_MAX = 20; +constexpr size_t GAMEPAD_LEN = 23; namespace Handle { - static constexpr uint16_t FW_VERSION = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789020_01_VALUE_HANDLE; - static constexpr uint16_t FW_NAME = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789021_01_VALUE_HANDLE; + constexpr uint16_t FW_VERSION = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789020_01_VALUE_HANDLE; + constexpr uint16_t FW_NAME = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789021_01_VALUE_HANDLE; - static constexpr uint16_t START_UPDATE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789030_01_VALUE_HANDLE; - static constexpr uint16_t COMMIT_UPDATE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789031_01_VALUE_HANDLE; + constexpr uint16_t SETUP_READ = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789030_01_VALUE_HANDLE; + constexpr uint16_t SETUP_WRITE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789031_01_VALUE_HANDLE; + constexpr uint16_t GET_SETUP = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789032_01_VALUE_HANDLE; - static constexpr uint16_t SETUP_PACKET = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789040_01_VALUE_HANDLE; - static constexpr uint16_t PROFILE_PT1 = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789041_01_VALUE_HANDLE; - static constexpr uint16_t PROFILE_PT2 = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789042_01_VALUE_HANDLE; - static constexpr uint16_t PROFILE_PT3 = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789043_01_VALUE_HANDLE; + constexpr uint16_t PROFILE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789040_01_VALUE_HANDLE; + + constexpr uint16_t GAMEPAD = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789050_01_VALUE_HANDLE; } namespace ADV @@ -62,23 +51,163 @@ namespace ADV std::memcpy(flags, FLAGS, sizeof(flags)); name_len = sizeof(FIRMWARE_NAME); name_type = NAME_TYPE; - std::memcpy(name, FIRMWARE_NAME, sizeof(name)); + std::string fw_name = FIRMWARE_NAME; + std::memcpy(name, fw_name.c_str(), std::min(sizeof(name), fw_name.size())); } }; static_assert(sizeof(Data) == 5 + sizeof(FIRMWARE_NAME) - 1, "BLEServer::ADV::Data struct size mismatch"); #pragma pack(pop) } -static int verify_write(const uint16_t buffer_size, const uint16_t expected_size, bool pending_write = false, bool expected_pending_write = false) +#pragma pack(push, 1) +struct SetupPacket +{ + DeviceDriverType device_type{DeviceDriverType::NONE}; + uint8_t max_gamepads{MAX_GAMEPADS}; + uint8_t player_idx{0}; + uint8_t profile_id{0}; +}; +static_assert(sizeof(SetupPacket) == 4, "BLEServer::SetupPacket struct size mismatch"); +#pragma pack(pop) + +class ProfileReader +{ +public: + ProfileReader() = default; + ~ProfileReader() = default; + + void set_setup_packet(const SetupPacket& setup_packet) + { + setup_packet_ = setup_packet; + current_offset_ = 0; + } + const SetupPacket& get_setup_packet() const + { + return setup_packet_; + } + uint16_t get_xfer_len() + { + return static_cast(std::min(static_cast(PACKET_LEN_MAX), sizeof(UserProfile) - current_offset_)); + } + uint16_t get_profile_data(uint8_t* buffer, uint16_t buffer_len) + { + size_t copy_len = get_xfer_len(); + if (!buffer || buffer_len < copy_len) + { + return 0; + } + + if (current_offset_ == 0 && !set_profile()) + { + return 0; + } + + std::memcpy(buffer, reinterpret_cast(&profile_) + current_offset_, copy_len); + + current_offset_ += copy_len; + if (current_offset_ >= sizeof(UserProfile)) + { + current_offset_ = 0; + OGXM_LOG("ProfileReader: Read complete for profile ID: %i\n", profile_.id); + } + return copy_len; + } + +private: + SetupPacket setup_packet_; + UserProfile profile_; + size_t current_offset_ = 0; + + bool set_profile() + { + if (setup_packet_.profile_id == 0xFF) + { + if (setup_packet_.player_idx >= UserSettings::MAX_PROFILES) + { + return false; + } + profile_ = UserSettings::get_instance().get_profile_by_index(setup_packet_.player_idx); + OGXM_LOG("ProfileReader: Reading profile for player %d\n", setup_packet_.player_idx); + } + else + { + if (setup_packet_.profile_id > UserSettings::MAX_PROFILES) + { + return false; + } + profile_ = UserSettings::get_instance().get_profile_by_id(setup_packet_.profile_id); + OGXM_LOG("ProfileReader: Reading profile with ID %d\n", setup_packet_.profile_id); + } + return true; + } +}; + +class ProfileWriter +{ +public: + ProfileWriter() = default; + ~ProfileWriter() = default; + + void set_setup_packet(const SetupPacket& setup_packet) + { + setup_packet_ = setup_packet; + current_offset_ = 0; + } + const SetupPacket& get_setup_packet() const + { + return setup_packet_; + } + uint16_t get_xfer_len() + { + return static_cast(std::min(static_cast(PACKET_LEN_MAX), sizeof(UserProfile) - current_offset_)); + } + size_t set_profile_data(const uint8_t* buffer, uint16_t buffer_len) + { + size_t copy_len = get_xfer_len(); + if (!buffer || buffer_len < copy_len) + { + return 0; + } + + std::memcpy(reinterpret_cast(&profile_) + current_offset_, buffer, copy_len); + + current_offset_ += copy_len; + size_t ret = current_offset_; + + if (current_offset_ >= sizeof(UserProfile)) + { + current_offset_ = 0; + } + return ret; + } + bool commit_profile() + { + if (setup_packet_.device_type != DeviceDriverType::NONE) + { + UserSettings::get_instance().store_profile_and_driver_type(setup_packet_.device_type, setup_packet_.player_idx, profile_); + } + else + { + UserSettings::get_instance().store_profile(setup_packet_.player_idx, profile_); + } + return true; + } + +private: + SetupPacket setup_packet_; + UserProfile profile_; + size_t current_offset_ = 0; +}; + +ProfileReader profile_reader_; +ProfileWriter profile_writer_; + +static int verify_write(const uint16_t buffer_size, const uint16_t expected_size) { if (buffer_size != expected_size) { return ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LENGTH; } - if (pending_write != expected_pending_write) - { - return ATT_ERROR_WRITE_NOT_PERMITTED; - } return 0; } @@ -88,8 +217,6 @@ static uint16_t att_read_callback( hci_con_handle_t connection_handle, uint8_t *buffer, uint16_t buffer_size) { - static UserProfile profile; - SetupPacket setup_packet_resp; std::string fw_version; std::string fw_name; @@ -107,45 +234,34 @@ static uint16_t att_read_callback( hci_con_handle_t connection_handle, fw_name = FIRMWARE_NAME; if (buffer) { - std::memcpy(buffer, reinterpret_cast(fw_name.c_str()), fw_name.size()); + std::memcpy(buffer, reinterpret_cast(fw_name.c_str()), fw_name.size());; } return static_cast(fw_name.size()); - case Handle::SETUP_PACKET: + case Handle::GET_SETUP: if (buffer) { - //App has already written a setup packet with the index - setup_packet_resp.max_gamepads = static_cast(MAX_GAMEPADS); - setup_packet_resp.index = setup_packet_.index; - setup_packet_resp.device_type = static_cast(UserSettings::get_instance().get_current_driver()); - setup_packet_resp.profile_id = UserSettings::get_instance().get_active_profile_id(setup_packet_.index); - - std::memcpy(buffer, &setup_packet_resp, sizeof(setup_packet_resp)); + buffer[0] = static_cast(UserSettings::get_instance().get_current_driver()); + buffer[1] = MAX_GAMEPADS; + buffer[2] = 0; + buffer[3] = UserSettings::get_instance().get_active_profile_id(0); } - return sizeof(setup_packet_); + return static_cast(sizeof(SetupPacket)); - case Handle::PROFILE_PT1: + case Handle::PROFILE: if (buffer) { - //App has already written the profile id it wants to the setup packet - profile = UserSettings::get_instance().get_profile_by_id(setup_packet_.profile_id); - std::memcpy(buffer, &profile, PACKET_LEN_MAX); + return profile_reader_.get_profile_data(buffer, buffer_size); } - return PACKET_LEN_MAX; + return profile_reader_.get_xfer_len(); - case Handle::PROFILE_PT2: + case Handle::GAMEPAD: if (buffer) { - std::memcpy(buffer, reinterpret_cast(&profile) + PACKET_LEN_MAX, PACKET_LEN_MAX); + I2CDriver::PacketIn packet_in = BTManager::get_instance().get_packet_in(0); + std::memcpy(buffer, &packet_in.dpad, 13); } - return PACKET_LEN_MAX; - - case Handle::PROFILE_PT3: - if (buffer) - { - std::memcpy(buffer, reinterpret_cast(&profile) + PACKET_LEN_MAX * 2, sizeof(UserProfile) - PACKET_LEN_MAX * 2); - } - return sizeof(UserProfile) - PACKET_LEN_MAX * 2; + return static_cast(13); default: break; @@ -153,84 +269,42 @@ static uint16_t att_read_callback( hci_con_handle_t connection_handle, return 0; } -static int att_write_callback(hci_con_handle_t connection_handle, - uint16_t att_handle, - uint16_t transaction_mode, - uint16_t offset, - uint8_t *buffer, - uint16_t buffer_size) +static int att_write_callback( hci_con_handle_t connection_handle, + uint16_t att_handle, + uint16_t transaction_mode, + uint16_t offset, + uint8_t *buffer, + uint16_t buffer_size) { - static UserProfile temp_profile; - static bool pending_write = false; - int ret = 0; switch (att_handle) { - case Handle::START_UPDATE: - pending_write = true; - break; - - case Handle::SETUP_PACKET: + case Handle::SETUP_READ: if ((ret = verify_write(buffer_size, sizeof(SetupPacket))) != 0) { break; } - - std::memcpy(&setup_packet_, buffer, buffer_size); - if (setup_packet_.index >= MAX_GAMEPADS) - { - setup_packet_.index = 0; - ret = ATT_ERROR_OUT_OF_RANGE; - } - if (setup_packet_.profile_id > UserSettings::MAX_PROFILES) - { - setup_packet_.profile_id = 1; - ret = ATT_ERROR_OUT_OF_RANGE; - } - if (ret) - { - break; - } - - if (pending_write) - { - //App wants to store a new device driver type - UserSettings::get_instance().store_driver_type(static_cast(setup_packet_.device_type)); - } + profile_reader_.set_setup_packet(*reinterpret_cast(buffer)); break; - case Handle::PROFILE_PT1: - if ((ret = verify_write(buffer_size, PACKET_LEN_MAX, pending_write, true)) != 0) + case Handle::SETUP_WRITE: + if ((ret = verify_write(buffer_size, sizeof(SetupPacket))) != 0) { break; } - std::memcpy(&temp_profile, buffer, buffer_size); + profile_writer_.set_setup_packet(*reinterpret_cast(buffer)); break; - case Handle::PROFILE_PT2: - if ((ret = verify_write(buffer_size, PACKET_LEN_MAX, pending_write, true)) != 0) + case Handle::PROFILE: + if ((ret = verify_write(buffer_size, profile_writer_.get_xfer_len())) != 0) { break; } - std::memcpy(reinterpret_cast(&temp_profile) + PACKET_LEN_MAX, buffer, buffer_size); - break; - - case Handle::PROFILE_PT3: - if ((ret = verify_write(buffer_size, sizeof(UserProfile) - PACKET_LEN_MAX * 2, pending_write, true)) != 0) + if (profile_writer_.set_profile_data(buffer, buffer_size) == sizeof(UserProfile)) { - break; + profile_writer_.commit_profile(); } - std::memcpy(reinterpret_cast(&temp_profile) + PACKET_LEN_MAX * 2, buffer, buffer_size); - break; - - case Handle::COMMIT_UPDATE: - if ((ret = verify_write(0, 0, pending_write, true)) != 0) - { - break; - } - UserSettings::get_instance().store_profile(setup_packet_.index, temp_profile); - pending_write = false; break; default: @@ -241,12 +315,8 @@ static int att_write_callback(hci_con_handle_t connection_handle, void init_server() { - UserSettings::get_instance().initialize_flash(); - - // setup ATT server att_server_init(profile_data, att_read_callback, att_write_callback); - // setup advertisements uint16_t adv_int_min = 0x0030; uint16_t adv_int_max = 0x0030; uint8_t adv_type = 0; diff --git a/Firmware/ESP32/main/BLEServer/BLEServer.h b/Firmware/ESP32/main/BLEServer/BLEServer.h index d72962e..6159971 100644 --- a/Firmware/ESP32/main/BLEServer/BLEServer.h +++ b/Firmware/ESP32/main/BLEServer/BLEServer.h @@ -3,6 +3,8 @@ #include +#include "Gamepad/Gamepad.h" + namespace BLEServer { void init_server(); diff --git a/Firmware/ESP32/main/BTManager/BTManager.cpp b/Firmware/ESP32/main/BTManager/BTManager.cpp index ad0b366..c17301d 100644 --- a/Firmware/ESP32/main/BTManager/BTManager.cpp +++ b/Firmware/ESP32/main/BTManager/BTManager.cpp @@ -7,6 +7,7 @@ #include "Board/ogxm_log.h" #include "Board/board_api.h" #include "BTManager/BTManager.h" +#include "BLEServer/BLEServer.h" void BTManager::run_task() { @@ -23,7 +24,8 @@ void BTManager::run_task() static_cast(CONFIG_I2C_PORT), static_cast(CONFIG_I2C_SDA_PIN), static_cast(CONFIG_I2C_SCL_PIN), - CONFIG_I2C_BAUDRATE); + CONFIG_I2C_BAUDRATE + ); xTaskCreatePinnedToCore( [](void* parameter) @@ -35,7 +37,8 @@ void BTManager::run_task() nullptr, configMAX_PRIORITIES-8, nullptr, - 1 ); + 1 + ); btstack_init(); @@ -54,6 +57,8 @@ void BTManager::run_task() btstack_run_loop_set_timer(&driver_update_timer, UserSettings::GP_CHECK_DELAY_MS); btstack_run_loop_add_timer(&driver_update_timer); + BLEServer::init_server(); + //Doesn't return btstack_run_loop_execute(); } @@ -102,8 +107,6 @@ void BTManager::check_led_cb(btstack_timer_source *ts) void BTManager::send_driver_type(DeviceDriverType driver_type) { - OGXM_LOG("BP32: Sending driver type: %s\n", DRIVER_NAME(driver_type).c_str()); - if constexpr (I2CDriver::MULTI_SLAVE) { for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) @@ -242,4 +245,13 @@ void BTManager::manage_connection(uint8_t index, bool connected) packet_in.index = index; i2c_driver_.write_packet(I2CDriver::MULTI_SLAVE ? packet_in.index + 1 : 0x01, packet_in); } +} + +I2CDriver::PacketIn BTManager::get_packet_in(uint8_t index) +{ + if (index >= devices_.size()) + { + return I2CDriver::PacketIn(); + } + return devices_[index].packet_in; } \ No newline at end of file diff --git a/Firmware/ESP32/main/BTManager/BTManager.h b/Firmware/ESP32/main/BTManager/BTManager.h index c5f9ce4..5d7d08b 100644 --- a/Firmware/ESP32/main/BTManager/BTManager.h +++ b/Firmware/ESP32/main/BTManager/BTManager.h @@ -9,7 +9,7 @@ #include #include "I2CDriver/I2CDriver.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" class BTManager { @@ -23,6 +23,7 @@ public: void run_task(); bool any_connected(); bool is_connected(uint8_t index); + I2CDriver::PacketIn get_packet_in(uint8_t index); private: BTManager() = default; diff --git a/Firmware/ESP32/main/BTManager/BTManager_BP32.cpp b/Firmware/ESP32/main/BTManager/BTManager_BP32.cpp index 016e9aa..c1cc1f7 100644 --- a/Firmware/ESP32/main/BTManager/BTManager_BP32.cpp +++ b/Firmware/ESP32/main/BTManager/BTManager_BP32.cpp @@ -5,6 +5,7 @@ #include "Board/ogxm_log.h" #include "BTManager/BTManager.h" +#include "BLEServer/BLEServer.h" void BTManager::init(int argc, const char** arg_V) { @@ -123,10 +124,16 @@ void BTManager::controller_data_cb(uni_hid_device_t* bp_device, uni_controller_t packet_in.trigger_l = mapper.scale_trigger_l<10>(static_cast(uni_gp->brake)); packet_in.trigger_r = mapper.scale_trigger_r<10>(static_cast(uni_gp->throttle)); - packet_in.joystick_lx = mapper.scale_joystick_lx<10>(uni_gp->axis_x); - packet_in.joystick_ly = mapper.scale_joystick_ly<10>(uni_gp->axis_y); - packet_in.joystick_rx = mapper.scale_joystick_rx<10>(uni_gp->axis_rx); - packet_in.joystick_ry = mapper.scale_joystick_ry<10>(uni_gp->axis_ry); + // auto joy_l = mapper.scale_joystick_l<10>(uni_gp->axis_x, uni_gp->axis_y); + // auto joy_r = mapper.scale_joystick_r<10>(uni_gp->axis_rx, uni_gp->axis_ry); + + // packet_in.joystick_lx = joy_l.first; + // packet_in.joystick_ly = joy_l.second; + // packet_in.joystick_rx = joy_r.first; + // packet_in.joystick_ry = joy_r.second; + + std::tie(packet_in.joystick_lx, packet_in.joystick_ly) = mapper.scale_joystick_l<10>(uni_gp->axis_x, uni_gp->axis_y); + std::tie(packet_in.joystick_rx, packet_in.joystick_ry) = mapper.scale_joystick_r<10>(uni_gp->axis_rx, uni_gp->axis_ry); i2c_driver_.write_packet(I2CDriver::MULTI_SLAVE ? packet_in.index + 1 : 0x01, packet_in); diff --git a/Firmware/ESP32/main/Board/ogxm_log.h b/Firmware/ESP32/main/Board/ogxm_log.h index d67985d..1913adf 100644 --- a/Firmware/ESP32/main/Board/ogxm_log.h +++ b/Firmware/ESP32/main/Board/ogxm_log.h @@ -33,9 +33,20 @@ namespace OGXM std::ostringstream hex_stream; hex_stream << std::hex << std::setfill('0'); + int char_num = 0; for (uint16_t i = 0; i < len; ++i) { hex_stream << std::setw(2) << static_cast(data[i]) << " "; + char_num++; + if (char_num == 16) + { + hex_stream << "\n"; + char_num = 0; + } + } + if (char_num != 0) + { + hex_stream << "\n"; } log(hex_stream.str()); diff --git a/Firmware/ESP32/main/CMakeLists.txt b/Firmware/ESP32/main/CMakeLists.txt index bee4fda..20603a4 100644 --- a/Firmware/ESP32/main/CMakeLists.txt +++ b/Firmware/ESP32/main/CMakeLists.txt @@ -11,13 +11,16 @@ idf_component_register( "I2CDriver/I2CDriver.cpp" "UserSettings/UserSettings.cpp" "UserSettings/UserProfile.cpp" + "UserSettings/TriggerSettings.cpp" + "UserSettings/JoystickSettings.cpp" INCLUDE_DIRS "." REQUIRES bluepad32 btstack driver - nvs_flash + nvs_flash + libfixmath ) target_compile_definitions(${COMPONENT_LIB} PRIVATE diff --git a/Firmware/ESP32/main/Gamepad.h b/Firmware/ESP32/main/Gamepad.h deleted file mode 100644 index c66e2ff..0000000 --- a/Firmware/ESP32/main/Gamepad.h +++ /dev/null @@ -1,364 +0,0 @@ -#ifndef GAMEPAD_H -#define GAMEPAD_H - -#include - -#include "sdkconfig.h" -#include "Range.h" -#include "UserSettings/UserProfile.h" -#include "UserSettings/UserSettings.h" -#include "Board/ogxm_log.h" - -#define MAX_GAMEPADS CONFIG_BLUEPAD32_MAX_DEVICES -static_assert( MAX_GAMEPADS > 0 && - MAX_GAMEPADS <= 4, - "MAX_GAMEPADS must be between 1 and 4"); - -namespace Gamepad -{ - static constexpr uint8_t DPAD_UP = 0x01; - static constexpr uint8_t DPAD_DOWN = 0x02; - static constexpr uint8_t DPAD_LEFT = 0x04; - static constexpr uint8_t DPAD_RIGHT = 0x08; - static constexpr uint8_t DPAD_UP_LEFT = DPAD_UP | DPAD_LEFT; - static constexpr uint8_t DPAD_UP_RIGHT = DPAD_UP | DPAD_RIGHT; - static constexpr uint8_t DPAD_DOWN_LEFT = DPAD_DOWN | DPAD_LEFT; - static constexpr uint8_t DPAD_DOWN_RIGHT = DPAD_DOWN | DPAD_RIGHT; - static constexpr uint8_t DPAD_NONE = 0x00; - - static constexpr uint16_t BUTTON_A = 0x0001; - static constexpr uint16_t BUTTON_B = 0x0002; - static constexpr uint16_t BUTTON_X = 0x0004; - static constexpr uint16_t BUTTON_Y = 0x0008; - static constexpr uint16_t BUTTON_L3 = 0x0010; - static constexpr uint16_t BUTTON_R3 = 0x0020; - static constexpr uint16_t BUTTON_BACK = 0x0040; - static constexpr uint16_t BUTTON_START = 0x0080; - static constexpr uint16_t BUTTON_LB = 0x0100; - static constexpr uint16_t BUTTON_RB = 0x0200; - static constexpr uint16_t BUTTON_SYS = 0x0400; - static constexpr uint16_t BUTTON_MISC = 0x0800; -} - -class GamepadMapper -{ -public: - uint8_t DPAD_UP = Gamepad::DPAD_UP ; - uint8_t DPAD_DOWN = Gamepad::DPAD_DOWN ; - uint8_t DPAD_LEFT = Gamepad::DPAD_LEFT ; - uint8_t DPAD_RIGHT = Gamepad::DPAD_RIGHT ; - uint8_t DPAD_UP_LEFT = Gamepad::DPAD_UP_LEFT ; - uint8_t DPAD_UP_RIGHT = Gamepad::DPAD_UP_RIGHT ; - uint8_t DPAD_DOWN_LEFT = Gamepad::DPAD_DOWN_LEFT ; - uint8_t DPAD_DOWN_RIGHT = Gamepad::DPAD_DOWN_RIGHT; - uint8_t DPAD_NONE = Gamepad::DPAD_NONE ; - - uint16_t BUTTON_A = Gamepad::BUTTON_A ; - uint16_t BUTTON_B = Gamepad::BUTTON_B ; - uint16_t BUTTON_X = Gamepad::BUTTON_X ; - uint16_t BUTTON_Y = Gamepad::BUTTON_Y ; - uint16_t BUTTON_L3 = Gamepad::BUTTON_L3 ; - uint16_t BUTTON_R3 = Gamepad::BUTTON_R3 ; - uint16_t BUTTON_BACK = Gamepad::BUTTON_BACK ; - uint16_t BUTTON_START = Gamepad::BUTTON_START; - uint16_t BUTTON_LB = Gamepad::BUTTON_LB ; - uint16_t BUTTON_RB = Gamepad::BUTTON_RB ; - uint16_t BUTTON_SYS = Gamepad::BUTTON_SYS ; - uint16_t BUTTON_MISC = Gamepad::BUTTON_MISC ; - - GamepadMapper() = default; - ~GamepadMapper() = default; - - void set_profile(const UserProfile& profile) - { - set_profile_options(profile); - set_profile_mappings(profile); - set_profile_deadzones(profile); - } - - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ - template - inline int16_t scale_joystick_rx(T value) const - { - int16_t joy_value = 0; - if constexpr (bits > 0) - { - joy_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - joy_value = Range::scale(value); - } - else - { - joy_value = value; - } - if (joy_value > dz_.joystick_r_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_r_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_r_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_r_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return joy_value; - } - - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ - template - inline int16_t scale_joystick_ry(T value, bool invert = false) const - { - int16_t joy_value = 0; - if constexpr (bits > 0) - { - joy_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - joy_value = Range::scale(value); - } - else - { - joy_value = value; - } - if (joy_value > dz_.joystick_r_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_r_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_r_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_r_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return profile_invert_ry_ ? (invert ? joy_value : Range::invert(joy_value)) : (invert ? Range::invert(joy_value) : joy_value); - } - - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ - template - inline int16_t scale_joystick_lx(T value) const - { - int16_t joy_value = 0; - if constexpr (bits > 0) - { - joy_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - joy_value = Range::scale(value); - } - else - { - joy_value = value; - } - if (joy_value > dz_.joystick_l_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_l_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_l_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_l_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return joy_value; - } - - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ - template - inline int16_t scale_joystick_ly(T value, bool invert = false) const - { - int16_t joy_value = 0; - if constexpr (bits > 0) - { - joy_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - joy_value = Range::scale(value); - } - else - { - joy_value = value; - } - if (joy_value > dz_.joystick_l_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_l_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_l_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_l_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return profile_invert_ly_ ? (invert ? joy_value : Range::invert(joy_value)) : (invert ? Range::invert(joy_value) : joy_value); - } - - /* Get trigger value adjusted for deadzones, scaling, and inversion. - param is optional, used for scaling speicifc bit values - as opposed to full range values */ - template - inline uint8_t scale_trigger_l(T value) const - { - uint8_t trigger_value = 0; - if constexpr (bits > 0) - { - trigger_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - trigger_value = Range::scale(value); - } - else - { - trigger_value = value; - } - return trigger_value > dz_.trigger_l ? Range::scale(trigger_value, dz_.trigger_l, Range::MAX) : 0; - } - - /* Get trigger value adjusted for deadzones, scaling, and inversion. - param is optional, used for scaling speicifc bit values - as opposed to full range values */ - template - inline uint8_t scale_trigger_r(T value) const - { - uint8_t trigger_value = 0; - if constexpr (bits > 0) - { - trigger_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - trigger_value = Range::scale(value); - } - else - { - trigger_value = value; - } - return trigger_value > dz_.trigger_l ? Range::scale(trigger_value, dz_.trigger_l, Range::MAX) : 0; - } - -private: - bool profile_invert_ly_{false}; - bool profile_invert_ry_{false}; - - struct Deadzones - { - uint8_t trigger_l{0}; - uint8_t trigger_r{0}; - int16_t joystick_l_neg{0}; - int16_t joystick_l_pos{0}; - int16_t joystick_r_neg{0}; - int16_t joystick_r_pos{0}; - } dz_; - - void set_profile_options(const UserProfile& profile) - { - profile_invert_ly_ = profile.invert_ly ? true : false; - profile_invert_ry_ = profile.invert_ry ? true : false; - } - - void set_profile_mappings(const UserProfile& profile) - { - DPAD_UP = profile.dpad_up; - DPAD_DOWN = profile.dpad_down; - DPAD_LEFT = profile.dpad_left; - DPAD_RIGHT = profile.dpad_right; - DPAD_UP_LEFT = profile.dpad_up | profile.dpad_left; - DPAD_UP_RIGHT = profile.dpad_up | profile.dpad_right; - DPAD_DOWN_LEFT = profile.dpad_down | profile.dpad_left; - DPAD_DOWN_RIGHT = profile.dpad_down | profile.dpad_right; - DPAD_NONE = 0; - - BUTTON_A = profile.button_a; - BUTTON_B = profile.button_b; - BUTTON_X = profile.button_x; - BUTTON_Y = profile.button_y; - BUTTON_L3 = profile.button_l3; - BUTTON_R3 = profile.button_r3; - BUTTON_BACK = profile.button_back; - BUTTON_START = profile.button_start; - BUTTON_LB = profile.button_lb; - BUTTON_RB = profile.button_rb; - BUTTON_SYS = profile.button_sys; - BUTTON_MISC = profile.button_misc; - - OGXM_LOG("Mappings: A: %i B: %i X: %i Y: %i L3: %i R3: %i BACK: %i START: %i LB: %i RB: %i SYS: %i MISC: %i\n", - BUTTON_A, BUTTON_B, BUTTON_X, BUTTON_Y, BUTTON_L3, BUTTON_R3, BUTTON_BACK, BUTTON_START, BUTTON_LB, BUTTON_RB, BUTTON_SYS, BUTTON_MISC); - } - - void set_profile_deadzones(const UserProfile& profile) //Deadzones in the profile are 0-255 (0-100%) - { - dz_.trigger_l = profile.dz_trigger_l; - dz_.trigger_r = profile.dz_trigger_r; - - dz_.joystick_l_pos = profile.dz_joystick_l ? Range::scale(static_cast(profile.dz_joystick_l / 2)) : 0; - dz_.joystick_l_neg = Range::invert(dz_.joystick_l_pos); - dz_.joystick_r_pos = profile.dz_joystick_r ? Range::scale(static_cast(profile.dz_joystick_r / 2)) : 0; - dz_.joystick_r_neg = Range::invert(dz_.joystick_r_pos); - - OGXM_LOG("Deadzones: TL: %i TR: %i JL: %i JR: %i\n", - dz_.trigger_l, dz_.trigger_r, dz_.joystick_l_pos, dz_.joystick_r_pos); - } - -}; // class GamepadMapper - -#endif // GAMEPAD_H \ No newline at end of file diff --git a/Firmware/ESP32/main/Gamepad/Gamepad.h b/Firmware/ESP32/main/Gamepad/Gamepad.h new file mode 100644 index 0000000..7fc2660 --- /dev/null +++ b/Firmware/ESP32/main/Gamepad/Gamepad.h @@ -0,0 +1,450 @@ +#ifndef GAMEPAD_H +#define GAMEPAD_H + +#include + +#include "sdkconfig.h" +#include "Gamepad/Range.h" +#include "Gamepad/fix16ext.h" +#include "UserSettings/UserProfile.h" +#include "UserSettings/UserSettings.h" +#include "Board/ogxm_log.h" + +#define MAX_GAMEPADS CONFIG_BLUEPAD32_MAX_DEVICES +static_assert( MAX_GAMEPADS > 0 && + MAX_GAMEPADS <= 4, + "MAX_GAMEPADS must be between 1 and 4"); + +namespace Gamepad +{ + static constexpr uint8_t DPAD_UP = 0x01; + static constexpr uint8_t DPAD_DOWN = 0x02; + static constexpr uint8_t DPAD_LEFT = 0x04; + static constexpr uint8_t DPAD_RIGHT = 0x08; + static constexpr uint8_t DPAD_UP_LEFT = DPAD_UP | DPAD_LEFT; + static constexpr uint8_t DPAD_UP_RIGHT = DPAD_UP | DPAD_RIGHT; + static constexpr uint8_t DPAD_DOWN_LEFT = DPAD_DOWN | DPAD_LEFT; + static constexpr uint8_t DPAD_DOWN_RIGHT = DPAD_DOWN | DPAD_RIGHT; + static constexpr uint8_t DPAD_NONE = 0x00; + + static constexpr uint16_t BUTTON_A = 0x0001; + static constexpr uint16_t BUTTON_B = 0x0002; + static constexpr uint16_t BUTTON_X = 0x0004; + static constexpr uint16_t BUTTON_Y = 0x0008; + static constexpr uint16_t BUTTON_L3 = 0x0010; + static constexpr uint16_t BUTTON_R3 = 0x0020; + static constexpr uint16_t BUTTON_BACK = 0x0040; + static constexpr uint16_t BUTTON_START = 0x0080; + static constexpr uint16_t BUTTON_LB = 0x0100; + static constexpr uint16_t BUTTON_RB = 0x0200; + static constexpr uint16_t BUTTON_SYS = 0x0400; + static constexpr uint16_t BUTTON_MISC = 0x0800; + + static constexpr uint8_t ANALOG_OFF_UP = 0; + static constexpr uint8_t ANALOG_OFF_DOWN = 1; + static constexpr uint8_t ANALOG_OFF_LEFT = 2; + static constexpr uint8_t ANALOG_OFF_RIGHT = 3; + static constexpr uint8_t ANALOG_OFF_A = 4; + static constexpr uint8_t ANALOG_OFF_B = 5; + static constexpr uint8_t ANALOG_OFF_X = 6; + static constexpr uint8_t ANALOG_OFF_Y = 7; + static constexpr uint8_t ANALOG_OFF_LB = 8; + static constexpr uint8_t ANALOG_OFF_RB = 9; +} + +class GamepadMapper +{ +public: + uint8_t DPAD_UP = Gamepad::DPAD_UP ; + uint8_t DPAD_DOWN = Gamepad::DPAD_DOWN ; + uint8_t DPAD_LEFT = Gamepad::DPAD_LEFT ; + uint8_t DPAD_RIGHT = Gamepad::DPAD_RIGHT ; + uint8_t DPAD_UP_LEFT = Gamepad::DPAD_UP_LEFT ; + uint8_t DPAD_UP_RIGHT = Gamepad::DPAD_UP_RIGHT ; + uint8_t DPAD_DOWN_LEFT = Gamepad::DPAD_DOWN_LEFT ; + uint8_t DPAD_DOWN_RIGHT = Gamepad::DPAD_DOWN_RIGHT; + uint8_t DPAD_NONE = Gamepad::DPAD_NONE ; + + uint16_t BUTTON_A = Gamepad::BUTTON_A ; + uint16_t BUTTON_B = Gamepad::BUTTON_B ; + uint16_t BUTTON_X = Gamepad::BUTTON_X ; + uint16_t BUTTON_Y = Gamepad::BUTTON_Y ; + uint16_t BUTTON_L3 = Gamepad::BUTTON_L3 ; + uint16_t BUTTON_R3 = Gamepad::BUTTON_R3 ; + uint16_t BUTTON_BACK = Gamepad::BUTTON_BACK ; + uint16_t BUTTON_START = Gamepad::BUTTON_START; + uint16_t BUTTON_LB = Gamepad::BUTTON_LB ; + uint16_t BUTTON_RB = Gamepad::BUTTON_RB ; + uint16_t BUTTON_SYS = Gamepad::BUTTON_SYS ; + uint16_t BUTTON_MISC = Gamepad::BUTTON_MISC ; + + GamepadMapper() = default; + ~GamepadMapper() = default; + + void set_profile(const UserProfile& profile) + { + set_profile_settings(profile); + set_profile_mappings(profile); + } + + template + inline std::pair scale_joystick_r(T x, T y, bool invert_y = false) const + { + int16_t joy_x = 0; + int16_t joy_y = 0; + if constexpr (bits > 0) + { + joy_x = Range::scale_from_bits(x); + joy_y = Range::scale_from_bits(y); + } + else if constexpr (!std::is_same_v) + { + joy_x = Range::scale(x); + joy_y = Range::scale(y); + } + else + { + joy_x = x; + joy_y = y; + } + + return joy_settings_r_en_ + ? apply_joystick_settings(joy_x, joy_y, joy_settings_r_, invert_y) + : std::make_pair(joy_x, (invert_y ? Range::invert(joy_y) : joy_y)); + } + + template + inline std::pair scale_joystick_l(T x, T y, bool invert_y = false) const + { + int16_t joy_x = 0; + int16_t joy_y = 0; + if constexpr (bits > 0) + { + joy_x = Range::scale_from_bits(x); + joy_y = Range::scale_from_bits(y); + } + else if constexpr (!std::is_same_v) + { + joy_x = Range::scale(x); + joy_y = Range::scale(y); + } + else + { + joy_x = x; + joy_y = y; + } + + return joy_settings_l_en_ + ? apply_joystick_settings(joy_x, joy_y, joy_settings_l_, invert_y) + : std::make_pair(joy_x, (invert_y ? Range::invert(joy_y) : joy_y)); + } + + template + inline uint8_t scale_trigger_l(T value) const + { + uint8_t trigger_value = 0; + if constexpr (bits > 0) + { + trigger_value = Range::scale_from_bits(value); + } + else if constexpr (!std::is_same_v) + { + trigger_value = Range::scale(value); + } + else + { + trigger_value = value; + } + return trig_settings_l_en_ + ? apply_trigger_settings(trigger_value, trig_settings_l_) + : trigger_value; + } + + template + inline uint8_t scale_trigger_r(T value) const + { + uint8_t trigger_value = 0; + if constexpr (bits > 0) + { + trigger_value = Range::scale_from_bits(value); + } + else if constexpr (!std::is_same_v) + { + trigger_value = Range::scale(value); + } + else + { + trigger_value = value; + } + return trig_settings_r_en_ + ? apply_trigger_settings(trigger_value, trig_settings_r_) + : trigger_value; + } + +private: + JoystickSettings joy_settings_l_; + JoystickSettings joy_settings_r_; + TriggerSettings trig_settings_l_; + TriggerSettings trig_settings_r_; + + bool joy_settings_l_en_{false}; + bool joy_settings_r_en_{false}; + bool trig_settings_l_en_{false}; + bool trig_settings_r_en_{false}; + + void set_profile_settings(const UserProfile& profile) + { + if ((joy_settings_l_en_ = !joy_settings_l_.is_same(profile.joystick_settings_l))) + { + joy_settings_l_.set_from_raw(profile.joystick_settings_l); + //This needs to be addressed in the webapp, just multiply here for now + joy_settings_l_.axis_restrict *= static_cast(100); + joy_settings_l_.angle_restrict *= static_cast(100); + joy_settings_l_.anti_dz_angular *= static_cast(100); + } + if ((joy_settings_r_en_ = !joy_settings_r_.is_same(profile.joystick_settings_r))) + { + joy_settings_r_.set_from_raw(profile.joystick_settings_r); + //This needs to be addressed in the webapp, just multiply here for now + joy_settings_r_.axis_restrict *= static_cast(100); + joy_settings_r_.angle_restrict *= static_cast(100); + joy_settings_r_.anti_dz_angular *= static_cast(100); + } + if ((trig_settings_l_en_ = !trig_settings_l_.is_same(profile.trigger_settings_l))) + { + trig_settings_l_.set_from_raw(profile.trigger_settings_l); + } + if ((trig_settings_r_en_ = !trig_settings_r_.is_same(profile.trigger_settings_r))) + { + trig_settings_r_.set_from_raw(profile.trigger_settings_r); + } + + OGXM_LOG("GamepadMapper: JoyL: %s, JoyR: %s, TrigL: %s, TrigR: %s\n", + joy_settings_l_en_ ? "Enabled" : "Disabled", + joy_settings_r_en_ ? "Enabled" : "Disabled", + trig_settings_l_en_ ? "Enabled" : "Disabled", + trig_settings_r_en_ ? "Enabled" : "Disabled"); + } + + void set_profile_mappings(const UserProfile& profile) + { + DPAD_UP = profile.dpad_up; + DPAD_DOWN = profile.dpad_down; + DPAD_LEFT = profile.dpad_left; + DPAD_RIGHT = profile.dpad_right; + DPAD_UP_LEFT = profile.dpad_up | profile.dpad_left; + DPAD_UP_RIGHT = profile.dpad_up | profile.dpad_right; + DPAD_DOWN_LEFT = profile.dpad_down | profile.dpad_left; + DPAD_DOWN_RIGHT = profile.dpad_down | profile.dpad_right; + DPAD_NONE = 0; + + BUTTON_A = profile.button_a; + BUTTON_B = profile.button_b; + BUTTON_X = profile.button_x; + BUTTON_Y = profile.button_y; + BUTTON_L3 = profile.button_l3; + BUTTON_R3 = profile.button_r3; + BUTTON_BACK = profile.button_back; + BUTTON_START = profile.button_start; + BUTTON_LB = profile.button_lb; + BUTTON_RB = profile.button_rb; + BUTTON_SYS = profile.button_sys; + BUTTON_MISC = profile.button_misc; + } + + static inline std::pair apply_joystick_settings( + int16_t gp_joy_x, + int16_t gp_joy_y, + const JoystickSettings& set, + bool invert_y) + { + static const Fix16 + FIX_0(0.0f), + FIX_1(1.0f), + FIX_2(2.0f), + FIX_45(45.0f), + FIX_90(90.0f), + FIX_100(100.0f), + FIX_180(180.0f), + FIX_EPSILON(0.0001f), + FIX_EPSILON2(0.001f), + FIX_ELLIPSE_DEF(1.570796f), + FIX_DIAG_DIVISOR(0.29289f); + + Fix16 x = (set.invert_x ? Fix16(Range::invert(gp_joy_x)) : Fix16(gp_joy_x)) / Range::MAX; + Fix16 y = ((set.invert_y ^ invert_y) ? Fix16(Range::invert(gp_joy_y)) : Fix16(gp_joy_y)) / Range::MAX; + + const Fix16 abs_x = fix16::abs(x); + const Fix16 abs_y = fix16::abs(y); + const Fix16 inv_axis_restrict = FIX_1 / (FIX_1 - set.axis_restrict); + + Fix16 rAngle = (abs_x < FIX_EPSILON) + ? FIX_90 + : fix16::rad2deg(fix16::abs(fix16::atan(y / x))); + + Fix16 axial_x = (abs_x <= set.axis_restrict && rAngle > FIX_45) + ? FIX_0 + : ((abs_x - set.axis_restrict) * inv_axis_restrict); + + Fix16 axial_y = (abs_y <= set.axis_restrict && rAngle <= FIX_45) + ? FIX_0 + : ((abs_y - set.axis_restrict) * inv_axis_restrict); + + Fix16 in_magnitude = fix16::sqrt(fix16::sq(axial_x) + fix16::sq(axial_y)); + + if (in_magnitude < set.dz_inner) + { + return { 0, 0 }; + } + + Fix16 angle = + fix16::abs(axial_x) < FIX_EPSILON + ? FIX_90 + : fix16::rad2deg(fix16::abs(fix16::atan(axial_y / axial_x))); + + Fix16 anti_r_scale = (set.anti_dz_square_y_scale == FIX_0) ? set.anti_dz_square : set.anti_dz_square_y_scale; + Fix16 anti_dz_c = set.anti_dz_circle; + + if (anti_r_scale > FIX_0 && anti_dz_c > FIX_0) + { + Fix16 anti_ellip_scale = anti_ellip_scale / anti_dz_c; + Fix16 ellipse_angle = fix16::atan((FIX_1 / anti_ellip_scale) * fix16::tan(fix16::rad2deg(rAngle))); + ellipse_angle = (ellipse_angle < FIX_0) ? FIX_ELLIPSE_DEF : ellipse_angle; + + Fix16 ellipse_x = fix16::cos(ellipse_angle); + Fix16 ellipse_y = fix16::sqrt(fix16::sq(anti_ellip_scale) * (FIX_1 - fix16::sq(ellipse_x))); + anti_dz_c *= fix16::sqrt(fix16::sq(ellipse_x) + fix16::sq(ellipse_y)); + } + + if (anti_dz_c > FIX_0) + { + anti_dz_c = anti_dz_c / ((anti_dz_c * (FIX_1 - set.anti_dz_circle / set.dz_outer)) / (anti_dz_c * (FIX_1 - set.anti_dz_square))); + } + + if (abs_x > set.axis_restrict && abs_y > set.axis_restrict) + { + const Fix16 FIX_ANGLE_MAX = set.angle_restrict / 2.0f; + + if (angle > FIX_0 && angle < FIX_ANGLE_MAX) + { + angle = FIX_0; + } + if (angle > (FIX_90 - FIX_ANGLE_MAX)) + { + angle = FIX_90; + } + if (angle > FIX_ANGLE_MAX && angle < (FIX_90 - FIX_ANGLE_MAX)) + { + angle = ((angle - FIX_ANGLE_MAX) * FIX_90) / ((FIX_90 - FIX_ANGLE_MAX) - FIX_ANGLE_MAX); + } + } + + Fix16 ref_angle = (angle < FIX_EPSILON2) ? FIX_0 : angle; + Fix16 diagonal = (angle > FIX_45) ? (((angle - FIX_45) * (-FIX_45)) / FIX_45) + FIX_45 : angle; + + const Fix16 angle_comp = set.angle_restrict / FIX_2; + + if (angle < FIX_90 && angle > FIX_0) + { + angle = ((angle * ((FIX_90 - angle_comp) - angle_comp)) / FIX_90) + angle_comp; + } + + if (axial_x < FIX_0 && axial_y > FIX_0) + { + angle = -angle; + } + if (axial_x > FIX_0 && axial_y < FIX_0) + { + angle = angle - FIX_180; + } + if (axial_x < FIX_0 && axial_y < FIX_0) + { + angle = angle + FIX_180; + } + + //Deadzone Warp + Fix16 out_magnitude = (in_magnitude - set.dz_inner) / (set.anti_dz_outer - set.dz_inner); + out_magnitude = fix16::pow(out_magnitude, (FIX_1 / set.curve)) * (set.dz_outer - anti_dz_c) + anti_dz_c; + out_magnitude = (out_magnitude > set.dz_outer && !set.uncap_radius) ? set.dz_outer : out_magnitude; + + Fix16 d_scale = (((out_magnitude - anti_dz_c) * (set.diag_scale_max - set.diag_scale_min)) / (set.dz_outer - anti_dz_c)) + set.diag_scale_min; + Fix16 c_scale = (diagonal * (FIX_1 / fix16::sqrt(FIX_2))) / FIX_45; //Both these lines scale the intensity of the warping + c_scale = FIX_1 - fix16::sqrt(FIX_1 - c_scale * c_scale); //based on a circular curve to the perfect diagonal + d_scale = (c_scale * (d_scale - FIX_1)) / FIX_DIAG_DIVISOR + FIX_1; + + out_magnitude = out_magnitude * d_scale; + + //Scaling values for square antideadzone + Fix16 new_x = fix16::cos(fix16::deg2rad(angle)) * out_magnitude; + Fix16 new_y = fix16::sin(fix16::deg2rad(angle)) * out_magnitude; + + //Magic angle wobble fix by user ME. + // if (angle > 45.0 && angle < 225.0) { + // newX = inv(Math.sin(deg2rad(angle - 90.0)))*outputMagnitude; + // newY = inv(Math.cos(deg2rad(angle - 270.0)))*outputMagnitude; + // } + + //Square antideadzone scaling + Fix16 output_x = fix16::abs(new_x) * (FIX_1 - set.anti_dz_square / set.dz_outer) + set.anti_dz_square; + if (x < FIX_0) + { + output_x = -output_x; + } + if (ref_angle == FIX_90) + { + output_x = FIX_0; + } + + Fix16 output_y = fix16::abs(new_y) * (FIX_1 - anti_r_scale / set.dz_outer) + anti_r_scale; + if (y < FIX_0) + { + output_y = -output_y; + } + if (ref_angle == FIX_0) + { + output_y = FIX_0; + } + + output_x = fix16::clamp(output_x, -FIX_1, FIX_1) * Range::MAX; + output_y = fix16::clamp(output_y, -FIX_1, FIX_1) * Range::MAX; + + return { static_cast(fix16_to_int(output_x)), static_cast(fix16_to_int(output_y)) }; + } + + static inline uint8_t apply_trigger_settings(uint8_t value, const TriggerSettings& set) + { + Fix16 abs_value = fix16::abs(Fix16(static_cast(value)) / static_cast(Range::MAX)); + + if (abs_value < set.dz_inner) + { + return 0; + } + + static const Fix16 + FIX_0(0.0f), + FIX_1(1.0f), + FIX_2(2.0f); + + Fix16 value_out = (abs_value - set.dz_inner) / (set.anti_dz_outer - set.dz_inner); + value_out = fix16::clamp(value_out, FIX_0, FIX_1); + + if (set.anti_dz_inner > FIX_0) + { + value_out = set.anti_dz_inner + (FIX_1 - set.anti_dz_inner) * value_out; + } + if (set.curve != FIX_1) + { + value_out = fix16::pow(value_out, FIX_1 / set.curve); + } + if (set.anti_dz_outer < FIX_1) + { + value_out = fix16::clamp(value_out * (FIX_1 / (FIX_1 - set.anti_dz_outer)), FIX_0, FIX_1); + } + + value_out *= set.dz_outer; + return static_cast(fix16_to_int(value_out * static_cast(Range::MAX))); + } + +}; // class GamepadMapper + +#endif // GAMEPAD_H \ No newline at end of file diff --git a/Firmware/RP2040/src/Range.h b/Firmware/ESP32/main/Gamepad/Range.h similarity index 65% rename from Firmware/RP2040/src/Range.h rename to Firmware/ESP32/main/Gamepad/Range.h index 115db96..667bc61 100644 --- a/Firmware/RP2040/src/Range.h +++ b/Firmware/ESP32/main/Gamepad/Range.h @@ -79,77 +79,31 @@ namespace Range { requires std::is_integral_v && std::is_integral_v static inline To clamp(From value) { - if constexpr (std::is_signed_v != std::is_signed_v) - { - using CommonType = std::common_type_t; - return static_cast((static_cast(value) < static_cast(Range::MIN)) - ? Range::MIN - : (static_cast(value) > static_cast(Range::MAX)) - ? Range::MAX - : static_cast(value)); - } - else - { - return static_cast((value < Range::MIN) - ? Range::MIN - : (value > Range::MAX) - ? Range::MAX - : value); - } + return static_cast((value < Range::MIN) + ? Range::MIN + : (value > Range::MAX) + ? Range::MAX + : value); } template - requires std::is_integral_v static inline T clamp(T value, T min, T max) { return (value < min) ? min : (value > max) ? max : value; } + template + static inline To clamp(From value, To min_to, To max_to) + { + return (value < min_to) ? min_to : (value > max_to) ? max_to : static_cast(value); + } + template requires std::is_integral_v && std::is_integral_v - static inline To scale(From value, From min_from, From max_from, To min_to, To max_to) + static constexpr To scale(From value, From min_from, From max_from, To min_to, To max_to) { - if constexpr (std::is_unsigned_v && std::is_unsigned_v) - { - // Both unsigned - uint64_t scaled = static_cast(value - min_from) * - (max_to - min_to) / - (max_from - min_from) + min_to; - return static_cast(scaled); - } - else if constexpr (std::is_signed_v && std::is_unsigned_v) - { - // From signed, To unsigned - uint64_t shift_from = static_cast(-min_from); - uint64_t u_value = static_cast(value) + shift_from; - uint64_t u_min_from = static_cast(min_from) + shift_from; - uint64_t u_max_from = static_cast(max_from) + shift_from; - - uint64_t scaled = (u_value - u_min_from) * - (max_to - min_to) / - (u_max_from - u_min_from) + min_to; - return static_cast(scaled); - } - else if constexpr (std::is_unsigned_v && std::is_signed_v) - { - // From unsigned, To signed - uint64_t shift_to = static_cast(-min_to); - uint64_t scaled = static_cast(value - min_from) * - (static_cast(max_to) + shift_to - static_cast(min_to) - shift_to) / - (max_from - min_from) + static_cast(min_to) + shift_to; - return static_cast(scaled - shift_to); - } - else - { - // Both signed - int64_t shift_from = -min_from; - int64_t shift_to = -min_to; - - int64_t scaled = (static_cast(value) + shift_from - (min_from + shift_from)) * - (max_to + shift_to - (min_to + shift_to)) / - (max_from - min_from) + (min_to + shift_to); - return static_cast(scaled - shift_to); - } + return static_cast( + (static_cast(value - min_from) * (max_to - min_to) / (max_from - min_from)) + min_to); } template diff --git a/Firmware/ESP32/main/Gamepad/fix16ext.h b/Firmware/ESP32/main/Gamepad/fix16ext.h new file mode 100644 index 0000000..b59b027 --- /dev/null +++ b/Firmware/ESP32/main/Gamepad/fix16ext.h @@ -0,0 +1,106 @@ +#ifndef FIX16_EXT_H +#define FIX16_EXT_H + +#include + +#include "libfixmath/fix16.hpp" + +namespace fix16 { + +inline Fix16 abs(Fix16 x) +{ + return Fix16(fix16_abs(x.value)); +} + +inline Fix16 rad2deg(Fix16 rad) +{ + return Fix16(fix16_rad_to_deg(rad.value)); +} + +inline Fix16 deg2rad(Fix16 deg) +{ + return Fix16(fix16_deg_to_rad(deg.value)); +} + +inline Fix16 atan(Fix16 x) +{ + return Fix16(fix16_atan(x.value)); +} + +inline Fix16 atan2(Fix16 y, Fix16 x) +{ + return Fix16(fix16_atan2(y.value, x.value)); +} + +inline Fix16 tan(Fix16 x) +{ + return Fix16(fix16_tan(x.value)); +} + +inline Fix16 cos(Fix16 x) +{ + return Fix16(fix16_cos(x.value)); +} + +inline Fix16 sin(Fix16 x) +{ + return Fix16(fix16_sin(x.value)); +} + +inline Fix16 sqrt(Fix16 x) +{ + return Fix16(fix16_sqrt(x.value)); +} + +inline Fix16 sq(Fix16 x) +{ + return Fix16(fix16_sq(x.value)); +} + +inline Fix16 clamp(Fix16 x, Fix16 min, Fix16 max) +{ + return Fix16(fix16_clamp(x.value, min.value, max.value)); +} + +inline Fix16 pow(Fix16 x, Fix16 y) +{ + fix16_t& base = x.value; + fix16_t& exponent = y.value; + + if (exponent == F16(0.0)) + return Fix16(fix16_from_int(1)); + if (base == F16(0.0)) + return Fix16(fix16_from_int(0)); + + int32_t int_exp = fix16_to_int(exponent); + + if (fix16_from_int(int_exp) == exponent) + { + fix16_t result = F16(1.0); + fix16_t current_base = base; + + if (int_exp < 0) + { + current_base = fix16_div(F16(1.0), base); + int_exp = -int_exp; + } + + while (int_exp) + { + if (int_exp & 1) + { + result = fix16_mul(result, current_base); + } + current_base = fix16_mul(current_base, current_base); + int_exp >>= 1; + } + + return Fix16(result); + } + + return Fix16(fix16_exp(fix16_mul(exponent, fix16_log(base)))); +} + +} // namespace fix16 + +#endif // FIX16_EXT_H \ No newline at end of file diff --git a/Firmware/ESP32/main/I2CDriver/I2CDriver.cpp b/Firmware/ESP32/main/I2CDriver/I2CDriver.cpp index 493e29a..453443a 100644 --- a/Firmware/ESP32/main/I2CDriver/I2CDriver.cpp +++ b/Firmware/ESP32/main/I2CDriver/I2CDriver.cpp @@ -15,7 +15,7 @@ void I2CDriver::initialize_i2c(i2c_port_t i2c_port, gpio_num_t sda, gpio_num_t s { if (initialized_) { - i2c_driver_delete(i2c_port_); + return; } i2c_port_ = i2c_port; diff --git a/Firmware/ESP32/main/UserSettings/JoystickSettings.cpp b/Firmware/ESP32/main/UserSettings/JoystickSettings.cpp new file mode 100644 index 0000000..9e5bc99 --- /dev/null +++ b/Firmware/ESP32/main/UserSettings/JoystickSettings.cpp @@ -0,0 +1,63 @@ +#include "Board/ogxm_log.h" +#include "UserSettings/JoystickSettings.h" + +bool JoystickSettings::is_same(const JoystickSettingsRaw& raw) const +{ + return dz_inner == Fix16(raw.dz_inner) && + dz_outer == Fix16(raw.dz_outer) && + anti_dz_circle == Fix16(raw.anti_dz_circle) && + anti_dz_circle_y_scale == Fix16(raw.anti_dz_circle_y_scale) && + anti_dz_square == Fix16(raw.anti_dz_square) && + anti_dz_square_y_scale == Fix16(raw.anti_dz_square_y_scale) && + anti_dz_angular == Fix16(raw.anti_dz_angular) && + anti_dz_outer == Fix16(raw.anti_dz_outer) && + axis_restrict == Fix16(raw.axis_restrict) && + angle_restrict == Fix16(raw.angle_restrict) && + diag_scale_min == Fix16(raw.diag_scale_min) && + diag_scale_max == Fix16(raw.diag_scale_max) && + curve == Fix16(raw.curve) && + uncap_radius == raw.uncap_radius && + invert_y == raw.invert_y && + invert_x == raw.invert_x; +} + +void JoystickSettings::set_from_raw(const JoystickSettingsRaw& raw) +{ + dz_inner = Fix16(raw.dz_inner); + dz_outer = Fix16(raw.dz_outer); + anti_dz_circle = Fix16(raw.anti_dz_circle); + anti_dz_circle_y_scale = Fix16(raw.anti_dz_circle_y_scale); + anti_dz_square = Fix16(raw.anti_dz_square); + anti_dz_square_y_scale = Fix16(raw.anti_dz_square_y_scale); + anti_dz_angular = Fix16(raw.anti_dz_angular); + anti_dz_outer = Fix16(raw.anti_dz_outer); + axis_restrict = Fix16(raw.axis_restrict); + angle_restrict = Fix16(raw.angle_restrict); + diag_scale_min = Fix16(raw.diag_scale_min); + diag_scale_max = Fix16(raw.diag_scale_max); + curve = Fix16(raw.curve); + uncap_radius = raw.uncap_radius; + invert_y = raw.invert_y; + invert_x = raw.invert_x; +} + +void JoystickSettingsRaw::log_values() +{ + OGXM_LOG("dz_inner: %f\n", fix16_to_float(dz_inner)); + OGXM_LOG_HEX("dz_inner: ", reinterpret_cast(&dz_inner), sizeof(dz_inner)); + OGXM_LOG("dz_outer: %f\n", fix16_to_float(dz_outer)); + OGXM_LOG("anti_dz_circle: %f\n", fix16_to_float(anti_dz_circle)); + OGXM_LOG("anti_dz_circle_y_scale: %f\n", fix16_to_float(anti_dz_circle_y_scale)); + OGXM_LOG("anti_dz_square: %f\n", fix16_to_float(anti_dz_square)); + OGXM_LOG("anti_dz_square_y_scale: %f\n", fix16_to_float(anti_dz_square_y_scale)); + OGXM_LOG("anti_dz_angular: %f\n", fix16_to_float(anti_dz_angular)); + OGXM_LOG("anti_dz_outer: %f\n", fix16_to_float(anti_dz_outer)); + OGXM_LOG("axis_restrict: %f\n", fix16_to_float(axis_restrict)); + OGXM_LOG("angle_restrict: %f\n", fix16_to_float(angle_restrict)); + OGXM_LOG("diag_scale_min: %f\n", fix16_to_float(diag_scale_min)); + OGXM_LOG("diag_scale_max: %f\n", fix16_to_float(diag_scale_max)); + OGXM_LOG("curve: %f\n", fix16_to_float(curve)); + OGXM_LOG("uncap_radius: %d\n", uncap_radius); + OGXM_LOG("invert_y: %d\n", invert_y); + OGXM_LOG("invert_x: %d\n", invert_x); +} \ No newline at end of file diff --git a/Firmware/ESP32/main/UserSettings/JoystickSettings.h b/Firmware/ESP32/main/UserSettings/JoystickSettings.h new file mode 100644 index 0000000..88b6109 --- /dev/null +++ b/Firmware/ESP32/main/UserSettings/JoystickSettings.h @@ -0,0 +1,68 @@ +#ifndef _JOYSTICK_SETTINGS_H_ +#define _JOYSTICK_SETTINGS_H_ + +#include + +#include "libfixmath/fix16.hpp" + +struct JoystickSettingsRaw; + +struct JoystickSettings +{ + Fix16 dz_inner{Fix16(0.0f)}; + Fix16 dz_outer{Fix16(1.0f)}; + + Fix16 anti_dz_circle{Fix16(0.0f)}; + Fix16 anti_dz_circle_y_scale{Fix16(0.0f)}; + Fix16 anti_dz_square{Fix16(0.0f)}; + Fix16 anti_dz_square_y_scale{Fix16(0.0f)}; + Fix16 anti_dz_angular{Fix16(0.0f)}; + Fix16 anti_dz_outer{Fix16(1.0f)}; + + Fix16 axis_restrict{Fix16(0.0f)}; + Fix16 angle_restrict{Fix16(0.0f)}; + + Fix16 diag_scale_min{Fix16(1.0f)}; + Fix16 diag_scale_max{Fix16(1.0f)}; + + Fix16 curve{Fix16(1.0f)}; + + bool uncap_radius{true}; + bool invert_y{false}; + bool invert_x{false}; + + bool is_same(const JoystickSettingsRaw& raw) const; + void set_from_raw(const JoystickSettingsRaw& raw); +}; + +#pragma pack(push, 1) +struct JoystickSettingsRaw +{ + fix16_t dz_inner{fix16_from_int(0)}; + fix16_t dz_outer{fix16_from_int(1)}; + + fix16_t anti_dz_circle{fix16_from_int(0)}; + fix16_t anti_dz_circle_y_scale{fix16_from_int(0)}; + fix16_t anti_dz_square{fix16_from_int(0)}; + fix16_t anti_dz_square_y_scale{fix16_from_int(0)}; + fix16_t anti_dz_angular{fix16_from_int(0)}; + fix16_t anti_dz_outer{fix16_from_int(1)}; + + fix16_t axis_restrict{fix16_from_int(0)}; + fix16_t angle_restrict{fix16_from_int(0)}; + + fix16_t diag_scale_min{fix16_from_int(1)}; + fix16_t diag_scale_max{fix16_from_int(1)}; + + fix16_t curve{fix16_from_int(1)}; + + uint8_t uncap_radius{true}; + uint8_t invert_y{false}; + uint8_t invert_x{false}; + + void log_values(); +}; +static_assert(sizeof(JoystickSettingsRaw) == 55, "JoystickSettingsRaw is an unexpected size"); +#pragma pack(pop) + +#endif // _JOYSTICK_SETTINGS_H_ \ No newline at end of file diff --git a/Firmware/ESP32/main/UserSettings/NVSHelper.h b/Firmware/ESP32/main/UserSettings/NVSHelper.h index c28ec49..e4390b7 100644 --- a/Firmware/ESP32/main/UserSettings/NVSHelper.h +++ b/Firmware/ESP32/main/UserSettings/NVSHelper.h @@ -106,7 +106,7 @@ private: SemaphoreHandle_t nvs_mutex_; - static constexpr const char NVS_NAMESPACE[] = "user_data"; + static constexpr char NVS_NAMESPACE[] = "user_data"; }; // class NVSHelper diff --git a/Firmware/ESP32/main/UserSettings/TriggerSettings.cpp b/Firmware/ESP32/main/UserSettings/TriggerSettings.cpp new file mode 100644 index 0000000..de2806b --- /dev/null +++ b/Firmware/ESP32/main/UserSettings/TriggerSettings.cpp @@ -0,0 +1,29 @@ +#include "Board/ogxm_log.h" +#include "UserSettings/TriggerSettings.h" + +bool TriggerSettings::is_same(const TriggerSettingsRaw& raw) const +{ + return dz_inner == Fix16(raw.dz_inner) && + dz_outer == Fix16(raw.dz_outer) && + anti_dz_inner == Fix16(raw.anti_dz_inner) && + anti_dz_outer == Fix16(raw.anti_dz_outer) && + curve == Fix16(raw.curve); +} + +void TriggerSettings::set_from_raw(const TriggerSettingsRaw& raw) +{ + dz_inner = Fix16(raw.dz_inner); + dz_outer = Fix16(raw.dz_outer); + anti_dz_inner = Fix16(raw.anti_dz_inner); + anti_dz_outer = Fix16(raw.anti_dz_outer); + curve = Fix16(raw.curve); +} + +void TriggerSettingsRaw::log_values() +{ + OGXM_LOG("dz_inner: %f\n", fix16_to_float(dz_inner)); + OGXM_LOG("dz_outer: %f\n", fix16_to_float(dz_outer)); + OGXM_LOG("anti_dz_inner: %f\n", fix16_to_float(anti_dz_inner)); + OGXM_LOG("anti_dz_outer: %f\n", fix16_to_float(anti_dz_outer)); + OGXM_LOG("curve: %f\n", fix16_to_float(curve)); +} \ No newline at end of file diff --git a/Firmware/ESP32/main/UserSettings/TriggerSettings.h b/Firmware/ESP32/main/UserSettings/TriggerSettings.h new file mode 100644 index 0000000..5a8f807 --- /dev/null +++ b/Firmware/ESP32/main/UserSettings/TriggerSettings.h @@ -0,0 +1,40 @@ +#ifndef TRIGGER_SETTINGS_H +#define TRIGGER_SETTINGS_H + +#include + +#include "libfixmath/fix16.hpp" + +struct TriggerSettingsRaw; + +struct TriggerSettings +{ + Fix16 dz_inner{Fix16(0.0f)}; + Fix16 dz_outer{Fix16(1.0f)}; + + Fix16 anti_dz_inner{Fix16(0.0f)}; + Fix16 anti_dz_outer{Fix16(1.0f)}; + + Fix16 curve{Fix16(1.0f)}; + + bool is_same(const TriggerSettingsRaw& raw) const; + void set_from_raw(const TriggerSettingsRaw& raw); +}; + +#pragma pack(push, 1) +struct TriggerSettingsRaw +{ + fix16_t dz_inner{fix16_from_int(0)}; + fix16_t dz_outer{fix16_from_int(1)}; + + fix16_t anti_dz_inner{fix16_from_int(0)}; + fix16_t anti_dz_outer{fix16_from_int(1)}; + + fix16_t curve{fix16_from_int(1)}; + + void log_values(); +}; +static_assert(sizeof(TriggerSettingsRaw) == 20, "TriggerSettingsRaw is an unexpected size"); +#pragma pack(pop) + +#endif // TRIGGER_SETTINGS_H \ No newline at end of file diff --git a/Firmware/ESP32/main/UserSettings/UserProfile.cpp b/Firmware/ESP32/main/UserSettings/UserProfile.cpp index 1f77b59..4e50d97 100644 --- a/Firmware/ESP32/main/UserSettings/UserProfile.cpp +++ b/Firmware/ESP32/main/UserSettings/UserProfile.cpp @@ -1,20 +1,12 @@ #include -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "UserSettings/UserProfile.h" UserProfile::UserProfile() { id = 1; - dz_trigger_l = 0; - dz_trigger_r = 0; - dz_joystick_l = 0; - dz_joystick_r = 0; - - invert_ly = 0; - invert_ry = 0; - dpad_up = Gamepad::DPAD_UP; dpad_down = Gamepad::DPAD_DOWN; dpad_left = Gamepad::DPAD_LEFT; @@ -33,16 +25,16 @@ UserProfile::UserProfile() button_sys = Gamepad::BUTTON_SYS; button_misc = Gamepad::BUTTON_MISC; - analog_enabled = 1; + analog_enabled = 0; - analog_off_up = 0; - analog_off_down = 1; - analog_off_left = 2; - analog_off_right = 3; - analog_off_a = 4; - analog_off_b = 5; - analog_off_x = 6; - analog_off_y = 7; - analog_off_lb = 8; - analog_off_rb = 9; + analog_off_up = Gamepad::ANALOG_OFF_UP; + analog_off_down = Gamepad::ANALOG_OFF_DOWN; + analog_off_left = Gamepad::ANALOG_OFF_LEFT; + analog_off_right = Gamepad::ANALOG_OFF_RIGHT; + analog_off_a = Gamepad::ANALOG_OFF_A; + analog_off_b = Gamepad::ANALOG_OFF_B; + analog_off_x = Gamepad::ANALOG_OFF_X; + analog_off_y = Gamepad::ANALOG_OFF_Y; + analog_off_lb = Gamepad::ANALOG_OFF_LB; + analog_off_rb = Gamepad::ANALOG_OFF_RB; } \ No newline at end of file diff --git a/Firmware/ESP32/main/UserSettings/UserProfile.h b/Firmware/ESP32/main/UserSettings/UserProfile.h index 2efa406..6bd28f2 100644 --- a/Firmware/ESP32/main/UserSettings/UserProfile.h +++ b/Firmware/ESP32/main/UserSettings/UserProfile.h @@ -3,19 +3,18 @@ #include +#include "UserSettings/JoystickSettings.h" +#include "UserSettings/TriggerSettings.h" + #pragma pack(push, 1) struct UserProfile { uint8_t id; - uint8_t dz_trigger_l; - uint8_t dz_trigger_r; - - uint8_t dz_joystick_l; - uint8_t dz_joystick_r; - - uint8_t invert_ly; - uint8_t invert_ry; + JoystickSettingsRaw joystick_settings_l; + JoystickSettingsRaw joystick_settings_r; + TriggerSettingsRaw trigger_settings_l; + TriggerSettingsRaw trigger_settings_r; uint8_t dpad_up; uint8_t dpad_down; @@ -50,7 +49,7 @@ struct UserProfile UserProfile(); }; -static_assert(sizeof(UserProfile) == 46, "UserProfile struct size mismatch"); +static_assert(sizeof(UserProfile) == 190, "UserProfile struct size mismatch"); #pragma pack(pop) #endif // _USER_PROFILE_H_ \ No newline at end of file diff --git a/Firmware/ESP32/main/UserSettings/UserSettings.cpp b/Firmware/ESP32/main/UserSettings/UserSettings.cpp index 00e9852..28d2ffe 100644 --- a/Firmware/ESP32/main/UserSettings/UserSettings.cpp +++ b/Firmware/ESP32/main/UserSettings/UserSettings.cpp @@ -6,7 +6,7 @@ #include #include -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "UserSettings/NVSHelper.h" static constexpr uint32_t BUTTON_COMBO(const uint16_t& buttons, const uint8_t& dpad = 0) @@ -85,7 +85,6 @@ const std::string UserSettings::FIRMWARE_VER_KEY() return std::string("firmware_ver"); } -//Checks for first boot and initializes user profiles, call before tusb is inited. void UserSettings::initialize_flash() { ESP_LOGD("UserSettings", "Checking for UserSettings init flag"); @@ -100,6 +99,9 @@ void UserSettings::initialize_flash() return; } + ESP_ERROR_CHECK(nvs_helper_.erase_all()); + OGXM_LOG("Initializing UserSettings\n"); + current_driver_ = DEFAULT_DRIVER(); uint8_t driver_type = static_cast(current_driver_); ESP_ERROR_CHECK(nvs_helper_.write(DRIVER_TYPE_KEY(), &driver_type, sizeof(driver_type))); @@ -198,20 +200,24 @@ void UserSettings::store_driver_type(DeviceDriverType new_driver_type) nvs_helper_.write(DRIVER_TYPE_KEY(), &new_driver, sizeof(new_driver)); } -void UserSettings::store_profile(const uint8_t index, const UserProfile& profile) +void UserSettings::store_profile(const uint8_t index, UserProfile& profile) { if (index > MAX_GAMEPADS || profile.id < 1 || profile.id > MAX_PROFILES) { return; } + OGXM_LOG("Storing profile %d for gamepad %d\n", profile.id, index); + if (nvs_helper_.write(PROFILE_KEY(profile.id), &profile, sizeof(UserProfile)) == ESP_OK) { + OGXM_LOG("Profile %d stored successfully\n", profile.id); + nvs_helper_.write(ACTIVE_PROFILE_KEY(index), &profile.id, sizeof(profile.id)); } } -void UserSettings::store_profile_and_driver_type(DeviceDriverType new_driver_type, const uint8_t index, const UserProfile& profile) +void UserSettings::store_profile_and_driver_type(DeviceDriverType new_driver_type, const uint8_t index, UserProfile& profile) { store_driver_type(new_driver_type); store_profile(index, profile); @@ -243,10 +249,10 @@ UserProfile UserSettings::get_profile_by_id(const uint8_t profile_id) profile_id > MAX_PROFILES || nvs_helper_.read(PROFILE_KEY(profile_id), &profile, sizeof(UserProfile)) != ESP_OK) { - return profile; + return UserProfile(); } - return UserProfile(); + return profile; } DeviceDriverType UserSettings::DEFAULT_DRIVER() diff --git a/Firmware/ESP32/main/UserSettings/UserSettings.h b/Firmware/ESP32/main/UserSettings/UserSettings.h index 16f016a..8868a34 100644 --- a/Firmware/ESP32/main/UserSettings/UserSettings.h +++ b/Firmware/ESP32/main/UserSettings/UserSettings.h @@ -32,8 +32,8 @@ public: uint8_t get_active_profile_id(const uint8_t index); void store_driver_type(DeviceDriverType new_driver_type); - void store_profile(uint8_t index, const UserProfile& profile); - void store_profile_and_driver_type(DeviceDriverType new_driver_type, uint8_t index, const UserProfile& profile); + void store_profile(uint8_t index, UserProfile& profile); + void store_profile_and_driver_type(DeviceDriverType new_driver_type, uint8_t index, UserProfile& profile); private: UserSettings() = default; @@ -42,7 +42,7 @@ private: UserSettings& operator=(const UserSettings&) = delete; static constexpr uint8_t GP_CHECK_COUNT = 3000 / GP_CHECK_DELAY_MS; - static constexpr uint8_t INIT_FLAG = 0x82; + static constexpr uint8_t INIT_FLAG = 0x12; NVSHelper& nvs_helper_{NVSHelper::get_instance()}; DeviceDriverType current_driver_{DeviceDriverType::NONE}; diff --git a/Firmware/ESP32/main/1btstack_config.h b/Firmware/ESP32/main/btstack_config.h similarity index 98% rename from Firmware/ESP32/main/1btstack_config.h rename to Firmware/ESP32/main/btstack_config.h index be4f0c4..7c3c33c 100644 --- a/Firmware/ESP32/main/1btstack_config.h +++ b/Firmware/ESP32/main/btstack_config.h @@ -55,8 +55,8 @@ // #define NVM_NUM_LINK_KEYS 16 // We don't give btstack a malloc, so use a fixed-size ATT DB. -// #define MAX_ATT_DB_SIZE 512 -#define HAVE_MALLOC +#define MAX_ATT_DB_SIZE 512 +// #define HAVE_MALLOC // BTstack HAL configuration // #define HAVE_EMBEDDED_TIME_MS diff --git a/Firmware/ESP32/sdkconfig b/Firmware/ESP32/sdkconfig index 014fb2f..ed156d5 100644 --- a/Firmware/ESP32/sdkconfig +++ b/Firmware/ESP32/sdkconfig @@ -1381,17 +1381,20 @@ CONFIG_HEAP_TRACING_OFF=y # # Log output # -# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set +CONFIG_LOG_DEFAULT_LEVEL_NONE=y # CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set # CONFIG_LOG_DEFAULT_LEVEL_WARN is not set -CONFIG_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_LOG_DEFAULT_LEVEL_INFO is not set # CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set # CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set -CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_DEFAULT_LEVEL=0 CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y +# CONFIG_LOG_MAXIMUM_LEVEL_ERROR is not set +# CONFIG_LOG_MAXIMUM_LEVEL_WARN is not set +# CONFIG_LOG_MAXIMUM_LEVEL_INFO is not set # CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set # CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set -CONFIG_LOG_MAXIMUM_LEVEL=3 +CONFIG_LOG_MAXIMUM_LEVEL=0 CONFIG_LOG_COLORS=y CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y # CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set @@ -1980,11 +1983,11 @@ CONFIG_BLUEPAD32_PLATFORM_CUSTOM=y # CONFIG_BLUEPAD32_PLATFORM_MAKEFILE is not set CONFIG_BLUEPAD32_MAX_DEVICES=1 CONFIG_BLUEPAD32_GAP_SECURITY=y -# CONFIG_BLUEPAD32_LOG_LEVEL_NONE is not set +CONFIG_BLUEPAD32_LOG_LEVEL_NONE=y # CONFIG_BLUEPAD32_LOG_LEVEL_ERROR is not set -CONFIG_BLUEPAD32_LOG_LEVEL_INFO=y +# CONFIG_BLUEPAD32_LOG_LEVEL_INFO is not set # CONFIG_BLUEPAD32_LOG_LEVEL_DEBUG is not set -CONFIG_BLUEPAD32_LOG_LEVEL=2 +CONFIG_BLUEPAD32_LOG_LEVEL=0 # CONFIG_BLUEPAD32_USB_CONSOLE_ENABLE is not set CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT=y CONFIG_BLUEPAD32_MAX_ALLOWLIST=4 diff --git a/Firmware/RP2040/.vscode/settings.json b/Firmware/RP2040/.vscode/settings.json index 26c31ef..a3decc0 100644 --- a/Firmware/RP2040/.vscode/settings.json +++ b/Firmware/RP2040/.vscode/settings.json @@ -2,7 +2,7 @@ "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", "cmake.configureArgs": [ - "-DOGXM_BOARD=PI_PICOW", + "-DOGXM_BOARD=ADA_FEATHER", "-DMAX_GAMEPADS=1" ], diff --git a/Firmware/RP2040/CMakeLists.txt b/Firmware/RP2040/CMakeLists.txt index 43f46d9..0b156ad 100644 --- a/Firmware/RP2040/CMakeLists.txt +++ b/Firmware/RP2040/CMakeLists.txt @@ -18,15 +18,19 @@ include(${CMAKE_CURRENT_LIST_DIR}/../cmake/patch_libs.cmake) include(${CMAKE_CURRENT_LIST_DIR}/../cmake/generate_gatt_header.cmake) include(${CMAKE_CURRENT_LIST_DIR}/cmake/get_pico_sdk.cmake) -get_pico_sdk(${EXTERNAL_DIR} ${PICOSDK_VERSION_TAG}) -init_git_submodules(${EXTERNAL_DIR}) -apply_lib_patches(${EXTERNAL_DIR}) - set(PICO_PIO_USB_PATH ${EXTERNAL_DIR}/Pico-PIO-USB) set(PICO_TINYUSB_PATH ${EXTERNAL_DIR}/tinyusb) set(BLUEPAD32_ROOT ${EXTERNAL_DIR}/bluepad32) set(BTSTACK_ROOT ${BLUEPAD32_ROOT}/external/btstack) set(PICO_BTSTACK_PATH ${BTSTACK_ROOT}) +set(LIBFIXMATH_PATH ${EXTERNAL_DIR}/libfixmath) + +get_pico_sdk(${EXTERNAL_DIR} ${PICOSDK_VERSION_TAG}) +init_git_submodules(${EXTERNAL_DIR} + ${BLUEPAD32_ROOT} + ${PICO_TINYUSB_PATH} +) +apply_lib_patches(${EXTERNAL_DIR}) set(SOURCES_BOARD ${SRC}/main.cpp @@ -48,6 +52,8 @@ set(SOURCES_BOARD ${SRC}/UserSettings/UserSettings.cpp ${SRC}/UserSettings/UserProfile.cpp + ${SRC}/UserSettings/JoystickSettings.cpp + ${SRC}/UserSettings/TriggerSettings.cpp ${SRC}/USBDevice/tud_callbacks.cpp ${SRC}/USBDevice/DeviceManager.cpp @@ -77,22 +83,21 @@ set(LIBS_BOARD # UART hardware_uart hardware_irq + #fix16 + libfixmath ) set(INC_DIRS_BOARD ) # Config options -# Max gamepads set(MAX_GAMEPADS 1 CACHE STRING "Set number of gamepads, 1 to 4") if (MAX_GAMEPADS GREATER 4 OR MAX_GAMEPADS LESS 1) message(FATAL_ERROR "MAX_GAMEPADS must be between 1 and 4") endif() add_definitions(-DMAX_GAMEPADS=${MAX_GAMEPADS}) -# Board type set(OGXM_BOARD "PI_PICO" CACHE STRING "Set board type, options can be found in src/board_config.h") - set(FLASH_SIZE_MB 2) set(PICO_BOARD none) @@ -136,6 +141,11 @@ elseif(OGXM_BOARD STREQUAL "PICO_ESP32") set(EN_ESP32 TRUE) set(EN_UART_BRIDGE TRUE) + if(OGXM_RETAIL STREQUAL "TRUE") + message(STATUS "Retail mode enabled.") + add_definitions(-DOGXM_ESP32_RETAIL) + endif() + else() message(FATAL_ERROR "Invalid OGXM_BOARD value. See options in src/board_config.h") @@ -246,6 +256,8 @@ if(EN_UART_BRIDGE) ) endif() +string(TIMESTAMP CURRENT_DATETIME "%Y-%m-%d %H:%M:%S") +add_compile_definitions(BUILD_DATETIME="${CURRENT_DATETIME}") add_compile_definitions(FIRMWARE_NAME="${FW_NAME}") add_compile_definitions(FIRMWARE_VERSION="${FW_VERSION}") add_compile_definitions(PICO_FLASH_SIZE_BYTES=${FLASH_SIZE_MB}*1024*1024) @@ -288,8 +300,8 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "Debug build enabled.") set(BUILD_STR "-Debug") - set(TX_PIN 0) - set(RX_PIN 1) + set(TX_PIN 4) + set(RX_PIN 5) set(UART_PORT) include(${CMAKE_CURRENT_LIST_DIR}/cmake/get_pico_uart_port.cmake) @@ -304,7 +316,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") PICO_DEFAULT_UART_RX_PIN=${RX_PIN} ) - add_compile_definitions(CFG_TUSB_DEBUG=2) + add_compile_definitions(CFG_TUSB_DEBUG=1) add_compile_definitions(OGXM_DEBUG=1) target_compile_options(${FW_NAME} PRIVATE @@ -338,7 +350,9 @@ endif() pico_set_program_name(${FW_NAME} ${FW_NAME}) pico_set_program_version(${FW_NAME} ${FW_VERSION}) -target_include_directories(${FW_NAME} PRIVATE ${SRC}) +target_include_directories(${FW_NAME} PRIVATE + ${SRC} +) if(EN_RGB) pico_generate_pio_header(${FW_NAME} ${SRC}/Board/Pico_WS2812/WS2812.pio) @@ -354,9 +368,26 @@ if(EN_BLUETOOTH) add_subdirectory(${BLUEPAD32_ROOT}/src/components/bluepad32 libbluepad32) endif() +add_subdirectory(${LIBFIXMATH_PATH} libfixmath) + target_link_libraries(${FW_NAME} PRIVATE ${LIBS_BOARD}) -set(EXE_FILENAME "${FW_NAME}-${FW_VERSION}-${OGXM_BOARD}${BUILD_STR}") +target_compile_definitions(libfixmath PRIVATE + FIXMATH_FAST_SIN + FIXMATH_NO_64BIT + FIXMATH_NO_CACHE + FIXMATH_NO_HARD_DIVISION + FIXMATH_NO_OVERFLOW + # FIXMATH_NO_ROUNDING + # FIXMATH_OPTIMIZE_8BIT +) + +if(OGXM_RETAIL STREQUAL "TRUE") + set(EXE_FILENAME "${FW_NAME}-${FW_VERSION}-${OGXM_BOARD}${BUILD_STR}-Retail") +else() + set(EXE_FILENAME "${FW_NAME}-${FW_VERSION}-${OGXM_BOARD}${BUILD_STR}") +endif() + set_target_properties(${FW_NAME} PROPERTIES OUTPUT_NAME ${EXE_FILENAME}) pico_add_extra_outputs(${FW_NAME}) \ No newline at end of file diff --git a/Firmware/RP2040/src/BLEServer/BLEServer.cpp b/Firmware/RP2040/src/BLEServer/BLEServer.cpp index ab85c65..881858b 100644 --- a/Firmware/RP2040/src/BLEServer/BLEServer.cpp +++ b/Firmware/RP2040/src/BLEServer/BLEServer.cpp @@ -12,20 +12,20 @@ namespace BLEServer { -static constexpr uint16_t PACKET_LEN_MAX = 18; +static constexpr uint16_t PACKET_LEN_MAX = 20; namespace Handle { static constexpr uint16_t FW_VERSION = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789020_01_VALUE_HANDLE; static constexpr uint16_t FW_NAME = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789021_01_VALUE_HANDLE; - static constexpr uint16_t START_UPDATE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789030_01_VALUE_HANDLE; - static constexpr uint16_t COMMIT_UPDATE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789031_01_VALUE_HANDLE; + static constexpr uint16_t SETUP_READ = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789030_01_VALUE_HANDLE; + static constexpr uint16_t SETUP_WRITE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789031_01_VALUE_HANDLE; + static constexpr uint16_t GET_SETUP = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789032_01_VALUE_HANDLE; - static constexpr uint16_t SETUP_PACKET = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789040_01_VALUE_HANDLE; - static constexpr uint16_t PROFILE_PT1 = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789041_01_VALUE_HANDLE; - static constexpr uint16_t PROFILE_PT2 = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789042_01_VALUE_HANDLE; - static constexpr uint16_t PROFILE_PT3 = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789043_01_VALUE_HANDLE; + static constexpr uint16_t PROFILE = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789040_01_VALUE_HANDLE; + + static constexpr uint16_t GAMEPAD = ATT_CHARACTERISTIC_12345678_1234_1234_1234_123456789050_01_VALUE_HANDLE; } namespace ADV @@ -58,26 +58,159 @@ namespace ADV #pragma pack(push, 1) struct SetupPacket { - uint8_t max_gamepads{1}; - uint8_t index{0}; - uint8_t device_type{0}; - uint8_t profile_id{1}; + DeviceDriverType device_type{DeviceDriverType::NONE}; + uint8_t max_gamepads{MAX_GAMEPADS}; + uint8_t player_idx{0}; + uint8_t profile_id{0}; }; static_assert(sizeof(SetupPacket) == 4, "BLEServer::SetupPacket struct size mismatch"); #pragma pack(pop) -SetupPacket setup_packet_; +class ProfileReader +{ +public: + ProfileReader() = default; + ~ProfileReader() = default; -static int verify_write(const uint16_t buffer_size, const uint16_t expected_size, bool pending_write = false, bool expected_pending_write = false) + void set_setup_packet(const SetupPacket& setup_packet) + { + setup_packet_ = setup_packet; + current_offset_ = 0; + } + const SetupPacket& get_setup_packet() const + { + return setup_packet_; + } + uint16_t get_xfer_len() + { + return static_cast(std::min(static_cast(PACKET_LEN_MAX), sizeof(UserProfile) - current_offset_)); + } + uint16_t get_profile_data(uint8_t* buffer, uint16_t buffer_len) + { + size_t copy_len = get_xfer_len(); + if (!buffer || buffer_len < copy_len) + { + return 0; + } + + if (current_offset_ == 0 && !set_profile()) + { + return 0; + } + + std::memcpy(buffer, reinterpret_cast(&profile_) + current_offset_, copy_len); + + current_offset_ += copy_len; + if (current_offset_ >= sizeof(UserProfile)) + { + current_offset_ = 0; + } + return copy_len; + } + +private: + SetupPacket setup_packet_; + UserProfile profile_; + size_t current_offset_ = 0; + + bool set_profile() + { + if (setup_packet_.profile_id == 0xFF) + { + if (setup_packet_.player_idx >= UserSettings::MAX_PROFILES) + { + return false; + } + profile_ = UserSettings::get_instance().get_profile_by_index(setup_packet_.player_idx); + } + else + { + if (setup_packet_.profile_id > UserSettings::MAX_PROFILES) + { + return false; + } + profile_ = UserSettings::get_instance().get_profile_by_id(setup_packet_.profile_id); + } + return true; + } +}; + +class ProfileWriter +{ +public: + ProfileWriter() = default; + ~ProfileWriter() = default; + + void set_setup_packet(const SetupPacket& setup_packet) + { + setup_packet_ = setup_packet; + current_offset_ = 0; + } + const SetupPacket& get_setup_packet() const + { + return setup_packet_; + } + uint16_t get_xfer_len() + { + return static_cast(std::min(static_cast(PACKET_LEN_MAX), sizeof(UserProfile) - current_offset_)); + } + size_t set_profile_data(const uint8_t* buffer, uint16_t buffer_len) + { + size_t copy_len = get_xfer_len(); + if (!buffer || buffer_len < copy_len) + { + return 0; + } + + std::memcpy(reinterpret_cast(&profile_) + current_offset_, buffer, copy_len); + + current_offset_ += copy_len; + size_t ret = current_offset_; + + if (current_offset_ >= sizeof(UserProfile)) + { + current_offset_ = 0; + } + return ret; + } + bool commit_profile() + { + bool success = false; + if (setup_packet_.device_type != DeviceDriverType::NONE) + { + success = TaskQueue::Core0::queue_delayed_task(TaskQueue::Core0::get_new_task_id(), 1000, false, + [driver_type = setup_packet_.device_type, profile = profile_, index = setup_packet_.player_idx] + { + UserSettings::get_instance().store_profile_and_driver_type(driver_type, index, profile); + }); + } + else + { + success = TaskQueue::Core0::queue_delayed_task(TaskQueue::Core0::get_new_task_id(), 1000, false, + [index = setup_packet_.player_idx, profile = profile_] + { + UserSettings::get_instance().store_profile(index, profile); + }); + } + return success; + } + +private: + SetupPacket setup_packet_; + UserProfile profile_; + size_t current_offset_ = 0; +}; + +std::array gamepads_; +ProfileReader profile_reader_; +ProfileWriter profile_writer_; + +static int verify_write(const uint16_t buffer_size, const uint16_t expected_size) { if (buffer_size != expected_size) { return ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LENGTH; } - if (pending_write != expected_pending_write) - { - return ATT_ERROR_WRITE_NOT_PERMITTED; - } return 0; } @@ -107,10 +240,9 @@ static uint16_t att_read_callback( hci_con_handle_t connection_handle, uint8_t *buffer, uint16_t buffer_size) { - static UserProfile profile; - SetupPacket setup_packet_resp; std::string fw_version; std::string fw_name; + Gamepad::PadIn pad_in; switch (att_handle) { @@ -130,41 +262,30 @@ static uint16_t att_read_callback( hci_con_handle_t connection_handle, } return static_cast(fw_name.size()); - case Handle::SETUP_PACKET: + case Handle::GET_SETUP: if (buffer) { - setup_packet_resp.max_gamepads = static_cast(MAX_GAMEPADS); - setup_packet_resp.index = setup_packet_.index; - setup_packet_resp.device_type = static_cast(UserSettings::get_instance().get_current_driver()); - //App has already written a setup packet with the player index - setup_packet_resp.profile_id = UserSettings::get_instance().get_active_profile_id(setup_packet_.index); - - std::memcpy(buffer, &setup_packet_resp, sizeof(setup_packet_resp)); + buffer[0] = static_cast(UserSettings::get_instance().get_current_driver()); + buffer[1] = MAX_GAMEPADS; + buffer[2] = 0; + buffer[3] = UserSettings::get_instance().get_active_profile_id(0); } - return sizeof(setup_packet_); + return static_cast(sizeof(SetupPacket)); - case Handle::PROFILE_PT1: + case Handle::PROFILE: if (buffer) { - //App has already written the profile id it wants to the setup packet - profile = UserSettings::get_instance().get_profile_by_id(setup_packet_.profile_id); - std::memcpy(buffer, &profile, PACKET_LEN_MAX); + return profile_reader_.get_profile_data(buffer, buffer_size); } - return PACKET_LEN_MAX; + return profile_reader_.get_xfer_len(); - case Handle::PROFILE_PT2: + case Handle::GAMEPAD: if (buffer) { - std::memcpy(buffer, reinterpret_cast(&profile) + PACKET_LEN_MAX, PACKET_LEN_MAX); + pad_in = gamepads_.front()->get_pad_in(); + std::memcpy(buffer, &pad_in, sizeof(Gamepad::PadIn)); } - return PACKET_LEN_MAX; - - case Handle::PROFILE_PT3: - if (buffer) - { - std::memcpy(buffer, reinterpret_cast(&profile) + PACKET_LEN_MAX * 2, sizeof(UserProfile) - PACKET_LEN_MAX * 2); - } - return sizeof(UserProfile) - PACKET_LEN_MAX * 2; + return static_cast(sizeof(Gamepad::PadIn)); default: break; @@ -179,98 +300,36 @@ static int att_write_callback( hci_con_handle_t connection_handle, uint8_t *buffer, uint16_t buffer_size) { - static UserProfile temp_profile; - static DeviceDriverType temp_driver_type = DeviceDriverType::NONE; - static bool pending_write = false; - int ret = 0; switch (att_handle) { - case Handle::START_UPDATE: - pending_write = true; - break; - - case Handle::SETUP_PACKET: + case Handle::SETUP_READ: if ((ret = verify_write(buffer_size, sizeof(SetupPacket))) != 0) { break; } - - std::memcpy(&setup_packet_, buffer, buffer_size); - if (setup_packet_.index >= MAX_GAMEPADS) - { - setup_packet_.index = 0; - ret = ATT_ERROR_OUT_OF_RANGE; - } - if (setup_packet_.profile_id > UserSettings::MAX_PROFILES) - { - setup_packet_.profile_id = 1; - ret = ATT_ERROR_OUT_OF_RANGE; - } - if (ret) - { - break; - } - - if (pending_write) - { - //App wants to store a new device driver type - temp_driver_type = static_cast(setup_packet_.device_type); - } + profile_reader_.set_setup_packet(*reinterpret_cast(buffer)); break; - case Handle::PROFILE_PT1: - if ((ret = verify_write(buffer_size, PACKET_LEN_MAX, pending_write, true)) != 0) + case Handle::SETUP_WRITE: + if ((ret = verify_write(buffer_size, sizeof(SetupPacket))) != 0) { break; } - std::memcpy(&temp_profile, buffer, buffer_size); + profile_writer_.set_setup_packet(*reinterpret_cast(buffer)); break; - case Handle::PROFILE_PT2: - if ((ret = verify_write(buffer_size, PACKET_LEN_MAX, pending_write, true)) != 0) + case Handle::PROFILE: + if ((ret = verify_write(buffer_size, profile_writer_.get_xfer_len())) != 0) { break; } - std::memcpy(reinterpret_cast(&temp_profile) + PACKET_LEN_MAX, buffer, buffer_size); - break; - - case Handle::PROFILE_PT3: - if ((ret = verify_write(buffer_size, sizeof(UserProfile) - PACKET_LEN_MAX * 2, pending_write, true)) != 0) + if (profile_writer_.set_profile_data(buffer, buffer_size) == sizeof(UserProfile)) { - break; + queue_disconnect(connection_handle, 500); + profile_writer_.commit_profile(); } - std::memcpy(reinterpret_cast(&temp_profile) + PACKET_LEN_MAX * 2, buffer, buffer_size); - break; - - case Handle::COMMIT_UPDATE: - if ((ret = verify_write(0, 0, pending_write, true)) != 0) - { - break; - } - - //Delay until after we've returned - queue_disconnect(connection_handle, 500); - - //We don't want to write to the flash from here, it'll reset core1 before writing, queue the task on core0 - if (temp_driver_type != DeviceDriverType::NONE && temp_driver_type != UserSettings::get_instance().get_current_driver()) - { - TaskQueue::Core0::queue_delayed_task(TaskQueue::Core0::get_new_task_id(), 1000, false, - []{ - UserSettings::get_instance().store_profile_and_driver_type(temp_driver_type, setup_packet_.index, temp_profile); - }); - } - else - { - TaskQueue::Core0::queue_delayed_task(TaskQueue::Core0::get_new_task_id(), 1000, false, - []{ - UserSettings::get_instance().store_profile(setup_packet_.index, temp_profile); - }); - } - - temp_driver_type = DeviceDriverType::NONE; - pending_write = false; break; default: @@ -279,8 +338,13 @@ static int att_write_callback( hci_con_handle_t connection_handle, return ret; } -void init_server() +void init_server(Gamepad(&gamepads)[MAX_GAMEPADS]) { + for (uint8_t i = 0; i < MAX_GAMEPADS; i++) + { + gamepads_[i] = &gamepads[i]; + } + UserSettings::get_instance().initialize_flash(); // setup ATT server diff --git a/Firmware/RP2040/src/BLEServer/BLEServer.h b/Firmware/RP2040/src/BLEServer/BLEServer.h index d72962e..567b6f9 100644 --- a/Firmware/RP2040/src/BLEServer/BLEServer.h +++ b/Firmware/RP2040/src/BLEServer/BLEServer.h @@ -3,9 +3,11 @@ #include +#include "Gamepad/Gamepad.h" + namespace BLEServer { - void init_server(); + void init_server(Gamepad(&gamepads)[MAX_GAMEPADS]); } #endif // BLE_SERVER_H \ No newline at end of file diff --git a/Firmware/RP2040/src/BLEServer/att_delayed_response.gatt b/Firmware/RP2040/src/BLEServer/att_delayed_response.gatt index 4f059d3..035138b 100644 --- a/Firmware/RP2040/src/BLEServer/att_delayed_response.gatt +++ b/Firmware/RP2040/src/BLEServer/att_delayed_response.gatt @@ -13,20 +13,17 @@ CHARACTERISTIC, 12345678-1234-1234-1234-123456789020, READ | DYNAMIC, // Handle::FW_NAME CHARACTERISTIC, 12345678-1234-1234-1234-123456789021, READ | DYNAMIC, -// Handle::UPDATE_START +// Handle::SETUP_READ CHARACTERISTIC, 12345678-1234-1234-1234-123456789030, WRITE | DYNAMIC, -// Handle::UPDATE_COMMIT +// Handle::SETUP_WRITE CHARACTERISTIC, 12345678-1234-1234-1234-123456789031, WRITE | DYNAMIC, -// Handle::SETUP_PACKET +// Handle::GET_SETUP +CHARACTERISTIC, 12345678-1234-1234-1234-123456789032, READ | DYNAMIC, + +// Handle::PROFILE CHARACTERISTIC, 12345678-1234-1234-1234-123456789040, READ | WRITE | DYNAMIC, -// Handle::PROFILE_PT1 -CHARACTERISTIC, 12345678-1234-1234-1234-123456789041, READ | WRITE | DYNAMIC, - -// Handle::PROFILE_PT2 -CHARACTERISTIC, 12345678-1234-1234-1234-123456789042, READ | WRITE | DYNAMIC, - -// Handle::PROFILE_PT3 -CHARACTERISTIC, 12345678-1234-1234-1234-123456789043, READ | WRITE | DYNAMIC, \ No newline at end of file +// Handle::GAMEPAD +CHARACTERISTIC, 12345678-1234-1234-1234-123456789050, READ | WRITE | DYNAMIC, \ No newline at end of file diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp index d0df285..4c9b0a5 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp @@ -9,7 +9,6 @@ #include "sdkconfig.h" #include "Bluepad32/Bluepad32.h" -#include "BLEServer/BLEServer.h" #include "Board/board_api.h" #include "Board/ogxm_log.h" @@ -269,11 +268,9 @@ static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* contr gp_in.trigger_l = gamepad->scale_trigger_l<10>(static_cast(uni_gp->brake)); gp_in.trigger_r = gamepad->scale_trigger_r<10>(static_cast(uni_gp->throttle)); - - gp_in.joystick_lx = gamepad->scale_joystick_lx<10>(uni_gp->axis_x); - gp_in.joystick_ly = gamepad->scale_joystick_ly<10>(uni_gp->axis_y); - gp_in.joystick_rx = gamepad->scale_joystick_rx<10>(uni_gp->axis_rx); - gp_in.joystick_ry = gamepad->scale_joystick_ry<10>(uni_gp->axis_ry); + + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad->scale_joystick_l<10>(uni_gp->axis_x, uni_gp->axis_y); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad->scale_joystick_r<10>(uni_gp->axis_rx, uni_gp->axis_ry); gamepad->set_pad_in(gp_in); } @@ -303,15 +300,13 @@ uni_platform* get_driver() //Public API -void run_task(Gamepad (&gamepads)[MAX_GAMEPADS]) +void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) { for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { bt_devices_[i].gamepad = &gamepads[i]; } - BLEServer::init_server(); - uni_platform_set_custom(get_driver()); uni_init(0, nullptr); diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.h b/Firmware/RP2040/src/Bluepad32/Bluepad32.h index 747f190..dfcec61 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.h +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.h @@ -4,7 +4,7 @@ #include #include -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "board_config.h" /* NOTE: Everything bluepad32/uni needs to be wrapped @@ -12,7 +12,7 @@ namespace bluepad32 { - void run_task(Gamepad (&gamepads)[MAX_GAMEPADS]); + void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]); } #endif // _BLUEPAD_32_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Board/board_api.cpp b/Firmware/RP2040/src/Board/board_api.cpp index 04f90bf..b711126 100644 --- a/Firmware/RP2040/src/Board/board_api.cpp +++ b/Firmware/RP2040/src/Board/board_api.cpp @@ -59,10 +59,11 @@ bool usb::host_connected() void usb::disconnect_all() { OGXM_LOG("Disconnecting USB and resetting Core1\n"); - tud_disconnect(); - sleep_ms(300); + multicore_reset_core1(); sleep_ms(300); + tud_disconnect(); + sleep_ms(300); } // If using PicoW, only use this method from the core running btstack and after you've called init_bluetooth diff --git a/Firmware/RP2040/src/Gamepad.h b/Firmware/RP2040/src/Gamepad/Gamepad.h similarity index 50% rename from Firmware/RP2040/src/Gamepad.h rename to Firmware/RP2040/src/Gamepad/Gamepad.h index 4ea0e66..6b6465f 100644 --- a/Firmware/RP2040/src/Gamepad.h +++ b/Firmware/RP2040/src/Gamepad/Gamepad.h @@ -6,10 +6,16 @@ #include #include #include +#include #include -#include "Range.h" +#include "libfixmath/fix16.hpp" + +#include "Gamepad/Range.h" +#include "Gamepad/fix16ext.h" #include "UserSettings/UserProfile.h" +#include "UserSettings/JoystickSettings.h" +#include "UserSettings/TriggerSettings.h" #include "Board/ogxm_log.h" class Gamepad @@ -192,9 +198,8 @@ public: void set_profile(const UserProfile& user_profile) { - set_profile_options(user_profile); set_profile_mappings(user_profile); - set_profile_deadzones(user_profile); + set_profile_settings(user_profile); } inline void set_pad_in(PadIn pad_in) @@ -243,185 +248,58 @@ public: mutex_exit(&chatpad_in_mutex_); } - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ template - inline int16_t scale_joystick_rx(T value) const + inline std::pair scale_joystick_r(T x, T y, bool invert_y = false) const { - int16_t joy_value = 0; + int16_t joy_x = 0; + int16_t joy_y = 0; if constexpr (bits > 0) { - joy_value = Range::scale_from_bits(value); + joy_x = Range::scale_from_bits(x); + joy_y = Range::scale_from_bits(y); } else if constexpr (!std::is_same_v) { - joy_value = Range::scale(value); + joy_x = Range::scale(x); + joy_y = Range::scale(y); } else { - joy_value = value; + joy_x = x; + joy_y = y; } - if (joy_value > dz_.joystick_r_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_r_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_r_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_r_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return joy_value; + + return joy_settings_r_en_ + ? apply_joystick_settings(joy_x, joy_y, joy_settings_r_, invert_y) + : std::make_pair(joy_x, invert_y ? Range::invert(joy_y) : joy_y); } - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ template - inline int16_t scale_joystick_ry(T value, bool invert = false) const + inline std::pair scale_joystick_l(T x, T y, bool invert_y = false) const { - int16_t joy_value = 0; + int16_t joy_x = 0; + int16_t joy_y = 0; if constexpr (bits > 0) { - joy_value = Range::scale_from_bits(value); + joy_x = Range::scale_from_bits(x); + joy_y = Range::scale_from_bits(y); } else if constexpr (!std::is_same_v) { - joy_value = Range::scale(value); + joy_x = Range::scale(x); + joy_y = Range::scale(y); } else { - joy_value = value; + joy_x = x; + joy_y = y; } - if (joy_value > dz_.joystick_r_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_r_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_r_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_r_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return profile_invert_ry_ ? (invert ? joy_value : Range::invert(joy_value)) : (invert ? Range::invert(joy_value) : joy_value); + + return joy_settings_l_en_ + ? apply_joystick_settings(joy_x, joy_y, joy_settings_l_, invert_y) + : std::make_pair(joy_x, invert_y ? Range::invert(joy_y) : joy_y); } - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ - template - inline int16_t scale_joystick_lx(T value) const - { - int16_t joy_value = 0; - if constexpr (bits > 0) - { - joy_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - joy_value = Range::scale(value); - } - else - { - joy_value = value; - } - if (joy_value > dz_.joystick_l_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_l_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_l_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_l_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return joy_value; - } - - /* Get joy value adjusted for deadzones, scaling, and inversion settings. - param is optional, used for scaling specific bit values as opposed - to full range values. */ - template - inline int16_t scale_joystick_ly(T value, bool invert = false) const - { - int16_t joy_value = 0; - if constexpr (bits > 0) - { - joy_value = Range::scale_from_bits(value); - } - else if constexpr (!std::is_same_v) - { - joy_value = Range::scale(value); - } - else - { - joy_value = value; - } - if (joy_value > dz_.joystick_l_pos) - { - joy_value = Range::scale( - joy_value, - dz_.joystick_l_pos, - Range::MAX, - Range::MID, - Range::MAX); - } - else if (joy_value < dz_.joystick_l_neg) - { - joy_value = Range::scale( - joy_value, - Range::MIN, - dz_.joystick_l_neg, - Range::MIN, - Range::MID); - } - else - { - joy_value = 0; - } - return profile_invert_ly_ ? (invert ? joy_value : Range::invert(joy_value)) : (invert ? Range::invert(joy_value) : joy_value); - } - - /* Get trigger value adjusted for deadzones, scaling, and inversion. - param is optional, used for scaling speicifc bit values - as opposed to full range values */ template inline uint8_t scale_trigger_l(T value) const { @@ -438,12 +316,11 @@ public: { trigger_value = value; } - return trigger_value > dz_.trigger_l ? Range::scale(trigger_value, dz_.trigger_l, Range::MAX) : 0; + return trig_settings_l_en_ + ? apply_trigger_settings(trigger_value, trig_settings_l_) + : trigger_value; } - /* Get trigger value adjusted for deadzones, scaling, and inversion. - param is optional, used for scaling speicifc bit values - as opposed to full range values */ template inline uint8_t scale_trigger_r(T value) const { @@ -460,17 +337,19 @@ public: { trigger_value = value; } - return trigger_value > dz_.trigger_l ? Range::scale(trigger_value, dz_.trigger_l, Range::MAX) : 0; + return trig_settings_r_en_ + ? apply_trigger_settings(trigger_value, trig_settings_r_) + : trigger_value; } -private: +private: mutex_t pad_in_mutex_; mutex_t pad_out_mutex_; mutex_t chatpad_in_mutex_; PadOut pad_out_; PadIn pad_in_; - ChatpadIn chatpad_in_; + ChatpadIn chatpad_in_{0}; std::atomic new_pad_in_{false}; std::atomic new_pad_out_{false}; @@ -479,25 +358,53 @@ private: std::atomic analog_host_{false}; std::atomic analog_device_{false}; - bool profile_invert_ly_{false}; - bool profile_invert_ry_{false}; bool profile_analog_enabled_{false}; - struct Deadzones - { - uint8_t trigger_l{0}; - uint8_t trigger_r{0}; - int16_t joystick_l_neg{0}; - int16_t joystick_l_pos{0}; - int16_t joystick_r_neg{0}; - int16_t joystick_r_pos{0}; - } dz_; + JoystickSettings joy_settings_l_; + JoystickSettings joy_settings_r_; + TriggerSettings trig_settings_l_; + TriggerSettings trig_settings_r_; - void set_profile_options(const UserProfile& profile) + bool joy_settings_l_en_{false}; + bool joy_settings_r_en_{false}; + bool trig_settings_l_en_{false}; + bool trig_settings_r_en_{false}; + + void set_profile_settings(const UserProfile& profile) { - profile_invert_ly_ = profile.invert_ly ? true : false; - profile_invert_ry_ = profile.invert_ry ? true : false; profile_analog_enabled_ = profile.analog_enabled ? true : false; + OGXM_LOG("profile_analog_enabled_: %d\n", profile_analog_enabled_); + + if ((joy_settings_l_en_ = !joy_settings_l_.is_same(profile.joystick_settings_l))) + { + joy_settings_l_.set_from_raw(profile.joystick_settings_l); + //This needs to be addressed in the webapp, just multiply here for now + joy_settings_l_.axis_restrict *= static_cast(100); + joy_settings_l_.angle_restrict *= static_cast(100); + joy_settings_l_.anti_dz_angular *= static_cast(100); + } + if ((joy_settings_r_en_ = !joy_settings_r_.is_same(profile.joystick_settings_r))) + { + joy_settings_r_.set_from_raw(profile.joystick_settings_r); + //This needs to be addressed in the webapp, just multiply here for now + joy_settings_r_.axis_restrict *= static_cast(100); + joy_settings_r_.angle_restrict *= static_cast(100); + joy_settings_r_.anti_dz_angular *= static_cast(100); + } + if ((trig_settings_l_en_ = !trig_settings_l_.is_same(profile.trigger_settings_l))) + { + trig_settings_l_.set_from_raw(profile.trigger_settings_l); + } + if ((trig_settings_r_en_ = !trig_settings_r_.is_same(profile.trigger_settings_r))) + { + trig_settings_r_.set_from_raw(profile.trigger_settings_r); + } + + OGXM_LOG("GamepadMapper: JoyL: %s, JoyR: %s, TrigL: %s, TrigR: %s\n", + joy_settings_l_en_ ? "Enabled" : "Disabled", + joy_settings_r_en_ ? "Enabled" : "Disabled", + trig_settings_l_en_ ? "Enabled" : "Disabled", + trig_settings_r_en_ ? "Enabled" : "Disabled"); } void set_profile_mappings(const UserProfile& profile) @@ -537,27 +444,197 @@ private: MAP_ANALOG_OFF_RB = profile.analog_off_rb; } - void set_profile_deadzones(const UserProfile& profile) //Deadzones in the profile are 0-255 (0-100%) + static inline std::pair apply_joystick_settings( + int16_t gp_joy_x, + int16_t gp_joy_y, + const JoystickSettings& set, + bool invert_y) { - dz_.trigger_l = profile.dz_trigger_l; - dz_.trigger_r = profile.dz_trigger_r; + static const Fix16 + FIX_0(0.0f), + FIX_1(1.0f), + FIX_2(2.0f), + FIX_45(45.0f), + FIX_90(90.0f), + FIX_100(100.0f), + FIX_180(180.0f), + FIX_EPSILON(0.0001f), + FIX_EPSILON2(0.001f), + FIX_ELLIPSE_DEF(1.570796f), + FIX_DIAG_DIVISOR(0.29289f); - OGXM_LOG("dz_.trigger_l: %d\n", dz_.trigger_l); - OGXM_LOG("dz_.trigger_r: %d\n", dz_.trigger_r); - OGXM_LOG("profile.dz_joystick_l: %d\n", profile.dz_joystick_l); - OGXM_LOG("profile.dz_joystick_r: %d\n", profile.dz_joystick_r); + Fix16 x = (set.invert_x ? Fix16(Range::invert(gp_joy_x)) : Fix16(gp_joy_x)) / Range::MAX; + Fix16 y = ((set.invert_y ^ invert_y) ? Fix16(Range::invert(gp_joy_y)) : Fix16(gp_joy_y)) / Range::MAX; - dz_.joystick_l_pos = profile.dz_joystick_l ? Range::scale(static_cast(profile.dz_joystick_l / 2)) : 0; - dz_.joystick_l_neg = Range::invert(dz_.joystick_l_pos); - dz_.joystick_r_pos = profile.dz_joystick_r ? Range::scale(static_cast(profile.dz_joystick_r / 2)) : 0; - dz_.joystick_r_neg = Range::invert(dz_.joystick_r_pos); + const Fix16 abs_x = fix16::abs(x); + const Fix16 abs_y = fix16::abs(y); + const Fix16 inv_axis_restrict = FIX_1 / (FIX_1 - set.axis_restrict); - OGXM_LOG("dz_.trigger_l: %d\n", dz_.trigger_l); - OGXM_LOG("dz_.trigger_r: %d\n", dz_.trigger_r); - OGXM_LOG("dz_.joystick_l_pos: %d\n", dz_.joystick_l_pos); - OGXM_LOG("dz_.joystick_l_neg: %d\n", dz_.joystick_l_neg); - OGXM_LOG("dz_.joystick_r_pos: %d\n", dz_.joystick_r_pos); - OGXM_LOG("dz_.joystick_r_neg: %d\n", dz_.joystick_r_neg); + Fix16 rAngle = (abs_x < FIX_EPSILON) + ? FIX_90 + : fix16::rad2deg(fix16::abs(fix16::atan(y / x))); + + Fix16 axial_x = (abs_x <= set.axis_restrict && rAngle > FIX_45) + ? FIX_0 + : ((abs_x - set.axis_restrict) * inv_axis_restrict); + + Fix16 axial_y = (abs_y <= set.axis_restrict && rAngle <= FIX_45) + ? FIX_0 + : ((abs_y - set.axis_restrict) * inv_axis_restrict); + + Fix16 in_magnitude = fix16::sqrt(fix16::sq(axial_x) + fix16::sq(axial_y)); + + if (in_magnitude < set.dz_inner) + { + return { 0, 0 }; + } + + Fix16 angle = + fix16::abs(axial_x) < FIX_EPSILON + ? FIX_90 + : fix16::rad2deg(fix16::abs(fix16::atan(axial_y / axial_x))); + + Fix16 anti_r_scale = (set.anti_dz_square_y_scale == FIX_0) ? set.anti_dz_square : set.anti_dz_square_y_scale; + Fix16 anti_dz_c = set.anti_dz_circle; + + if (anti_r_scale > FIX_0 && anti_dz_c > FIX_0) + { + Fix16 anti_ellip_scale = anti_ellip_scale / anti_dz_c; + Fix16 ellipse_angle = fix16::atan((FIX_1 / anti_ellip_scale) * fix16::tan(fix16::rad2deg(rAngle))); + ellipse_angle = (ellipse_angle < FIX_0) ? FIX_ELLIPSE_DEF : ellipse_angle; + + Fix16 ellipse_x = fix16::cos(ellipse_angle); + Fix16 ellipse_y = fix16::sqrt(fix16::sq(anti_ellip_scale) * (FIX_1 - fix16::sq(ellipse_x))); + anti_dz_c *= fix16::sqrt(fix16::sq(ellipse_x) + fix16::sq(ellipse_y)); + } + + if (anti_dz_c > FIX_0) + { + anti_dz_c = anti_dz_c / ((anti_dz_c * (FIX_1 - set.anti_dz_circle / set.dz_outer)) / (anti_dz_c * (FIX_1 - set.anti_dz_square))); + } + + if (abs_x > set.axis_restrict && abs_y > set.axis_restrict) + { + const Fix16 FIX_ANGLE_MAX = set.angle_restrict / 2.0f; + + if (angle > FIX_0 && angle < FIX_ANGLE_MAX) + { + angle = FIX_0; + } + if (angle > (FIX_90 - FIX_ANGLE_MAX)) + { + angle = FIX_90; + } + if (angle > FIX_ANGLE_MAX && angle < (FIX_90 - FIX_ANGLE_MAX)) + { + angle = ((angle - FIX_ANGLE_MAX) * FIX_90) / ((FIX_90 - FIX_ANGLE_MAX) - FIX_ANGLE_MAX); + } + } + + Fix16 ref_angle = (angle < FIX_EPSILON2) ? FIX_0 : angle; + Fix16 diagonal = (angle > FIX_45) ? (((angle - FIX_45) * (-FIX_45)) / FIX_45) + FIX_45 : angle; + + const Fix16 angle_comp = set.angle_restrict / FIX_2; + + if (angle < FIX_90 && angle > FIX_0) + { + angle = ((angle * ((FIX_90 - angle_comp) - angle_comp)) / FIX_90) + angle_comp; + } + + if (axial_x < FIX_0 && axial_y > FIX_0) + { + angle = -angle; + } + if (axial_x > FIX_0 && axial_y < FIX_0) + { + angle = angle - FIX_180; + } + if (axial_x < FIX_0 && axial_y < FIX_0) + { + angle = angle + FIX_180; + } + + //Deadzone Warp + Fix16 out_magnitude = (in_magnitude - set.dz_inner) / (set.anti_dz_outer - set.dz_inner); + out_magnitude = fix16::pow(out_magnitude, (FIX_1 / set.curve)) * (set.dz_outer - anti_dz_c) + anti_dz_c; + out_magnitude = (out_magnitude > set.dz_outer && !set.uncap_radius) ? set.dz_outer : out_magnitude; + + Fix16 d_scale = (((out_magnitude - anti_dz_c) * (set.diag_scale_max - set.diag_scale_min)) / (set.dz_outer - anti_dz_c)) + set.diag_scale_min; + Fix16 c_scale = (diagonal * (FIX_1 / fix16::sqrt(FIX_2))) / FIX_45; //Both these lines scale the intensity of the warping + c_scale = FIX_1 - fix16::sqrt(FIX_1 - c_scale * c_scale); //based on a circular curve to the perfect diagonal + d_scale = (c_scale * (d_scale - FIX_1)) / FIX_DIAG_DIVISOR + FIX_1; + + out_magnitude = out_magnitude * d_scale; + + //Scaling values for square antideadzone + Fix16 new_x = fix16::cos(fix16::deg2rad(angle)) * out_magnitude; + Fix16 new_y = fix16::sin(fix16::deg2rad(angle)) * out_magnitude; + + //Magic angle wobble fix by user ME. + // if (angle > 45.0 && angle < 225.0) { + // newX = inv(Math.sin(deg2rad(angle - 90.0)))*outputMagnitude; + // newY = inv(Math.cos(deg2rad(angle - 270.0)))*outputMagnitude; + // } + + //Square antideadzone scaling + Fix16 output_x = fix16::abs(new_x) * (FIX_1 - set.anti_dz_square / set.dz_outer) + set.anti_dz_square; + if (x < FIX_0) + { + output_x = -output_x; + } + if (ref_angle == FIX_90) + { + output_x = FIX_0; + } + + Fix16 output_y = fix16::abs(new_y) * (FIX_1 - anti_r_scale / set.dz_outer) + anti_r_scale; + if (y < FIX_0) + { + output_y = -output_y; + } + if (ref_angle == FIX_0) + { + output_y = FIX_0; + } + + output_x = fix16::clamp(output_x, -FIX_1, FIX_1) * Range::MAX; + output_y = fix16::clamp(output_y, -FIX_1, FIX_1) * Range::MAX; + + return { static_cast(fix16_to_int(output_x)), static_cast(fix16_to_int(output_y)) }; + } + + uint8_t apply_trigger_settings(uint8_t value, const TriggerSettings& set) const + { + Fix16 abs_value = fix16::abs(Fix16(static_cast(value)) / static_cast(Range::MAX)); + + if (abs_value < set.dz_inner) + { + return 0; + } + + static const Fix16 + FIX_0(0.0f), + FIX_1(1.0f), + FIX_2(2.0f); + + Fix16 value_out = (abs_value - set.dz_inner) / (set.anti_dz_outer - set.dz_inner); + value_out = fix16::clamp(value_out, FIX_0, FIX_1); + + if (set.anti_dz_inner > FIX_0) + { + value_out = set.anti_dz_inner + (FIX_1 - set.anti_dz_inner) * value_out; + } + if (set.curve != FIX_1) + { + value_out = fix16::pow(value_out, FIX_1 / set.curve); + } + if (set.anti_dz_outer < FIX_1) + { + value_out = fix16::clamp(value_out * (FIX_1 / (FIX_1 - set.anti_dz_outer)), FIX_0, FIX_1); + } + + value_out *= set.dz_outer; + return static_cast(fix16_to_int(value_out * static_cast(Range::MAX))); } }; diff --git a/Firmware/ESP32/main/Range.h b/Firmware/RP2040/src/Gamepad/Range.h similarity index 57% rename from Firmware/ESP32/main/Range.h rename to Firmware/RP2040/src/Gamepad/Range.h index ae08dbb..667bc61 100644 --- a/Firmware/ESP32/main/Range.h +++ b/Firmware/RP2040/src/Gamepad/Range.h @@ -4,6 +4,7 @@ #include #include #include +#include "Board/ogxm_log.h" namespace Range { @@ -78,77 +79,31 @@ namespace Range { requires std::is_integral_v && std::is_integral_v static inline To clamp(From value) { - if constexpr (std::is_signed_v != std::is_signed_v) - { - using CommonType = std::common_type_t; - return static_cast((static_cast(value) < static_cast(Range::MIN)) - ? Range::MIN - : (static_cast(value) > static_cast(Range::MAX)) - ? Range::MAX - : static_cast(value)); - } - else - { - return static_cast((value < Range::MIN) - ? Range::MIN - : (value > Range::MAX) - ? Range::MAX - : value); - } + return static_cast((value < Range::MIN) + ? Range::MIN + : (value > Range::MAX) + ? Range::MAX + : value); } template - requires std::is_integral_v static inline T clamp(T value, T min, T max) { return (value < min) ? min : (value > max) ? max : value; } + template + static inline To clamp(From value, To min_to, To max_to) + { + return (value < min_to) ? min_to : (value > max_to) ? max_to : static_cast(value); + } + template requires std::is_integral_v && std::is_integral_v - static inline To scale(From value, From min_from, From max_from, To min_to, To max_to) + static constexpr To scale(From value, From min_from, From max_from, To min_to, To max_to) { - if constexpr (std::is_unsigned_v && std::is_unsigned_v) - { - // Both unsigned - uint64_t scaled = static_cast(value - min_from) * - (max_to - min_to) / - (max_from - min_from) + min_to; - return static_cast(scaled); - } - else if constexpr (std::is_signed_v && std::is_unsigned_v) - { - // From signed, To unsigned - uint64_t shift_from = static_cast(-min_from); - uint64_t u_value = static_cast(value) + shift_from; - uint64_t u_min_from = static_cast(min_from) + shift_from; - uint64_t u_max_from = static_cast(max_from) + shift_from; - - uint64_t scaled = (u_value - u_min_from) * - (max_to - min_to) / - (u_max_from - u_min_from) + min_to; - return static_cast(scaled); - } - else if constexpr (std::is_unsigned_v && std::is_signed_v) - { - // From unsigned, To signed - uint64_t shift_to = static_cast(-min_to); - uint64_t scaled = static_cast(value - min_from) * - (static_cast(max_to) + shift_to - static_cast(min_to) - shift_to) / - (max_from - min_from) + static_cast(min_to) + shift_to; - return static_cast(scaled - shift_to); - } - else - { - // Both signed - int64_t shift_from = -min_from; - int64_t shift_to = -min_to; - - int64_t scaled = (static_cast(value) + shift_from - (min_from + shift_from)) * - (max_to + shift_to - (min_to + shift_to)) / - (max_from - min_from) + (min_to + shift_to); - return static_cast(scaled - shift_to); - } + return static_cast( + (static_cast(value - min_from) * (max_to - min_to) / (max_from - min_from)) + min_to); } template @@ -189,4 +144,61 @@ namespace Range { } // namespace Range +namespace Scale //Scale and invert values +{ + static inline uint8_t int16_to_uint8(int16_t value) + { + uint16_t shifted_value = static_cast(value + Range::MID); + return static_cast(shifted_value >> 8); + } + static inline uint16_t int16_to_uint16(int16_t value) + { + return static_cast(value + Range::MID); + } + static inline int8_t int16_to_int8(int16_t value) + { + return static_cast((value + Range::MID) >> 8); + } + + static inline uint8_t uint16_to_uint8(uint16_t value) + { + return static_cast(value >> 8); + } + static inline int16_t uint16_to_int16(uint16_t value) + { + return static_cast(value - Range::MID); + } + static inline int8_t uint16_to_int8(uint16_t value) + { + return static_cast((value >> 8) - Range::MID); + } + + static inline int16_t uint8_to_int16(uint8_t value) + { + return static_cast((static_cast(value) << 8) - Range::MID); + } + static inline uint16_t uint8_to_uint16(uint8_t value) + { + return static_cast(value) << 8; + } + static inline int8_t uint8_to_int8(uint8_t value) + { + return static_cast(value - Range::MID); + } + + static inline int16_t int8_to_int16(int8_t value) + { + return static_cast(value) << 8; + } + static inline uint16_t int8_to_uint16(int8_t value) + { + return static_cast((value + Range::MID) << 8); + } + static inline uint8_t int8_to_uint8(int8_t value) + { + return static_cast(value + Range::MID); + } + +} // namespace Scale + #endif // _RANGE_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Gamepad/fix16ext.h b/Firmware/RP2040/src/Gamepad/fix16ext.h new file mode 100644 index 0000000..b59b027 --- /dev/null +++ b/Firmware/RP2040/src/Gamepad/fix16ext.h @@ -0,0 +1,106 @@ +#ifndef FIX16_EXT_H +#define FIX16_EXT_H + +#include + +#include "libfixmath/fix16.hpp" + +namespace fix16 { + +inline Fix16 abs(Fix16 x) +{ + return Fix16(fix16_abs(x.value)); +} + +inline Fix16 rad2deg(Fix16 rad) +{ + return Fix16(fix16_rad_to_deg(rad.value)); +} + +inline Fix16 deg2rad(Fix16 deg) +{ + return Fix16(fix16_deg_to_rad(deg.value)); +} + +inline Fix16 atan(Fix16 x) +{ + return Fix16(fix16_atan(x.value)); +} + +inline Fix16 atan2(Fix16 y, Fix16 x) +{ + return Fix16(fix16_atan2(y.value, x.value)); +} + +inline Fix16 tan(Fix16 x) +{ + return Fix16(fix16_tan(x.value)); +} + +inline Fix16 cos(Fix16 x) +{ + return Fix16(fix16_cos(x.value)); +} + +inline Fix16 sin(Fix16 x) +{ + return Fix16(fix16_sin(x.value)); +} + +inline Fix16 sqrt(Fix16 x) +{ + return Fix16(fix16_sqrt(x.value)); +} + +inline Fix16 sq(Fix16 x) +{ + return Fix16(fix16_sq(x.value)); +} + +inline Fix16 clamp(Fix16 x, Fix16 min, Fix16 max) +{ + return Fix16(fix16_clamp(x.value, min.value, max.value)); +} + +inline Fix16 pow(Fix16 x, Fix16 y) +{ + fix16_t& base = x.value; + fix16_t& exponent = y.value; + + if (exponent == F16(0.0)) + return Fix16(fix16_from_int(1)); + if (base == F16(0.0)) + return Fix16(fix16_from_int(0)); + + int32_t int_exp = fix16_to_int(exponent); + + if (fix16_from_int(int_exp) == exponent) + { + fix16_t result = F16(1.0); + fix16_t current_base = base; + + if (int_exp < 0) + { + current_base = fix16_div(F16(1.0), base); + int_exp = -int_exp; + } + + while (int_exp) + { + if (int_exp & 1) + { + result = fix16_mul(result, current_base); + } + current_base = fix16_mul(current_base, current_base); + int_exp >>= 1; + } + + return Fix16(result); + } + + return Fix16(fix16_exp(fix16_mul(exponent, fix16_log(base)))); +} + +} // namespace fix16 + +#endif // FIX16_EXT_H \ No newline at end of file diff --git a/Firmware/RP2040/src/I2CDriver/4Channel/I2CManager.h b/Firmware/RP2040/src/I2CDriver/4Channel/I2CManager.h index e31780a..a06b1a3 100644 --- a/Firmware/RP2040/src/I2CDriver/4Channel/I2CManager.h +++ b/Firmware/RP2040/src/I2CDriver/4Channel/I2CManager.h @@ -7,7 +7,7 @@ #include #include "board_config.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "I2CDriver/4Channel/I2CMaster.h" #include "I2CDriver/4Channel/I2CSlave.h" #include "I2CDriver/4Channel/I2CDriver.h" diff --git a/Firmware/RP2040/src/I2CDriver/4Channel/I2CMaster.h b/Firmware/RP2040/src/I2CDriver/4Channel/I2CMaster.h index 922fa7a..2825c8d 100644 --- a/Firmware/RP2040/src/I2CDriver/4Channel/I2CMaster.h +++ b/Firmware/RP2040/src/I2CDriver/4Channel/I2CMaster.h @@ -7,7 +7,7 @@ #include #include "board_config.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "I2CDriver/4Channel/I2CDriver.h" class I2CMaster : public I2CDriver diff --git a/Firmware/RP2040/src/I2CDriver/4Channel/I2CSlave.h b/Firmware/RP2040/src/I2CDriver/4Channel/I2CSlave.h index 396ba46..19ad2e0 100644 --- a/Firmware/RP2040/src/I2CDriver/4Channel/I2CSlave.h +++ b/Firmware/RP2040/src/I2CDriver/4Channel/I2CSlave.h @@ -8,7 +8,7 @@ #include #include "board_config.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "I2CDriver/4Channel/I2CDriver.h" class I2CSlave : public I2CDriver diff --git a/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.cpp b/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.cpp index a4d75c8..edc16db 100644 --- a/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.cpp +++ b/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.cpp @@ -4,7 +4,6 @@ #include #include -#include "Gamepad.h" #include "board_config.h" #include "Board/board_api.h" #include "I2CDriver/ESP32/I2CDriver.h" @@ -76,8 +75,6 @@ static inline void slave_handler(i2c_inst_t *i2c, i2c_slave_event_t event) break; case PacketID::SET_DRIVER: - OGXM_LOG("I2C: Received SET_DRIVER packet: " + OGXM_TO_STRING(packet_in.device_type) + "\n"); - if (packet_in.device_type != DeviceDriverType::NONE && packet_in.device_type != current_device_type) { diff --git a/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.h b/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.h index 61191fa..81a3b2b 100644 --- a/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.h +++ b/Firmware/RP2040/src/I2CDriver/ESP32/I2CDriver.h @@ -3,7 +3,7 @@ #include -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" namespace I2CDriver { diff --git a/Firmware/RP2040/src/OGXMini/OGXMini_ESP32.cpp b/Firmware/RP2040/src/OGXMini/OGXMini_ESP32.cpp index c1671c8..3510b39 100644 --- a/Firmware/RP2040/src/OGXMini/OGXMini_ESP32.cpp +++ b/Firmware/RP2040/src/OGXMini/OGXMini_ESP32.cpp @@ -10,7 +10,7 @@ #include "Board/board_api.h" #include "OGXMini/OGXMini.h" #include "I2CDriver/ESP32/I2CDriver.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "TaskQueue/TaskQueue.h" namespace OGXMini { @@ -30,14 +30,30 @@ void core1_task() } } -void run_uart_bridge() +void run_uart_bridge(UserSettings& user_settings) { DeviceManager& device_manager = DeviceManager::get_instance(); device_manager.initialize_driver(DeviceDriverType::UART_BRIDGE, gamepads_); board_api::esp32::enter_programming_mode(); + + OGXM_LOG("Entering UART Bridge mode\n"); - device_manager.get_driver()->process(0, gamepads_[0]); //Runs UART Bridge task, doesn't return + device_manager.get_driver()->process(0, gamepads_[0]); //Runs UART Bridge task, doesn't return unless programming is complete + + OGXM_LOG("Exiting UART Bridge mode\n"); + + board_api::usb::disconnect_all(); + user_settings.write_datetime(); + board_api::reboot(); +} + +bool update_needed(UserSettings& user_settings) +{ +#if defined(OGXM_ESP32_RETAIL) + return !user_settings.verify_datetime(); +#endif + return false; } void run_program() @@ -48,9 +64,9 @@ void run_program() user_settings.initialize_flash(); //MODE_SEL_PIN is used to determine if UART bridge should be run - if (board_api::esp32::uart_bridge_mode()) + if (board_api::esp32::uart_bridge_mode() || update_needed(user_settings)) { - run_uart_bridge(); + run_uart_bridge(user_settings); return; } diff --git a/Firmware/RP2040/src/OGXMini/OGXMini_PicoW.cpp b/Firmware/RP2040/src/OGXMini/OGXMini_PicoW.cpp index dd87118..72f13ea 100644 --- a/Firmware/RP2040/src/OGXMini/OGXMini_PicoW.cpp +++ b/Firmware/RP2040/src/OGXMini/OGXMini_PicoW.cpp @@ -11,7 +11,8 @@ #include "Board/board_api.h" #include "Bluepad32/Bluepad32.h" #include "OGXMini/OGXMini.h" -#include "Gamepad.h" +#include "BLEServer/BLEServer.h" +#include "Gamepad/Gamepad.h" #include "TaskQueue/TaskQueue.h" namespace OGXMini { @@ -25,6 +26,7 @@ void core1_task() { board_api::init_bluetooth(); board_api::set_led(true); + BLEServer::init_server(gamepads_); bluepad32::run_task(gamepads_); } diff --git a/Firmware/RP2040/src/OGXMini/OGXMini_Standard.cpp b/Firmware/RP2040/src/OGXMini/OGXMini_Standard.cpp index b1db7f0..7162282 100644 --- a/Firmware/RP2040/src/OGXMini/OGXMini_Standard.cpp +++ b/Firmware/RP2040/src/OGXMini/OGXMini_Standard.cpp @@ -11,7 +11,7 @@ #include "USBDevice/DeviceManager.h" #include "OGXMini/OGXMini.h" #include "TaskQueue/TaskQueue.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "Board/board_api.h" #include "Board/ogxm_log.h" @@ -78,7 +78,6 @@ void set_gp_check_timer(uint32_t task_id, UserSettings& user_settings) { TaskQueue::Core0::queue_delayed_task(task_id, UserSettings::GP_CHECK_DELAY_MS, true, [&user_settings] { - OGXM_LOG("Checking for driver change.\n"); //Check gamepad inputs for button combo to change usb device driver if (user_settings.check_for_driver_change(gamepads_[0])) { @@ -101,17 +100,25 @@ void run_program() gamepads_[i].set_profile(user_settings.get_profile_by_index(i)); } + DeviceDriverType current_driver = user_settings.get_current_driver(); DeviceManager& device_manager = DeviceManager::get_instance(); - device_manager.initialize_driver(user_settings.get_current_driver(), gamepads_); + device_manager.initialize_driver(current_driver, gamepads_); multicore_reset_core1(); multicore_launch_core1(core1_task); - // Wait for something to call host_mounted() - while (!tud_inited()) + if (current_driver != DeviceDriverType::WEBAPP) { - TaskQueue::Core0::process_tasks(); - sleep_ms(100); + // Wait for something to call host_mounted() + while (!tud_inited()) + { + TaskQueue::Core0::process_tasks(); + sleep_ms(100); + } + } + else //Connect immediately in WebApp mode + { + host_mounted(true); } uint32_t tid_gp_check = TaskQueue::Core0::get_new_task_id(); diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriver.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriver.h index ed6a749..fed7709 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriver.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriver.h @@ -7,7 +7,7 @@ #include "class/hid/hid.h" #include "device/usbd_pvt.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL #define TUD_DRV_NAME(name) name diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/PSClassic/PSClassic.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/PSClassic/PSClassic.h index bef81da..8f760ac 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/PSClassic/PSClassic.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/PSClassic/PSClassic.h @@ -5,7 +5,6 @@ #include "tusb.h" -#include "Gamepad.h" #include "Descriptors/PSClassic.h" #include "USBDevice/DeviceDriver/DeviceDriver.h" diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.c b/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.c index 004d534..7d11093 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.c +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.c @@ -31,6 +31,9 @@ #define DEF_PARITY 0 #define DEF_DATA_BITS 8 +const char COMPLETE_FLAG[] = "PROGRAMMING_COMPLETE"; +const size_t COMPLETE_FLAG_READ_LEN = 22; + typedef struct { uart_inst_t *const inst; uint irq; @@ -74,6 +77,7 @@ const uart_id_t UART_ID[CFG_TUD_CDC] = { }; uart_data_t UART_DATA[CFG_TUD_CDC]; +bool programming_complete = false; static inline uint databits_usb2uart(uint8_t data_bits) { @@ -143,13 +147,28 @@ void usb_read_bytes(uint8_t itf) uart_data_t *ud = &UART_DATA[itf]; uint32_t len = tud_cdc_n_available(itf); - if (len && - mutex_try_enter(&ud->usb_mtx, NULL)) { + if (len && mutex_try_enter(&ud->usb_mtx, NULL)) + { len = MIN(len, BUFFER_SIZE - ud->usb_pos); - if (len) { + if (len) + { uint32_t count; - count = tud_cdc_n_read(itf, &ud->usb_buffer[ud->usb_pos], len); + + if (count >= COMPLETE_FLAG_READ_LEN) + { + for (uint32_t i = 0; i < count; i++) + { + uint32_t remaining = BUFFER_SIZE - ud->usb_pos - i; + if (remaining >= sizeof(COMPLETE_FLAG) - 1 && + memcmp(&ud->usb_buffer[ud->usb_pos + i], COMPLETE_FLAG, sizeof(COMPLETE_FLAG) - 1) == 0) + { + programming_complete = true; + break; + } + } + } + ud->usb_pos += count; } @@ -288,14 +307,14 @@ void init_uart_data(uint8_t itf) void core1_entry(void) { - for (int itf = 0; itf < CFG_TUD_CDC; itf++) + for (uint8_t itf = 0; itf < CFG_TUD_CDC; itf++) { init_uart_data(0); } while (1) { - for (int itf = 0; itf < CFG_TUD_CDC; itf++) + for (uint8_t itf = 0; itf < CFG_TUD_CDC; itf++) { update_uart_cfg(itf); uart_write_bytes(itf); @@ -316,13 +335,19 @@ int uart_bridge_run(void) { tud_task(); - for (int itf = 0; itf < CFG_TUD_CDC; itf++) + for (uint8_t itf = 0; itf < CFG_TUD_CDC; itf++) { if (tud_cdc_n_connected(itf)) { usb_cdc_process(itf); } } + + if (programming_complete) + { + multicore_reset_core1(); + break; + } } return 0; diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.h index 6a31d49..f4edd82 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/UARTBridge/uart_bridge/uart_bridge.h @@ -6,9 +6,6 @@ extern "C" { #endif int uart_bridge_run(void); -// const uint8_t *uart_bridge_descriptor_device_cb(void); -// const uint8_t *uart_bridge_descriptor_configuration_cb(uint8_t index); -// const uint16_t *uart_bridge_descriptor_string_cb(uint8_t index, uint16_t langid); #ifdef __cplusplus } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.cpp index 9346cc6..4c25701 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.cpp @@ -1,6 +1,7 @@ #include "class/cdc/cdc_device.h" #include "bsp/board_api.h" +#include "Board/ogxm_log.h" #include "Descriptors/CDCDev.h" #include "USBDevice/DeviceDriver/WebApp/WebApp.h" @@ -19,62 +20,245 @@ void WebAppDevice::initialize() }; } +bool WebAppDevice::read_serial(void* buffer, size_t len, bool block) +{ + if (!block && !tud_cdc_available()) + { + return false; + } + + uint8_t* buf_ptr = reinterpret_cast(buffer); + size_t total_read = 0; + + while (total_read < len) + { + if (!tud_cdc_connected()) + { + return false; + } + tud_task(); + + if (tud_cdc_available()) + { + size_t read = tud_cdc_read(buf_ptr + total_read, len - total_read); + total_read += read; + } + } + tud_cdc_read_flush(); + return total_read == len; +} + +bool WebAppDevice::write_serial(const void* buffer, size_t len) +{ + const uint8_t* buf_ptr = reinterpret_cast(buffer); + size_t total_written = 0; + + while (total_written < len) + { + if (!tud_cdc_connected()) + { + return false; + } + tud_task(); + + if (tud_cdc_write_available()) + { + size_t written = tud_cdc_write(buf_ptr + total_written, len - total_written); + total_written += written; + tud_cdc_write_flush(); + } + } + return total_written == len; +} + +bool WebAppDevice::write_packet(const Packet& packet) +{ + if (!write_serial(&packet, sizeof(Packet))) + { + return false; + } + return true; +} + +bool WebAppDevice::read_packet(Packet& packet, bool block) +{ + if (!read_serial(&packet, sizeof(Packet), block)) + { + return false; + } + return true; +} + +//Blocking read +bool WebAppDevice::read_profile(UserProfile& profile) +{ + uint8_t* profile_data = reinterpret_cast(&profile); + uint8_t current_chunk = 0; + uint8_t expected_chunks = 0; + + while (current_chunk < expected_chunks || expected_chunks == 0) + { + if (!read_packet(packet_out_, true)) + { + return false; + } + if (packet_out_.header.packet_id != PacketID::SET_PROFILE) + { + return false; + } + if (expected_chunks == 0 && packet_out_.header.chunks_total > 0) + { + expected_chunks = packet_out_.header.chunks_total; + } + else if (expected_chunks == 0) + { + return false; + } + + size_t offset = packet_out_.header.chunk_idx * packet_out_.header.chunk_len; + size_t bytes_to_copy = std::min(static_cast(packet_out_.header.chunk_len), sizeof(UserProfile) - offset); + + std::memcpy(profile_data + offset, packet_out_.data.data(), bytes_to_copy); + current_chunk++; + } + return true; +} + +bool WebAppDevice::write_profile(uint8_t index, const UserProfile& profile) +{ + const uint8_t* profile_data = reinterpret_cast(&profile); + uint8_t total_chunks = static_cast((sizeof(UserProfile) + packet_in_.data.size() - 1) / packet_in_.data.size()); + uint8_t current_chunk = 0; + + while (current_chunk < total_chunks) + { + size_t offset = current_chunk * packet_in_.data.size(); + size_t remaining_bytes = sizeof(UserProfile) - offset; + uint8_t current_chunk_len = static_cast(std::min(packet_in_.data.size(), remaining_bytes)); + + packet_in_.header.packet_id = PacketID::GET_PROFILE_RESP_OK; + packet_in_.header.max_gamepads = MAX_GAMEPADS; + packet_in_.header.player_idx = index; + packet_in_.header.profile_id = profile.id; + packet_in_.header.chunks_total = total_chunks; + packet_in_.header.chunk_idx = current_chunk; + packet_in_.header.chunk_len = current_chunk_len; + + std::memcpy(packet_in_.data.data(), profile_data + offset, packet_in_.header.chunk_len); + + if (!write_packet(packet_in_)) + { + return false; + } + current_chunk++; + } + return true; +} + +bool WebAppDevice::write_gamepad(uint8_t index, const Gamepad::PadIn& pad_in) +{ + const uint8_t* pad_in_data = reinterpret_cast(&pad_in); + const uint8_t total_chunks = static_cast((sizeof(Gamepad::PadIn) + packet_in_.data.size() - 1) / packet_in_.data.size()); + uint8_t current_chunk = 0; + + while (current_chunk < total_chunks) + { + size_t offset = current_chunk * packet_in_.data.size(); + size_t remaining_bytes = sizeof(Gamepad::PadIn) - offset; + uint8_t current_chunk_len = static_cast(std::min(packet_in_.data.size(), remaining_bytes)); + + packet_in_.header.packet_id = PacketID::SET_GP_IN; + packet_in_.header.max_gamepads = MAX_GAMEPADS; + packet_in_.header.player_idx = index; + packet_in_.header.chunks_total = total_chunks; + packet_in_.header.chunk_idx = current_chunk; + packet_in_.header.chunk_len = current_chunk_len; + + std::memcpy(packet_in_.data.data(), pad_in_data + offset, packet_in_.header.chunk_len); + + if (!write_packet(packet_in_)) + { + return false; + } + current_chunk++; + } + return true; +} + +void WebAppDevice::write_error() +{ + packet_in_.header.packet_id = PacketID::RESP_ERROR; + write_packet(packet_in_); +} + void WebAppDevice::process(const uint8_t idx, Gamepad& gamepad) { - if (!tud_cdc_available() || !tud_cdc_connected()) + if (!tud_cdc_connected()) { return; } tud_cdc_write_flush(); - tud_cdc_read(reinterpret_cast(&in_report_), sizeof(Report)); - tud_cdc_read_flush(); + bool success = false; - bool success = false; + OGXM_LOG("Processing WebApp device\n"); - switch (in_report_.report_id) + if (read_packet(packet_out_, false)) { - case ReportID::INIT_READ: - in_report_.input_mode = static_cast(user_settings_.get_current_driver()); - in_report_.player_idx = 0; - in_report_.report_id = ReportID::RESP_OK; - in_report_.max_gamepads = MAX_GAMEPADS; + OGXM_LOG("Received packet with ID: %d\n", packet_out_.header.packet_id); + + switch (packet_out_.header.packet_id) + { + case PacketID::GET_PROFILE_BY_ID: + profile_ = user_settings_.get_profile_by_id(packet_out_.header.profile_id); + if (!write_profile(packet_out_.header.profile_id, profile_)) + { + write_error(); + return; + } + break; - in_report_.profile.id = user_settings_.get_active_profile_id(in_report_.player_idx); - in_report_.profile = user_settings_.get_profile_by_id(in_report_.profile.id); + case PacketID::GET_PROFILE_BY_IDX: + profile_ = user_settings_.get_profile_by_index(packet_out_.header.player_idx); + if (!write_profile(packet_out_.header.player_idx, profile_)) + { + write_error(); + return; + } + break; - tud_cdc_write(reinterpret_cast(&in_report_), sizeof(Report)); - tud_cdc_write_flush(); - break; + case PacketID::SET_PROFILE_START: + if (!read_profile(profile_)) + { + write_error(); + return; + } + if (packet_out_.header.device_driver != DeviceDriverType::WEBAPP && + user_settings_.is_valid_driver(packet_out_.header.device_driver)) + { + success = user_settings_.store_profile_and_driver_type(packet_out_.header.device_driver, packet_out_.header.player_idx, profile_); + } + else + { + success = user_settings_.store_profile(packet_out_.header.player_idx, profile_); + } + if (!success) + { + write_error(); + return; + } + break; - case ReportID::READ_PROFILE: - in_report_.input_mode = static_cast(user_settings_.get_current_driver()); - in_report_.profile = user_settings_.get_profile_by_id(in_report_.profile.id); - in_report_.report_id = ReportID::RESP_OK; - - tud_cdc_write(reinterpret_cast(&in_report_), sizeof(Report)); - tud_cdc_write_flush(); - break; - - case ReportID::WRITE_PROFILE: - if (user_settings_.is_valid_driver(static_cast(in_report_.input_mode))) - { - success = user_settings_.store_profile_and_driver_type(static_cast(in_report_.input_mode), in_report_.player_idx, in_report_.profile); - } - else - { - success = user_settings_.store_profile(in_report_.player_idx, in_report_.profile); - } - if (!success) - { - in_report_.report_id = ReportID::RESP_ERROR; - tud_cdc_write(reinterpret_cast(&in_report_), sizeof(Report)); - tud_cdc_write_flush(); - } - break; - - default: - return; + default: + write_error(); + return; + } + } + else if (gamepad.new_pad_in()) + { + OGXM_LOG("Writing gamepad input\n"); + Gamepad::PadIn gp_in = gamepad.get_pad_in(); + write_gamepad(idx, gp_in); } } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.h index 9477a21..d9d1be2 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WebApp/WebApp.h @@ -1,6 +1,8 @@ #ifndef _WEBAAPP_DEVICE_H_ #define _WEBAAPP_DEVICE_H_ +#include + #include "USBDevice/DeviceDriver/DeviceDriver.h" #include "UserSettings/UserSettings.h" #include "UserSettings/UserProfile.h" @@ -20,30 +22,56 @@ public: const uint8_t* get_descriptor_device_qualifier_cb() override; private: - struct ReportID + enum class PacketID : uint8_t { - static constexpr uint8_t INIT_READ = 0x88; - static constexpr uint8_t READ_PROFILE = 0x01; - static constexpr uint8_t WRITE_PROFILE = 0x02; - static constexpr uint8_t RESP_OK = 0x10; - static constexpr uint8_t RESP_ERROR = 0x11; + NONE = 0, + GET_PROFILE_BY_ID = 0x50, + GET_PROFILE_BY_IDX = 0x51, + GET_PROFILE_RESP_OK = 0x52, + SET_PROFILE_START = 0x60, + SET_PROFILE = 0x61, + SET_PROFILE_RESP_OK = 0x62, + SET_GP_IN = 0x80, + SET_GP_OUT = 0x81, + RESP_ERROR = 0xFF }; - + #pragma pack(push, 1) - struct Report + struct PacketHeader { - uint8_t report_id{0}; - uint8_t input_mode{0}; + uint8_t packet_len{64}; + PacketID packet_id{PacketID::NONE}; + DeviceDriverType device_driver{DeviceDriverType::WEBAPP}; uint8_t max_gamepads{MAX_GAMEPADS}; uint8_t player_idx{0}; - UserProfile profile{UserProfile()}; + uint8_t profile_id{0}; + uint8_t chunks_total{0}; + uint8_t chunk_idx{0}; + uint8_t chunk_len{0}; + }; + static_assert(sizeof(PacketHeader) == 9, "WebApp report size mismatch"); + + struct Packet + { + PacketHeader header; + std::array data{0}; }; - static_assert(sizeof(Report) == 50, "WebApp report size mismatch"); #pragma pack(pop) + Packet packet_in_; + Packet packet_out_; + UserSettings& user_settings_{UserSettings::get_instance()}; - Report in_report_{Report()}; - DeviceDriverType driver_type_{DeviceDriverType::WEBAPP}; + UserProfile profile_; + + bool read_profile(UserProfile& profile); + bool read_serial(void* buffer, size_t len, bool block); + bool read_packet(Packet& packet, bool block); + bool write_serial(const void* buffer, size_t len); + bool write_packet(const Packet& packet); + bool write_profile(uint8_t index, const UserProfile& profile); + bool write_gamepad(uint8_t index, const Gamepad::PadIn& pad_in); + void write_error(); }; #endif // _WEBAAPP_DEVICE_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_SB.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_SB.h index 02f99b1..ea98c99 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_SB.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XboxOG/XboxOG_SB.h @@ -4,7 +4,6 @@ #include #include -#include "Gamepad.h" #include "USBDevice/DeviceDriver/DeviceDriver.h" #include "Descriptors/XboxOG.h" diff --git a/Firmware/RP2040/src/USBDevice/DeviceManager.h b/Firmware/RP2040/src/USBDevice/DeviceManager.h index 515f688..6a76853 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceManager.h +++ b/Firmware/RP2040/src/USBDevice/DeviceManager.h @@ -4,7 +4,6 @@ #include #include -#include "Gamepad.h" #include "USBDevice/DeviceDriver/DeviceDriverTypes.h" #include "USBDevice/DeviceDriver/DeviceDriver.h" diff --git a/Firmware/RP2040/src/USBHost/HostDriver/DInput/DInput.cpp b/Firmware/RP2040/src/USBHost/HostDriver/DInput/DInput.cpp index 267ebba..e1e97eb 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/DInput/DInput.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/DInput/DInput.cpp @@ -96,10 +96,8 @@ void DInputHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t insta gp_in.trigger_r = (in_report->buttons[0] & DInput::Buttons0::R2) ? Range::MAX : Range::MIN; } - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_ry); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/HIDGeneric/HIDGeneric.cpp b/Firmware/RP2040/src/USBHost/HostDriver/HIDGeneric/HIDGeneric.cpp index 7fa1b16..0065bf1 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/HIDGeneric/HIDGeneric.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/HIDGeneric/HIDGeneric.cpp @@ -68,10 +68,8 @@ void HIDHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance break; } - gp_in.joystick_lx = gamepad.scale_joystick_lx(hid_joystick_data_.X); - gp_in.joystick_ly = gamepad.scale_joystick_ly(hid_joystick_data_.Y); - gp_in.joystick_rx = gamepad.scale_joystick_rx(hid_joystick_data_.Z); - gp_in.joystick_ry = gamepad.scale_joystick_ry(hid_joystick_data_.Rz); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(hid_joystick_data_.X, hid_joystick_data_.Y); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(hid_joystick_data_.Z, hid_joystick_data_.Rz); if (hid_joystick_data_.buttons[1]) gp_in.buttons |= gamepad.MAP_BUTTON_X; if (hid_joystick_data_.buttons[2]) gp_in.buttons |= gamepad.MAP_BUTTON_A; diff --git a/Firmware/RP2040/src/USBHost/HostDriver/HostDriver.h b/Firmware/RP2040/src/USBHost/HostDriver/HostDriver.h index d213e20..0af51b0 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/HostDriver.h +++ b/Firmware/RP2040/src/USBHost/HostDriver/HostDriver.h @@ -5,7 +5,7 @@ #include "UserSettings/UserProfile.h" #include "UserSettings/UserSettings.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "USBHost/HostDriver/HostDriverTypes.h" //Use HostManager, don't use this directly diff --git a/Firmware/RP2040/src/USBHost/HostDriver/N64/N64.cpp b/Firmware/RP2040/src/USBHost/HostDriver/N64/N64.cpp index ffef075..a5939aa 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/N64/N64.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/N64/N64.cpp @@ -94,14 +94,11 @@ void N64Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance break; } - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_y); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_x); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_x, in_report->joystick_y); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(joy_rx, joy_ry); gp_in.trigger_l = (in_report->buttons & N64::Buttons::L) ? Range::MAX : Range::MIN; - gp_in.joystick_ly = gamepad.scale_joystick_ly(joy_ry); - gp_in.joystick_lx = gamepad.scale_joystick_lx(joy_rx); - gamepad.set_pad_in(gp_in); tuh_hid_receive_report(address, instance); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp b/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp index cd34fbb..075acfb 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS3/PS3.cpp @@ -140,10 +140,8 @@ void PS3Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance gp_in.trigger_l = gamepad.scale_trigger_l(in_report->l2_axis); gp_in.trigger_r = gamepad.scale_trigger_r(in_report->r2_axis); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ly(in_report->joystick_ry); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp b/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp index 3ac3ebf..8b8b39d 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS4/PS4.cpp @@ -72,10 +72,8 @@ void PS4Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance gp_in.trigger_l = gamepad.scale_trigger_l(in_report_.trigger_l); gp_in.trigger_r = gamepad.scale_trigger_r(in_report_.trigger_r); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report_.joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report_.joystick_ly); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report_.joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report_.joystick_ry); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report_.joystick_lx, in_report_.joystick_ly); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report_.joystick_rx, in_report_.joystick_ry); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp b/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp index c710182..0389b78 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/PS5/PS5.cpp @@ -89,10 +89,8 @@ void PS5Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t instance gp_in.trigger_l = gamepad.scale_trigger_l(in_report->trigger_l); gp_in.trigger_r = gamepad.scale_trigger_r(in_report->trigger_r); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_ry); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp index e5d7caf..105f308 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchPro/SwitchPro.cpp @@ -168,10 +168,11 @@ void SwitchProHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t in uint16_t joy_rx = in_report->joysticks[3] | ((in_report->joysticks[4] & 0xF) << 8); uint16_t joy_ry = (in_report->joysticks[4] >> 4) | (in_report->joysticks[5] << 4); - gp_in.joystick_lx = gamepad.scale_joystick_lx(normalize_axis(joy_lx)); - gp_in.joystick_ly = gamepad.scale_joystick_ly(normalize_axis(joy_ly), true); - gp_in.joystick_rx = gamepad.scale_joystick_rx(normalize_axis(joy_rx)); - gp_in.joystick_ry = gamepad.scale_joystick_ry(normalize_axis(joy_ry), true); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = + gamepad.scale_joystick_l(normalize_axis(joy_lx), normalize_axis(joy_ly), true); + + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = + gamepad.scale_joystick_r(normalize_axis(joy_rx), normalize_axis(joy_ry), true); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp b/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp index 8c8c7c7..b5c6210 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp @@ -67,10 +67,8 @@ void SwitchWiredHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t gp_in.trigger_l = (in_report->buttons & SwitchWired::Buttons::ZL) ? Range::MAX : Range::MIN; gp_in.trigger_r = (in_report->buttons & SwitchWired::Buttons::ZR) ? Range::MAX : Range::MIN; - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_ry); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp index ae3776e..58e7505 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360.cpp @@ -42,10 +42,8 @@ void Xbox360Host::process_report(Gamepad& gamepad, uint8_t address, uint8_t inst gp_in.trigger_l = gamepad.scale_trigger_l(in_report_->trigger_l); gp_in.trigger_r = gamepad.scale_trigger_r(in_report_->trigger_r); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report_->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report_->joystick_ly, true); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report_->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report_->joystick_ry, true); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report_->joystick_lx, in_report_->joystick_ly, true); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report_->joystick_rx, in_report_->joystick_ry, true); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp index 1e1dbc3..39a4803 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/Xbox360W.cpp @@ -74,10 +74,8 @@ void Xbox360WHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t ins gp_in.trigger_l = gamepad.scale_trigger_l(in_report->trigger_l); gp_in.trigger_r = gamepad.scale_trigger_r(in_report->trigger_r); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly, true); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_ry, true); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly, true); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry, true); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOG.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOG.cpp index 858b073..78122c3 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOG.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOG.cpp @@ -57,10 +57,8 @@ void XboxOGHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t insta gp_in.trigger_l = gamepad.scale_trigger_l(in_report->trigger_l); gp_in.trigger_r = gamepad.scale_trigger_r(in_report->trigger_r); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly, true); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_ry, true); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly, true); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry, true); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp index bf3667c..6dab811 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp @@ -42,10 +42,8 @@ void XboxOneHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t inst gp_in.trigger_l = gamepad.scale_trigger_l(static_cast(in_report->trigger_l >> 2)); gp_in.trigger_r = gamepad.scale_trigger_r(static_cast(in_report->trigger_r >> 2)); - gp_in.joystick_lx = gamepad.scale_joystick_lx(in_report->joystick_lx); - gp_in.joystick_ly = gamepad.scale_joystick_ly(in_report->joystick_ly, true); - gp_in.joystick_rx = gamepad.scale_joystick_rx(in_report->joystick_rx); - gp_in.joystick_ry = gamepad.scale_joystick_ry(in_report->joystick_ry, true); + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly, true); + std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry, true); gamepad.set_pad_in(gp_in); diff --git a/Firmware/RP2040/src/USBHost/HostManager.h b/Firmware/RP2040/src/USBHost/HostManager.h index 0826204..d15ab57 100644 --- a/Firmware/RP2040/src/USBHost/HostManager.h +++ b/Firmware/RP2040/src/USBHost/HostManager.h @@ -9,8 +9,6 @@ #include #include "board_config.h" -#include "Gamepad.h" -#include "OGXMini/OGXMini.h" #include "USBHost/HardwareIDs.h" #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/HostDriver.h" diff --git a/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp b/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp new file mode 100644 index 0000000..e50321d --- /dev/null +++ b/Firmware/RP2040/src/UserSettings/JoystickSettings.cpp @@ -0,0 +1,41 @@ +#include "UserSettings/JoystickSettings.h" + +bool JoystickSettings::is_same(const JoystickSettingsRaw& raw) const +{ + return dz_inner == Fix16(raw.dz_inner) && + dz_outer == Fix16(raw.dz_outer) && + anti_dz_circle == Fix16(raw.anti_dz_circle) && + anti_dz_circle_y_scale == Fix16(raw.anti_dz_circle_y_scale) && + anti_dz_square == Fix16(raw.anti_dz_square) && + anti_dz_square_y_scale == Fix16(raw.anti_dz_square_y_scale) && + anti_dz_angular == Fix16(raw.anti_dz_angular) && + anti_dz_outer == Fix16(raw.anti_dz_outer) && + axis_restrict == Fix16(raw.axis_restrict) && + angle_restrict == Fix16(raw.angle_restrict) && + diag_scale_min == Fix16(raw.diag_scale_min) && + diag_scale_max == Fix16(raw.diag_scale_max) && + curve == Fix16(raw.curve) && + uncap_radius == raw.uncap_radius && + invert_y == raw.invert_y && + invert_x == raw.invert_x; +} + +void JoystickSettings::set_from_raw(const JoystickSettingsRaw& raw) +{ + dz_inner = Fix16(raw.dz_inner); + dz_outer = Fix16(raw.dz_outer); + anti_dz_circle = Fix16(raw.anti_dz_circle); + anti_dz_circle_y_scale = Fix16(raw.anti_dz_circle_y_scale); + anti_dz_square = Fix16(raw.anti_dz_square); + anti_dz_square_y_scale = Fix16(raw.anti_dz_square_y_scale); + anti_dz_angular = Fix16(raw.anti_dz_angular); + anti_dz_outer = Fix16(raw.anti_dz_outer); + axis_restrict = Fix16(raw.axis_restrict); + angle_restrict = Fix16(raw.angle_restrict); + diag_scale_min = Fix16(raw.diag_scale_min); + diag_scale_max = Fix16(raw.diag_scale_max); + curve = Fix16(raw.curve); + uncap_radius = raw.uncap_radius; + invert_y = raw.invert_y; + invert_x = raw.invert_x; +} \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/JoystickSettings.h b/Firmware/RP2040/src/UserSettings/JoystickSettings.h new file mode 100644 index 0000000..2e7e06c --- /dev/null +++ b/Firmware/RP2040/src/UserSettings/JoystickSettings.h @@ -0,0 +1,66 @@ +#ifndef _JOYSTICK_SETTINGS_H_ +#define _JOYSTICK_SETTINGS_H_ + +#include + +#include "libfixmath/fix16.hpp" + +struct JoystickSettingsRaw; + +struct JoystickSettings +{ + Fix16 dz_inner{Fix16(0.0f)}; + Fix16 dz_outer{Fix16(1.0f)}; + + Fix16 anti_dz_circle{Fix16(0.0f)}; + Fix16 anti_dz_circle_y_scale{Fix16(0.0f)}; + Fix16 anti_dz_square{Fix16(0.0f)}; + Fix16 anti_dz_square_y_scale{Fix16(0.0f)}; + Fix16 anti_dz_angular{Fix16(0.0f)}; + Fix16 anti_dz_outer{Fix16(1.0f)}; + + Fix16 axis_restrict{Fix16(0.0f)}; + Fix16 angle_restrict{Fix16(0.0f)}; + + Fix16 diag_scale_min{Fix16(1.0f)}; + Fix16 diag_scale_max{Fix16(1.0f)}; + + Fix16 curve{Fix16(1.0f)}; + + bool uncap_radius{true}; + bool invert_y{false}; + bool invert_x{false}; + + bool is_same(const JoystickSettingsRaw& raw) const; + void set_from_raw(const JoystickSettingsRaw& raw); +}; + +#pragma pack(push, 1) +struct JoystickSettingsRaw +{ + fix16_t dz_inner{fix16_from_int(0)}; + fix16_t dz_outer{fix16_from_int(1)}; + + fix16_t anti_dz_circle{fix16_from_int(0)}; + fix16_t anti_dz_circle_y_scale{fix16_from_int(0)}; + fix16_t anti_dz_square{fix16_from_int(0)}; + fix16_t anti_dz_square_y_scale{fix16_from_int(0)}; + fix16_t anti_dz_angular{fix16_from_int(0)}; + fix16_t anti_dz_outer{fix16_from_int(1)}; + + fix16_t axis_restrict{fix16_from_int(0)}; + fix16_t angle_restrict{fix16_from_int(0)}; + + fix16_t diag_scale_min{fix16_from_int(1)}; + fix16_t diag_scale_max{fix16_from_int(1)}; + + fix16_t curve{fix16_from_int(1)}; + + uint8_t uncap_radius{true}; + uint8_t invert_y{false}; + uint8_t invert_x{false}; +}; +static_assert(sizeof(JoystickSettingsRaw) == 55, "JoystickSettingsRaw is an unexpected size"); +#pragma pack(pop) + +#endif // _JOYSTICK_SETTINGS_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/NVSTool.h b/Firmware/RP2040/src/UserSettings/NVSTool.h index 740b8ce..1cce5a2 100644 --- a/Firmware/RP2040/src/UserSettings/NVSTool.h +++ b/Firmware/RP2040/src/UserSettings/NVSTool.h @@ -80,6 +80,27 @@ public: return false; } + void erase_all() + { + mutex_enter_blocking(&nvs_mutex_); + + for (uint32_t i = 0; i < NVS_SECTORS; ++i) + { + flash_range_erase(NVS_START_OFFSET + i * FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE); + } + + Entry entry; + + for (uint32_t i = 0; i < MAX_ENTRIES + 1; ++i) + { + flash_range_program(NVS_START_OFFSET + i * sizeof(Entry), + reinterpret_cast(&entry), + sizeof(Entry)); + } + + mutex_exit(&nvs_mutex_); + } + private: NVSTool() { @@ -89,23 +110,7 @@ private: if (std::strcmp(initial_entry->key, INVALID_KEY) != 0) { - mutex_enter_blocking(&nvs_mutex_); - - for (uint32_t i = 0; i < NVS_SECTORS; ++i) - { - flash_range_erase(NVS_START_OFFSET + i * FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE); - } - - Entry entry; - - for (uint32_t i = 0; i < MAX_ENTRIES; ++i) - { - flash_range_program(NVS_START_OFFSET + i * sizeof(Entry), - reinterpret_cast(&entry), - sizeof(Entry)); - } - - mutex_exit(&nvs_mutex_); + erase_all(); } } diff --git a/Firmware/RP2040/src/UserSettings/TriggerSettings.cpp b/Firmware/RP2040/src/UserSettings/TriggerSettings.cpp new file mode 100644 index 0000000..6508f07 --- /dev/null +++ b/Firmware/RP2040/src/UserSettings/TriggerSettings.cpp @@ -0,0 +1,19 @@ +#include "UserSettings/TriggerSettings.h" + +bool TriggerSettings::is_same(const TriggerSettingsRaw& raw) const +{ + return dz_inner == Fix16(raw.dz_inner) && + dz_outer == Fix16(raw.dz_outer) && + anti_dz_inner == Fix16(raw.anti_dz_inner) && + anti_dz_outer == Fix16(raw.anti_dz_outer) && + curve == Fix16(raw.curve); +} + +void TriggerSettings::set_from_raw(const TriggerSettingsRaw& raw) +{ + dz_inner = Fix16(raw.dz_inner); + dz_outer = Fix16(raw.dz_outer); + anti_dz_inner = Fix16(raw.anti_dz_inner); + anti_dz_outer = Fix16(raw.anti_dz_outer); + curve = Fix16(raw.curve); +} \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/TriggerSettings.h b/Firmware/RP2040/src/UserSettings/TriggerSettings.h new file mode 100644 index 0000000..ad29487 --- /dev/null +++ b/Firmware/RP2040/src/UserSettings/TriggerSettings.h @@ -0,0 +1,38 @@ +#ifndef TRIGGER_SETTINGS_H +#define TRIGGER_SETTINGS_H + +#include + +#include "libfixmath/fix16.hpp" + +struct TriggerSettingsRaw; + +struct TriggerSettings +{ + Fix16 dz_inner{Fix16(0.0f)}; + Fix16 dz_outer{Fix16(1.0f)}; + + Fix16 anti_dz_inner{Fix16(0.0f)}; + Fix16 anti_dz_outer{Fix16(1.0f)}; + + Fix16 curve{Fix16(1.0f)}; + + bool is_same(const TriggerSettingsRaw& raw) const; + void set_from_raw(const TriggerSettingsRaw& raw); +}; + +#pragma pack(push, 1) +struct TriggerSettingsRaw +{ + fix16_t dz_inner{fix16_from_int(0)}; + fix16_t dz_outer{fix16_from_int(1)}; + + fix16_t anti_dz_inner{fix16_from_int(0)}; + fix16_t anti_dz_outer{fix16_from_int(1)}; + + fix16_t curve{fix16_from_int(1)}; +}; +static_assert(sizeof(TriggerSettingsRaw) == 20, "TriggerSettingsRaw is an unexpected size"); +#pragma pack(pop) + +#endif // TRIGGER_SETTINGS_H \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/UserProfile.cpp b/Firmware/RP2040/src/UserSettings/UserProfile.cpp index 8375735..332993a 100644 --- a/Firmware/RP2040/src/UserSettings/UserProfile.cpp +++ b/Firmware/RP2040/src/UserSettings/UserProfile.cpp @@ -1,20 +1,12 @@ #include -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" #include "UserSettings/UserProfile.h" UserProfile::UserProfile() { id = 1; - dz_trigger_l = 0; - dz_trigger_r = 0; - dz_joystick_l = 0; - dz_joystick_r = 0; - - invert_ly = 0; - invert_ry = 0; - dpad_up = Gamepad::DPAD_UP; dpad_down = Gamepad::DPAD_DOWN; dpad_left = Gamepad::DPAD_LEFT; diff --git a/Firmware/RP2040/src/UserSettings/UserProfile.h b/Firmware/RP2040/src/UserSettings/UserProfile.h index 2efa406..6bd28f2 100644 --- a/Firmware/RP2040/src/UserSettings/UserProfile.h +++ b/Firmware/RP2040/src/UserSettings/UserProfile.h @@ -3,19 +3,18 @@ #include +#include "UserSettings/JoystickSettings.h" +#include "UserSettings/TriggerSettings.h" + #pragma pack(push, 1) struct UserProfile { uint8_t id; - uint8_t dz_trigger_l; - uint8_t dz_trigger_r; - - uint8_t dz_joystick_l; - uint8_t dz_joystick_r; - - uint8_t invert_ly; - uint8_t invert_ry; + JoystickSettingsRaw joystick_settings_l; + JoystickSettingsRaw joystick_settings_r; + TriggerSettingsRaw trigger_settings_l; + TriggerSettingsRaw trigger_settings_r; uint8_t dpad_up; uint8_t dpad_down; @@ -50,7 +49,7 @@ struct UserProfile UserProfile(); }; -static_assert(sizeof(UserProfile) == 46, "UserProfile struct size mismatch"); +static_assert(sizeof(UserProfile) == 190, "UserProfile struct size mismatch"); #pragma pack(pop) #endif // _USER_PROFILE_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/UserSettings.cpp b/Firmware/RP2040/src/UserSettings/UserSettings.cpp index 3820675..9b4a10e 100644 --- a/Firmware/RP2040/src/UserSettings/UserSettings.cpp +++ b/Firmware/RP2040/src/UserSettings/UserSettings.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include "tusb.h" @@ -94,9 +95,9 @@ const std::string UserSettings::DRIVER_TYPE_KEY() return std::string("driver_type"); } -const std::string UserSettings::FIRMWARE_VER_KEY() +const std::string UserSettings::DATETIME_KEY() { - return std::string("firmware_ver"); + return std::string("datetime"); } DeviceDriverType UserSettings::DEFAULT_DRIVER() @@ -201,10 +202,10 @@ bool UserSettings::store_profile_and_driver_type(DeviceDriverType new_driver_typ board_api::usb::disconnect_all(); + nvs_tool_.write(DRIVER_TYPE_KEY(), reinterpret_cast(&new_driver_type), sizeof(new_driver_type)); nvs_tool_.write(ACTIVE_PROFILE_KEY(index), &profile.id, sizeof(uint8_t)); nvs_tool_.write(PROFILE_KEY(profile.id), &profile, sizeof(UserProfile)); - nvs_tool_.write(DRIVER_TYPE_KEY(), &new_driver_type, sizeof(uint8_t)); - + board_api::reboot(); return true; @@ -298,22 +299,21 @@ DeviceDriverType UserSettings::get_current_driver() return current_driver_; } -bool UserSettings::verify_firmware_version() +void UserSettings::write_datetime() { - std::string fw_version = FIRMWARE_VERSION; - char read_fw_version[fw_version.size()]; - std::fill(read_fw_version, read_fw_version + fw_version.size(), '\0'); - - nvs_tool_.read(FIRMWARE_VER_KEY(), read_fw_version, fw_version.size()); - - return (std::memcmp(read_fw_version, fw_version.c_str(), fw_version.size()) == 0); + nvs_tool_.write(DATETIME_KEY(), DATETIME_TAG.c_str(), DATETIME_TAG.size() + 1); } -bool UserSettings::write_firmware_version() +bool UserSettings::verify_datetime() { - std::string fw_version = FIRMWARE_VERSION; - nvs_tool_.write(FIRMWARE_VER_KEY(), fw_version.c_str(), fw_version.size()); - return verify_firmware_version(); + char read_dt_tag[DATETIME_TAG.size() + 1] = {0}; + + if (!nvs_tool_.read(DATETIME_KEY(), read_dt_tag, sizeof(read_dt_tag)) || + (std::strcmp(read_dt_tag, DATETIME_TAG.c_str()) != 0)) + { + return false; + } + return true; } //Checks for first boot and initializes user profiles, call before tusb is inited. @@ -326,16 +326,13 @@ void UserSettings::initialize_flash() if (read_init_flag == FLASH_INIT_FLAG) { - OGXM_LOG("Flash already initialized\n"); - - if (!verify_firmware_version()) - { - OGXM_LOG("Firmware version mismatch, writing new version\n"); - write_firmware_version(); - } + OGXM_LOG("Flash already initialized: %i\n", read_init_flag); return; } + OGXM_LOG("Flash not initialized, erasing\n"); + nvs_tool_.erase_all(); + OGXM_LOG("Writing default driver\n"); uint8_t device_mode_buffer = static_cast(DEFAULT_DRIVER()); @@ -352,11 +349,14 @@ void UserSettings::initialize_flash() OGXM_LOG("Writing default profiles\n"); { - UserProfile profile = UserProfile(); + UserProfile profile; + OGXM_LOG("Profile size: %i\n", sizeof(UserProfile)); + for (uint8_t i = 0; i < MAX_PROFILES; i++) { profile.id = i + 1; nvs_tool_.write(PROFILE_KEY(profile.id), &profile, sizeof(UserProfile)); + OGXM_LOG("Profile " + std::to_string(profile.id) + " written\n"); } } @@ -365,7 +365,5 @@ void UserSettings::initialize_flash() uint8_t init_flag_buffer = FLASH_INIT_FLAG; nvs_tool_.write(INIT_FLAG_KEY(), &init_flag_buffer, sizeof(uint8_t)); - write_firmware_version(); - OGXM_LOG("Flash initialized\n"); } \ No newline at end of file diff --git a/Firmware/RP2040/src/UserSettings/UserSettings.h b/Firmware/RP2040/src/UserSettings/UserSettings.h index 8d6e83a..f65c4f3 100644 --- a/Firmware/RP2040/src/UserSettings/UserSettings.h +++ b/Firmware/RP2040/src/UserSettings/UserSettings.h @@ -8,7 +8,7 @@ #include "USBDevice/DeviceDriver/DeviceDriverTypes.h" #include "UserSettings/UserProfile.h" #include "UserSettings/NVSTool.h" -#include "Gamepad.h" +#include "Gamepad/Gamepad.h" /* Only write/store flash from Core0 */ class UserSettings @@ -25,10 +25,9 @@ public: void initialize_flash(); - bool verify_firmware_version(); - bool write_firmware_version(); - bool is_valid_driver(DeviceDriverType driver); + bool verify_datetime(); + void write_datetime(); DeviceDriverType get_current_driver(); bool check_for_driver_change(Gamepad& gamepad); @@ -48,7 +47,8 @@ private: UserSettings& operator=(const UserSettings&) = delete; static constexpr uint8_t GP_CHECK_COUNT = 3000 / GP_CHECK_DELAY_MS; - static constexpr uint8_t FLASH_INIT_FLAG = 0x21; + static constexpr uint8_t FLASH_INIT_FLAG = 0xF8; + const std::string DATETIME_TAG = BUILD_DATETIME; NVSTool& nvs_tool_{NVSTool::get_instance()}; DeviceDriverType current_driver_{DeviceDriverType::NONE}; @@ -58,7 +58,7 @@ private: const std::string PROFILE_KEY(const uint8_t profile_id); const std::string ACTIVE_PROFILE_KEY(const uint8_t index); const std::string DRIVER_TYPE_KEY(); - const std::string FIRMWARE_VER_KEY(); + const std::string DATETIME_KEY(); }; #endif // _USER_SETTINGS_H_ \ No newline at end of file diff --git a/Firmware/cmake/init_submodules.cmake b/Firmware/cmake/init_submodules.cmake index 468514d..d7ff985 100644 --- a/Firmware/cmake/init_submodules.cmake +++ b/Firmware/cmake/init_submodules.cmake @@ -1,18 +1,20 @@ -function(init_git_submodules EXTERNAL_DIR) +function(init_git_submodules EXTERNAL_DIR SUBMODULE_PATHS) set(REPO_DIR "${EXTERNAL_DIR}/../../") message(STATUS "Initializing submodules in ${REPO_DIR}") - execute_process( - COMMAND git submodule update --init --recursive - WORKING_DIRECTORY ${REPO_DIR} - RESULT_VARIABLE INIT_SUBMODULES_RESULT - OUTPUT_VARIABLE INIT_SUBMODULES_OUTPUT - ERROR_VARIABLE INIT_SUBMODULES_ERROR - ) + foreach(SUBMODULE_PATH ${SUBMODULE_PATHS}) + execute_process( + COMMAND git submodule update --init --recursive ${SUBMODULE_PATH} + WORKING_DIRECTORY ${REPO_DIR} + RESULT_VARIABLE INIT_SUBMODULE_RESULT + OUTPUT_VARIABLE INIT_SUBMODULE_OUTPUT + ERROR_VARIABLE INIT_SUBMODULE_ERROR + ) - if(INIT_SUBMODULES_RESULT EQUAL 0) - message(STATUS "Subnmodules initialized successfully.") - else() - message(FATAL_ERROR "Failed to initialize submodules: ${INIT_SUBMODULES_ERROR}") - endif() + if(INIT_SUBMODULE_RESULT EQUAL 0) + message(STATUS "Submodules initialized successfully.") + else() + message(FATAL_ERROR "Failed to initialize submodules: ${INIT_SUBMODULES_ERROR}") + endif() + endforeach() endfunction() \ No newline at end of file diff --git a/Firmware/external/libfixmath b/Firmware/external/libfixmath new file mode 160000 index 0000000..d308e46 --- /dev/null +++ b/Firmware/external/libfixmath @@ -0,0 +1 @@ +Subproject commit d308e466e1a09118d03f677c52e5fbf402f6fdd0 diff --git a/README.md b/README.md index a6bd64f..79aaa49 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Firmware for the RP2040, capable of emulating gamepads for several game consoles. The firmware comes in many flavors, supported on the [Adafruit Feather USB Host board](https://www.adafruit.com/product/5723), Pi Pico, Pi Pico 2, Pi Pico W, Pi Pico 2 W, Waveshare RP2040-Zero, Pico/ESP32 hybrid, and a 4-Channel RP2040-Zero setup. -[**Visit the web app here**](https://wiredopposite.github.io/OGX-Mini-WebApp/) to change your mappings and deadzone settings. To pair the OGX-Mini with the web app, plug your controller in, then connect it to your PC, hold **Start + Left Bumper + Right Bumper** to enter web app mode. Click "Connect" in the web app and select the OGX-Mini. +[**Visit the web app here**](https://wiredopposite.github.io/OGX-Mini-WebApp/) to change your mappings and deadzone settings. To pair the OGX-Mini with the web app via USB, plug your controller in, then connect it to your PC, hold **Start + Left Bumper + Right Bumper** to enter web app mode. Click "Connect via USB" in the web app and select the OGX-Mini. You can also pair via Bluetooth, no extra steps are needed in that case. ## Supported platforms - Original Xbox @@ -72,21 +72,20 @@ Note: There are some third party controllers that can change their VID/PID, thes Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supported_gamepads/) for a more comprehensive list of supported controllers and Bluetooth pairing instructions. ## Features new to v1.0.0 -- Bluetooth functionality for the Pico W and Pico+ESP32. +- Bluetooth functionality for the Pico W, Pico 2 W, and Pico+ESP32. - Web application (connectable via USB or Bluetooth) for configuring deadzones and buttons mappings, supports up to 8 saved profiles. - Pi Pico 2 and Pico 2 W (RP2350) support. -- Reduced latency by about 3-4 ms, graphs showing comparisons are coming +- Reduced latency by about 3-4 ms, graphs showing comparisons are coming. - 4 channel functionality, connect 4 Picos and use one Xbox 360 wireless adapter to control all 4. - Delayed USB mount until a controller is plugged in, useful for internal installation (non-Bluetooth boards only). - Generic HID controller support. - Dualshock 3 emulation (minus gyros), rumble now works. - Steel Battalion controller emulation with a wireless Xbox 360 chatpad. -- Xbox DVD dongle emulation. You must provide or dump your own firmware, see the Tools directory. +- Xbox DVD dongle emulation. You must provide or dump your own dongle firmware, see the Tools directory. - Analog button support on OG Xbox and PS3. - RGB LED support for RP2040-Zero and Adafruit Feather boards. ## Planned additions -- Anti-deadzone settings - More accurate report parser for unknown HID controllers - Hardware design for internal OG Xbox install - Hardware design for 4 channel RP2040-Zero adapter @@ -95,6 +94,7 @@ Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supporte - Switch (as input) rumble support - OG Xbox communicator support (in some form) - Generic bluetooth dongle support +- Button macros ## Hardware For Pi Pico, RP2040-Zero, 4 channel, and ESP32 configurations, please see the hardware folder for diagrams. diff --git a/WebApp b/WebApp index 0e572ef..79b0077 160000 --- a/WebApp +++ b/WebApp @@ -1 +1 @@ -Subproject commit 0e572eff08b25950c06f77fb9c8eac71d9f79a48 +Subproject commit 79b00778da0e7be925d055884ea89c3168d587f0 diff --git a/images/WebAppPreview.png b/images/WebAppPreview.png new file mode 100644 index 0000000000000000000000000000000000000000..996d10cc6c430e45550f325346e9e167df9d8b9e GIT binary patch literal 91606 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 OUrO?7a>Y+yz4