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