From db7d8f2f91ade3697b0fd204254630859d4e630e Mon Sep 17 00:00:00 2001 From: TheTrain <32771064+TheTrainGoes@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:38:45 -0400 Subject: [PATCH] Add experimental amiibo support to Switch 2 Pro mode This draft PR adds experimental amiibo support to the new Switch 2 Pro input mode. It allows you to upload four amiibo .bin files that you have gotten from your own amiibos. You can assign each of those four amiibo slots to either a GPIO pin or a hotkey. When in a game and spot that supports amiibos, pressing that GPIO or hotkey will cause the amiibo to activate. Please note that currently this draft does not support write-back to the amiibo, it is read-only. If you are able to source and upload the necessary files you can also set each amiibo slot to generate a new serial number on each press. Without those files these will work as your standard amiibo and allow interaction with the game once a day (or however the game normally interacts with the amiibo). --- CMakeLists.txt | 3 + headers/addons/amiibo.h | 46 ++ headers/addons/amiibo_crypto.h | 6 + .../display/ui/screens/ButtonLayoutScreen.h | 36 +- headers/display/ui/screens/MainMenuScreen.h | 1 + .../switch2pro/Switch2ProDescriptors.h | 181 +++++ headers/drivers/switch2pro/Switch2ProDriver.h | 38 + headers/gamepad.h | 1 + headers/gpdriver.h | 1 + headers/mbedtls_config.h | 1 + proto/config.proto | 12 + proto/enums.proto | 28 + src/addons/AMIITOOL_LICENSE | 23 + src/addons/amiibo.cpp | 246 ++++++ src/addons/amiibo_crypto.cpp | 100 +++ src/addons/neopicoleds.cpp | 1 + src/config_utils.cpp | 25 + src/display/ui/screens/ButtonLayoutScreen.cpp | 2 + src/drivermanager.cpp | 4 + src/drivers/switch2pro/Switch2ProDriver.cpp | 715 ++++++++++++++++++ src/gamepad.cpp | 9 + src/gp2040.cpp | 2 + src/hosts/DualshockPS4Host.cpp | 2 +- src/usbdriver.cpp | 5 + src/webconfig.cpp | 119 +++ www/server/app.js | 93 ++- www/src/Addons/Amiibo.tsx | 315 ++++++++ www/src/Data/Buttons.js | 37 + www/src/Data/InputBootModes.ts | 9 + www/src/Icons/Switch2ProController.tsx | 54 ++ www/src/Locales/en/AddonsConfig.jsx | 50 ++ www/src/Locales/en/PinMapping.jsx | 4 + www/src/Locales/en/Proto/GpioAction.jsx | 4 + www/src/Locales/en/SettingsPage.jsx | 24 + www/src/Pages/AddonsConfigPage.tsx | 4 + www/src/Pages/SettingsPage.jsx | 223 +++++- www/src/Pages/SettingsPage.scss | 39 + www/src/Services/WebApi.js | 71 ++ www/src_gen/enums.ts | 24 +- 39 files changed, 2538 insertions(+), 20 deletions(-) create mode 100644 headers/addons/amiibo.h create mode 100644 headers/addons/amiibo_crypto.h create mode 100644 headers/drivers/switch2pro/Switch2ProDescriptors.h create mode 100644 headers/drivers/switch2pro/Switch2ProDriver.h create mode 100644 src/addons/AMIITOOL_LICENSE create mode 100644 src/addons/amiibo.cpp create mode 100644 src/addons/amiibo_crypto.cpp create mode 100644 src/drivers/switch2pro/Switch2ProDriver.cpp create mode 100644 www/src/Addons/Amiibo.tsx create mode 100644 www/src/Icons/Switch2ProController.tsx diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c3bd5d3fc..f6d5c0dfe8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -223,6 +223,7 @@ src/drivers/psclassic/PSClassicDriver.cpp src/drivers/sinput/SInputDriver.cpp src/drivers/switch/SwitchDriver.cpp src/drivers/switchpro/SwitchProDriver.cpp +src/drivers/switch2pro/Switch2ProDriver.cpp src/drivers/xbone/XBOneAuth.cpp src/drivers/xbone/XBOneAuthUSBListener.cpp src/drivers/xbone/XBOneDriver.cpp @@ -286,6 +287,8 @@ src/addons/playerleds.cpp src/addons/reactiveleds.cpp src/addons/rotaryencoder.cpp src/addons/reverse.cpp +src/addons/amiibo.cpp +src/addons/amiibo_crypto.cpp src/addons/drv8833_rumble.cpp src/addons/turbo.cpp src/addons/slider_socd.cpp diff --git a/headers/addons/amiibo.h b/headers/addons/amiibo.h new file mode 100644 index 0000000000..2337a29dc0 --- /dev/null +++ b/headers/addons/amiibo.h @@ -0,0 +1,46 @@ +#ifndef _Amiibo_H +#define _Amiibo_H + +#include "gpaddon.h" + +#define AmiiboName "Amiibo" + +#define AMIIBO_SLOT_COUNT 4 +#define AMIIBO_DATA_SIZE 540 +#define AMIIBO_SIGNATURE_SIZE 32 +#define AMIIBO_FILE_SIZE (AMIIBO_DATA_SIZE + AMIIBO_SIGNATURE_SIZE) +#define AMIIBO_NAME_SIZE 32 +#define AMIIBO_TAP_MS 2000 +#define AMIIBO_KEY_SIZE 80 + +class AmiiboAddon : public GPAddon { +public: + virtual bool available(); + virtual void setup(); + virtual void preprocess() {} + virtual void process(); + virtual void postprocess(bool sent) {} + virtual void reinit(); + virtual std::string name() { return AmiiboName; } + + static void tap(uint8_t slot); + static bool tagPresent(); + static const uint8_t * tagData(); + static const uint8_t * tagSignature(); + static bool slotFilled(uint8_t slot); + static uint16_t slotSize(uint8_t slot); + static const char * slotName(uint8_t slot); + static bool slotRandomizeSerial(uint8_t slot); + static bool setSlotRandomizeSerial(uint8_t slot, bool enabled); + static bool writeSlot(uint8_t slot, const uint8_t * data, uint16_t length, const char * name); + static bool clearSlot(uint8_t slot); + static bool keyPresent(bool locked); + static bool writeKey(bool locked, const uint8_t * data, size_t length); + static bool clearKeys(); + static uint32_t tapGeneration(); +private: + Mask_t slotPinMasks[AMIIBO_SLOT_COUNT] = { }; + Mask_t pressedPins = 0; +}; + +#endif diff --git a/headers/addons/amiibo_crypto.h b/headers/addons/amiibo_crypto.h new file mode 100644 index 0000000000..2c4e72e658 --- /dev/null +++ b/headers/addons/amiibo_crypto.h @@ -0,0 +1,6 @@ +#pragma once +#include + +bool amiiboKeyValid(const uint8_t *key, bool locked); +bool amiiboRandomize(const uint8_t *original540, uint8_t *output540, + const uint8_t *unfixed80, const uint8_t *locked80, const uint8_t *uid7); diff --git a/headers/display/ui/screens/ButtonLayoutScreen.h b/headers/display/ui/screens/ButtonLayoutScreen.h index 088a691fdf..b0b747e78b 100644 --- a/headers/display/ui/screens/ButtonLayoutScreen.h +++ b/headers/display/ui/screens/ButtonLayoutScreen.h @@ -14,8 +14,8 @@ #include "GPGFX_UI_widgets.h" #include "GPGFX_UI_layouts.h" -#define INPUT_HISTORY_MAX_INPUTS 22 -#define INPUT_HISTORY_MAX_MODES 12 +#define INPUT_HISTORY_MAX_INPUTS 23 +#define INPUT_HISTORY_MAX_MODES 13 // Static to ensure memory is never doubled static const char * displayNames[INPUT_HISTORY_MAX_MODES][INPUT_HISTORY_MAX_INPUTS] __attribute__((unused)) = { @@ -24,84 +24,91 @@ static const char * displayNames[INPUT_HISTORY_MAX_MODES][INPUT_HISTORY_MAX_INPU CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, CHAR_CROSS, CHAR_CIRCLE, CHAR_SQUARE, CHAR_TRIANGLE, "L1", "R1", "L2", "R2", - "SL", "ST", "L3", "R3", "PS", "A2" + "SL", "ST", "L3", "R3", "PS", "A2", "" }, { // Switch - 1 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "B", "A", "Y", "X", "L", "R", "ZL", "ZR", - "-", "+", "LS", "RS", CHAR_HOME_S, CHAR_CAP_S + "-", "+", "LS", "RS", CHAR_HOME_S, CHAR_CAP_S, "" }, { // XInput - 2 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "A", "B", "X", "Y", "LB", "RB", "LT", "RT", - CHAR_VIEW_X, CHAR_MENU_X, "LS", "RS", CHAR_HOME_X, "A2" + CHAR_VIEW_X, CHAR_MENU_X, "LS", "RS", CHAR_HOME_X, "A2", "" }, { // Keyboard / HID-KB - 3 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "B1", "B2", "B3", "B4", "L1", "R1", "L2", "R2", - "S1", "S2", "L3", "R3", "A1", "A2" + "S1", "S2", "L3", "R3", "A1", "A2", "" }, { // PS4/PS5 - 4 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, CHAR_CROSS, CHAR_CIRCLE, CHAR_SQUARE, CHAR_TRIANGLE, "L1", "R1", "L2", "R2", - CHAR_SHARE_P, "OP", "L3", "R3", CHAR_HOME_P, CHAR_TPAD_P + CHAR_SHARE_P, "OP", "L3", "R3", CHAR_HOME_P, CHAR_TPAD_P, "" }, { // GEN/MD Mini - 5 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "A", "B", "X", "Y", "", "Z", "", "C", - "M", "S", "", "", "", "" + "M", "S", "", "", "", "", "" }, { // Neo Geo Mini - 6 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "B", "D", "A", "C", "", "", "", "", - "SE", "ST", "", "", "", "" + "SE", "ST", "", "", "", "", "" }, { // PC Engine/TG16 Mini - 7 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "I", "II", "", "", "", "", "", "", - "SE", "RUN", "", "", "", "" + "SE", "RUN", "", "", "", "", "" }, { // Egret II Mini - 8 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "A", "B", "C", "D", "", "E", "", "F", - "CRD", "ST", "", "", "MN", "" + "CRD", "ST", "", "", "MN", "", "" }, { // Astro City Mini - 9 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "A", "B", "D", "E", "", "C", "", "F", - "CRD", "ST", "", "", "", "" + "CRD", "ST", "", "", "", "", "" }, { // Original Xbox - 10 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "A", "B", "X", "Y", "BL", "WH", "L", "R", - "BK", "ST", "LS", "RS", "", "" + "BK", "ST", "LS", "RS", "", "", "" }, { // HID / DINPUT - 11 CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, "2", "3", "1", "4", "5", "6", "7", "8", - "9", "10", "11", "12", "13", "14" + "9", "10", "11", "12", "13", "14", "" + }, + { // Switch 2 Pro - 12 + CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, + CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, + "B", "A", "Y", "X", + "L", "R", "ZL", "ZR", + "-", "+", "LS", "RS", CHAR_HOME_S, CHAR_CAP_S, "C" } }; @@ -134,6 +141,7 @@ class ButtonLayoutScreen : public GPScreen { const std::map displayModeLookup = { {INPUT_MODE_PS3, 0}, {INPUT_MODE_SWITCH, 1}, + {INPUT_MODE_SWITCH2_PRO, 12}, {INPUT_MODE_XINPUT, 2}, {INPUT_MODE_XBONE, 2}, {INPUT_MODE_KEYBOARD, 3}, diff --git a/headers/display/ui/screens/MainMenuScreen.h b/headers/display/ui/screens/MainMenuScreen.h index 49561fb73e..4647e9bfeb 100644 --- a/headers/display/ui/screens/MainMenuScreen.h +++ b/headers/display/ui/screens/MainMenuScreen.h @@ -25,6 +25,7 @@ #define INPUT_MODE_P5GENERAL_NAME "P5 General" #define INPUT_MODE_SWITCH_PRO_NAME "Nintendo Switch Pro" #define INPUT_MODE_SINPUT_NAME "SInput" +#define INPUT_MODE_SWITCH2_PRO_NAME "Nintendo Switch 2 Pro" #define INPUT_MODE_CONFIG_NAME "Web Config" #define SOCD_MODE_UP_PRIORITY_NAME "Up Priority" diff --git a/headers/drivers/switch2pro/Switch2ProDescriptors.h b/headers/drivers/switch2pro/Switch2ProDescriptors.h new file mode 100644 index 0000000000..5ebc66e74e --- /dev/null +++ b/headers/drivers/switch2pro/Switch2ProDescriptors.h @@ -0,0 +1,181 @@ +/* + * SPDX-License-Identifier: MIT + * Nintendo Switch 2 Pro Controller (057E:2069) USB identity. + * + * Descriptors and factory data follow mizuyoukanao/NX2-ProController-Emulation + * (RP2040 device-side emulation for Switch 2) and ndeadly/switch2_controller_research. + */ + +#ifndef _SWITCH2_PRO_DESCRIPTORS_H_ +#define _SWITCH2_PRO_DESCRIPTORS_H_ + +#include + +#define SWITCH2_GAMECUBE_PRODUCT_ID 0x2073 + +#define SWITCH2_FW_TYPE_PRO 0x02 +#define SWITCH2_FW_TYPE_GAMECUBE 0x03 + +#define SWITCH2_PRO_DEVICE_PID_OFFSET 10 +#define SWITCH2_PRO_FACTORY_PID_OFFSET 0x14 +#define SWITCH2_PRO_GRIP_PID_OFFSET 24 +#define SWITCH2_PRO_STRING_PRODUCT 2 + +#define SWITCH2_PRO_DEVICE_INFO_BT_OFFSET 10 +#define SWITCH2_PRO_BT_RANDOM_BYTES 3 +#define SWITCH2_PRO_FACTORY_SERIAL_DIGITS 5 +#define SWITCH2_PRO_FACTORY_SERIAL_COUNT 11 + +#define SWITCH2_PRO_INPUT_REPORT_ID 0x09 +#define SWITCH2_PRO_INPUT_REPORT_LEN 63 + +#define SWITCH2_PRO_ITF_HID 0 +#define SWITCH2_PRO_ITF_VENDOR 1 +#define SWITCH2_PRO_EP_BULK_OUT 0x02 +#define SWITCH2_PRO_EP_BULK_IN 0x82 + +#define SWITCH2_PRO_MASK_B 0x01 +#define SWITCH2_PRO_MASK_A 0x02 +#define SWITCH2_PRO_MASK_Y 0x04 +#define SWITCH2_PRO_MASK_X 0x08 +#define SWITCH2_PRO_MASK_R 0x10 +#define SWITCH2_PRO_MASK_ZR 0x20 +#define SWITCH2_PRO_MASK_PLUS 0x40 +#define SWITCH2_PRO_MASK_RSTICK 0x80 + +#define SWITCH2_PRO_MASK_DOWN 0x01 +#define SWITCH2_PRO_MASK_RIGHT 0x02 +#define SWITCH2_PRO_MASK_LEFT 0x04 +#define SWITCH2_PRO_MASK_UP 0x08 +#define SWITCH2_PRO_MASK_L 0x10 +#define SWITCH2_PRO_MASK_ZL 0x20 +#define SWITCH2_PRO_MASK_MINUS 0x40 +#define SWITCH2_PRO_MASK_LSTICK 0x80 + +#define SWITCH2_PRO_MASK_HOME 0x01 +#define SWITCH2_PRO_MASK_CAPTURE 0x02 +#define SWITCH2_PRO_MASK_GR 0x04 +#define SWITCH2_PRO_MASK_GL 0x08 +#define SWITCH2_PRO_MASK_C 0x10 + +#define SWITCH2_PRO_STICK_MAX 0xFFF + +static const uint8_t switch2pro_device_descriptor[] = { + 0x12, // bLength + 0x01, // bDescriptorType (Device) + 0x00, 0x02, // bcdUSB 2.00 + 0xEF, // bDeviceClass (Miscellaneous) + 0x02, // bDeviceSubClass (Common) + 0x01, // bDeviceProtocol (Interface Association) + 0x40, // bMaxPacketSize0 64 + 0x7E, 0x05, // idVendor 0x057E + 0x69, 0x20, // idProduct 0x2069 + 0x01, 0x02, // bcdDevice 2.01 + 0x01, // iManufacturer + 0x02, // iProduct + 0x03, // iSerialNumber + 0x01, // bNumConfigurations +}; + +static const uint8_t switch2pro_device_qualifier[] = { + 0x0A, // bLength + 0x06, // bDescriptorType (Device Qualifier) + 0x00, 0x02, // bcdUSB 2.00 + 0x00, 0x00, 0x00, + 0x40, // bMaxPacketSize0 64 + 0x01, // bNumConfigurations + 0x00, // bReserved +}; + +static const uint8_t switch2pro_report_descriptor[] = { + 0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x05, 0x05, 0xFF, 0x09, 0x01, + 0x15, 0x00, 0x26, 0xFF, 0x00, 0x95, 0x3F, 0x75, 0x08, 0x81, 0x02, 0x85, + 0x09, 0x09, 0x01, 0x95, 0x02, 0x81, 0x02, 0x05, 0x09, 0x19, 0x01, 0x29, + 0x15, 0x25, 0x01, 0x95, 0x15, 0x75, 0x01, 0x81, 0x02, 0x95, 0x01, 0x75, + 0x03, 0x81, 0x03, 0x05, 0x01, 0x09, 0x01, 0xA1, 0x00, 0x09, 0x30, 0x09, + 0x31, 0x09, 0x33, 0x09, 0x35, 0x26, 0xFF, 0x0F, 0x95, 0x04, 0x75, 0x0C, + 0x81, 0x02, 0xC0, 0x05, 0xFF, 0x09, 0x02, 0x26, 0xFF, 0x00, 0x95, 0x34, + 0x75, 0x08, 0x81, 0x02, 0x85, 0x02, 0x09, 0x01, 0x95, 0x3F, 0x91, 0x02, + 0xC0 +}; + +static const uint8_t switch2pro_configuration_descriptor[] = { + 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x04, 0xC0, 0xFA, 0x08, 0x0B, 0x00, + 0x01, 0x03, 0x00, 0x00, 0x00, 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, + 0x00, 0x05, 0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0x61, 0x00, 0x07, + 0x05, 0x81, 0x03, 0x40, 0x00, 0x04, 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, + 0x04, 0x08, 0x0B, 0x01, 0x01, 0xFF, 0x00, 0x00, 0x00, 0x09, 0x04, 0x01, + 0x00, 0x02, 0xFF, 0x00, 0x00, 0x06, 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, + 0x00, 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00 +}; + +static const char * const switch2pro_strings[] = { + nullptr, // 0: language IDs (handled separately) + "Nintendo", // 1: manufacturer + "Switch 2 Pro Controller", // 2: product + "00", // 3: serial + "Config_0", // 4: configuration + "If_Hid", // 5: HID interface + "Switch 2 Pro Controller", // 6: vendor interface +}; +static const char * const switch2gamecube_product_string = "Nintendo GameCube Controller"; +static const uint16_t switch2pro_language_ids[] = { 0x0409, 0x0402, 0x0801 }; + +static const uint8_t switch2pro_device_info[] = { + 0x02, 0x01, 0x04, + 0x0C, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x03, + 0xE8, 0x20, 0x17, 0x05, 0x48, 0xC8, +}; +static const uint8_t switch2pro_bt_address_reversed[] = { 0xE8, 0x20, 0x17, 0x05, 0x48, 0xC8 }; + +#define SWITCH2_PRO_FACTORY_BODY_COLOR 0x19 +#define SWITCH2_PRO_FACTORY_BUTTONS_COLOR 0x1C +#define SWITCH2_PRO_FACTORY_HIGHLIGHT_COLOR 0x1F +#define SWITCH2_PRO_FACTORY_GRIP_COLOR 0x22 + +static const uint8_t switch2pro_factory_data[] = { + 0x01, 0x00, + 0x48, 0x45, 0x4A, 0x31, 0x30, 0x30, 0x30, 0x30, 0x33, 0x31, 0x34, 0x33, 0x37, 0x32, 0x00, 0x00, // serial + 0x7E, 0x05, // VID + 0x69, 0x20, // PID + 0x01, 0x06, 0x01, + 0x23, 0x23, 0x23, // body colour + 0xA0, 0xA0, 0xA0, // buttons colour + 0xE6, 0xE6, 0xE6, // highlight colour + 0x32, 0x32, 0x32, // grip colour +}; + +static const uint8_t switch2pro_flash_13040[] = { + 0x85, 0x2e, 0xec, 0x41, 0x49, 0xec, 0x20, 0xbc, 0xc5, 0x5d, 0x3f, 0xbc, 0xad, 0x11, 0x42, 0x3b, +}; +static const uint8_t switch2pro_flash_13080[] = { + 0x01, 0xad, 0xd9, 0x9a, 0x55, 0x56, 0x65, 0xa0, 0x00, 0x0a, 0xa0, 0x00, 0x0a, 0xe2, 0x20, 0x0e, + 0xe2, 0x20, 0x0e, 0x9a, 0xad, 0xd9, 0x9a, 0xad, 0xd9, 0x0a, 0xa5, 0x50, 0x0a, 0xa5, 0x50, 0x2f, + 0xf6, 0x62, 0x2f, 0xf6, 0x62, 0x0a, 0xff, 0xff, 0xbc, 0x97, 0x85, 0x4f, 0x16, 0x5f, 0x2e, 0x96, + 0x61, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; +static const uint8_t switch2pro_flash_130c0[] = { + 0x01, 0xad, 0xd9, 0x9a, 0x55, 0x56, 0x65, 0xa0, 0x00, 0x0a, 0xa0, 0x00, 0x0a, 0xe2, 0x20, 0x0e, + 0xe2, 0x20, 0x0e, 0x9a, 0xad, 0xd9, 0x9a, 0xad, 0xd9, 0x0a, 0xa5, 0x50, 0x0a, 0xa5, 0x50, 0x2f, + 0xf6, 0x62, 0x2f, 0xf6, 0x62, 0x0a, 0xff, 0xff, 0x73, 0x38, 0x82, 0xf5, 0x55, 0x60, 0x2a, 0x56, + 0x64, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +}; +static const uint8_t switch2pro_flash_13100[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xd2, 0x7e, 0xbd, + 0x33, 0x8a, 0xbe, 0xbe, 0xaa, 0x82, 0x20, 0x41, +}; + +static const uint8_t switch2pro_charging_grip_data[] = { + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x48, 0x45, 0x4a, 0x37, 0x31, 0x30, 0x30, 0x31, 0x31, 0x32, + 0x31, 0x32, 0x34, 0x37, 0x00, 0x00, 0x7e, 0x05, 0x69, 0x20, 0x01, 0x06, 0x01, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, +}; + +static const uint8_t switch2pro_device_key[] = { + 0x10, 0x5f, 0x1a, 0xc4, 0x25, 0x63, 0x2b, 0xba, 0xe1, 0x05, 0xdf, 0x2c, 0x79, 0xee, 0xf6, 0x5c, +}; + +#endif // _SWITCH2_PRO_DESCRIPTORS_H_ diff --git a/headers/drivers/switch2pro/Switch2ProDriver.h b/headers/drivers/switch2pro/Switch2ProDriver.h new file mode 100644 index 0000000000..9fac1d915c --- /dev/null +++ b/headers/drivers/switch2pro/Switch2ProDriver.h @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: MIT + * Nintendo Switch 2 Pro Controller (057E:2069) wired USB emulation. + */ + +#ifndef _SWITCH2_PRO_DRIVER_H_ +#define _SWITCH2_PRO_DRIVER_H_ + +#include "gpdriver.h" +#include "drivers/switch2pro/Switch2ProDescriptors.h" + +#define SWITCH2_PRO_KEEPALIVE_MS 50 + +class Switch2ProDriver : public GPDriver { +public: + virtual void initialize(); + virtual bool process(Gamepad * gamepad); + virtual void initializeAux() {} + virtual void processAux() {} + virtual uint16_t get_report(uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen); + virtual void set_report(uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize); + virtual bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request); + virtual const uint16_t * get_descriptor_string_cb(uint8_t index, uint16_t langid); + virtual const uint8_t * get_descriptor_device_cb(); + virtual const uint8_t * get_hid_descriptor_report_cb(uint8_t itf); + virtual const uint8_t * get_descriptor_configuration_cb(uint8_t index); + virtual const uint8_t * get_descriptor_device_qualifier_cb(); + virtual const uint8_t * get_descriptor_other_speed_configuration_cb(uint8_t index); + virtual uint16_t GetJoystickMidValue(); + virtual USBListener * get_usb_auth_listener() { return nullptr; } +private: + uint8_t inputReport[SWITCH2_PRO_INPUT_REPORT_LEN] = { }; + uint8_t lastSentState[11] = { }; + uint32_t lastSentMs = 0; + uint8_t reportCounter = 0; +}; + +#endif // _SWITCH2_PRO_DRIVER_H_ diff --git a/headers/gamepad.h b/headers/gamepad.h index 2725c6897b..0fa5e8605a 100644 --- a/headers/gamepad.h +++ b/headers/gamepad.h @@ -209,6 +209,7 @@ class Gamepad { (options.inputMode == INPUT_MODE_PS3 || options.inputMode == INPUT_MODE_SWITCH || options.inputMode == INPUT_MODE_SWITCH_PRO || + options.inputMode == INPUT_MODE_SWITCH2_PRO || options.inputMode == INPUT_MODE_NEOGEO || options.inputMode == INPUT_MODE_PS4)) ? SOCD_MODE_NEUTRAL : options.socdMode; diff --git a/headers/gpdriver.h b/headers/gpdriver.h index fc885e3552..500e43bfcf 100644 --- a/headers/gpdriver.h +++ b/headers/gpdriver.h @@ -37,6 +37,7 @@ class GPDriver { virtual const uint8_t * get_hid_descriptor_report_cb(uint8_t itf) = 0; virtual const uint8_t * get_descriptor_configuration_cb(uint8_t index) = 0; virtual const uint8_t * get_descriptor_device_qualifier_cb() = 0; + virtual const uint8_t * get_descriptor_other_speed_configuration_cb(uint8_t index) { return nullptr; } virtual uint16_t GetJoystickMidValue() = 0; const usbd_class_driver_t * get_class_driver() { return &class_driver; } virtual USBListener * get_usb_auth_listener() = 0; diff --git a/headers/mbedtls_config.h b/headers/mbedtls_config.h index 3491c8bcde..0f04e7cf00 100644 --- a/headers/mbedtls_config.h +++ b/headers/mbedtls_config.h @@ -3,6 +3,7 @@ #define MBEDTLS_CHECK_PARAMS_ASSERT #define MBEDTLS_ENTROPY_HARDWARE_ALT +#define MBEDTLS_AES_C #define MBEDTLS_AES_ROM_TABLES #define MBEDTLS_AES_FEWER_TABLES #define MBEDTLS_CAMELLIA_SMALL_MEMORY diff --git a/proto/config.proto b/proto/config.proto index 47a13a5c28..8550bbf9a6 100644 --- a/proto/config.proto +++ b/proto/config.proto @@ -52,6 +52,12 @@ message GamepadOptions optional uint32 usbVendorID = 31; optional uint32 miniMenuGamepadInput = 32; optional InputModeDeviceType inputDeviceType = 33; + optional Switch2ProColorPreset switch2ProColorPreset = 34; + optional uint32 switch2ProBodyColor = 35; + optional uint32 switch2ProButtonsColor = 36; + optional uint32 switch2ProHighlightColor = 37; + optional uint32 switch2ProGripColor = 38; + optional Switch2ProIdentity switch2ProIdentity = 39; } message KeyboardMapping @@ -964,6 +970,11 @@ message HETriggerOptions repeated HETriggerMux muxes = 15 [(nanopb).max_count = 4]; } +message AmiiboOptions +{ + optional bool enabled = 1; +} + message AddonOptions { optional BootselButtonOptions bootselButtonOptions = 1; @@ -998,6 +1009,7 @@ message AddonOptions optional HETriggerOptions heTriggerOptions = 30; optional ProfileSliderOptions profileSliderOptions = 31; optional AnalogADS1115Options analogADS1115Options = 32; + optional AmiiboOptions amiiboOptions = 33; } message MigrationHistory diff --git a/proto/enums.proto b/proto/enums.proto index 80b254884e..2eda3952b3 100644 --- a/proto/enums.proto +++ b/proto/enums.proto @@ -158,6 +158,7 @@ enum InputMode INPUT_MODE_SWITCH_PRO = 15; INPUT_MODE_P5GENERAL = 16; INPUT_MODE_SINPUT = 17; + INPUT_MODE_SWITCH2_PRO = 19; INPUT_MODE_CONFIG = 255; } @@ -370,6 +371,11 @@ enum GpioAction SUSTAIN_PROFILE_SLIDER_POSITION_6 = 136; SUSTAIN_PROFILE_SLIDER_POSITION_7 = 137; SUSTAIN_PROFILE_SLIDER_POSITION_8 = 138; + BUTTON_PRESS_AMIIBO_1 = 139; + BUTTON_PRESS_AMIIBO_2 = 140; + BUTTON_PRESS_AMIIBO_3 = 141; + BUTTON_PRESS_AMIIBO_4 = 142; + reserved 143; } enum GpioDirection @@ -463,6 +469,11 @@ enum GamepadHotkey HOTKEY_RS_RIGHT = 85; HOTKEY_REBOOT_WEBCONFIG = 86; HOTKEY_REBOOT_USB = 87; + HOTKEY_AMIIBO_1 = 88; + HOTKEY_AMIIBO_2 = 89; + HOTKEY_AMIIBO_3 = 90; + HOTKEY_AMIIBO_4 = 91; + reserved 92; } // This has to be kept in sync with LEDFormat in NeoPico.h @@ -689,6 +700,23 @@ enum ReactiveLEDMode REACTIVE_LED_FADE_OUT = 3; }; +enum Switch2ProIdentity +{ + option (nanopb_enumopt).long_names = false; + + SWITCH2_PRO_IDENTITY_PRO = 0; + SWITCH2_PRO_IDENTITY_GAMECUBE = 1; +} + +enum Switch2ProColorPreset +{ + option (nanopb_enumopt).long_names = false; + + SWITCH2_PRO_COLOR_STOCK = 0; + SWITCH2_PRO_COLOR_GP2040 = 1; + SWITCH2_PRO_COLOR_CUSTOM = 2; +} + enum PS4ControllerIDMode { option (nanopb_enumopt).long_names = false; diff --git a/src/addons/AMIITOOL_LICENSE b/src/addons/AMIITOOL_LICENSE new file mode 100644 index 0000000000..7183bb0253 --- /dev/null +++ b/src/addons/AMIITOOL_LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2015-2017 Marcos Del Sol Vives +Copyright (c) 2016 javiMaD +Copyright (c) 2016 Michael Armbruster + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/addons/amiibo.cpp b/src/addons/amiibo.cpp new file mode 100644 index 0000000000..adf665b12d --- /dev/null +++ b/src/addons/amiibo.cpp @@ -0,0 +1,246 @@ +#include "addons/amiibo.h" +#include "addons/amiibo_crypto.h" +#include "drivermanager.h" +#include "pico/rand.h" +#include "storagemanager.h" +#include "FlashPROM.h" +#include "hardware/flash.h" +#include "hardware/sync.h" +#include "pico/multicore.h" + +#include + +#define AMIIBO_SLOT_ADDRESS(i) (EEPROM_ADDRESS_START - ((i) + 1) * FLASH_SECTOR_SIZE) +#define AMIIBO_MAGIC 0x4F424D41 + +typedef struct { + uint32_t magic; + uint16_t length; + uint16_t reserved; + char name[AMIIBO_NAME_SIZE]; + uint8_t data[AMIIBO_FILE_SIZE]; +} AmiiboSlot; + +extern char __flash_binary_end; + +#define AMIIBO_KEY_START (EEPROM_ADDRESS_START - 7 * FLASH_SECTOR_SIZE) +#define AMIIBO_KEY_MAGIC 0x59454B41 + +struct AmiiboKeys { + uint32_t magic; + uint8_t present[2]; + uint8_t data[2][AMIIBO_KEY_SIZE]; +}; + +static const AmiiboKeys * storedKeys() { + return reinterpret_cast(AMIIBO_KEY_START); +} + +static uint32_t tapUntilMs = 0; +static uint32_t generation = 0; +static uint8_t activeData[AMIIBO_DATA_SIZE]; +static uint8_t activeSignature[AMIIBO_SIGNATURE_SIZE]; +static bool hasSignature = false; + +static const AmiiboSlot * storedSlot(uint8_t slot) { + return reinterpret_cast(AMIIBO_SLOT_ADDRESS(slot)); +} + +static bool storageAvailable() { + return (uintptr_t)&__flash_binary_end <= AMIIBO_KEY_START; +} + +static void programSector(uint32_t address, const uint8_t * data, uint32_t length) { + multicore_lockout_start_blocking(); + uint32_t interrupts = save_and_disable_interrupts(); + flash_range_erase(address - XIP_BASE, FLASH_SECTOR_SIZE); + if (data != nullptr) + flash_range_program(address - XIP_BASE, data, length); + restore_interrupts(interrupts); + multicore_lockout_end_blocking(); +} + +static void programSlot(uint8_t slot, const uint8_t * page) { + programSector(AMIIBO_SLOT_ADDRESS(slot), page, sizeof(AmiiboSlot) + FLASH_PAGE_SIZE - (sizeof(AmiiboSlot) % FLASH_PAGE_SIZE)); +} + +bool AmiiboAddon::available() { + return Storage::getInstance().getAddonOptions().amiiboOptions.enabled; +} + +void AmiiboAddon::setup() { + GpioMappingInfo * pinMappings = Storage::getInstance().getProfilePinMappings(); + for (uint8_t i = 0; i < AMIIBO_SLOT_COUNT; i++) { + slotPinMasks[i] = 0; + for (Pin_t pin = 0; pin < (Pin_t)NUM_BANK0_GPIOS; pin++) { + if (pinMappings[pin].action == GpioAction::BUTTON_PRESS_AMIIBO_1 + i) + slotPinMasks[i] |= Mask_t{1} << pin; + } + } +} + +void AmiiboAddon::reinit() { + setup(); +} + +void AmiiboAddon::process() { + const Mask_t gpio = Storage::getInstance().GetGamepad()->debouncedGpio; + Mask_t pressed = 0; + for (uint8_t i = 0; i < AMIIBO_SLOT_COUNT; i++) { + const Mask_t slotPins = gpio & slotPinMasks[i]; + if (slotPins && !(pressedPins & slotPinMasks[i])) { + tap(i); + } + pressed |= slotPins; + } + pressedPins = pressed; +} + +void AmiiboAddon::tap(uint8_t slot) { + if (slot >= AMIIBO_SLOT_COUNT || !slotFilled(slot)) + return; + tapUntilMs = 0; + hasSignature = false; + const uint8_t * original = storedSlot(slot)->data; + if (slotRandomizeSerial(slot) && keyPresent(false) && keyPresent(true)) { + uint8_t uid[7] = { 0x04 }; + do { + for (size_t i = 1; i < sizeof(uid); i++) + uid[i] = get_rand_32() & 0xFF; + } while ((memcmp(uid, activeData, 3) == 0 && memcmp(uid + 3, activeData + 4, 4) == 0) + || (memcmp(uid, original, 3) == 0 && memcmp(uid + 3, original + 4, 4) == 0)); + if (!amiiboRandomize(original, activeData, storedKeys()->data[0], storedKeys()->data[1], uid)) { + return; + } + } else { + memcpy(activeData, original, sizeof(activeData)); + hasSignature = slotSize(slot) == AMIIBO_FILE_SIZE; + if (hasSignature) + memcpy(activeSignature, original + AMIIBO_DATA_SIZE, sizeof(activeSignature)); + } + generation++; + tapUntilMs = to_ms_since_boot(get_absolute_time()) + AMIIBO_TAP_MS; +} + +uint32_t AmiiboAddon::tapGeneration() { + return generation; +} + +bool AmiiboAddon::tagPresent() { + return tapUntilMs != 0 && (int32_t)(tapUntilMs - to_ms_since_boot(get_absolute_time())) > 0; +} + +const uint8_t * AmiiboAddon::tagData() { + return tagPresent() ? activeData : nullptr; +} + +const uint8_t * AmiiboAddon::tagSignature() { + return tagPresent() && hasSignature ? activeSignature : nullptr; +} + +bool AmiiboAddon::slotFilled(uint8_t slot) { + if (slot >= AMIIBO_SLOT_COUNT || !storageAvailable()) + return false; + const AmiiboSlot * stored = storedSlot(slot); + return stored->magic == AMIIBO_MAGIC && (stored->length == AMIIBO_DATA_SIZE || stored->length == AMIIBO_FILE_SIZE); +} + +uint16_t AmiiboAddon::slotSize(uint8_t slot) { + return slotFilled(slot) ? storedSlot(slot)->length : 0; +} + +const char * AmiiboAddon::slotName(uint8_t slot) { + return slotFilled(slot) ? storedSlot(slot)->name : ""; +} + +bool AmiiboAddon::slotRandomizeSerial(uint8_t slot) { + return slotFilled(slot) && storedSlot(slot)->reserved == 0; +} + +bool AmiiboAddon::setSlotRandomizeSerial(uint8_t slot, bool enabled) { + if (!DriverManager::getInstance().isConfigMode() || !slotFilled(slot)) + return false; + if (slotRandomizeSerial(slot) == enabled) + return true; + const AmiiboSlot * original = storedSlot(slot); + if (enabled && keyPresent(false) && keyPresent(true)) { + uint8_t uid[7]; + memcpy(uid, original->data, 3); + memcpy(uid + 3, original->data + 4, 4); + static uint8_t checked[AMIIBO_DATA_SIZE]; + if (!amiiboRandomize(original->data, checked, storedKeys()->data[0], storedKeys()->data[1], uid)) + return false; + } + static uint8_t page[sizeof(AmiiboSlot) + FLASH_PAGE_SIZE]; + memset(page, 0xFF, sizeof(page)); + memcpy(page, original, sizeof(AmiiboSlot)); + reinterpret_cast(page)->reserved = enabled ? 0 : 1; + programSlot(slot, page); + return slotFilled(slot) && slotRandomizeSerial(slot) == enabled; +} + +bool AmiiboAddon::writeSlot(uint8_t slot, const uint8_t * data, uint16_t length, const char * name) { + if (!DriverManager::getInstance().isConfigMode() || data == nullptr || name == nullptr + || slot >= AMIIBO_SLOT_COUNT || !storageAvailable() || (length != AMIIBO_DATA_SIZE && length != AMIIBO_FILE_SIZE)) + return false; + if (slotRandomizeSerial(slot) && keyPresent(false) && keyPresent(true)) { + uint8_t uid[7]; + memcpy(uid, data, 3); + memcpy(uid + 3, data + 4, 4); + static uint8_t checked[AMIIBO_DATA_SIZE]; + if (!amiiboRandomize(data, checked, storedKeys()->data[0], storedKeys()->data[1], uid)) + return false; + } + static uint8_t page[sizeof(AmiiboSlot) + FLASH_PAGE_SIZE]; + memset(page, 0xFF, sizeof(page)); + AmiiboSlot * stored = reinterpret_cast(page); + stored->magic = AMIIBO_MAGIC; + stored->length = length; + stored->reserved = slotRandomizeSerial(slot) ? 0 : 1; + memset(stored->name, 0, sizeof(stored->name)); + strncpy(stored->name, name, sizeof(stored->name) - 1); + memcpy(stored->data, data, length); + programSlot(slot, page); + return slotFilled(slot); +} + +bool AmiiboAddon::clearSlot(uint8_t slot) { + if (!DriverManager::getInstance().isConfigMode() || slot >= AMIIBO_SLOT_COUNT || !storageAvailable()) + return false; + programSlot(slot, nullptr); + return !slotFilled(slot); +} + +bool AmiiboAddon::keyPresent(bool locked) { + const AmiiboKeys * keys = storedKeys(); + const size_t index = locked ? 1 : 0; + return storageAvailable() && keys->magic == AMIIBO_KEY_MAGIC && keys->present[index] == 1 + && amiiboKeyValid(keys->data[index], locked); +} + +bool AmiiboAddon::writeKey(bool locked, const uint8_t * data, size_t length) { + if (!DriverManager::getInstance().isConfigMode() || !storageAvailable() + || data == nullptr || length != AMIIBO_KEY_SIZE || !amiiboKeyValid(data, locked)) + return false; + uint8_t page[FLASH_PAGE_SIZE] = {}; + AmiiboKeys * keys = reinterpret_cast(page); + keys->magic = AMIIBO_KEY_MAGIC; + for (size_t i = 0; i < 2; i++) { + if (keyPresent(i == 1)) { + keys->present[i] = 1; + memcpy(keys->data[i], storedKeys()->data[i], AMIIBO_KEY_SIZE); + } + } + const size_t index = locked ? 1 : 0; + keys->present[index] = 1; + memcpy(keys->data[index], data, length); + programSector(AMIIBO_KEY_START, page, sizeof(page)); + return keyPresent(locked) && memcmp(storedKeys()->data[index], data, length) == 0; +} + +bool AmiiboAddon::clearKeys() { + if (!DriverManager::getInstance().isConfigMode() || !storageAvailable()) + return false; + programSector(AMIIBO_KEY_START, nullptr, 0); + return !keyPresent(false) && !keyPresent(true); +} diff --git a/src/addons/amiibo_crypto.cpp b/src/addons/amiibo_crypto.cpp new file mode 100644 index 0000000000..9c738951f7 --- /dev/null +++ b/src/addons/amiibo_crypto.cpp @@ -0,0 +1,100 @@ +// Format and derivation based on amiitool, Copyright 2015-2017 Marcos Del Sol Vives, +// Copyright 2016 javiMaD, used under the MIT license (see AMIITOOL_LICENSE). +#include "addons/amiibo_crypto.h" +#include "mbedtls/aes.h" +#include "mbedtls/md.h" +#include + +static bool hmac(const uint8_t *key, const uint8_t *data, size_t size, uint8_t *out) { + return mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), key, 16, data, size, out) == 0; +} + +bool amiiboKeyValid(const uint8_t *key, bool locked) { + if (!key || key[31] > 16) return false; + const char *type = locked ? "locked secret" : "unfixed infos"; + return memcmp(key + 16, type, 14) == 0; +} + +static bool derive(const uint8_t *key, const uint8_t *data, uint8_t *derived) { + uint8_t seed[64] = {}, prepared[80] = {}, block[32]; + memcpy(seed, data + 0x29, 2); + memcpy(seed + 16, data + 0x1D4, 8); + memcpy(seed + 24, data + 0x1D4, 8); + memcpy(seed + 32, data + 0x1E8, 32); + size_t size = 2; + memcpy(prepared + size, key + 16, 14); size += 14; + memcpy(prepared + size, seed, 16 - key[31]); size += 16 - key[31]; + memcpy(prepared + size, key + 32, key[31]); size += key[31]; + memcpy(prepared + size, seed + 16, 16); size += 16; + for (size_t i = 0; i < 32; i++) prepared[size++] = seed[32 + i] ^ key[48 + i]; + if (!hmac(key, prepared, size, block)) return false; + memcpy(derived, block, 32); + prepared[1] = 1; + if (!hmac(key, prepared, size, block)) return false; + memcpy(derived + 32, block, 16); + return true; +} + +static void unpackLayout(const uint8_t *tag, uint8_t *data) { + memcpy(data, tag + 8, 8); + memcpy(data + 8, tag + 0x80, 32); + memcpy(data + 0x28, tag + 0x10, 0x24); + memcpy(data + 0x4C, tag + 0xA0, 0x168); + memcpy(data + 0x1B4, tag + 0x34, 32); + memcpy(data + 0x1D4, tag, 8); + memcpy(data + 0x1DC, tag + 0x54, 0x2C); +} + +static void packLayout(const uint8_t *data, uint8_t *tag) { + memcpy(tag + 8, data, 8); + memcpy(tag + 0x80, data + 8, 32); + memcpy(tag + 0x10, data + 0x28, 0x24); + memcpy(tag + 0xA0, data + 0x4C, 0x168); + memcpy(tag + 0x34, data + 0x1B4, 32); + memcpy(tag, data + 0x1D4, 8); + memcpy(tag + 0x54, data + 0x1DC, 0x2C); +} + +static bool cipher(uint8_t *data, const uint8_t *keys) { + mbedtls_aes_context ctx; + mbedtls_aes_init(&ctx); + bool ok = mbedtls_aes_setkey_enc(&ctx, keys, 128) == 0; + uint8_t counter[16], stream[16]; + memcpy(counter, keys + 16, 16); + for (size_t offset = 0; ok && offset < 0x188; offset += 16) { + ok = mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, counter, stream) == 0; + if (ok) for (size_t i = 0; i < 16 && offset + i < 0x188; i++) data[0x2C + offset + i] ^= stream[i]; + for (int i = 15; i >= 0 && ++counter[i] == 0; i--) {} + } + mbedtls_aes_free(&ctx); + return ok; +} + +static bool authenticate(uint8_t *data, const uint8_t *dataKeys, const uint8_t *tagKeys) { + return hmac(tagKeys + 32, data + 0x1D4, 0x34, data + 0x1B4) + && hmac(dataKeys + 32, data + 0x29, 0x1DF, data + 8); +} + +bool amiiboRandomize(const uint8_t *original540, uint8_t *output540, + const uint8_t *unfixed80, const uint8_t *locked80, const uint8_t *uid7) { + if (!original540 || !output540 || !uid7 || uid7[0] != 4 + || !amiiboKeyValid(unfixed80, false) || !amiiboKeyValid(locked80, true)) return false; + uint8_t data[520], dataKeys[48], tagKeys[48], signatures[64]; + unpackLayout(original540, data); + memcpy(signatures, data + 8, 32); + memcpy(signatures + 32, data + 0x1B4, 32); + if (!derive(unfixed80, data, dataKeys) || !derive(locked80, data, tagKeys) + || !cipher(data, dataKeys) || !authenticate(data, dataKeys, tagKeys)) return false; + uint8_t difference = 0; + for (size_t i = 0; i < 32; i++) difference |= (signatures[i] ^ data[8 + i]) | (signatures[32 + i] ^ data[0x1B4 + i]); + if (difference) return false; + memcpy(data + 0x1D4, uid7, 3); + data[0x1D7] = 0x88 ^ uid7[0] ^ uid7[1] ^ uid7[2]; + memcpy(data + 0x1D8, uid7 + 3, 4); + data[0] = uid7[3] ^ uid7[4] ^ uid7[5] ^ uid7[6]; + if (!derive(unfixed80, data, dataKeys) || !derive(locked80, data, tagKeys) + || !authenticate(data, dataKeys, tagKeys) || !cipher(data, dataKeys)) return false; + memcpy(output540, original540, 540); + packLayout(data, output540); + return true; +} diff --git a/src/addons/neopicoleds.cpp b/src/addons/neopicoleds.cpp index 4ca1a23431..f99f79c982 100644 --- a/src/addons/neopicoleds.cpp +++ b/src/addons/neopicoleds.cpp @@ -309,6 +309,7 @@ void NeoPicoLEDAddon::process() animationState = getXBoneAnimationNEOPICO(gamepad); break; case INPUT_MODE_SWITCH_PRO: + case INPUT_MODE_SWITCH2_PRO: animationState = getSwitchProAnimationNEOPICO(gamepad->auxState.playerID.ledValue); break; default: diff --git a/src/config_utils.cpp b/src/config_utils.cpp index 2c476fab34..e2470b7a67 100644 --- a/src/config_utils.cpp +++ b/src/config_utils.cpp @@ -149,6 +149,25 @@ #define DEFAULT_PS4_ID_MODE PS4_ID_CONSOLE #endif +#ifndef DEFAULT_SWITCH2_PRO_IDENTITY + #define DEFAULT_SWITCH2_PRO_IDENTITY SWITCH2_PRO_IDENTITY_PRO +#endif +#ifndef DEFAULT_SWITCH2_PRO_COLOR_PRESET + #define DEFAULT_SWITCH2_PRO_COLOR_PRESET SWITCH2_PRO_COLOR_STOCK +#endif +#ifndef DEFAULT_SWITCH2_PRO_BODY_COLOR + #define DEFAULT_SWITCH2_PRO_BODY_COLOR 0x232323 +#endif +#ifndef DEFAULT_SWITCH2_PRO_BUTTONS_COLOR + #define DEFAULT_SWITCH2_PRO_BUTTONS_COLOR 0xA0A0A0 +#endif +#ifndef DEFAULT_SWITCH2_PRO_HIGHLIGHT_COLOR + #define DEFAULT_SWITCH2_PRO_HIGHLIGHT_COLOR 0xE6E6E6 +#endif +#ifndef DEFAULT_SWITCH2_PRO_GRIP_COLOR + #define DEFAULT_SWITCH2_PRO_GRIP_COLOR 0x323232 +#endif + #ifndef DEFAULT_USB_DESC_OVERRIDE #define DEFAULT_USB_DESC_OVERRIDE false #endif @@ -369,6 +388,12 @@ void ConfigUtils::initUnsetPropertiesWithDefaults(Config& config) INIT_UNSET_PROPERTY(config.gamepadOptions, ps5AuthType, DEFAULT_PS5AUTHENTICATION_TYPE); INIT_UNSET_PROPERTY(config.gamepadOptions, xinputAuthType, DEFAULT_XINPUTAUTHENTICATION_TYPE); INIT_UNSET_PROPERTY(config.gamepadOptions, ps4ControllerIDMode, DEFAULT_PS4_ID_MODE); + INIT_UNSET_PROPERTY(config.gamepadOptions, switch2ProIdentity, DEFAULT_SWITCH2_PRO_IDENTITY); + INIT_UNSET_PROPERTY(config.gamepadOptions, switch2ProColorPreset, DEFAULT_SWITCH2_PRO_COLOR_PRESET); + INIT_UNSET_PROPERTY(config.gamepadOptions, switch2ProBodyColor, DEFAULT_SWITCH2_PRO_BODY_COLOR); + INIT_UNSET_PROPERTY(config.gamepadOptions, switch2ProButtonsColor, DEFAULT_SWITCH2_PRO_BUTTONS_COLOR); + INIT_UNSET_PROPERTY(config.gamepadOptions, switch2ProHighlightColor, DEFAULT_SWITCH2_PRO_HIGHLIGHT_COLOR); + INIT_UNSET_PROPERTY(config.gamepadOptions, switch2ProGripColor, DEFAULT_SWITCH2_PRO_GRIP_COLOR); INIT_UNSET_PROPERTY(config.gamepadOptions, usbDescOverride, DEFAULT_USB_DESC_OVERRIDE); INIT_UNSET_PROPERTY_STR(config.gamepadOptions, usbDescProduct, DEFAULT_USB_DESC_PRODUCT); INIT_UNSET_PROPERTY_STR(config.gamepadOptions, usbDescManufacturer, DEFAULT_USB_DESC_MANUFACTURER); diff --git a/src/display/ui/screens/ButtonLayoutScreen.cpp b/src/display/ui/screens/ButtonLayoutScreen.cpp index 197d8ee7ee..8cc2115b6c 100644 --- a/src/display/ui/screens/ButtonLayoutScreen.cpp +++ b/src/display/ui/screens/ButtonLayoutScreen.cpp @@ -285,6 +285,7 @@ void ButtonLayoutScreen::generateHeader() { case INPUT_MODE_PSCLASSIC: statusBar += "PSC"; break; case INPUT_MODE_XBOXORIGINAL: statusBar += "OGXBOX"; break; case INPUT_MODE_SWITCH_PRO: statusBar += "SWPRO"; break; + case INPUT_MODE_SWITCH2_PRO: statusBar += "SW2PRO"; break; case INPUT_MODE_SINPUT: statusBar += "SINPUT"; break; case INPUT_MODE_PS4: statusBar += "PS4"; @@ -491,6 +492,7 @@ void ButtonLayoutScreen::processInputHistory() { getProcessedGamepad()->pressedR3(), getProcessedGamepad()->pressedA1(), getProcessedGamepad()->pressedA2(), + getProcessedGamepad()->pressedA3(), }; uint8_t mode = ((displayModeLookup.count(inputMode) > 0) ? displayModeLookup.at(inputMode) : 0); diff --git a/src/drivermanager.cpp b/src/drivermanager.cpp index 1c3545171e..18df81b0e9 100644 --- a/src/drivermanager.cpp +++ b/src/drivermanager.cpp @@ -13,6 +13,7 @@ #include "drivers/ps4/PS4Driver.h" #include "drivers/switch/SwitchDriver.h" #include "drivers/switchpro/SwitchProDriver.h" +#include "drivers/switch2pro/Switch2ProDriver.h" #include "drivers/xbone/XBOneDriver.h" #include "drivers/xboxog/XboxOriginalDriver.h" #include "drivers/xinput/XInputDriver.h" @@ -77,6 +78,9 @@ void DriverManager::setup(InputMode mode) { case INPUT_MODE_SWITCH_PRO: driver = new SwitchProDriver(); break; + case INPUT_MODE_SWITCH2_PRO: + driver = new Switch2ProDriver(); + break; case INPUT_MODE_SINPUT: driver = new SInputDriver(); break; diff --git a/src/drivers/switch2pro/Switch2ProDriver.cpp b/src/drivers/switch2pro/Switch2ProDriver.cpp new file mode 100644 index 0000000000..094b1c8e65 --- /dev/null +++ b/src/drivers/switch2pro/Switch2ProDriver.cpp @@ -0,0 +1,715 @@ +/* + * SPDX-License-Identifier: MIT + * Nintendo Switch 2 Pro Controller (057E:2069) wired USB emulation. + */ + +#include "drivers/switch2pro/Switch2ProDriver.h" +#include "storagemanager.h" +#include "addons/amiibo.h" +#include "mbedtls/aes.h" +#include "pico/rand.h" + +#include + +#define CMD_DIRECTION_REQUEST 0x91 +#define CMD_DIRECTION_REPLY 0x01 +#define CMD_TRANSPORT_USB 0x00 +#define CMD_ACK 0xF8 +#define CMD_HEADER_LEN 8 +#define CMD_BUFFER_LEN 64 +#define CMD_REPLY_MAX 640 + +#define VENDOR_REQ_DEVICE_INFO 0x02 +#define VENDOR_REQ_FACTORY_DATA 0x03 +#define VENDOR_REQ_SET_MAX_LEN 0x04 + + +typedef enum { + CMD_NFC = 0x01, + CMD_FLASH_MEMORY = 0x02, + CMD_INIT = 0x03, + CMD_UNKNOWN07 = 0x07, + CMD_CHARGE_GRIP = 0x08, + CMD_PLAYER_LED = 0x09, + CMD_VIBRATION = 0x0A, + CMD_BATTERY = 0x0B, + CMD_FEATURE = 0x0C, + CMD_FW_INFO = 0x10, + CMD_UNKNOWN11 = 0x11, + CMD_PAIRING = 0x15, + CMD_UNKNOWN16 = 0x16, + CMD_UNKNOWN18 = 0x18, +} Switch2ProCommand; + +static uint8_t cmdRequest[CMD_BUFFER_LEN]; +static uint8_t cmdReply[CMD_REPLY_MAX]; +static uint8_t nfcCounter; +static uint8_t nfcState; +static bool nfcRead; +static uint16_t cmdReplyLen; +static bool cmdReplyPending; +static bool reportsEnabled; +static uint8_t playerLedMask; +static uint8_t btLongTermKey[16]; +static uint8_t hostAddress[6]; + + +static uint8_t deviceDescriptor[sizeof(switch2pro_device_descriptor)]; +static uint8_t factoryData[sizeof(switch2pro_factory_data)]; +static uint8_t deviceInfo[sizeof(switch2pro_device_info)]; +static uint8_t btAddressReversed[sizeof(switch2pro_bt_address_reversed)]; +static uint8_t deviceKey[sizeof(switch2pro_device_key)]; +static uint8_t chargingGripData[sizeof(switch2pro_charging_grip_data)]; +static const char *productString; +static uint8_t firmwareType; + +static uint8_t otherSpeedConfiguration[sizeof(switch2pro_configuration_descriptor)]; +static uint16_t stringDescriptor[64]; + +static void resetProtocolState() { + cmdReplyLen = 0; + cmdReplyPending = false; + reportsEnabled = false; + playerLedMask = 0; + nfcCounter = 0; + nfcState = 0x09; + nfcRead = false; +} + +static void setReply(uint8_t command, uint8_t subcommand, const uint8_t *payload, uint16_t payloadLen) { + if (payloadLen > CMD_REPLY_MAX - CMD_HEADER_LEN) + payloadLen = CMD_REPLY_MAX - CMD_HEADER_LEN; + memset(cmdReply, 0, CMD_HEADER_LEN); + cmdReply[0] = command; + cmdReply[1] = CMD_DIRECTION_REPLY; + cmdReply[2] = CMD_TRANSPORT_USB; + cmdReply[3] = subcommand; + cmdReply[5] = CMD_ACK; + if (payload != nullptr && payloadLen > 0) + memcpy(&cmdReply[CMD_HEADER_LEN], payload, payloadLen); + cmdReplyLen = CMD_HEADER_LEN + payloadLen; +} + +static void overlayFlash(uint32_t address, uint8_t *out, uint16_t length, uint32_t blockAddress, const uint8_t *block, uint16_t blockLen) { + for (uint16_t i = 0; i < length; i++) { + uint32_t a = address + i; + if (a >= blockAddress && a < blockAddress + blockLen) + out[i] = block[a - blockAddress]; + } +} + +static void replyFlashRead(const uint8_t *req) { + uint8_t length = req[8]; + if (length > 0x40) + length = 0x40; + uint32_t address = req[12] | (req[13] << 8) | (req[14] << 16) | ((uint32_t)req[15] << 24); + + uint8_t payload[8 + 0x40]; + memset(payload, 0x00, 8); + payload[0] = length; + memcpy(&payload[4], &req[12], 4); + uint8_t *data = &payload[8]; + memset(data, 0xFF, length); + overlayFlash(address, data, length, 0x13000, factoryData, sizeof(factoryData)); + overlayFlash(address, data, length, 0x13040, switch2pro_flash_13040, sizeof(switch2pro_flash_13040)); + overlayFlash(address, data, length, 0x13080, switch2pro_flash_13080, sizeof(switch2pro_flash_13080)); + overlayFlash(address, data, length, 0x130C0, switch2pro_flash_130c0, sizeof(switch2pro_flash_130c0)); + overlayFlash(address, data, length, 0x13100, switch2pro_flash_13100, sizeof(switch2pro_flash_13100)); + setReply(CMD_FLASH_MEMORY, req[3], payload, 8 + length); +} + +static void replyPairing(const uint8_t *req) { + switch (req[3]) { + case 0x01: { + uint8_t payload[9] = { 0x01, 0x04, 0x01 }; + memcpy(&payload[3], btAddressReversed, 6); + setReply(CMD_PAIRING, req[3], payload, sizeof(payload)); + break; + } + case 0x02: { + uint8_t challenge[16]; + for (int i = 0; i < 16; i++) + challenge[i] = req[24 - i]; + uint8_t payload[17] = { 0x01 }; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + mbedtls_aes_setkey_enc(&aes, btLongTermKey, 128); + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, challenge, &payload[1]); + mbedtls_aes_free(&aes); + setReply(CMD_PAIRING, req[3], payload, sizeof(payload)); + break; + } + case 0x03: { + uint8_t payload[1] = { 0x01 }; + setReply(CMD_PAIRING, req[3], payload, sizeof(payload)); + break; + } + case 0x04: { + for (int i = 0; i < 16; i++) + btLongTermKey[i] = req[24 - i] ^ deviceKey[i]; + uint8_t payload[17] = { 0x01 }; + for (int i = 0; i < 16; i++) + payload[1 + i] = deviceKey[15 - i]; + setReply(CMD_PAIRING, req[3], payload, sizeof(payload)); + break; + } + default: + setReply(CMD_PAIRING, req[3], nullptr, 0); + break; + } +} + +static void copyTagUid(uint8_t *out, const uint8_t *tag) { + memcpy(out, tag, 3); + memcpy(out + 3, tag + 4, 4); +} + +static void replyNfc(const uint8_t *req) { + const uint8_t sub = req[3]; + const uint8_t *tag = AmiiboAddon::tagPresent() ? AmiiboAddon::tagData() : nullptr; + switch (sub) { + case 0x0C: { + const uint8_t payload[] = { 0x61, 0x12, 0x50, 0x10 }; + setReply(CMD_NFC, sub, payload, sizeof(payload)); + break; + } + case 0x05: { + uint8_t payload[61] = { 0x09, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x00, 0x07 }; + if (tag != nullptr) { + payload[0] = nfcState; + copyTagUid(&payload[9], tag); + } + setReply(CMD_NFC, sub, payload, sizeof(payload)); + break; + } + case 0x15: { + if (tag == nullptr) { + setReply(CMD_NFC, sub, nullptr, 0); + break; + } + static const uint8_t header[] = { 0x01, 0x58, 0x02, 0x04, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x07 }; + static const uint8_t footer[] = { 0x03, 0x00, 0x3B, 0x3C, 0x77, 0x78, 0x86, 0x00, 0x00 }; + static uint8_t payload[sizeof(header) + 7 + 4 + AMIIBO_SIGNATURE_SIZE + sizeof(footer) + AMIIBO_DATA_SIZE + 19]; + memset(payload, 0, sizeof(payload)); + uint8_t *p = payload; + memcpy(p, header, sizeof(header)); p += sizeof(header); + copyTagUid(p, tag); p += 7; + p += 4; + const uint8_t *signature = AmiiboAddon::tagSignature(); + if (signature != nullptr) + memcpy(p, signature, AMIIBO_SIGNATURE_SIZE); + p += AMIIBO_SIGNATURE_SIZE; + memcpy(p, footer, sizeof(footer)); p += sizeof(footer); + memcpy(p, tag, AMIIBO_DATA_SIZE); + setReply(CMD_NFC, sub, payload, sizeof(payload)); + nfcRead = true; + break; + } + case 0x03: + if (tag != nullptr && req[5] == 0x05 && req[11] == 0x2C && req[12] == 0x01) + nfcCounter = (nfcCounter + 1) & 0x07; + if (nfcRead) { + nfcRead = false; + nfcState = 0x09; + } + setReply(CMD_NFC, sub, nullptr, 0); + break; + case 0x06: + if (tag != nullptr) + nfcCounter = (nfcCounter + 1) & 0x07; + nfcState = 0x04; + setReply(CMD_NFC, sub, nullptr, 0); + break; + default: + setReply(CMD_NFC, sub, nullptr, 0); + break; + } +} + +static void handleCommand(const uint8_t *req) { + const uint8_t command = req[0]; + const uint8_t sub = req[3]; + + switch (command) { + case CMD_NFC: + replyNfc(req); + break; + case CMD_FLASH_MEMORY: + if (sub == 0x01 || sub == 0x04) { + replyFlashRead(req); + } else if (sub == 0x02) { + setReply(command, sub, &req[8], 8); + } else if (sub == 0x03) { + const uint8_t payload[4] = { }; + setReply(command, sub, payload, sizeof(payload)); + } else if (sub == 0x05) { + uint8_t payload[8] = { }; + memcpy(&payload[4], &req[12], 4); + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_INIT: + if (sub == 0x07) { + for (int i = 0; i < 6; i++) + hostAddress[i] = req[13 - i]; + for (int i = 0; i < 16; i++) + btLongTermKey[i] = req[29 - i]; + setReply(command, sub, nullptr, 0); + } else if (sub == 0x0D) { + reportsEnabled = true; + for (int i = 0; i < 6; i++) + hostAddress[i] = req[15 - i]; + const uint8_t payload[] = { 0x01, 0x00, 0x00, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else if (sub == 0x0F) { + const uint8_t payload[] = { 0x05, 0x00, 0x00, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_UNKNOWN07: + if (sub == 0x01) { + const uint8_t payload[] = { 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_CHARGE_GRIP: + if (sub == 0x01) + setReply(command, sub, chargingGripData, 36); + else if (sub == 0x03) + setReply(command, sub, chargingGripData, 68); + else + setReply(command, sub, nullptr, 0); + break; + case CMD_PLAYER_LED: + if (sub == 0x07) + playerLedMask = req[8]; + setReply(command, sub, nullptr, 0); + break; + case CMD_VIBRATION: + if (sub == 0x02) { + const uint8_t payload[] = { req[8], 0x00, 0x00, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_BATTERY: + if (sub == 0x03) { + const uint8_t payload[] = { 0xA5, 0x0E, 0x00, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else if (sub == 0x04) { + const uint8_t payload[] = { 0x34, 0x00, 0x83, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else if (sub == 0x06) { + const uint8_t payload[] = { 0x11, 0x00, 0x00, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_FEATURE: + if (sub == 0x01) { + uint8_t payload[12] = { }; + if (req[8] & 0x01) payload[4] = 0x07; + if (req[8] & 0x02) payload[5] = 0x07; + if (req[8] & 0x04) payload[6] = 0x01; + if (req[8] & 0x80) payload[7] = 0x01; + if (req[8] & 0x10) payload[8] = 0x01; + if (req[8] & 0x20) payload[9] = 0x03; + setReply(command, sub, payload, sizeof(payload)); + } else { + if (sub == 0x02 && (req[8] & 0x01) && (req[8] & 0x02)) + reportsEnabled = true; + else if (sub == 0x03) + reportsEnabled = false; + const uint8_t payload[4] = { }; + if (sub >= 0x02 && sub <= 0x05) + setReply(command, sub, payload, sizeof(payload)); + else + setReply(command, sub, nullptr, 0); + } + break; + case CMD_FW_INFO: + if (sub == 0x01) { + const uint8_t payload[] = { 0x02, 0x01, 0x04, firmwareType, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x03, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_UNKNOWN11: + if (sub == 0x01) { + const uint8_t payload[] = { 0x01, 0x00, 0x00, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else if (sub == 0x03) { + const uint8_t payload[] = { + 0x01, 0x20, 0x03, 0x00, 0x00, 0x0a, 0xe8, 0x1c, 0x3b, 0x79, 0x7d, 0x8b, 0x3a, 0x0a, 0xe8, + 0x9c, 0x42, 0x58, 0xa0, 0x0b, 0x42, 0x0a, 0xe8, 0x9c, 0x41, 0x58, 0xa0, 0x0b, 0x41 }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_PAIRING: + replyPairing(req); + break; + case CMD_UNKNOWN16: + if (sub == 0x01) { + uint8_t payload[0x18] = { }; + payload[12] = 0x7C; + payload[13] = 0x06; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + case CMD_UNKNOWN18: + if (sub == 0x01) { + const uint8_t payload[] = { 0x00, 0x00, 0x40, 0xF0, 0x00, 0x00, 0x60, 0x00 }; + setReply(command, sub, payload, sizeof(payload)); + } else if (sub == 0x03) { + const uint8_t payload[] = { req[8] }; + setReply(command, sub, payload, sizeof(payload)); + } else { + setReply(command, sub, nullptr, 0); + } + break; + default: + setReply(command, sub, nullptr, 0); + break; + } +} + +static void sendReply(uint8_t rhport) { + if (!cmdReplyPending || usbd_edpt_busy(rhport, SWITCH2_PRO_EP_BULK_IN)) + return; + cmdReplyPending = false; + usbd_edpt_xfer(rhport, SWITCH2_PRO_EP_BULK_IN, cmdReply, cmdReplyLen, false); +} + +static void switch2pro_init(void) { + hidd_init(); + resetProtocolState(); +} + +static bool switch2pro_deinit(void) { + return hidd_deinit(); +} + +static void switch2pro_reset(uint8_t rhport) { + hidd_reset(rhport); + resetProtocolState(); +} + +static uint16_t switch2pro_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len) { + const uint8_t itf = itf_desc->bInterfaceNumber; + if (itf == SWITCH2_PRO_ITF_HID && itf_desc->bInterfaceClass == TUSB_CLASS_HID) + return hidd_open(rhport, itf_desc, max_len); + + if (itf != SWITCH2_PRO_ITF_VENDOR || itf_desc->bInterfaceClass != TUSB_CLASS_VENDOR_SPECIFIC) + return 0; + + uint8_t const *p = (uint8_t const *)itf_desc; + uint8_t const *end = p + max_len; + uint16_t len = 0; + while (p < end) { + const uint8_t type = tu_desc_type(p); + if (len > 0 && type == TUSB_DESC_INTERFACE_ASSOCIATION) + break; + if (type == TUSB_DESC_ENDPOINT) { + tusb_desc_endpoint_t const *ep = (tusb_desc_endpoint_t const *)p; + TU_ASSERT(usbd_edpt_open(rhport, ep), 0); + if (ep->bEndpointAddress == SWITCH2_PRO_EP_BULK_OUT) + usbd_edpt_xfer(rhport, SWITCH2_PRO_EP_BULK_OUT, cmdRequest, sizeof(cmdRequest), false); + } + len += tu_desc_len(p); + p = tu_desc_next(p); + } + return len; +} + +static bool switch2pro_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + if (request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_INTERFACE) + return false; + const uint8_t itf = tu_u16_low(request->wIndex); + if (itf == SWITCH2_PRO_ITF_HID) + return hidd_control_xfer_cb(rhport, stage, request); + return false; +} + +static bool switch2pro_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + switch (ep_addr) { + case SWITCH2_PRO_EP_BULK_OUT: + if (result == XFER_RESULT_SUCCESS && xferred_bytes >= 4 && + cmdRequest[1] == CMD_DIRECTION_REQUEST && cmdRequest[2] == CMD_TRANSPORT_USB) { + if (xferred_bytes < sizeof(cmdRequest)) + memset(&cmdRequest[xferred_bytes], 0, sizeof(cmdRequest) - xferred_bytes); + handleCommand(cmdRequest); + cmdReplyPending = true; + sendReply(rhport); + } + usbd_edpt_xfer(rhport, SWITCH2_PRO_EP_BULK_OUT, cmdRequest, sizeof(cmdRequest), false); + return true; + case SWITCH2_PRO_EP_BULK_IN: + sendReply(rhport); + return true; + default: + return hidd_xfer_cb(rhport, ep_addr, result, xferred_bytes); + } +} + +static void setFactoryColor(uint8_t offset, uint32_t rgb) { + factoryData[offset] = (rgb >> 16) & 0xFF; + factoryData[offset + 1] = (rgb >> 8) & 0xFF; + factoryData[offset + 2] = rgb & 0xFF; +} + +static void setProductId(uint8_t *data, uint16_t pid) { + data[0] = pid & 0xFF; + data[1] = pid >> 8; +} + +static void loadIdentity() { + memcpy(deviceDescriptor, switch2pro_device_descriptor, sizeof(deviceDescriptor)); + memcpy(factoryData, switch2pro_factory_data, sizeof(factoryData)); + memcpy(chargingGripData, switch2pro_charging_grip_data, sizeof(chargingGripData)); + + memcpy(btAddressReversed, switch2pro_bt_address_reversed, sizeof(btAddressReversed)); + for (int i = 0; i < SWITCH2_PRO_BT_RANDOM_BYTES; i++) + btAddressReversed[i] = get_rand_32() & 0xFF; + memcpy(deviceInfo, switch2pro_device_info, sizeof(deviceInfo)); + memcpy(&deviceInfo[SWITCH2_PRO_DEVICE_INFO_BT_OFFSET], btAddressReversed, sizeof(btAddressReversed)); + for (size_t i = 0; i < sizeof(deviceKey); i++) + deviceKey[i] = get_rand_32() & 0xFF; + for (int i = 0; i < SWITCH2_PRO_FACTORY_SERIAL_COUNT; i++) + factoryData[SWITCH2_PRO_FACTORY_SERIAL_DIGITS + i] = '0' + (get_rand_32() % 10); + productString = switch2pro_strings[SWITCH2_PRO_STRING_PRODUCT]; + firmwareType = SWITCH2_FW_TYPE_PRO; + + const GamepadOptions & options = Storage::getInstance().getGamepadOptions(); + if (options.switch2ProIdentity == SWITCH2_PRO_IDENTITY_GAMECUBE) { + setProductId(&deviceDescriptor[SWITCH2_PRO_DEVICE_PID_OFFSET], SWITCH2_GAMECUBE_PRODUCT_ID); + setProductId(&factoryData[SWITCH2_PRO_FACTORY_PID_OFFSET], SWITCH2_GAMECUBE_PRODUCT_ID); + setProductId(&chargingGripData[SWITCH2_PRO_GRIP_PID_OFFSET], SWITCH2_GAMECUBE_PRODUCT_ID); + productString = switch2gamecube_product_string; + firmwareType = SWITCH2_FW_TYPE_GAMECUBE; + return; + } + + uint32_t body, buttons, highlight, grip; + switch (options.switch2ProColorPreset) { + case SWITCH2_PRO_COLOR_GP2040: + body = 0x1B1B1D; + buttons = 0xFFFFFF; + highlight = 0x00FF00; + grip = 0xEC008C; + break; + case SWITCH2_PRO_COLOR_CUSTOM: + body = options.switch2ProBodyColor; + buttons = options.switch2ProButtonsColor; + highlight = options.switch2ProHighlightColor; + grip = options.switch2ProGripColor; + break; + default: + return; + } + setFactoryColor(SWITCH2_PRO_FACTORY_BODY_COLOR, body); + setFactoryColor(SWITCH2_PRO_FACTORY_BUTTONS_COLOR, buttons); + setFactoryColor(SWITCH2_PRO_FACTORY_HIGHLIGHT_COLOR, highlight); + setFactoryColor(SWITCH2_PRO_FACTORY_GRIP_COLOR, grip); +} + +void Switch2ProDriver::initialize() { + memset(inputReport, 0, sizeof(inputReport)); + reportCounter = 0; + loadIdentity(); + + class_driver = { + #if CFG_TUSB_DEBUG >= 2 + .name = "SWITCH2PRO", + #endif + .init = switch2pro_init, + .deinit = switch2pro_deinit, + .reset = switch2pro_reset, + .open = switch2pro_open, + .control_xfer_cb = switch2pro_control_xfer_cb, + .xfer_cb = switch2pro_xfer_cb, + .sof = NULL + }; +} + +static void packStick(uint8_t *out, uint16_t x16, uint16_t y16) { + const uint16_t x = x16 >> 4; + const uint16_t y = SWITCH2_PRO_STICK_MAX - (y16 >> 4); + out[0] = x & 0xFF; + out[1] = ((x >> 8) & 0x0F) | ((y & 0x0F) << 4); + out[2] = (y >> 4) & 0xFF; +} + +bool Switch2ProDriver::process(Gamepad * gamepad) { + static bool tagWasPresent = false; + static uint32_t lastTapGeneration = 0; + const bool tagIsPresent = AmiiboAddon::tagPresent(); + const uint32_t tapGeneration = AmiiboAddon::tapGeneration(); + if (tagIsPresent && (!tagWasPresent || tapGeneration != lastTapGeneration)) { + nfcCounter = (nfcCounter + 1) & 0x07; + nfcState = 0x09; + } + tagWasPresent = tagIsPresent; + lastTapGeneration = tapGeneration; + Gamepad * processedGamepad = Storage::getInstance().GetProcessedGamepad(); + processedGamepad->auxState.playerID.active = reportsEnabled; + processedGamepad->auxState.playerID.ledValue = playerLedMask; + processedGamepad->auxState.playerID.value = playerLedMask; + + if (tud_suspended()) + tud_remote_wakeup(); + + if (!reportsEnabled) + return false; + + uint8_t b0 = 0, b1 = 0, b2 = 0; + if (gamepad->pressedB1()) b0 |= SWITCH2_PRO_MASK_B; + if (gamepad->pressedB2()) b0 |= SWITCH2_PRO_MASK_A; + if (gamepad->pressedB3()) b0 |= SWITCH2_PRO_MASK_Y; + if (gamepad->pressedB4()) b0 |= SWITCH2_PRO_MASK_X; + if (gamepad->pressedR1()) b0 |= SWITCH2_PRO_MASK_R; + if (gamepad->pressedR2()) b0 |= SWITCH2_PRO_MASK_ZR; + if (gamepad->pressedS2()) b0 |= SWITCH2_PRO_MASK_PLUS; + if (gamepad->pressedR3()) b0 |= SWITCH2_PRO_MASK_RSTICK; + + if (gamepad->pressedDown()) b1 |= SWITCH2_PRO_MASK_DOWN; + if (gamepad->pressedRight()) b1 |= SWITCH2_PRO_MASK_RIGHT; + if (gamepad->pressedLeft()) b1 |= SWITCH2_PRO_MASK_LEFT; + if (gamepad->pressedUp()) b1 |= SWITCH2_PRO_MASK_UP; + if (gamepad->pressedL1()) b1 |= SWITCH2_PRO_MASK_L; + if (gamepad->pressedL2()) b1 |= SWITCH2_PRO_MASK_ZL; + if (gamepad->pressedS1()) b1 |= SWITCH2_PRO_MASK_MINUS; + if (gamepad->pressedL3()) b1 |= SWITCH2_PRO_MASK_LSTICK; + + if (gamepad->pressedA1()) b2 |= SWITCH2_PRO_MASK_HOME; + if (gamepad->pressedA2()) b2 |= SWITCH2_PRO_MASK_CAPTURE; + if (gamepad->pressedA3()) b2 |= SWITCH2_PRO_MASK_C; + if (gamepad->pressedE1()) b2 |= SWITCH2_PRO_MASK_GL; + if (gamepad->pressedE2()) b2 |= SWITCH2_PRO_MASK_GR; + inputReport[1] = 0x23; + inputReport[2] = b0; + inputReport[3] = b1; + inputReport[4] = b2; + packStick(&inputReport[5], gamepad->state.lx, gamepad->state.ly); + packStick(&inputReport[8], gamepad->state.rx, gamepad->state.ry); + inputReport[11] = 0x38; + inputReport[12] = nfcCounter; + const uint32_t now = to_ms_since_boot(get_absolute_time()); + const bool changed = memcmp(&inputReport[2], &lastSentState[0], sizeof(lastSentState)) != 0; + if (!changed && (now - lastSentMs) < SWITCH2_PRO_KEEPALIVE_MS) + return false; + if (!tud_hid_n_ready(0)) + return false; + + inputReport[0] = reportCounter; + if (!tud_hid_n_report(0, SWITCH2_PRO_INPUT_REPORT_ID, inputReport, sizeof(inputReport))) + return false; + memcpy(lastSentState, &inputReport[2], sizeof(lastSentState)); + lastSentMs = now; + reportCounter++; + return true; +} + +uint16_t Switch2ProDriver::get_report(uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) { + return 0; +} + +void Switch2ProDriver::set_report(uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) { +} + +bool Switch2ProDriver::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + if (stage != CONTROL_STAGE_SETUP) + return true; + + if (request->bmRequestType_bit.direction == TUSB_DIR_IN) { + static uint8_t factory[64]; + switch (request->bRequest) { + case VENDOR_REQ_DEVICE_INFO: + return tud_control_xfer(rhport, request, deviceInfo, sizeof(deviceInfo)); + case VENDOR_REQ_FACTORY_DATA: + memset(factory, 0xFF, sizeof(factory)); + memcpy(factory, factoryData, sizeof(factoryData)); + return tud_control_xfer(rhport, request, factory, sizeof(factory)); + default: + return false; + } + } + + if (request->bRequest == VENDOR_REQ_SET_MAX_LEN) { + static uint8_t sink[64]; + if (request->wLength == 0) + return tud_control_status(rhport, request); + return tud_control_xfer(rhport, request, sink, request->wLength < sizeof(sink) ? request->wLength : sizeof(sink)); + } + return false; +} + +const uint16_t * Switch2ProDriver::get_descriptor_string_cb(uint8_t index, uint16_t langid) { + uint8_t count = 0; + if (index == 0) { + for (uint16_t lang : switch2pro_language_ids) + stringDescriptor[1 + count++] = lang; + } else if (index == 0xEE) { + const char *signature = "MSFT100"; + for (; signature[count] != '\0'; count++) + stringDescriptor[1 + count] = signature[count]; + stringDescriptor[1 + count++] = 0x01; + } else if (index < TU_ARRAY_SIZE(switch2pro_strings)) { + const char *value = (index == SWITCH2_PRO_STRING_PRODUCT) ? productString : switch2pro_strings[index]; + for (; value[count] != '\0' && count < TU_ARRAY_SIZE(stringDescriptor) - 1; count++) + stringDescriptor[1 + count] = value[count]; + } else { + return nullptr; + } + stringDescriptor[0] = (TUSB_DESC_STRING << 8) | (2 * count + 2); + return stringDescriptor; +} + +const uint8_t * Switch2ProDriver::get_descriptor_device_cb() { + return deviceDescriptor; +} + +const uint8_t * Switch2ProDriver::get_hid_descriptor_report_cb(uint8_t itf) { + return switch2pro_report_descriptor; +} + +const uint8_t * Switch2ProDriver::get_descriptor_configuration_cb(uint8_t index) { + return switch2pro_configuration_descriptor; +} + +const uint8_t * Switch2ProDriver::get_descriptor_device_qualifier_cb() { + return switch2pro_device_qualifier; +} + +const uint8_t * Switch2ProDriver::get_descriptor_other_speed_configuration_cb(uint8_t index) { + memcpy(otherSpeedConfiguration, switch2pro_configuration_descriptor, sizeof(otherSpeedConfiguration)); + otherSpeedConfiguration[1] = TUSB_DESC_OTHER_SPEED_CONFIG; + for (size_t offset = 0; offset < sizeof(otherSpeedConfiguration);) { + const uint8_t length = otherSpeedConfiguration[offset]; + if (length == 0) + break; + if (otherSpeedConfiguration[offset + 1] == TUSB_DESC_ENDPOINT) { + const uint8_t xfer = otherSpeedConfiguration[offset + 3] & 0x03; + if (xfer == TUSB_XFER_INTERRUPT) + otherSpeedConfiguration[offset + 6] = 6; + } + offset += length; + } + return otherSpeedConfiguration; +} + +uint16_t Switch2ProDriver::GetJoystickMidValue() { + return GAMEPAD_JOYSTICK_MID; +} diff --git a/src/gamepad.cpp b/src/gamepad.cpp index 5c71718a6e..db29444338 100644 --- a/src/gamepad.cpp +++ b/src/gamepad.cpp @@ -15,6 +15,7 @@ #include "drivermanager.h" #include "storagemanager.h" #include "system.h" +#include "addons/amiibo.h" // MUST BE DEFINED for mpgs uint32_t getMillis() { @@ -589,6 +590,14 @@ void Gamepad::processHotkeyAction(GamepadHotkey action) { System::reboot(System::BootMode::USB); } break; + case HOTKEY_AMIIBO_1: + case HOTKEY_AMIIBO_2: + case HOTKEY_AMIIBO_3: + case HOTKEY_AMIIBO_4: + if (action != lastAction) { + AmiiboAddon::tap(action - HOTKEY_AMIIBO_1); + } + break; case HOTKEY_SAVE_CONFIG: if (action != lastAction) { Storage::getInstance().save(true); diff --git a/src/gp2040.cpp b/src/gp2040.cpp index 58ca88d899..03c038a2a7 100644 --- a/src/gp2040.cpp +++ b/src/gp2040.cpp @@ -23,6 +23,7 @@ #include "addons/i2canalog1115.h" #include "addons/i2canalog1219.h" #include "addons/reverse.h" +#include "addons/amiibo.h" #include "addons/turbo.h" #include "addons/slider_socd.h" #include "addons/spi_analog_ads1256.h" @@ -123,6 +124,7 @@ void GP2040::setup() { // Input override addons addons.LoadAddon(new ReverseInput()); + addons.LoadAddon(new AmiiboAddon()); addons.LoadAddon(new TurboInput()); // Turbo overrides button states and should be close to the end addons.LoadAddon(new InputMacro()); diff --git a/src/hosts/DualshockPS4Host.cpp b/src/hosts/DualshockPS4Host.cpp index 9d881081f5..9902a20a8a 100644 --- a/src/hosts/DualshockPS4Host.cpp +++ b/src/hosts/DualshockPS4Host.cpp @@ -113,7 +113,7 @@ void DualshockPS4Host::update() { } else if ( inputMode == INPUT_MODE_XINPUT || inputMode == INPUT_MODE_XBONE || inputMode == INPUT_MODE_XBOXORIGINAL ) { controller_output.enableUpdateLED = true; controller_output.ledGreen = UINT8_MAX/2; - } else if ( inputMode == INPUT_MODE_SWITCH || inputMode == INPUT_MODE_SWITCH_PRO ) { + } else if ( inputMode == INPUT_MODE_SWITCH || inputMode == INPUT_MODE_SWITCH_PRO || inputMode == INPUT_MODE_SWITCH2_PRO ) { controller_output.enableUpdateLED = true; controller_output.ledRed = UINT8_MAX/2; } diff --git a/src/usbdriver.cpp b/src/usbdriver.cpp index 3710f1d3bf..2c21ca6f19 100644 --- a/src/usbdriver.cpp +++ b/src/usbdriver.cpp @@ -99,4 +99,9 @@ uint8_t const* tud_descriptor_device_qualifier_cb() { return DriverManager::getInstance().getDriver()->get_descriptor_device_qualifier_cb(); } +// Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request +uint8_t const* tud_descriptor_other_speed_configuration_cb(uint8_t index) { + return DriverManager::getInstance().getDriver()->get_descriptor_other_speed_configuration_cb(index); +} + #endif diff --git a/src/webconfig.cpp b/src/webconfig.cpp index d932ccb7d3..8b83daf485 100644 --- a/src/webconfig.cpp +++ b/src/webconfig.cpp @@ -1,5 +1,6 @@ #include "config.pb.h" #include "base64.h" +#include "addons/amiibo.h" #include "hardware/adc.h" #include "addons/analog.h" #include "helper.h" @@ -527,6 +528,103 @@ std::string getDisplayOptions() // Manually set Document Attributes for the disp return serialize_json(doc); } +std::string getAmiiboSlots() +{ + DynamicJsonDocument doc(LWIP_HTTPD_POST_MAX_PAYLOAD_LEN); + JsonArray slots = doc.createNestedArray("slots"); + for (uint8_t i = 0; i < AMIIBO_SLOT_COUNT; i++) { + JsonObject slot = slots.createNestedObject(); + slot["filled"] = AmiiboAddon::slotFilled(i); + slot["name"] = AmiiboAddon::slotName(i); + slot["size"] = AmiiboAddon::slotSize(i); + slot["randomizeSerial"] = AmiiboAddon::slotRandomizeSerial(i); + } + return serialize_json(doc); +} + +static bool decodeAmiiboUpload(const std::string & encoded, std::string & decoded) +{ + return encoded.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=") == std::string::npos + && Base64::Decode(encoded, decoded) && Base64::Encode(decoded) == encoded; +} + +std::string setAmiiboSlot() +{ + DynamicJsonDocument doc = get_post_data(); + bool success = false; + if (doc["slot"].is() && doc["data"].is()) { + const int slot = doc["slot"]; + const std::string encoded = doc["data"]; + const std::string name = doc["name"] | ""; + std::string decoded; + if (slot >= 0 && slot < AMIIBO_SLOT_COUNT && (encoded.size() == 720 || encoded.size() == 764) + && decodeAmiiboUpload(encoded, decoded) + && (decoded.size() == AMIIBO_DATA_SIZE || decoded.size() == AMIIBO_FILE_SIZE)) + success = AmiiboAddon::writeSlot(slot, reinterpret_cast(decoded.data()), decoded.size(), name.c_str()); + } + DynamicJsonDocument result(128); + result["success"] = success; + return serialize_json(result); +} + +std::string setAmiiboSlotOptions() +{ + DynamicJsonDocument doc = get_post_data(); + bool success = false; + if (doc["slot"].is() && doc["randomizeSerial"].is()) { + const int slot = doc["slot"]; + if (slot >= 0 && slot < AMIIBO_SLOT_COUNT) + success = AmiiboAddon::setSlotRandomizeSerial(slot, doc["randomizeSerial"].as()); + } + DynamicJsonDocument result(128); + result["success"] = success; + return serialize_json(result); +} + +std::string clearAmiiboSlot() +{ + DynamicJsonDocument doc = get_post_data(); + const int slot = doc["slot"].is() ? doc["slot"].as() : -1; + DynamicJsonDocument result(128); + result["success"] = slot >= 0 && slot < AMIIBO_SLOT_COUNT && AmiiboAddon::clearSlot(slot); + return serialize_json(result); +} + +std::string getAmiiboKeys() +{ + DynamicJsonDocument doc(256); + const bool unfixed = AmiiboAddon::keyPresent(false); + const bool locked = AmiiboAddon::keyPresent(true); + doc["unfixed"] = unfixed; + doc["locked"] = locked; + doc["ready"] = unfixed && locked; + return serialize_json(doc); +} + +std::string setAmiiboKey() +{ + DynamicJsonDocument doc = get_post_data(); + bool success = false; + if (doc["type"].is() && doc["data"].is()) { + const std::string type = doc["type"]; + const std::string encoded = doc["data"]; + std::string decoded; + if ((type == "unfixed" || type == "locked") && encoded.size() == 108 + && decodeAmiiboUpload(encoded, decoded) && decoded.size() == AMIIBO_KEY_SIZE) + success = AmiiboAddon::writeKey(type == "locked", reinterpret_cast(decoded.data()), decoded.size()); + } + DynamicJsonDocument result(128); + result["success"] = success; + return serialize_json(result); +} + +std::string clearAmiiboKeys() +{ + DynamicJsonDocument result(128); + result["success"] = AmiiboAddon::clearKeys(); + return serialize_json(result); +} + std::string getSplashImage() { const DisplayOptions& displayOptions = Storage::getInstance().getDisplayOptions(); @@ -707,6 +805,12 @@ std::string setGamepadOptions() readDoc(gamepadOptions.ps5AuthType, doc, "ps5AuthType"); readDoc(gamepadOptions.xinputAuthType, doc, "xinputAuthType"); readDoc(gamepadOptions.ps4ControllerIDMode, doc, "ps4ControllerIDMode"); + readDoc(gamepadOptions.switch2ProIdentity, doc, "switch2ProIdentity"); + readDoc(gamepadOptions.switch2ProColorPreset, doc, "switch2ProColorPreset"); + readDoc(gamepadOptions.switch2ProBodyColor, doc, "switch2ProBodyColor"); + readDoc(gamepadOptions.switch2ProButtonsColor, doc, "switch2ProButtonsColor"); + readDoc(gamepadOptions.switch2ProHighlightColor, doc, "switch2ProHighlightColor"); + readDoc(gamepadOptions.switch2ProGripColor, doc, "switch2ProGripColor"); readDoc(gamepadOptions.usbDescOverride, doc, "usbDescOverride"); readDoc(gamepadOptions.miniMenuGamepadInput, doc, "miniMenuGamepadInput"); // Copy USB descriptor strings @@ -777,6 +881,12 @@ std::string getGamepadOptions() writeDoc(doc, "ps5AuthType", gamepadOptions.ps5AuthType); writeDoc(doc, "xinputAuthType", gamepadOptions.xinputAuthType); writeDoc(doc, "ps4ControllerIDMode", gamepadOptions.ps4ControllerIDMode); + writeDoc(doc, "switch2ProIdentity", gamepadOptions.switch2ProIdentity); + writeDoc(doc, "switch2ProColorPreset", gamepadOptions.switch2ProColorPreset); + writeDoc(doc, "switch2ProBodyColor", gamepadOptions.switch2ProBodyColor); + writeDoc(doc, "switch2ProButtonsColor", gamepadOptions.switch2ProButtonsColor); + writeDoc(doc, "switch2ProHighlightColor", gamepadOptions.switch2ProHighlightColor); + writeDoc(doc, "switch2ProGripColor", gamepadOptions.switch2ProGripColor); writeDoc(doc, "usbDescOverride", gamepadOptions.usbDescOverride); writeDoc(doc, "usbDescManufacturer", gamepadOptions.usbDescManufacturer); writeDoc(doc, "usbDescProduct", gamepadOptions.usbDescProduct); @@ -2348,6 +2458,7 @@ std::string setAddonOptions() ReactiveLEDOptions& reactiveLEDOptions = Storage::getInstance().getAddonOptions().reactiveLEDOptions; docToValue(reactiveLEDOptions.enabled, doc, "ReactiveLEDAddonEnabled"); + docToValue(Storage::getInstance().getAddonOptions().amiiboOptions.enabled, doc, "AmiiboAddonEnabled"); DRV8833RumbleOptions& drv8833RumbleOptions = Storage::getInstance().getAddonOptions().drv8833RumbleOptions; docToValue(drv8833RumbleOptions.enabled, doc, "DRV8833RumbleAddonEnabled"); @@ -2855,6 +2966,7 @@ std::string getAddonOptions() ReactiveLEDOptions& reactiveLEDOptions = Storage::getInstance().getAddonOptions().reactiveLEDOptions; writeDoc(doc, "ReactiveLEDAddonEnabled", reactiveLEDOptions.enabled); + writeDoc(doc, "AmiiboAddonEnabled", Storage::getInstance().getAddonOptions().amiiboOptions.enabled); const DRV8833RumbleOptions& drv8833RumbleOptions = Storage::getInstance().getAddonOptions().drv8833RumbleOptions; writeDoc(doc, "DRV8833RumbleAddonEnabled", drv8833RumbleOptions.enabled); @@ -3280,6 +3392,13 @@ static const std::pair handlerFuncs[] = { "/api/getReactiveLEDs", getReactiveLEDs }, { "/api/setKeyMappings", setKeyMappings }, { "/api/setAddonsOptions", setAddonOptions }, + { "/api/getAmiiboSlots", getAmiiboSlots }, + { "/api/setAmiiboSlotOptions", setAmiiboSlotOptions }, + { "/api/getAmiiboKeys", getAmiiboKeys }, + { "/api/setAmiiboKey", setAmiiboKey }, + { "/api/clearAmiiboKeys", clearAmiiboKeys }, + { "/api/setAmiiboSlot", setAmiiboSlot }, + { "/api/clearAmiiboSlot", clearAmiiboSlot }, { "/api/setMacroAddonOptions", setMacroAddonOptions }, { "/api/setPS4Options", setPS4Options }, { "/api/setWiiControls", setWiiControls }, diff --git a/www/server/app.js b/www/server/app.js index 7809cf5067..54fa8d90b6 100644 --- a/www/server/app.js +++ b/www/server/app.js @@ -430,6 +430,90 @@ app.get('/api/getAnimationProtoOptions', (req, res) => { }); }); +// Track only presence in the mock; never retain or return uploaded key material. +const amiiboKeys = { unfixed: false, locked: false }; +app.get('/api/getAmiiboKeys', (req, res) => { + return res.send({ + ...amiiboKeys, + ready: amiiboKeys.unfixed && amiiboKeys.locked, + }); +}); +app.post('/api/setAmiiboKey', (req, res) => { + const { type, data } = req.body; + if ( + !['unfixed', 'locked'].includes(type) || + typeof data !== 'string' || + !/^[A-Za-z0-9+/]{107}=$/.test(data) || + Buffer.from(data, 'base64').length !== 80 + ) { + return res.status(400).send({ success: false }); + } + amiiboKeys[type] = true; + return res.send({ success: true }); +}); +app.post('/api/clearAmiiboKeys', (req, res) => { + amiiboKeys.unfixed = false; + amiiboKeys.locked = false; + return res.send({ success: true }); +}); + +// Synthetic metadata only: the mock does not retain dumps or perform cryptography. +const emptyAmiiboSlot = () => ({ + filled: false, + name: '', + size: 0, + randomizeSerial: false, +}); +const amiiboSlots = [ + { filled: true, name: 'Example', size: 540, randomizeSerial: true }, + ...Array.from({ length: 3 }, emptyAmiiboSlot), +]; +const validAmiiboSlot = (slot) => + Number.isInteger(slot) && slot >= 0 && slot < amiiboSlots.length; +app.get('/api/getAmiiboSlots', (req, res) => res.send({ slots: amiiboSlots })); +app.post('/api/setAmiiboSlot', (req, res) => { + const { slot, name, data } = req.body; + if ( + !validAmiiboSlot(slot) || + typeof name !== 'string' || + typeof data !== 'string' || + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( + data, + ) + ) + return res.status(400).send({ success: false }); + const size = Buffer.from(data, 'base64').length; + if (![540, 572].includes(size)) + return res.status(400).send({ success: false }); + const randomizeSerial = amiiboSlots[slot].filled + ? amiiboSlots[slot].randomizeSerial + : false; + amiiboSlots[slot] = { + filled: true, + name: name.substring(0, 31), + size, + randomizeSerial, + }; + return res.send({ success: true }); +}); +app.post('/api/setAmiiboSlotOptions', (req, res) => { + const { slot, randomizeSerial } = req.body; + if ( + !validAmiiboSlot(slot) || + typeof randomizeSerial !== 'boolean' || + !amiiboSlots[slot].filled + ) + return res.status(400).send({ success: false }); + amiiboSlots[slot].randomizeSerial = randomizeSerial; + return res.send({ success: true }); +}); +app.post('/api/clearAmiiboSlot', (req, res) => { + const { slot } = req.body; + if (!validAmiiboSlot(slot)) return res.status(400).send({ success: false }); + amiiboSlots[slot] = emptyAmiiboSlot(); + return res.send({ success: true }); +}); + app.get('/api/getGamepadOptions', (req, res) => { return res.send({ dpadMode: 0, @@ -455,6 +539,12 @@ app.get('/api/getGamepadOptions', (req, res) => { ps5AuthType: 0, xinputAuthType: 0, ps4ControllerIDMode: 0, + switch2ProIdentity: 0, + switch2ProColorPreset: 0, + switch2ProBodyColor: 0x232323, + switch2ProButtonsColor: 0xa0a0a0, + switch2ProHighlightColor: 0xe6e6e6, + switch2ProGripColor: 0x323232, usbDescOverride: 0, usbDescProduct: 'GP2040-CE (Custom)', usbDescManufacturer: 'Open Stick Community', @@ -785,6 +875,7 @@ app.get('/api/getAddonsOptions', (req, res) => { keyboardHostMouseMovement: 0, AnalogInputEnabled: 1, BoardLedAddonEnabled: 1, + AmiiboAddonEnabled: 1, FocusModeAddonEnabled: 1, focusModeMacroLockEnabled: 0, BuzzerSpeakerAddonEnabled: 1, @@ -1417,6 +1508,6 @@ app.post('/api/*', (req, res) => { return res.send(req.body); }); -app.listen(port, () => { +app.listen(port, process.env.HOST, () => { console.log(`Dev app listening at http://localhost:${port}`); }); diff --git a/www/src/Addons/Amiibo.tsx b/www/src/Addons/Amiibo.tsx new file mode 100644 index 0000000000..c013b4eda8 --- /dev/null +++ b/www/src/Addons/Amiibo.tsx @@ -0,0 +1,315 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, FormCheck, FormControl } from 'react-bootstrap'; +import * as yup from 'yup'; + +import Section from '../Components/Section'; +import WebApi from '../Services/WebApi'; +import { AddonPropTypes } from '../Pages/AddonsConfigPage'; + +const AMIIBO_FILE_SIZES = [540, 572]; + +type AmiiboKeyType = 'unfixed' | 'locked'; +type AmiiboKeys = { unfixed: boolean; locked: boolean; ready: boolean }; + +type AmiiboSlot = { + filled: boolean; + name: string; + size: number; + randomizeSerial: boolean; +}; + +export const amiiboScheme = { + AmiiboAddonEnabled: yup.number().required().label('Amiibo Add-On Enabled'), +}; + +export const amiiboState = { + AmiiboAddonEnabled: 0, +}; + +const toBase64 = (bytes: Uint8Array) => { + let binary = ''; + bytes.forEach((b) => (binary += String.fromCharCode(b))); + return btoa(binary); +}; + +const Amiibo = ({ values, handleChange, handleCheckbox }: AddonPropTypes) => { + const { t } = useTranslation(); + const [keys, setKeys] = useState(); + const [keysLoading, setKeysLoading] = useState(true); + const [keysBusy, setKeysBusy] = useState(false); + const [keyMessage, setKeyMessage] = useState(''); + const [keyStatusFailed, setKeyStatusFailed] = useState(false); + const refreshKeys = useCallback(async () => { + setKeysLoading(true); + const data = await WebApi.getAmiiboKeys(); + const valid = + data && + typeof data.unfixed === 'boolean' && + typeof data.locked === 'boolean' && + typeof data.ready === 'boolean'; + setKeys(valid ? data : undefined); + setKeyStatusFailed(!valid); + setKeysLoading(false); + }, []); + const uploadKey = async (type: AmiiboKeyType, file?: File) => { + if (!file) return; + if (file.size !== 80) { + setKeyMessage( + t('AddonsConfig:amiibo-key-invalid-size', { size: file.size }), + ); + return; + } + setKeysBusy(true); + setKeyMessage(t('AddonsConfig:amiibo-keys-saving')); + try { + const data = toBase64(new Uint8Array(await file.arrayBuffer())); + const success = await WebApi.setAmiiboKey({ type, data }); + setKeyMessage( + t( + success + ? 'AddonsConfig:amiibo-key-saved' + : 'AddonsConfig:amiibo-key-failed', + ), + ); + await refreshKeys(); + } catch { + setKeyMessage(t('AddonsConfig:amiibo-key-failed')); + } finally { + setKeysBusy(false); + } + }; + const clearKeys = async () => { + setKeysBusy(true); + setKeyMessage(t('AddonsConfig:amiibo-keys-clearing')); + try { + const success = await WebApi.clearAmiiboKeys(); + setKeyMessage( + t( + success + ? 'AddonsConfig:amiibo-keys-cleared' + : 'AddonsConfig:amiibo-keys-clear-failed', + ), + ); + await refreshKeys(); + } finally { + setKeysBusy(false); + } + }; + const [slots, setSlots] = useState([]); + const [slotBusy, setSlotBusy] = useState(false); + const [messages, setMessages] = useState>({}); + + const refresh = useCallback(async () => { + const data = await WebApi.getAmiiboSlots(); + if (data) setSlots(data); + }, []); + + useEffect(() => { + refresh(); + refreshKeys(); + }, [refresh, refreshKeys]); + + const setMessage = (slot: number, message: string) => + setMessages((current) => ({ ...current, [slot]: message })); + + const saveSlotOptions = async (slot: number, randomizeSerial: boolean) => { + setSlotBusy(true); + setMessage(slot, t('AddonsConfig:amiibo-option-saving')); + try { + const success = await WebApi.setAmiiboSlotOptions({ + slot, + randomizeSerial, + }); + if (success) + setSlots((current) => + current.map((item, index) => + index === slot ? { ...item, randomizeSerial } : item, + ), + ); + setMessage( + slot, + t( + success + ? 'AddonsConfig:amiibo-option-saved' + : 'AddonsConfig:amiibo-option-failed', + ), + ); + } finally { + setSlotBusy(false); + } + }; + + const upload = async (slot: number, file?: File) => { + if (!file) return; + setSlotBusy(true); + setMessage(slot, t('AddonsConfig:amiibo-option-saving')); + try { + const bytes = new Uint8Array(await file.arrayBuffer()); + if (!AMIIBO_FILE_SIZES.includes(bytes.length)) { + setMessage( + slot, + t('AddonsConfig:amiibo-invalid-size', { size: bytes.length }), + ); + return; + } + const name = file.name.replace(/\.[^.]+$/, '').substring(0, 31); + const success = await WebApi.setAmiiboSlot({ + slot, + name, + data: toBase64(bytes), + }); + if (success) await refresh(); + setMessage( + slot, + t( + success + ? 'AddonsConfig:amiibo-saved' + : 'AddonsConfig:amiibo-save-failed', + ), + ); + } catch { + setMessage(slot, t('AddonsConfig:amiibo-save-failed')); + } finally { + setSlotBusy(false); + } + }; + + const clear = async (slot: number) => { + setSlotBusy(true); + setMessage(slot, t('AddonsConfig:amiibo-option-saving')); + try { + const success = await WebApi.clearAmiiboSlot(slot); + if (success) await refresh(); + setMessage(slot, success ? '' : t('AddonsConfig:amiibo-clear-failed')); + } finally { + setSlotBusy(false); + } + }; + + return ( +
+ + { + handleCheckbox('AmiiboAddonEnabled'); + handleChange(e); + }} + /> +
+ ); +}; + +export default Amiibo; diff --git a/www/src/Data/Buttons.js b/www/src/Data/Buttons.js index f3efd08afd..fb8c84c676 100644 --- a/www/src/Data/Buttons.js +++ b/www/src/Data/Buttons.js @@ -147,6 +147,43 @@ export const BUTTONS = { E12: '-', Fn: 'Function', }, + switch2: { + label: 'Switch 2 Pro', + value: 'switch2', + Up: 'Up', + Down: 'Down', + Left: 'Left', + Right: 'Right', + B1: 'B', + B2: 'A', + B3: 'Y', + B4: 'X', + L1: 'L', + R1: 'R', + L2: 'ZL', + R2: 'ZR', + S1: 'Minus', + S2: 'Plus', + L3: 'LS', + R3: 'RS', + A1: 'Home', + A2: 'Capture', + A3: 'C (GameChat)', + A4: '-', + E1: 'GL', + E2: 'GR', + E3: '-', + E4: '-', + E5: '-', + E6: '-', + E7: '-', + E8: '-', + E9: '-', + E10: '-', + E11: '-', + E12: '-', + Fn: 'Function', + }, ps3: { label: 'PS3', value: 'ps3', diff --git a/www/src/Data/InputBootModes.ts b/www/src/Data/InputBootModes.ts index 73e7275033..10fd3feb94 100644 --- a/www/src/Data/InputBootModes.ts +++ b/www/src/Data/InputBootModes.ts @@ -181,6 +181,15 @@ export const INPUT_MODE_OPTIONS: InputModeOptions[] = [ authentication: [], deviceTypes: [], }, + { + labelKey: 'input-mode-options.nintendo-switch2-pro', + value: InputMode.INPUT_MODE_SWITCH2_PRO, + group: 'primary', + required: [], + optional: [], + authentication: [], + deviceTypes: [], + }, { labelKey: 'input-mode-options.sinput', value: InputMode.INPUT_MODE_SINPUT, diff --git a/www/src/Icons/Switch2ProController.tsx b/www/src/Icons/Switch2ProController.tsx new file mode 100644 index 0000000000..d4eade47dd --- /dev/null +++ b/www/src/Icons/Switch2ProController.tsx @@ -0,0 +1,54 @@ +type Switch2ProControllerProps = { + body: string; + buttons: string; + highlight: string; + grip: string; + active?: 'body' | 'buttons' | 'highlight' | 'grip' | null; +}; + +const Switch2ProController = ({ + body, + buttons, + highlight, + grip, + active = null, +}: Switch2ProControllerProps) => { + const part = (name: Switch2ProControllerProps['active']) => ({ + opacity: active && active !== name ? 0.2 : 1, + style: { transition: 'opacity 150ms' }, + }); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; +export default Switch2ProController; diff --git a/www/src/Locales/en/AddonsConfig.jsx b/www/src/Locales/en/AddonsConfig.jsx index 356a6a011c..ae2099d6fe 100644 --- a/www/src/Locales/en/AddonsConfig.jsx +++ b/www/src/Locales/en/AddonsConfig.jsx @@ -200,4 +200,54 @@ export default { 'tg16-extension-data-pin2-label': 'Data GPIO Pin 2 (D_SELECT)', 'tg16-extension-data-pin3-label': 'Data GPIO Pin 3 (L_RUN)', 'tg16-extension-desc-header': 'PC Engine/TurboGrafx-16 Extension Mapping', + 'amiibo-header-text': 'Amiibo', + 'amiibo-sub-header-text': + 'Load amiibo dumps (.bin, 540 or 572 bytes) of amiibo you own.', + 'amiibo-assign-help': + 'Assign each slot (Amiibo 1 through Amiibo 4) to a GPIO button on the Pin Mapping page or set it as a hotkey action. Then press the assigned control while the game is waiting for an amiibo to be tapped.', + 'amiibo-mode-warning': + "Works only in the 'Nintendo Switch 2 Pro Controller' input mode. Amiibo reading is supported, but writing changes back to the amiibo is not currently supported.", + 'amiibo-keys-title': 'Keys for serial randomization', + 'amiibo-keys-help': + 'Upload both the unfixed-info.bin and locked-secret.bin files below to enable serial randomization on each press. If either file is missing or a slot’s toggle is off, the amiibo uses its original serial and, in some games, may only be usable once per day.', + 'amiibo-keys-warning': + 'The GP2040-CE team will not provide these files. Please do not ask.', + 'amiibo-randomize-label': 'Randomize serial on every press', + 'amiibo-option-saving': 'Saving…', + 'amiibo-option-saved': 'Slot setting saved to the controller.', + 'amiibo-option-failed': + 'Could not save the setting. Check the connection; enabling randomization with both keys loaded requires an authenticated dump.', + 'amiibo-clear-failed': + 'Could not clear the slot. Check the connection and retry.', + 'amiibo-key-unfixed': 'unfixed-info.bin', + 'amiibo-key-locked': 'locked-secret.bin', + 'amiibo-key-loaded': 'Loaded', + 'amiibo-key-missing': 'Missing', + 'amiibo-key-unknown': 'Unknown', + 'amiibo-keys-loading': 'Loading key status…', + 'amiibo-keys-status-failed': + 'Could not read key status. Retry to check the controller.', + 'amiibo-keys-ready': + 'Both keys loaded. Serial randomization is available for enabled slots.', + 'amiibo-keys-saving': 'Saving key…', + 'amiibo-key-saved': 'Key saved to the controller.', + 'amiibo-key-failed': + 'Could not save this key. Check the file type and connection, then retry.', + 'amiibo-key-invalid-size': + 'This file is {{size}} bytes. Each key must be exactly 80 bytes.', + 'amiibo-keys-clear': 'Clear both keys', + 'amiibo-keys-clearing': 'Clearing keys…', + 'amiibo-keys-cleared': 'Both keys cleared. Serial randomization is inactive.', + 'amiibo-keys-clear-failed': + 'Could not clear keys. Retry and check the controller status.', + 'amiibo-keys-retry': 'Refresh key status', + 'amiibo-slot-label': 'Slot {{slot}}', + 'amiibo-empty': 'Empty', + 'amiibo-unnamed': 'Unnamed', + 'amiibo-clear': 'Clear', + 'amiibo-saved': 'Saved to the controller.', + 'amiibo-save-failed': + 'Could not save the amiibo. Check the connection and file; slots with randomization enabled require an authenticated dump when both keys are loaded.', + 'amiibo-invalid-size': + 'This file is {{size}} bytes. Amiibo dumps are 540 or 572 bytes.', }; diff --git a/www/src/Locales/en/PinMapping.jsx b/www/src/Locales/en/PinMapping.jsx index 818a8f73cd..5ae3fc9c29 100644 --- a/www/src/Locales/en/PinMapping.jsx +++ b/www/src/Locales/en/PinMapping.jsx @@ -91,6 +91,10 @@ export default { ANALOG_DIRECTION_MOD_LOW: 'Analog Stick Tilt 1', ANALOG_DIRECTION_MOD_HIGH: 'Analog Stick Tilt 2', BUTTON_PRESS_INPUT_REVERSE: 'Reverse Input', + BUTTON_PRESS_AMIIBO_1: 'Amiibo 1', + BUTTON_PRESS_AMIIBO_2: 'Amiibo 2', + BUTTON_PRESS_AMIIBO_3: 'Amiibo 3', + BUTTON_PRESS_AMIIBO_4: 'Amiibo 4', SUSTAIN_FOCUS_MODE: 'Focus Mode Enable', SUSTAIN_4_8_WAY_MODE: 'Toggle 4-Way Mode', MENU_NAVIGATION_UP: 'Menu Up', diff --git a/www/src/Locales/en/Proto/GpioAction.jsx b/www/src/Locales/en/Proto/GpioAction.jsx index fef5bc32dc..455283795d 100644 --- a/www/src/Locales/en/Proto/GpioAction.jsx +++ b/www/src/Locales/en/Proto/GpioAction.jsx @@ -140,4 +140,8 @@ export default { SUSTAIN_PROFILE_SLIDER_POSITION_6: 'Profile Slider Position 6', SUSTAIN_PROFILE_SLIDER_POSITION_7: 'Profile Slider Position 7', SUSTAIN_PROFILE_SLIDER_POSITION_8: 'Profile Slider Position 8', + BUTTON_PRESS_AMIIBO_1: 'Amiibo 1', + BUTTON_PRESS_AMIIBO_2: 'Amiibo 2', + BUTTON_PRESS_AMIIBO_3: 'Amiibo 3', + BUTTON_PRESS_AMIIBO_4: 'Amiibo 4', }; diff --git a/www/src/Locales/en/SettingsPage.jsx b/www/src/Locales/en/SettingsPage.jsx index 23c5ebe216..8c454f5d7a 100644 --- a/www/src/Locales/en/SettingsPage.jsx +++ b/www/src/Locales/en/SettingsPage.jsx @@ -11,6 +11,7 @@ export default { xinput: 'XInput', 'nintendo-switch': 'Nintendo Switch', 'nintendo-switch-pro': 'Nintendo Switch Pro', + 'nintendo-switch2-pro': 'Nintendo Switch 2 Pro', ps3: 'PS3', generic: 'Generic HID', keyboard: 'Keyboard', @@ -70,6 +71,25 @@ export default { 'ps4-id-mode-label': 'Identification Mode', 'ps4-id-mode-explanation-text': '
  • Console mode is used when connecting primarily to a PS4 console.
  • Remote/Emulation mode should only be used when connecting to an emulation layer or remote playing environment that requires a DualShock 4-compatible controller.
', + 'switch2-pro-identity-label': 'Controller Type', + 'switch2-pro-identity-options': { + pro2: 'Switch 2 Pro Controller', + gamecube: 'GameCube Controller', + }, + 'switch2-pro-color-label': 'Controller Display Colors', + 'switch2-pro-color-explanation': + 'Sets the colors the Switch 2 shows for this controller.', + 'switch2-pro-color-presets': { + stock: 'Stock', + gp2040: 'GP2040-CE', + custom: 'Custom', + }, + 'switch2-pro-color-parts': { + 'face-plate': 'Face Plate', + buttons: 'Buttons', + highlight: 'Highlight', + 'main-body': 'Main Body', + }, 'ps4-id-mode-options': { console: 'Console', emulation: 'Remote/Emulation', @@ -139,6 +159,10 @@ export default { 'reboot-default': 'Reboot GP2040-CE', 'reboot-webconfig': 'Reboot GP2040-CE into web-config mode', 'reboot-usb': 'Reboot GP2040-CE into BOOTSEL/USB mode', + 'amiibo-1': 'Amiibo 1', + 'amiibo-2': 'Amiibo 2', + 'amiibo-3': 'Amiibo 3', + 'amiibo-4': 'Amiibo 4', 'save-config': 'Save Config', 'next-profile': 'Next Profile', 'previous-profile': 'Previous Profile', diff --git a/www/src/Pages/AddonsConfigPage.tsx b/www/src/Pages/AddonsConfigPage.tsx index 0a5314d7c1..fe4dab5891 100644 --- a/www/src/Pages/AddonsConfigPage.tsx +++ b/www/src/Pages/AddonsConfigPage.tsx @@ -24,6 +24,7 @@ import Analog1256, { analog1256State, } from '../Addons/Analog1256'; import Bootsel, { bootselScheme, bootselState } from '../Addons/Bootsel'; +import Amiibo, { amiiboScheme, amiiboState } from '../Addons/Amiibo'; import Buzzer, { buzzerScheme, buzzerState } from '../Addons/Buzzer'; import DualDirection, { dualDirectionScheme, @@ -107,6 +108,7 @@ const schema = yup.object().shape({ ...reactiveLEDScheme, ...gamepadUSBHostScheme, ...HETriggerScheme, + ...amiiboScheme, }); export const DEFAULT_VALUES = { @@ -134,6 +136,7 @@ export const DEFAULT_VALUES = { ...reactiveLEDState, ...gamepadUSBHostState, ...HETriggerState, + ...amiiboState, } as const; const ADDONS = [ @@ -161,6 +164,7 @@ const ADDONS = [ DRV8833Rumble, ReactiveLED, HETrigger, + Amiibo, ]; const FormContext = ({ setStoredData }) => { diff --git a/www/src/Pages/SettingsPage.jsx b/www/src/Pages/SettingsPage.jsx index 11947746a5..ded366c2ce 100644 --- a/www/src/Pages/SettingsPage.jsx +++ b/www/src/Pages/SettingsPage.jsx @@ -12,13 +12,19 @@ import { AppContext } from '../Contexts/AppContext'; import ContextualHelpOverlay from '../Components/ContextualHelpOverlay'; import KeyboardMapper from '../Components/KeyboardMapper'; +import Switch2ProController from '../Icons/Switch2ProController'; import Section from '../Components/Section'; import WebApi, { baseButtonMappings } from '../Services/WebApi'; import { BUTTON_MASKS_OPTIONS, getButtonLabels } from '../Data/Buttons'; -import { hexToInt } from '../Services/Utilities'; +import { hexToInt, rgbIntToHex } from '../Services/Utilities'; -import { InputModeDeviceType, PS4ControllerType } from '@proto/enums'; +import { + InputModeDeviceType, + PS4ControllerType, + Switch2ProColorPreset, + Switch2ProIdentity, +} from '@proto/enums'; import './SettingsPage.scss'; import { INPUT_MODE_OPTIONS as INPUT_MODES } from '../Data/InputBootModes'; @@ -170,6 +176,11 @@ const INPUT_BOOT_MODES = [ value: 15, group: 'primary', }, + { + labelKey: 'input-mode-options.nintendo-switch2-pro', + value: 19, + group: 'primary', + }, { labelKey: 'input-mode-options.keyboard', value: 3, group: 'primary' }, { labelKey: 'input-mode-options.generic', value: 14, group: 'primary' }, { labelKey: 'input-mode-options.mdmini', value: 6, group: 'mini' }, @@ -210,6 +221,52 @@ const PS4_ID_MODES = [ { labelKey: 'ps4-id-mode-options.emulation', value: 1 }, ]; +const SWITCH2_PRO_IDENTITIES = [ + { labelKey: 'pro2', value: Switch2ProIdentity.SWITCH2_PRO_IDENTITY_PRO }, + { + labelKey: 'gamecube', + value: Switch2ProIdentity.SWITCH2_PRO_IDENTITY_GAMECUBE, + }, +]; + +const SWITCH2_PRO_COLOR_PARTS = [ + { labelKey: 'face-plate', field: 'switch2ProBodyColor', svgPart: 'body' }, + { labelKey: 'buttons', field: 'switch2ProButtonsColor', svgPart: 'buttons' }, + { + labelKey: 'highlight', + field: 'switch2ProHighlightColor', + svgPart: 'highlight', + }, + { labelKey: 'main-body', field: 'switch2ProGripColor', svgPart: 'grip' }, +]; + +const SWITCH2_PRO_COLOR_PRESETS = [ + { + labelKey: 'stock', + value: Switch2ProColorPreset.SWITCH2_PRO_COLOR_STOCK, + colors: { + switch2ProBodyColor: 0x232323, + switch2ProButtonsColor: 0xa0a0a0, + switch2ProHighlightColor: 0xe6e6e6, + switch2ProGripColor: 0x323232, + }, + }, + { + labelKey: 'gp2040', + value: Switch2ProColorPreset.SWITCH2_PRO_COLOR_GP2040, + colors: { + switch2ProBodyColor: 0x1b1b1d, + switch2ProButtonsColor: 0xffffff, + switch2ProHighlightColor: 0x00ff00, + switch2ProGripColor: 0xec008c, + }, + }, + { + labelKey: 'custom', + value: Switch2ProColorPreset.SWITCH2_PRO_COLOR_CUSTOM, + }, +]; + const AUTHENTICATION_TYPES = [ { labelKey: 'input-mode-authentication.none', value: 0 }, { labelKey: 'input-mode-authentication.key', value: 1 }, @@ -249,6 +306,10 @@ const HOTKEY_ACTIONS = [ { labelKey: 'hotkey-actions.reboot-default', value: 22 }, { labelKey: 'hotkey-actions.reboot-webconfig', value: 86 }, { labelKey: 'hotkey-actions.reboot-usb', value: 87 }, + { labelKey: 'hotkey-actions.amiibo-1', value: 88 }, + { labelKey: 'hotkey-actions.amiibo-2', value: 89 }, + { labelKey: 'hotkey-actions.amiibo-3', value: 90 }, + { labelKey: 'hotkey-actions.amiibo-4', value: 91 }, { labelKey: 'hotkey-actions.save-config', value: 43 }, { labelKey: 'hotkey-actions.b1-button', value: 23 }, { labelKey: 'hotkey-actions.b2-button', value: 24 }, @@ -384,6 +445,18 @@ const schema = yup.object().shape({ .required() .oneOf(PS4_ID_MODES.map((o) => o.value)) .label('PS4 Controller Identification Mode'), + switch2ProIdentity: yup + .number() + .oneOf(SWITCH2_PRO_IDENTITIES.map((o) => o.value)) + .label('Switch 2 Controller Type'), + switch2ProColorPreset: yup + .number() + .oneOf(SWITCH2_PRO_COLOR_PRESETS.map((o) => o.value)) + .label('Switch 2 Pro Controller Colors'), + switch2ProBodyColor: yup.number().label('Switch 2 Pro Body Color'), + switch2ProButtonsColor: yup.number().label('Switch 2 Pro Buttons Color'), + switch2ProHighlightColor: yup.number().label('Switch 2 Pro Highlight Color'), + switch2ProGripColor: yup.number().label('Switch 2 Pro Grip Color'), forcedSetupMode: yup .number() .required() @@ -493,6 +566,10 @@ const FormContext = ({ setButtonLabels, setKeyMappings }) => { values.ps4ControllerIDMode = parseInt(values.ps4ControllerIDMode); if (!!values.inputDeviceType) values.inputDeviceType = parseInt(values.inputDeviceType); + if (!!values.switch2ProIdentity) + values.switch2ProIdentity = parseInt(values.switch2ProIdentity); + if (!!values.switch2ProColorPreset) + values.switch2ProColorPreset = parseInt(values.switch2ProColorPreset); setButtonLabels({ swapTpShareLabels: @@ -535,6 +612,7 @@ export default function SettingsPage() { }, []); const [saveMessage, setSaveMessage] = useState(''); + const [activeColorPart, setActiveColorPart] = useState(null); const [warning, setWarning] = useState({ show: false, acceptText: '' }); const [validated, setValidated] = useState(false); const [keyMappings, setKeyMappings] = useState(baseButtonMappings); @@ -1290,6 +1368,145 @@ export default function SettingsPage() { ); }; + const switch2ProModeSpecifics = (values, setFieldValue) => { + const isPro = + (values.switch2ProIdentity ?? + Switch2ProIdentity.SWITCH2_PRO_IDENTITY_PRO) === + Switch2ProIdentity.SWITCH2_PRO_IDENTITY_PRO; + const preset = SWITCH2_PRO_COLOR_PRESETS.find( + (o) => o.value === values.switch2ProColorPreset, + ); + const isCustom = + values.switch2ProColorPreset === + Switch2ProColorPreset.SWITCH2_PRO_COLOR_CUSTOM; + const colorFor = (field) => + rgbIntToHex((isCustom ? values[field] : preset?.colors?.[field]) ?? 0); + const setColor = (field, hex) => { + if (!isCustom) { + SWITCH2_PRO_COLOR_PARTS.forEach((part) => + setFieldValue(part.field, hexToInt(colorFor(part.field))), + ); + setFieldValue( + 'switch2ProColorPreset', + Switch2ProColorPreset.SWITCH2_PRO_COLOR_CUSTOM, + ); + } + setFieldValue(field, hexToInt(hex)); + }; + const active = SWITCH2_PRO_COLOR_PARTS.find( + ({ field }) => field === activeColorPart, + ); + + return ( +
+ + + + {t('SettingsPage:switch2-pro-identity-label')} + + + setFieldValue('switch2ProIdentity', parseInt(e.target.value)) + } + > + {SWITCH2_PRO_IDENTITIES.map((o) => ( + + ))} + + + + {isPro && ( + <> + + + + {t('SettingsPage:switch2-pro-color-label')} + +

+ {t('SettingsPage:switch2-pro-color-explanation')} +

+ + + + setFieldValue( + 'switch2ProColorPreset', + parseInt(e.target.value), + ) + } + > + {SWITCH2_PRO_COLOR_PRESETS.map((o) => ( + + ))} + + +
+
+
+ +
+ + {SWITCH2_PRO_COLOR_PARTS.map(({ labelKey, field }) => ( + + + + ))} + +
+ + )} +
+ ); + }; + const inputModeSpecifics = (values, errors, setFieldValue, handleChange) => { // Value hasn't been filled out yet if (Object.keys(values).length == 0) { @@ -1345,6 +1562,8 @@ export default function SettingsPage() { ); case 'input-mode-options.xbone': return xboneModeSpecifics(values, errors, setFieldValue, handleChange); + case 'input-mode-options.nintendo-switch2-pro': + return switch2ProModeSpecifics(values, setFieldValue); default: return ( diff --git a/www/src/Pages/SettingsPage.scss b/www/src/Pages/SettingsPage.scss index 52501c92e5..94acf22ba4 100644 --- a/www/src/Pages/SettingsPage.scss +++ b/www/src/Pages/SettingsPage.scss @@ -14,3 +14,42 @@ } } } + +.row > .switch2-pro-colors { + max-width: 28rem; +} + +.switch2-pro-color-chip { + display: flex; + align-items: center; + gap: 0.6rem; + width: 100%; + padding: 0.4rem 0.6rem; + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + cursor: pointer; + transition: border-color 150ms; + + &:hover, + &.is-active { + border-color: var(--bs-primary); + } + + input[type="color"] { + flex: none; + width: 2.25rem; + height: 2.25rem; + padding: 2px; + border: 1px solid var(--bs-border-color); + border-radius: var(--bs-border-radius-sm); + background: none; + cursor: pointer; + } +} + +.switch2-pro-color-chip-text { + display: flex; + flex-direction: column; + min-width: 0; + line-height: 1.25; +} diff --git a/www/src/Services/WebApi.js b/www/src/Services/WebApi.js index 8817058d38..9c4d87b855 100644 --- a/www/src/Services/WebApi.js +++ b/www/src/Services/WebApi.js @@ -282,6 +282,70 @@ async function setSplashImage({ splashImage }) { .catch(console.error); } +async function getAmiiboSlots() { + try { + const response = await Http.get(`${baseUrl}/api/getAmiiboSlots`); + return response.data?.slots; + } catch (error) { + console.error(error); + } +} + +async function setAmiiboSlot({ slot, name, data }) { + return Http.post(`${baseUrl}/api/setAmiiboSlot`, { slot, name, data }) + .then((response) => response.data?.success) + .catch(console.error); +} + +async function setAmiiboSlotOptions({ slot, randomizeSerial }) { + try { + const response = await Http.post(`${baseUrl}/api/setAmiiboSlotOptions`, { + slot, + randomizeSerial, + }); + return response.data?.success === true; + } catch { + return false; + } +} + +async function clearAmiiboSlot(slot) { + return Http.post(`${baseUrl}/api/clearAmiiboSlot`, { slot }) + .then((response) => response.data?.success) + .catch(console.error); +} + +// Never log upload errors: HTTP error objects may contain the key payload. +async function getAmiiboKeys() { + try { + const response = await Http.get(`${baseUrl}/api/getAmiiboKeys`); + return response.data; + } catch { + return undefined; + } +} + +async function setAmiiboKey({ type, data }) { + try { + const response = await Http.post(`${baseUrl}/api/setAmiiboKey`, { + type, + data, + }); + return response.data?.success === true; + } catch { + return false; + } +} + +async function clearAmiiboKeys() { + try { + const response = await Http.post(`${baseUrl}/api/clearAmiiboKeys`, {}); + return response.data?.success === true; + } catch { + return false; + } +} + async function getGamepadOptions(setLoading) { setLoading(true); @@ -773,6 +837,13 @@ export default { getButtonLayoutDefs, getSplashImage, setSplashImage, + getAmiiboKeys, + setAmiiboKey, + clearAmiiboKeys, + getAmiiboSlots, + setAmiiboSlot, + setAmiiboSlotOptions, + clearAmiiboSlot, setAnimationButtonTestMode, setAnimationButtonTestState, clearAnimationButtonTestMode, diff --git a/www/src_gen/enums.ts b/www/src_gen/enums.ts index e3bd528cb0..9b9ea962d6 100644 --- a/www/src_gen/enums.ts +++ b/www/src_gen/enums.ts @@ -138,6 +138,7 @@ export enum InputMode { INPUT_MODE_SWITCH_PRO = 15, INPUT_MODE_P5GENERAL = 16, INPUT_MODE_SINPUT = 17, + INPUT_MODE_SWITCH2_PRO = 19, INPUT_MODE_CONFIG = 255 } @@ -321,7 +322,11 @@ export enum GpioAction { SUSTAIN_PROFILE_SLIDER_POSITION_5 = 135, SUSTAIN_PROFILE_SLIDER_POSITION_6 = 136, SUSTAIN_PROFILE_SLIDER_POSITION_7 = 137, - SUSTAIN_PROFILE_SLIDER_POSITION_8 = 138 + SUSTAIN_PROFILE_SLIDER_POSITION_8 = 138, + BUTTON_PRESS_AMIIBO_1 = 139, + BUTTON_PRESS_AMIIBO_2 = 140, + BUTTON_PRESS_AMIIBO_3 = 141, + BUTTON_PRESS_AMIIBO_4 = 142 } export enum GpioDirection { @@ -406,7 +411,11 @@ export enum GamepadHotkey { HOTKEY_RS_LEFT = 84, HOTKEY_RS_RIGHT = 85, HOTKEY_REBOOT_WEBCONFIG = 86, - HOTKEY_REBOOT_USB = 87 + HOTKEY_REBOOT_USB = 87, + HOTKEY_AMIIBO_1 = 88, + HOTKEY_AMIIBO_2 = 89, + HOTKEY_AMIIBO_3 = 90, + HOTKEY_AMIIBO_4 = 91 } export enum LEDFormat_Proto { @@ -583,6 +592,17 @@ export enum ReactiveLEDMode { REACTIVE_LED_FADE_OUT = 3 } +export enum Switch2ProIdentity { + SWITCH2_PRO_IDENTITY_PRO = 0, + SWITCH2_PRO_IDENTITY_GAMECUBE = 1 +} + +export enum Switch2ProColorPreset { + SWITCH2_PRO_COLOR_STOCK = 0, + SWITCH2_PRO_COLOR_GP2040 = 1, + SWITCH2_PRO_COLOR_CUSTOM = 2 +} + export enum PS4ControllerIDMode { PS4_ID_CONSOLE = 0, PS4_ID_EMULATION = 1