From 7ef19625ad94196e0b464fc643b75929b940f639 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Tue, 4 Jul 2023 15:32:28 -0500 Subject: [PATCH] action and cardinal direction button pin mapping profiles up to three alternative pin mappings can be defined to profiles 2-4 (1 is the primary setting) which overlay their mappings on top of the primary settings. this also defines four hotkey actions for switching between the four profiles. all pre-profile configuration works the same and affects the same config objects, etc. as before. a new configuration page and config memory section is defined for profiles 2-4 (specifically the alternative pin mappings). profiles 2-4 default to the board mappings, as well, so users should be fine to ignore this feature entirely if they want --- normal function should not changed, and they shouldn't even be able to accidentally enable this, especially not into a broken configuration --- headers/gamepad.h | 1 + headers/storagemanager.h | 7 + proto/config.proto | 25 ++++ proto/enums.proto | 4 + src/config_utils.cpp | 40 +++++ src/configs/webconfig.cpp | 59 ++++++++ src/gamepad.cpp | 54 ++++++- src/storagemanager.cpp | 34 +++++ www/server/app.js | 46 ++++++ www/src/App.jsx | 2 + www/src/Components/Navigation.jsx | 1 + www/src/Locales/en/Index.jsx | 2 + www/src/Locales/en/Navigation.jsx | 1 + www/src/Locales/en/ProfileSettings.jsx | 9 ++ www/src/Locales/en/SettingsPage.jsx | 5 + www/src/Pages/ProfileSettings.jsx | 199 +++++++++++++++++++++++++ www/src/Pages/SettingsPage.jsx | 21 +++ www/src/Services/WebApi.js | 85 +++++++++++ 18 files changed, 592 insertions(+), 3 deletions(-) create mode 100644 www/src/Locales/en/ProfileSettings.jsx create mode 100644 www/src/Pages/ProfileSettings.jsx diff --git a/headers/gamepad.h b/headers/gamepad.h index 721a51d40..89c6b018e 100644 --- a/headers/gamepad.h +++ b/headers/gamepad.h @@ -62,6 +62,7 @@ class Gamepad { Gamepad(int debounceMS = 5); void setup(); + void teardown_and_reinit(const uint32_t profileNum); void process(); void read(); void save(); diff --git a/headers/storagemanager.h b/headers/storagemanager.h index cd00eae17..32365579f 100644 --- a/headers/storagemanager.h +++ b/headers/storagemanager.h @@ -44,9 +44,12 @@ class Storage { LEDOptions& getLedOptions() { return config.ledOptions; } AddonOptions& getAddonOptions() { return config.addonOptions; } AnimationOptions_Proto& getAnimationOptions() { return config.animationOptions; } + ProfileOptions& getProfileOptions() { return config.profileOptions; } bool save(); + PinMappings& getProfilePinMappings(); + // Perform saves that were enqueued from core1 void performEnqueuedSaves(); @@ -65,6 +68,9 @@ class Storage { void ClearFeatureData(); uint8_t * GetFeatureData(); + void setProfile(const uint32_t); // profile support for multiple mappings + void setFunctionalPinMappings(const uint32_t); + void ResetSettings(); // EEPROM Reset Feature private: @@ -79,6 +85,7 @@ class Storage { critical_section_t animationOptionsCs; uint32_t animationOptionsCrc = 0; AnimationOptions animationOptionsToSave = {}; + PinMappings* functionalPinMappings = nullptr; }; #endif diff --git a/proto/config.proto b/proto/config.proto index 3c5df52f0..276991661 100644 --- a/proto/config.proto +++ b/proto/config.proto @@ -13,6 +13,7 @@ message GamepadOptions optional bool switchTpShareForDs4 = 6; optional bool lockHotkeys = 7; optional bool fourWayMode = 8; + optional uint32 profileNumber = 9; } message KeyboardMapping @@ -115,6 +116,29 @@ message PinMappings optional int32 pinButtonFn = 19; } + +message AlternativePinMappings +{ + optional int32 pinButtonB1 = 1; + optional int32 pinButtonB2 = 2; + optional int32 pinButtonB3 = 3; + optional int32 pinButtonB4 = 4; + optional int32 pinButtonL1 = 5; + optional int32 pinButtonR1 = 6; + optional int32 pinButtonL2 = 7; + optional int32 pinButtonR2 = 8; + optional int32 pinDpadUp = 9; + optional int32 pinDpadDown = 10; + optional int32 pinDpadLeft = 11; + optional int32 pinDpadRight = 12; +} + + +message ProfileOptions +{ + repeated AlternativePinMappings alternativePinMappings = 1 [(nanopb).max_count = 3]; +} + message DisplayOptions { optional bool enabled = 1; @@ -465,4 +489,5 @@ message Config optional AnimationOptions_Proto animationOptions = 8; optional AddonOptions addonOptions = 9; optional ForcedSetupOptions forcedSetupOptions = 10; + optional ProfileOptions profileOptions = 11; } diff --git a/proto/enums.proto b/proto/enums.proto index 47c147424..88d0e0a6a 100644 --- a/proto/enums.proto +++ b/proto/enums.proto @@ -136,6 +136,10 @@ enum GamepadHotkey HOTKEY_SOCD_BYPASS = 12; HOTKEY_TOGGLE_4_WAY_MODE = 13; HOTKEY_TOGGLE_DDI_4_WAY_MODE = 14; + HOTKEY_LOAD_PROFILE_1 = 15; + HOTKEY_LOAD_PROFILE_2 = 16; + HOTKEY_LOAD_PROFILE_3 = 17; + HOTKEY_LOAD_PROFILE_4 = 18; } // This has to be kept in sync with LEDFormat in NeoPico.hpp diff --git a/src/config_utils.cpp b/src/config_utils.cpp index c78e7f084..365eca9fd 100644 --- a/src/config_utils.cpp +++ b/src/config_utils.cpp @@ -97,6 +97,7 @@ void ConfigUtils::initUnsetPropertiesWithDefaults(Config& config) INIT_UNSET_PROPERTY(config.gamepadOptions, switchTpShareForDs4, false); INIT_UNSET_PROPERTY(config.gamepadOptions, lockHotkeys, DEFAULT_LOCK_HOTKEYS); INIT_UNSET_PROPERTY(config.gamepadOptions, fourWayMode, false); + INIT_UNSET_PROPERTY(config.gamepadOptions, profileNumber, 1); // hotkeyOptions HotkeyOptions& hotkeyOptions = config.hotkeyOptions; @@ -227,6 +228,45 @@ void ConfigUtils::initUnsetPropertiesWithDefaults(Config& config) INIT_UNSET_PROPERTY(config.displayOptions, invert, !!DISPLAY_INVERT); INIT_UNSET_PROPERTY(config.displayOptions, displaySaverTimeout, DISPLAY_SAVER_TIMEOUT); + // alternate pin mappings + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonB1, PIN_BUTTON_B1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonB2, PIN_BUTTON_B2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonB3, PIN_BUTTON_B3); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonB4, PIN_BUTTON_B4); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonL1, PIN_BUTTON_L1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonR1, PIN_BUTTON_R1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonL2, PIN_BUTTON_L2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinButtonR2, PIN_BUTTON_R2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinDpadUp, PIN_DPAD_UP); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinDpadDown, PIN_DPAD_DOWN); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinDpadLeft, PIN_DPAD_LEFT); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[0], pinDpadRight, PIN_DPAD_RIGHT); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonB1, PIN_BUTTON_B1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonB2, PIN_BUTTON_B2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonB3, PIN_BUTTON_B3); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonB4, PIN_BUTTON_B4); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonL1, PIN_BUTTON_L1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonR1, PIN_BUTTON_R1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonL2, PIN_BUTTON_L2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinButtonR2, PIN_BUTTON_R2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinDpadUp, PIN_DPAD_UP); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinDpadDown, PIN_DPAD_DOWN); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinDpadLeft, PIN_DPAD_LEFT); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[1], pinDpadRight, PIN_DPAD_RIGHT); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonB1, PIN_BUTTON_B1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonB2, PIN_BUTTON_B2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonB3, PIN_BUTTON_B3); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonB4, PIN_BUTTON_B4); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonL1, PIN_BUTTON_L1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonR1, PIN_BUTTON_R1); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonL2, PIN_BUTTON_L2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinButtonR2, PIN_BUTTON_R2); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinDpadUp, PIN_DPAD_UP); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinDpadDown, PIN_DPAD_DOWN); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinDpadLeft, PIN_DPAD_LEFT); + INIT_UNSET_PROPERTY(config.profileOptions.alternativePinMappings[2], pinDpadRight, PIN_DPAD_RIGHT); + config.profileOptions.alternativePinMappings_count = 3; + // ledOptions INIT_UNSET_PROPERTY(config.ledOptions, dataPin, BOARD_LEDS_PIN); INIT_UNSET_PROPERTY(config.ledOptions, ledFormat, static_cast(LED_FORMAT)); diff --git a/src/configs/webconfig.cpp b/src/configs/webconfig.cpp index b7e89250e..d317cf532 100644 --- a/src/configs/webconfig.cpp +++ b/src/configs/webconfig.cpp @@ -1,4 +1,5 @@ #include "configs/webconfig.h" +#include "config.pb.h" #include "configs/base64.h" #include "storagemanager.h" @@ -503,6 +504,60 @@ std::string setSplashImage() return serialize_json(doc); } +std::string setProfileOptions() +{ + DynamicJsonDocument doc = get_post_data(); + + ProfileOptions& profileOptions = Storage::getInstance().getProfileOptions(); + JsonObject options = doc.as(); + JsonArray alts = options["alternativePinMappings"]; + int altsIndex = 0; + for (JsonObject alt : alts) { + profileOptions.alternativePinMappings[altsIndex].pinButtonB1 = alt["B1"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB2 = alt["B2"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB3 = alt["B3"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB4 = alt["B4"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonL1 = alt["L1"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonR1 = alt["R1"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonL2 = alt["L2"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonR2 = alt["R2"].as(); + profileOptions.alternativePinMappings[altsIndex].pinDpadUp = alt["Up"].as(); + profileOptions.alternativePinMappings[altsIndex].pinDpadDown = alt["Down"].as(); + profileOptions.alternativePinMappings[altsIndex].pinDpadLeft = alt["Left"].as(); + profileOptions.alternativePinMappings[altsIndex].pinDpadRight = alt["Right"].as(); + profileOptions.alternativePinMappings_count = ++altsIndex; + if (altsIndex > 2) break; + } + + Storage::getInstance().save(); + return serialize_json(doc); +} + +std::string getProfileOptions() +{ + DynamicJsonDocument doc(LWIP_HTTPD_POST_MAX_PAYLOAD_LEN); + + ProfileOptions& profileOptions = Storage::getInstance().getProfileOptions(); + JsonArray alts = doc.createNestedArray("alternativePinMappings"); + for (int i = 0; i < profileOptions.alternativePinMappings_count; i++) { + JsonObject altMappings = alts.createNestedObject(); + altMappings["B1"] = profileOptions.alternativePinMappings[i].pinButtonB1; + altMappings["B2"] = profileOptions.alternativePinMappings[i].pinButtonB2; + altMappings["B3"] = profileOptions.alternativePinMappings[i].pinButtonB3; + altMappings["B4"] = profileOptions.alternativePinMappings[i].pinButtonB4; + altMappings["L1"] = profileOptions.alternativePinMappings[i].pinButtonL1; + altMappings["R1"] = profileOptions.alternativePinMappings[i].pinButtonR1; + altMappings["L2"] = profileOptions.alternativePinMappings[i].pinButtonL2; + altMappings["R2"] = profileOptions.alternativePinMappings[i].pinButtonR2; + altMappings["Up"] = profileOptions.alternativePinMappings[i].pinDpadUp; + altMappings["Down"] = profileOptions.alternativePinMappings[i].pinDpadDown; + altMappings["Left"] = profileOptions.alternativePinMappings[i].pinDpadLeft; + altMappings["Right"] = profileOptions.alternativePinMappings[i].pinDpadRight; + } + + return serialize_json(doc); +} + std::string setGamepadOptions() { DynamicJsonDocument doc = get_post_data(); @@ -514,6 +569,7 @@ std::string setGamepadOptions() readDoc(gamepadOptions.switchTpShareForDs4, doc, "switchTpShareForDs4"); readDoc(gamepadOptions.lockHotkeys, doc, "lockHotkeys"); readDoc(gamepadOptions.fourWayMode, doc, "fourWayMode"); + readDoc(gamepadOptions.profileNumber, doc, "profileNumber"); HotkeyOptions& hotkeyOptions = Storage::getInstance().getHotkeyOptions(); save_hotkey(&hotkeyOptions.hotkey01, doc, "hotkey01"); @@ -548,6 +604,7 @@ std::string getGamepadOptions() writeDoc(doc, "switchTpShareForDs4", gamepadOptions.switchTpShareForDs4 ? 1 : 0); writeDoc(doc, "lockHotkeys", gamepadOptions.lockHotkeys ? 1 : 0); writeDoc(doc, "fourWayMode", gamepadOptions.fourWayMode ? 1 : 0); + writeDoc(doc, "profileNumber", gamepadOptions.profileNumber); const PinMappings& pinMappings = Storage::getInstance().getPinMappings(); writeDoc(doc, "fnButtonPin", pinMappings.pinButtonFn); @@ -1402,6 +1459,7 @@ static const std::pair handlerFuncs[] = { "/api/setCustomTheme", setCustomTheme }, { "/api/getCustomTheme", getCustomTheme }, { "/api/setPinMappings", setPinMappings }, + { "/api/setProfileOptions", setProfileOptions }, { "/api/setKeyMappings", setKeyMappings }, { "/api/setAddonsOptions", setAddonOptions }, { "/api/setPS4Options", setPS4Options }, @@ -1411,6 +1469,7 @@ static const std::pair handlerFuncs[] = { "/api/getGamepadOptions", getGamepadOptions }, { "/api/getLedOptions", getLedOptions }, { "/api/getPinMappings", getPinMappings }, + { "/api/getProfileOptions", getProfileOptions }, { "/api/getKeyMappings", getKeyMappings }, { "/api/getAddonsOptions", getAddonOptions }, { "/api/resetSettings", resetSettings }, diff --git a/src/gamepad.cpp b/src/gamepad.cpp index 391444113..86b315490 100644 --- a/src/gamepad.cpp +++ b/src/gamepad.cpp @@ -94,7 +94,7 @@ Gamepad::Gamepad(int debounceMS) : void Gamepad::setup() { // Configure pin mapping - const PinMappings& pinMappings = Storage::getInstance().getPinMappings(); + const PinMappings& pinMappings = Storage::getInstance().getProfilePinMappings(); const auto convertPin = [](int32_t pin) -> uint8_t { return isValidPin(pin) ? pin : 0xff; }; mapDpadUp = new GamepadButtonMapping(convertPin(pinMappings.pinDpadUp), GAMEPAD_MASK_UP); @@ -143,6 +143,31 @@ void Gamepad::setup() } } +/** + * @brief Undo setup(). + */ +void Gamepad::teardown_and_reinit(const uint32_t profileNum) +{ + const PinMappings& pinMappings = Storage::getInstance().getProfilePinMappings(); + // deinitialize the GPIO pins so we don't have orphans + for (int i = 0; i < GAMEPAD_DIGITAL_INPUT_COUNT; i++) + { + if (gamepadMappings[i]->isAssigned()) + { + gpio_deinit(gamepadMappings[i]->pin); + } + } + if (isValidPin(pinMappings.pinButtonFn)) { + gpio_deinit(pinMappings.pinButtonFn); + } + + // set to new profile + Storage::getInstance().setProfile(profileNum); + + // reinitialize pin mappings + this->setup(); +} + void Gamepad::process() { memcpy(&rawState, &state, sizeof(GamepadState)); @@ -212,8 +237,7 @@ void Gamepad::process() void Gamepad::read() { - const PinMappings& pinMappings = Storage::getInstance().getPinMappings(); - + const PinMappings& pinMappings = Storage::getInstance().getProfilePinMappings(); // Need to invert since we're using pullups uint32_t values = ~gpio_get_all(); @@ -332,6 +356,30 @@ void Gamepad::processHotkeyIfNewAction(GamepadHotkey action) reqSave = true; } break; + case HOTKEY_LOAD_PROFILE_1: + if (action != lastAction) { + this->teardown_and_reinit(1); + reqSave = true; + } + break; + case HOTKEY_LOAD_PROFILE_2: + if (action != lastAction) { + this->teardown_and_reinit(2); + reqSave = true; + } + break; + case HOTKEY_LOAD_PROFILE_3: + if (action != lastAction) { + this->teardown_and_reinit(3); + reqSave = true; + } + break; + case HOTKEY_LOAD_PROFILE_4: + if (action != lastAction) { + this->teardown_and_reinit(4); + reqSave = true; + } + break; } // only save if we did something different (except NONE because NONE doesn't get here) diff --git a/src/storagemanager.cpp b/src/storagemanager.cpp index f0fca8bda..75fbd73db 100644 --- a/src/storagemanager.cpp +++ b/src/storagemanager.cpp @@ -9,6 +9,7 @@ #include "AnimationStorage.hpp" #include "Effects/StaticColor.hpp" #include "FlashPROM.h" +#include "config.pb.h" #include "hardware/watchdog.h" #include "Animation.hpp" #include "CRC32.h" @@ -132,6 +133,39 @@ void Storage::ResetSettings() watchdog_reboot(0, SRAM_END, 2000); } +PinMappings& Storage::getProfilePinMappings() { + if (functionalPinMappings == nullptr) { + functionalPinMappings = (PinMappings*)malloc(sizeof(PinMappings)); + setFunctionalPinMappings(config.gamepadOptions.profileNumber); + } + return *functionalPinMappings; +} + +void Storage::setProfile(const uint32_t profileNum) +{ + setFunctionalPinMappings(profileNum); +} + +void Storage::setFunctionalPinMappings(const uint32_t profileNum) +{ + memcpy(functionalPinMappings, &config.pinMappings, sizeof(PinMappings)); + if (profileNum < 2 || profileNum > 4) return; + + AlternativePinMappings alts = this->config.profileOptions.alternativePinMappings[profileNum-2]; + if (isValidPin(alts.pinButtonB1)) functionalPinMappings->pinButtonB1 = alts.pinButtonB1; + if (isValidPin(alts.pinButtonB2)) functionalPinMappings->pinButtonB2 = alts.pinButtonB2; + if (isValidPin(alts.pinButtonB3)) functionalPinMappings->pinButtonB3 = alts.pinButtonB3; + if (isValidPin(alts.pinButtonB4)) functionalPinMappings->pinButtonB4 = alts.pinButtonB4; + if (isValidPin(alts.pinButtonL1)) functionalPinMappings->pinButtonL1 = alts.pinButtonL1; + if (isValidPin(alts.pinButtonR1)) functionalPinMappings->pinButtonR1 = alts.pinButtonR1; + if (isValidPin(alts.pinButtonL2)) functionalPinMappings->pinButtonL2 = alts.pinButtonL2; + if (isValidPin(alts.pinButtonR2)) functionalPinMappings->pinButtonR2 = alts.pinButtonR2; + if (isValidPin(alts.pinDpadUp)) functionalPinMappings->pinDpadUp = alts.pinDpadUp; + if (isValidPin(alts.pinDpadDown)) functionalPinMappings->pinDpadDown = alts.pinDpadDown; + if (isValidPin(alts.pinDpadLeft)) functionalPinMappings->pinDpadLeft = alts.pinDpadLeft; + if (isValidPin(alts.pinDpadRight)) functionalPinMappings->pinDpadRight = alts.pinDpadRight; +} + void Storage::SetConfigMode(bool mode) { // hack for config mode CONFIG_MODE = mode; previewDisplayOptions = config.displayOptions; diff --git a/www/server/app.js b/www/server/app.js index 8659379cc..3c133efbb 100644 --- a/www/server/app.js +++ b/www/server/app.js @@ -92,6 +92,7 @@ app.get("/api/getGamepadOptions", (req, res) => { lockHotkeys: 0, fourWayMode: 0, fnButtonPin: -1, + profileNumber: 1, hotkey01: { auxMask: 32768, buttonsMask: 66304, @@ -226,6 +227,51 @@ app.get("/api/getKeyMappings", (req, res) => res.send(mapValues(DEFAULT_KEYBOARD_MAPPING)) ); +app.get("/api/getProfileOptions", (req, res) => { + return res.send({ + alternativePinMappings: [{ + B1: 10, + B2: 6, + B3: 11, + B4: 12, + L1: 13, + R1: 9, + L2: 7, + R2: 8, + Up: 2, + Down: 3, + Left: 5, + Right: 4 + },{ + B1: 10, + B2: 11, + B3: 12, + B4: 13, + L1: 6, + R1: 8, + L2: 7, + R2: 9, + Up: 3, + Down: 2, + Left: 4, + Right: 5 + },{ + B1: 6, + B2: 7, + B3: 8, + B4: 9, + L1: 10, + R1: 12, + L2: 11, + R2: 13, + Up: 3, + Down: 5, + Left: 4, + Right: 2 + }] + }); +}); + app.get("/api/getAddonsOptions", (req, res) => { return res.send({ turboPin: -1, diff --git a/www/src/App.jsx b/www/src/App.jsx index d040982df..b6f646ab9 100644 --- a/www/src/App.jsx +++ b/www/src/App.jsx @@ -7,6 +7,7 @@ import Navigation from './Components/Navigation' import HomePage from './Pages/HomePage' import PinMappingPage from "./Pages/PinMapping"; +import ProfileSettingsPage from "./Pages/ProfileSettings"; import KeyboardMappingPage from "./Pages/KeyboardMapping"; import ResetSettingsPage from './Pages/ResetSettingsPage'; import SettingsPage from './Pages/SettingsPage'; @@ -31,6 +32,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/www/src/Components/Navigation.jsx b/www/src/Components/Navigation.jsx index f0eaa75d5..b8fb89678 100644 --- a/www/src/Components/Navigation.jsx +++ b/www/src/Components/Navigation.jsx @@ -51,6 +51,7 @@ const Navigation = (props) => { {t('Navigation:pin-mapping-label')} {t('Navigation:keyboard-mapping-label')} + {t('Navigation:profile-settings-label')} {t('Navigation:led-config-label')} {t('Navigation:custom-theme-label')} {t('Navigation:display-config-label')} diff --git a/www/src/Locales/en/Index.jsx b/www/src/Locales/en/Index.jsx index 67b1611de..80a039408 100644 --- a/www/src/Locales/en/Index.jsx +++ b/www/src/Locales/en/Index.jsx @@ -5,6 +5,7 @@ import SettingsPage from './SettingsPage'; import ResetSettings from './ResetSettings'; import Components from './Components'; import PinMapping from './PinMapping'; +import ProfileSettings from './ProfileSettings'; import KeyboardMapping from './KeyboardMapping'; import LedConfig from './LedConfig'; import CustomTheme from './CustomTheme'; @@ -20,6 +21,7 @@ export default { ResetSettings, Components, PinMapping, + ProfileSettings, KeyboardMapping, LedConfig, CustomTheme, diff --git a/www/src/Locales/en/Navigation.jsx b/www/src/Locales/en/Navigation.jsx index 3fb21711e..07d03fdb8 100644 --- a/www/src/Locales/en/Navigation.jsx +++ b/www/src/Locales/en/Navigation.jsx @@ -12,6 +12,7 @@ export default { 'led-config-label': 'LED Configuration', 'links-label': 'Links', 'pin-mapping-label': 'Pin Mapping', + "profile-settings-label": "Profile Settings", 'reboot-label': 'Reboot', 'reboot-modal-body': 'Select a mode to reboot to', 'reboot-modal-button-bootsel-label': 'USB (BOOTSEL)', diff --git a/www/src/Locales/en/ProfileSettings.jsx b/www/src/Locales/en/ProfileSettings.jsx new file mode 100644 index 000000000..a47f3fb33 --- /dev/null +++ b/www/src/Locales/en/ProfileSettings.jsx @@ -0,0 +1,9 @@ +export default { + "header-text": "Profiles", + "profile-pins-desc": "This page allows three additional button mappings to be configured as profiles 2 through 4, which can be loaded via hotkey. (The first profile is the core configuration from the Pin Mapping page.) A physical layout of the pins:", + "profile-1": "Profile 1", + "profile-2": "Profile 2", + "profile-3": "Profile 3", + "profile-4": "Profile 4", + "profile-pins-warning": "Try to avoid changing the buttons/directions used for your switch profile hotkeys, or else it will get hard to understand what profile you are selecting!", +} diff --git a/www/src/Locales/en/SettingsPage.jsx b/www/src/Locales/en/SettingsPage.jsx index 768fd2cf0..33c558418 100644 --- a/www/src/Locales/en/SettingsPage.jsx +++ b/www/src/Locales/en/SettingsPage.jsx @@ -24,6 +24,7 @@ export default { 'first-win': 'First Win', 'off': 'Off' }, + 'profile-number-label': 'Profile Number', 'hotkey-settings-label': 'Hotkey Settings', 'hotkey-settings-sub-header': "The <1>Fn slider provides a mappable Function button in the <3 exact='true' to='/pin-mapping'>Pin Mapping page. By selecting the <1>Fn slider option, the Function button must be held along with the selected hotkey settings.<5 />Additionally, select <1>None from the dropdown to unassign any button.", 'hotkey-settings-warning': 'Function button is not mapped. The Fn slider will be disabled.', @@ -43,6 +44,10 @@ export default { 'invert-y': 'Invert Y Axis', 'toggle-4way-joystick-mode': 'Toggle 4-Way Joystick Mode', 'toggle-ddi-4way-joystick-mode': 'Toggle DDI 4-Way Joystick Mode', + 'load-profile-1': 'Load Profile #1', + 'load-profile-2': 'Load Profile #2', + 'load-profile-3': 'Load Profile #3', + 'load-profile-4': 'Load Profile #4', }, 'forced-setup-mode-label': 'Forced Setup Mode', 'forced-setup-mode-options': { diff --git a/www/src/Pages/ProfileSettings.jsx b/www/src/Pages/ProfileSettings.jsx new file mode 100644 index 000000000..fd2f1fe22 --- /dev/null +++ b/www/src/Pages/ProfileSettings.jsx @@ -0,0 +1,199 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { NavLink } from "react-router-dom"; +import { Button, Form } from 'react-bootstrap'; +import { AppContext } from '../Contexts/AppContext'; +import Section from '../Components/Section'; +import WebApi, { baseProfileOptions, baseButtonMappings } from '../Services/WebApi'; +import boards from '../Data/Boards.json'; +import { BUTTONS } from '../Data/Buttons'; +import './PinMappings.scss'; +import { Trans, useTranslation } from 'react-i18next'; + +const requiredButtons = ['S2']; +const errorType = { + required: 'errors.required', + conflict: 'errors.conflict', + invalid: 'errors.invalid', + used: 'errors.used', +}; + +export default function ProfileOptionsPage() { + const { buttonLabels, setButtonLabels, usedPins, updateUsedPins } = useContext(AppContext); + const [validated, setValidated] = useState(false); + const [saveMessage, setSaveMessage] = useState(''); + const [buttonMappings, setButtonMappings] = useState(baseButtonMappings); + const [profileOptions, setProfileOptions] = useState(baseProfileOptions); + const [selectedBoard] = useState(import.meta.env.VITE_GP2040_BOARD); + const { buttonLabelType } = buttonLabels; + const { setLoading } = useContext(AppContext); + + const { t } = useTranslation(''); + + const translatedErrorType = Object.keys(errorType).reduce((a, k) => ({ ...a, [k]: t(`ProfileOptions:${errorType[k]}`) }), {}); + + useEffect(() => { + async function fetchData() { + setButtonMappings(await WebApi.getPinMappings(setLoading)); + setProfileOptions(await WebApi.getProfileOptions(setLoading)); + setButtonLabels({}); + } + + fetchData(); + }, [setButtonMappings, setProfileOptions]); + + const handlePinChange = (e, index, key) => { + const newProfileOptions = { ...profileOptions }; + if (e.target.value) + newProfileOptions['alternativePinMappings'][index][key].pin = parseInt(e.target.value); + else + newProfileOptions['alternativePinMappings'][index][key].pin = ''; + + validateMappings(newProfileOptions); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + let mappings = { ...profileOptions }; + validateMappings(mappings); + + if (Object.keys(mappings).filter(p => mappings[p].error).length > 0) { + setSaveMessage(t('Common:errors.validation-error')); + return; + } + + const success = await WebApi.setProfileOptions(mappings); + if (success) + updateUsedPins(); + setSaveMessage(success ? t('Common:saved-success-message') : t('Common:saved-error-message')); + }; + + const validateMappings = (mappings) => { + profileOptions['alternativePinMappings'].forEach((altMappings) => { + const buttons = Object.keys(altMappings); + + // Create some mapped pin groups for easier error checking + const mappedPins = buttons + .filter(p => altMappings[p].pin > -1) + .reduce((a, p) => { + a.push(altMappings[p].pin); + return a; + }, []); + const mappedPinCounts = mappedPins.reduce((a, p) => ({ ...a, [p]: (a[p] || 0) + 1 }), {}); + const uniquePins = mappedPins.filter((p, i, a) => a.indexOf(p) === i); + const conflictedPins = Object.keys(mappedPinCounts).filter(p => mappedPinCounts[p] > 1).map(parseInt); + const invalidPins = uniquePins.filter(p => boards[selectedBoard].invalidPins.indexOf(p) > -1); + const otherPins = usedPins.filter(p => uniquePins.indexOf(p) === -1); + + for (let button of buttons) { + altMappings[button].error = ''; + + // Validate required button + if ((altMappings[button].pin < boards[selectedBoard].minPin || altMappings[button].pin > boards[selectedBoard].maxPin) && requiredButtons.filter(b => b === button).length) + altMappings[button].error = translatedErrorType.required; + + // Identify conflicted pins + else if (conflictedPins.indexOf(altMappings[button].pin) > -1) + altMappings[button].error = translatedErrorType.conflict; + + // Identify invalid pin assignments + else if (invalidPins.indexOf(altMappings[button].pin) > -1) + altMappings[button].error = translatedErrorType.invalid; + + // Identify used pins + else if (otherPins.indexOf(altMappings[button].pin) > -1) + altMappings[button].error = translatedErrorType.used; + } + }); + + setProfileOptions(mappings); + setValidated(true); + }; + + const pinCell = (profile, button) => { + return + handlePinChange(e, profile, button)} + > + + {renderError(profile, button)} + + + } + + const renderError = (index, button) => { + if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.required) { + return {t('PinMapping:errors.required', { + button: BUTTONS[buttonLabelType][button] + })}; + } + else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.conflict) { + const conflictedMappings = Object.keys(profileOptions['alternativePinMappings'][index]) + .filter(b => b !== button) + .filter(b => profileOptions['alternativePinMappings'][index][b].pin === profileOptions['alternativePinMappings'][index][button].pin) + .map(b => BUTTONS[buttonLabelType][b]); + + return {t('PinMapping:errors.conflict', { + pin: profileOptions['alternativePinMappings'][index][button].pin, + conflictedMappings: conflictedMappings.join(', '), + })}; + } + else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.invalid) { + console.log(profileOptions['alternativePinMappings'][index][button].pin); + return {t('PinMapping:errors.invalid', { + pin: profileOptions['alternativePinMappings'][index][button].pin + })}; + } + else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.used) { + return {t('PinMapping:errors.used', { + pin: profileOptions['alternativePinMappings'][index][button].pin + })}; + } + + return <>; + }; + + return ( +
+

{t('ProfileSettings:profile-pins-desc')}

+
  {String(buttonMappings['Up'].pin).padStart(2)}
+ {String(buttonMappings['Left'].pin).padStart(2)}  {String(buttonMappings['Right'].pin).padStart(2)}      {String(buttonMappings['B3'].pin).padStart(2)} {String(buttonMappings['B4'].pin).padStart(2)} {String(buttonMappings['R1'].pin).padStart(2)} {String(buttonMappings['L1'].pin).padStart(2)}
+   {String(buttonMappings['Down'].pin).padStart(2)}        {String(buttonMappings['B1'].pin).padStart(2)} {String(buttonMappings['B2'].pin).padStart(2)} {String(buttonMappings['R2'].pin).padStart(2)} {String(buttonMappings['L2'].pin).padStart(2)}
+

{t('ProfileSettings:profile-pins-warning')}

+
+ + + + + + + + + + + + {console.log(Object.keys(profileOptions['alternativePinMappings'][0]))} + {Object.keys(profileOptions['alternativePinMappings'][0]).map((key) => ( + + + + {pinCell(0, key)} + {pinCell(1, key)} + {pinCell(2, key)} + + ))} + +
{BUTTONS[buttonLabelType].label}{t('ProfileSettings:profile-1')}{t('ProfileSettings:profile-2')}{t('ProfileSettings:profile-3')}{t('ProfileSettings:profile-4')}
{BUTTONS[buttonLabelType][key]}{buttonMappings[key].pin}
+ + {saveMessage ? {saveMessage} : null} +
+
+ ); +} diff --git a/www/src/Pages/SettingsPage.jsx b/www/src/Pages/SettingsPage.jsx index 940fe006c..290d3d044 100644 --- a/www/src/Pages/SettingsPage.jsx +++ b/www/src/Pages/SettingsPage.jsx @@ -49,6 +49,10 @@ const HOTKEY_ACTIONS = [ { labelKey: 'hotkey-actions.invert-y', value: 10 }, { labelKey: 'hotkey-actions.toggle-4way-joystick-mode', value: 13 }, { labelKey: 'hotkey-actions.toggle-ddi-4way-joystick-mode', value: 14 }, + { labelKey: 'hotkey-actions.load-profile-1', value: 15 }, + { labelKey: 'hotkey-actions.load-profile-2', value: 16 }, + { labelKey: 'hotkey-actions.load-profile-3', value: 17 }, + { labelKey: 'hotkey-actions.load-profile-4', value: 18 }, ]; const FORCED_SETUP_MODES = [ @@ -80,6 +84,7 @@ const schema = yup.object().shape({ forcedSetupMode : yup.number().required().oneOf(FORCED_SETUP_MODES.map(o => o.value)).label('SOCD Cleaning Mode'), lockHotkeys: yup.number().required().label('Lock Hotkeys'), fourWayMode: yup.number().required().label('4-Way Joystick Mode'), + profileNumber: yup.number().required().label('Profile Number'), }); const FormContext = ({ setButtonLabels }) => { @@ -110,6 +115,8 @@ const FormContext = ({ setButtonLabels }) => { values.lockHotkeys = parseInt(values.lockHotkeys); if (!!values.fourWayMode) values.fourWayMode = parseInt(values.fourWayMode); + if (!!values.profileNumber) + values.profileNumber = parseInt(values.profileNumber); setButtonLabels({ swapTpShareLabels: (values.switchTpShareForDs4 === 1) && (values.inputMode === 4) }); @@ -242,6 +249,20 @@ export default function SettingsPage() { checked={Boolean(values.fourWayMode)} onChange={(e) => { setFieldValue("fourWayMode", e.target.checked ? 1 : 0); }} /> + + {t('SettingsPage:profile-number-label')} +
+ { setFieldValue("profileNumber", parseInt(e.target.value));}} + > +
+
diff --git a/www/src/Services/WebApi.js b/www/src/Services/WebApi.js index a6187f41d..d3c7cb5ad 100644 --- a/www/src/Services/WebApi.js +++ b/www/src/Services/WebApi.js @@ -25,6 +25,49 @@ export const baseButtonMappings = { Fn: { pin: -1, key: 0, error: null }, }; +export const baseProfileOptions = { + alternativePinMappings: [{ + Up: { pin: -1, key: 0, error: null }, + Down: { pin: -1, key: 0, error: null }, + Left: { pin: -1, key: 0, error: null }, + Right: { pin: -1, key: 0, error: null }, + B1: { pin: -1, key: 0, error: null }, + B2: { pin: -1, key: 0, error: null }, + B3: { pin: -1, key: 0, error: null }, + B4: { pin: -1, key: 0, error: null }, + L1: { pin: -1, key: 0, error: null }, + R1: { pin: -1, key: 0, error: null }, + L2: { pin: -1, key: 0, error: null }, + R2: { pin: -1, key: 0, error: null }, + },{ + Up: { pin: -1, key: 0, error: null }, + Down: { pin: -1, key: 0, error: null }, + Left: { pin: -1, key: 0, error: null }, + Right: { pin: -1, key: 0, error: null }, + B1: { pin: -1, key: 0, error: null }, + B2: { pin: -1, key: 0, error: null }, + B3: { pin: -1, key: 0, error: null }, + B4: { pin: -1, key: 0, error: null }, + L1: { pin: -1, key: 0, error: null }, + R1: { pin: -1, key: 0, error: null }, + L2: { pin: -1, key: 0, error: null }, + R2: { pin: -1, key: 0, error: null }, + },{ + Up: { pin: -1, key: 0, error: null }, + Down: { pin: -1, key: 0, error: null }, + Left: { pin: -1, key: 0, error: null }, + Right: { pin: -1, key: 0, error: null }, + B1: { pin: -1, key: 0, error: null }, + B2: { pin: -1, key: 0, error: null }, + B3: { pin: -1, key: 0, error: null }, + B4: { pin: -1, key: 0, error: null }, + L1: { pin: -1, key: 0, error: null }, + R1: { pin: -1, key: 0, error: null }, + L2: { pin: -1, key: 0, error: null }, + R2: { pin: -1, key: 0, error: null }, + }] +}; + async function resetSettings() { return axios.get(`${baseUrl}/api/resetSettings`) .then((response) => response.data) @@ -235,6 +278,46 @@ async function setPinMappings(mappings) { }); } +async function getProfileOptions(setLoading) { + setLoading(true); + + try { + const response = await axios.get(`${baseUrl}/api/getProfileOptions`); + let profileOptions = { ...baseProfileOptions }; + response.data['alternativePinMappings'].forEach((altButtons, index) => { + for (let prop of Object.keys(altButtons)) + profileOptions['alternativePinMappings'][index][prop].pin = parseInt( + response.data['alternativePinMappings'][index][prop] + ); + }); + setLoading(false); + return profileOptions; + } catch (error) { + console.error(error); + return false; + } +} + +async function setProfileOptions(options) { + let data = {}; + data['alternativePinMappings'] = []; + options['alternativePinMappings'].forEach((altButtons, index) => { + let altMapping = {}; + Object.keys(options['alternativePinMappings'][index]).map((button, i) => altMapping[button] = altButtons[button].pin); + data['alternativePinMappings'].push(altMapping); + }); + + return axios.post(`${baseUrl}/api/setProfileOptions`, sanitizeRequest(data)) + .then((response) => { + console.log(response.data); + return true; + }) + .catch((err) => { + console.error(err); + return false; + }); +} + async function getKeyMappings(setLoading) { setLoading(true); @@ -385,6 +468,8 @@ const WebApi = { setCustomTheme, getPinMappings, setPinMappings, + getProfileOptions, + setProfileOptions, getKeyMappings, setKeyMappings, getAddonsOptions,