From 6d9be98b9124cb8f9c38966be1a8b13508adbf7e Mon Sep 17 00:00:00 2001 From: Gary Likovic <81448614+garylikovic@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:55:00 -0400 Subject: [PATCH 01/13] Add files via upload Added IDs for Wii U Pro Controller, 8BitDo Ultimate Bluetooth Controller, Switch 1 Joy cons (Left and Right) and Ouya Gen 1 Controller. --- Firmware/RP2040/src/USBHost/HardwareIDs.h | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Firmware/RP2040/src/USBHost/HardwareIDs.h b/Firmware/RP2040/src/USBHost/HardwareIDs.h index b02b7941..ccb924b9 100644 --- a/Firmware/RP2040/src/USBHost/HardwareIDs.h +++ b/Firmware/RP2040/src/USBHost/HardwareIDs.h @@ -67,6 +67,34 @@ static const HardwareID N64_IDS[] = {0x0079, 0x0006} // Retrolink N64 USB gamepad }; +static const HardwareID WIIU_PRO_IDS[] = +{ + {0x057E, 0x0330} // Wii U Pro Wireless Controller +}; + +static const HardwareID OUYA_IDS[] = +{ + {0x2836, 0x0001} // OUYA Controller +}; + +static const HardwareID Bit_Ult_IDS[] = +{ + {0x057E, 0x2009} // 8Bit Ultimate Bluetooth Controller +}; + +static const HardwareID Wii_IDS[] = +{ + {0x057e, 0x0306} // Wii Motion Plus Controller + +}; + +static const HardwareID Switch_Joycon_IDS[] = +{ + {0x057e, 0x2007} // Switch Joycon (R) + {0x057e, 0x2006} // Switch Joycon (L) + +}; + struct HostTypeMap { const HardwareID* ids; @@ -84,6 +112,10 @@ static const HostTypeMap HOST_TYPE_MAP[] = { SWITCH_PRO_IDS, sizeof(SWITCH_PRO_IDS) / sizeof(HardwareID), HostDriverType::SWITCH_PRO }, { PSCLASSIC_IDS, sizeof(PSCLASSIC_IDS) / sizeof(HardwareID), HostDriverType::PSCLASSIC }, { N64_IDS, sizeof(N64_IDS) / sizeof(HardwareID), HostDriverType::N64 }, + { WIIU_PRO_IDS, sizeof(WIIU_PRO_IDS) / sizeof(HardwareID), HostDriverType::WIIUPRO }, + { Bit_Ult_IDS, sizeof(Bit_Ult_IDS) / sizeof(HardwareID), HostDriverType::Bit_Ult }, + { Wii_IDS, sizeof(Wii_IDS) / sizeof(HardwareID), HostDriverType::Wii }, + { OUYA_IDS, sizeof(OUYA_IDS) / sizeof(HardwareID), HostDriverType::OUYA }, }; #endif // _HW_ID_H_ \ No newline at end of file From 93dd1bd769e4d2a891486657fcce386342996bac Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:56:31 -0500 Subject: [PATCH 02/13] Add files via upload Fixed Wii and Wii U controller support, RB LB and RT LT work now for both controllers --- Firmware/RP2040/src/Bluepad32/Bluepad32.cpp | 132 +++++++++++++++++--- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp index 836811e5..83df3b64 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -46,6 +47,27 @@ bool any_connected() return false; } +bool is_wii_controller_connected(uint8_t idx) +{ + if (idx >= MAX_GAMEPADS) { + return false; + } + uni_hid_device_t* bp_device = uni_hid_device_get_instance_for_idx(idx); + return (bp_device != nullptr && bp_device->controller_type == CONTROLLER_TYPE_WiiController); +} + +bool any_wii_controller_connected() +{ + for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) + { + if (bt_devices_[i].connected && is_wii_controller_connected(i)) + { + return true; + } + } + return false; +} + //This solves a function pointer/crash issue with bluepad32 void set_rumble(uni_hid_device_t* bp_device, uint16_t length, uint8_t rumble_l, uint8_t rumble_r) { @@ -132,9 +154,14 @@ static void init_complete_cb(void) { } static uni_error_t device_discovered_cb(bd_addr_t addr, const char* name, uint16_t cod, uint8_t rssi) { - if (!((cod & UNI_BT_COD_MINOR_MASK) & UNI_BT_COD_MINOR_GAMEPAD)) { + uint8_t minor = cod & UNI_BT_COD_MINOR_MASK; + + if (!(minor & (UNI_BT_COD_MINOR_GAMEPAD | + UNI_BT_COD_MINOR_JOYSTICK | + UNI_BT_COD_MINOR_REMOTE_CONTROL))) { return UNI_ERROR_IGNORE_DEVICE; } + return UNI_ERROR_SUCCESS; } @@ -190,6 +217,11 @@ static void oob_event_cb(uni_platform_oob_event_t event, void* data) { return; } +// Set to 1 to print all Bluepad32 controller inputs to UART (only when state changes) +#ifndef BLUEPAD32_UART_LOG_INPUT +#define BLUEPAD32_UART_LOG_INPUT 1 +#endif + static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* controller) { static uni_gamepad_t prev_uni_gp[MAX_GAMEPADS] = {}; @@ -199,6 +231,19 @@ static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* contr uni_gamepad_t *uni_gp = &controller->gamepad; int idx = uni_hid_device_get_idx_for_instance(device); + +#if BLUEPAD32_UART_LOG_INPUT + { + bool changed = std::memcmp(uni_gp, &prev_uni_gp[idx], sizeof(uni_gamepad_t)) != 0; + if (changed) { + printf("[BP32 idx=%d] dpad=0x%02x btns=0x%04x misc=0x%02x brake=%u throttle=%u " + "Lx=%d Ly=%d Rx=%d Ry=%d\n", + idx, (unsigned)uni_gp->dpad, (unsigned)uni_gp->buttons, (unsigned)uni_gp->misc_buttons, + (unsigned)uni_gp->brake, (unsigned)uni_gp->throttle, + (int)uni_gp->axis_x, (int)uni_gp->axis_y, (int)uni_gp->axis_rx, (int)uni_gp->axis_ry); + } + } +#endif Gamepad* gamepad = bt_devices_[idx].gamepad; Gamepad::PadIn gp_in; @@ -233,25 +278,82 @@ static void controller_data_cb(uni_hid_device_t* device, uni_controller_t* contr break; } - if (uni_gp->buttons & BUTTON_A) gp_in.buttons |= gamepad->MAP_BUTTON_A; - if (uni_gp->buttons & BUTTON_B) gp_in.buttons |= gamepad->MAP_BUTTON_B; - if (uni_gp->buttons & BUTTON_X) gp_in.buttons |= gamepad->MAP_BUTTON_X; - if (uni_gp->buttons & BUTTON_Y) gp_in.buttons |= gamepad->MAP_BUTTON_Y; - if (uni_gp->buttons & BUTTON_SHOULDER_L) gp_in.buttons |= gamepad->MAP_BUTTON_LB; - if (uni_gp->buttons & BUTTON_SHOULDER_R) gp_in.buttons |= gamepad->MAP_BUTTON_RB; - if (uni_gp->buttons & BUTTON_THUMB_L) gp_in.buttons |= gamepad->MAP_BUTTON_L3; - if (uni_gp->buttons & BUTTON_THUMB_R) gp_in.buttons |= gamepad->MAP_BUTTON_R3; - if (uni_gp->misc_buttons & MISC_BUTTON_BACK) gp_in.buttons |= gamepad->MAP_BUTTON_BACK; - if (uni_gp->misc_buttons & MISC_BUTTON_START) gp_in.buttons |= gamepad->MAP_BUTTON_START; - if (uni_gp->misc_buttons & MISC_BUTTON_SYSTEM) gp_in.buttons |= gamepad->MAP_BUTTON_SYS; - - gp_in.trigger_l = gamepad->scale_trigger_l<10>(static_cast(uni_gp->brake)); - gp_in.trigger_r = gamepad->scale_trigger_r<10>(static_cast(uni_gp->throttle)); + if (is_wii_controller_connected(idx)) { + if (uni_gp->buttons & BUTTON_A) gp_in.buttons |= gamepad->MAP_BUTTON_A; + if (uni_gp->buttons & BUTTON_B) gp_in.buttons |= gamepad->MAP_BUTTON_B; + if (uni_gp->buttons & BUTTON_X) gp_in.buttons |= gamepad->MAP_BUTTON_X; + if (uni_gp->buttons & BUTTON_Y) gp_in.buttons |= gamepad->MAP_BUTTON_Y; + if (uni_gp->buttons & BUTTON_SHOULDER_L) gp_in.buttons |= gamepad->MAP_BUTTON_LB; + if (uni_gp->buttons & BUTTON_SHOULDER_R) gp_in.buttons |= gamepad->MAP_BUTTON_RB; + //if (uni_gp->buttons & BUTTON_THUMB_L) gp_in.buttons |= gamepad->MAP_BUTTON_L3; + //if (uni_gp->buttons & BUTTON_THUMB_R) gp_in.buttons |= gamepad->MAP_BUTTON_R3; + if (uni_gp->misc_buttons & MISC_BUTTON_BACK) gp_in.buttons |= gamepad->MAP_BUTTON_BACK; + if (uni_gp->misc_buttons & MISC_BUTTON_START) gp_in.buttons |= gamepad->MAP_BUTTON_START; + if (uni_gp->misc_buttons & MISC_BUTTON_SYSTEM) gp_in.buttons |= gamepad->MAP_BUTTON_SYS; + } + else { + if (uni_gp->buttons & BUTTON_A) gp_in.buttons |= gamepad->MAP_BUTTON_A; + if (uni_gp->buttons & BUTTON_B) gp_in.buttons |= gamepad->MAP_BUTTON_B; + if (uni_gp->buttons & BUTTON_X) gp_in.buttons |= gamepad->MAP_BUTTON_X; + if (uni_gp->buttons & BUTTON_Y) gp_in.buttons |= gamepad->MAP_BUTTON_Y; + if (uni_gp->buttons & BUTTON_SHOULDER_L) gp_in.buttons |= gamepad->MAP_BUTTON_LB; + if (uni_gp->buttons & BUTTON_SHOULDER_R) gp_in.buttons |= gamepad->MAP_BUTTON_RB; + if (uni_gp->buttons & BUTTON_THUMB_L) gp_in.buttons |= gamepad->MAP_BUTTON_L3; + if (uni_gp->buttons & BUTTON_THUMB_R) gp_in.buttons |= gamepad->MAP_BUTTON_R3; + if (uni_gp->misc_buttons & MISC_BUTTON_BACK) gp_in.buttons |= gamepad->MAP_BUTTON_BACK; + if (uni_gp->misc_buttons & MISC_BUTTON_START) gp_in.buttons |= gamepad->MAP_BUTTON_START; + if (uni_gp->misc_buttons & MISC_BUTTON_SYSTEM) gp_in.buttons |= gamepad->MAP_BUTTON_SYS; + } + + // Check for disconnect combo: Start+Select for most controllers, L3+R3 for OUYA (no Start/Select) + static uint32_t disconnect_combo_hold_time[MAX_GAMEPADS] = {0}; + bool is_ouya = (device->controller_type == CONTROLLER_TYPE_OUYAController); + bool combo_pressed = is_ouya + ? ((uni_gp->buttons & BUTTON_THUMB_L) && (uni_gp->buttons & BUTTON_THUMB_R)) + : ((uni_gp->misc_buttons & MISC_BUTTON_START) && (uni_gp->misc_buttons & MISC_BUTTON_BACK)); + + if (combo_pressed) { + disconnect_combo_hold_time[idx]++; + // Require combo to be held for ~500ms (assuming ~60Hz callback rate, ~30 frames) + if (disconnect_combo_hold_time[idx] >= 30) { + printf("[BP32] Disconnect combo detected, disconnecting controller %d\n", idx); + uni_hid_device_disconnect(device); + disconnect_combo_hold_time[idx] = 0; + return; // Don't process further input after disconnect + } + } else { + disconnect_combo_hold_time[idx] = 0; + } + + // Prefer analog triggers (brake / throttle) when present, but fall back to + // digital trigger buttons (e.g. Wii U LT / RT) when analog value is zero. + // For Wii controllers: Z button (shoulder) reports brake/throttle, but we only want it to map to LB/RB, not triggers + // So skip trigger mapping when shoulder buttons are pressed on Wii controllers + bool wii_shoulder_pressed = is_wii_controller_connected(idx) && + ((uni_gp->buttons & BUTTON_SHOULDER_L) || (uni_gp->buttons & BUTTON_SHOULDER_R)); + + if (!wii_shoulder_pressed) { + gp_in.trigger_l = gamepad->scale_trigger_l<10>(static_cast(uni_gp->brake)); + gp_in.trigger_r = gamepad->scale_trigger_r<10>(static_cast(uni_gp->throttle)); + + if (gp_in.trigger_l == 0 && (uni_gp->buttons & BUTTON_TRIGGER_L)) { + gp_in.trigger_l = 0xFF; + } + if (gp_in.trigger_r == 0 && (uni_gp->buttons & BUTTON_TRIGGER_R)) { + gp_in.trigger_r = 0xFF; + } + } std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad->scale_joystick_l<10>(uni_gp->axis_x, uni_gp->axis_y); std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad->scale_joystick_r<10>(uni_gp->axis_rx, uni_gp->axis_ry); gamepad->set_pad_in(gp_in); + +#if BLUEPAD32_UART_LOG_INPUT + if (idx >= 0 && idx < static_cast(MAX_GAMEPADS)) { + std::memcpy(&prev_uni_gp[idx], uni_gp, sizeof(uni_gamepad_t)); + } +#endif } const uni_property_t* get_property_cb(uni_property_idx_t idx) From 0317bf93b043be422785137bfef7362cf3e7e0fa Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:11:29 -0500 Subject: [PATCH 03/13] Update README for OGX-Mini Fork and new features Updated README to reflect the fork status and added new features for Wii U Pro and Wii Remotes. Changed purchase link to the original creator's store. --- README.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3aa52f23..56a9f50f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OGX-Mini +# OGX-Mini Fork ![OGX-Mini Boards](images/OGX-Mini-github.jpg "OGX-Mini Boards") Firmware for the RP2040, capable of emulating gamepads for several game consoles. The firmware comes in many flavors, supported on the [Adafruit Feather USB Host board](https://www.adafruit.com/product/5723), Pi Pico, Pi Pico 2, Pi Pico W, Pi Pico 2 W, Waveshare RP2040-Zero, Pico/ESP32 hybrid, and a 4-Channel RP2040-Zero setup. @@ -37,6 +37,10 @@ Start = Plus (Switch) = Options (Dualsense/DS4) After a new mode is stored, the RP2040 will reset itself so you don't need to unplug it. +## Disconnecting Controllers +For most controllers pressing and holding Start+Select (+/-, etc) for the controller will disconnect it and restart pairing mode. +For the OUYA controller there is no Start+Select, the disconnection combo has been set to L3+R3. + ## Supported devices ### Wired controllers - Original Xbox Duke and S @@ -67,10 +71,22 @@ Note: There are some third party controllers that can change their VID/PID, thes - Switch Pro - Steam - Stadia -- And more +- Wii U Pro +- Wii Remote + - Supported Extensions: + - Gamepad + - Nunchuck + - GameCube Controller +- 8BitDo Ultimate Wireless (Switch layout) Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supported_gamepads/) for a more comprehensive list of supported controllers and Bluetooth pairing instructions. +# Features new to this fork: +Note: These features have been added to the Pico W/ Pico 2 W firmware support, I do not have the other boards to test and implement the same fixes at this time. +- Wii U Pro controllers are supported fully with working LT and RT +- Wii Remotes are supported along with the following controllers connected: Nunchuck/ GameCube/ Wii Gamepad. +- Disconnection Combo has been added: Start+Select for most controllers, L3+R3 for OUYA controllers. + ## Features new to v1.0.0 - Bluetooth functionality for the Pico W, Pico 2 W, and Pico+ESP32. - Web application (connectable via USB or Bluetooth) for configuring deadzones and buttons mappings, supports up to 8 saved profiles. @@ -85,7 +101,7 @@ Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supporte - Analog button support on OG Xbox and PS3. - RGB LED support for RP2040-Zero and Adafruit Feather boards. -## Planned additions +## Planned additions from the original creator - More accurate report parser for unknown HID controllers - Hardware design for internal OG Xbox install - Hardware design for 4 channel RP2040-Zero adapter @@ -97,6 +113,17 @@ Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supporte - Button macros - Rumble settings (intensity, enabled/disable, etc.) +## Planned additions for this fork +- Output to the following consoles: + - GameCube + - Wii U (GameCube controller adapter) + - PS2 + - DreamCast + - NES + - SNES + - Genesis + - Master System + ## Hardware For Pi Pico, RP2040-Zero, 4 channel, and ESP32 configurations, please see the hardware folder for diagrams. @@ -104,7 +131,7 @@ I've designed a PCB for the RP2040-Zero so you can make a small form-factor adap OGX-Mini Boards -If you would like a prebuilt unit, you can purchase one, with cable and Xbox adapter included, from my [**Etsy store**](https://www.etsy.com/listing/1426992904/ogx-mini-controller-adapter-for-original). +If you would like a prebuilt unit, you can purchase one, with cable and Xbox adapter included, from the original creators store: [**Etsy store**](https://www.etsy.com/listing/1426992904/ogx-mini-controller-adapter-for-original). ## Adding supported controllers If your third party controller isn't working, but the original version is listed above, send me the device's VID and PID and I'll add it so it's recognized properly. @@ -140,4 +167,8 @@ Please see the Hardware directory for a diagram showing how to hookup the ESP32 You will need ESP-IDF v5.1, esptool, python3, and git installed. If you use VSCode, you can install the ESP-IDF extension and configure the project for ESP-IDF v5.1, it'll download everything for you and then you just click the build button at the bottom of the window. -When you build with ESP-IDF, Cmake will run a python script that copies the necessary BTStack files into the components directory, this is needed since BTStack isn't configured as an ESP-IDF component when you download it with git. +When you build with ESP-IDF, Cmake will run a python script that copies the necessary BTStack files into the components directory, this is needed since BTStack isn't configured as an ESP-IDF + +# Credit to the original creator [https://wiredopposite.github.io/](https://github.com/wiredopposite/OGX-Mini/tree/master) for the original base of the project! + +component when you download it with git. From 78b191ab4338c0a357f7fdfe60c4d6dc693aee5d Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:13:09 -0500 Subject: [PATCH 04/13] Update repository URL in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 56a9f50f..bf1c4758 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ You can also set ```MAX_GAMEPADS``` which, if greater than one, will only suppor You'll need git, python3, CMake, Ninja and the GCC ARM toolchain installed. CMake scripts will patch some files in Bluepad32 and BTStack and also make sure all git submodules (plus their submodules and dependencies) are downloaded. Here's an example on Windows: ``` -git clone --recursive https://github.com/wiredopposite/OGX-Mini.git +git clone --recursive https://github.com/MegaCadeDev/OGX-Mini.git cd OGX-Mini/Firmware/RP2040 cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOGXM_BOARD=PI_PICOW -DMAX_GAMEPADS=1 cmake --build build From bf05140dd587cc0644b6977c25fef502a9641cc4 Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:15:19 -0500 Subject: [PATCH 05/13] Update README with clearer ESP-IDF build instructions Clarified the installation instructions for ESP-IDF and BTStack. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bf1c4758..a55e0819 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,8 @@ Please see the Hardware directory for a diagram showing how to hookup the ESP32 You will need ESP-IDF v5.1, esptool, python3, and git installed. If you use VSCode, you can install the ESP-IDF extension and configure the project for ESP-IDF v5.1, it'll download everything for you and then you just click the build button at the bottom of the window. -When you build with ESP-IDF, Cmake will run a python script that copies the necessary BTStack files into the components directory, this is needed since BTStack isn't configured as an ESP-IDF +When you build with ESP-IDF, Cmake will run a python script that copies the necessary BTStack files into the components directory, this is needed since BTStack isn't configured as an ESP-IDF component when you download it with git. + # Credit to the original creator [https://wiredopposite.github.io/](https://github.com/wiredopposite/OGX-Mini/tree/master) for the original base of the project! -component when you download it with git. From 02e5911dc28d33a24929719d2253df13be396647 Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:01:01 -0500 Subject: [PATCH 06/13] Update supported controllers list in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a55e0819..a0ae44d1 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supporte # Features new to this fork: Note: These features have been added to the Pico W/ Pico 2 W firmware support, I do not have the other boards to test and implement the same fixes at this time. -- Wii U Pro controllers are supported fully with working LT and RT +- Wii U & Switch Pro controllers are supported fully with working LT and RT - Wii Remotes are supported along with the following controllers connected: Nunchuck/ GameCube/ Wii Gamepad. - Disconnection Combo has been added: Start+Select for most controllers, L3+R3 for OUYA controllers. From 25d545fb05ed9e775fae72708b62ad3522d017aa Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Sun, 8 Feb 2026 04:34:59 -0500 Subject: [PATCH 07/13] Change repository URL in README Updated repository URL in the cloning instructions. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0ae44d1..317891bd 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ You can also set ```MAX_GAMEPADS``` which, if greater than one, will only suppor You'll need git, python3, CMake, Ninja and the GCC ARM toolchain installed. CMake scripts will patch some files in Bluepad32 and BTStack and also make sure all git submodules (plus their submodules and dependencies) are downloaded. Here's an example on Windows: ``` -git clone --recursive https://github.com/MegaCadeDev/OGX-Mini.git +git clone --recursive https://github.com/MegaCadeDev/OGX-Mini-2026.git cd OGX-Mini/Firmware/RP2040 cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOGXM_BOARD=PI_PICOW -DMAX_GAMEPADS=1 cmake --build build From 1fc6a784ecc421cb098d2533510cdb6799ab25b4 Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Sun, 8 Feb 2026 04:37:53 -0500 Subject: [PATCH 08/13] Fix directory path in README for cloning repo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 317891bd..a36b0db7 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ You can also set ```MAX_GAMEPADS``` which, if greater than one, will only suppor You'll need git, python3, CMake, Ninja and the GCC ARM toolchain installed. CMake scripts will patch some files in Bluepad32 and BTStack and also make sure all git submodules (plus their submodules and dependencies) are downloaded. Here's an example on Windows: ``` git clone --recursive https://github.com/MegaCadeDev/OGX-Mini-2026.git -cd OGX-Mini/Firmware/RP2040 +cd OGX-Mini-2026/Firmware/RP2040 cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOGXM_BOARD=PI_PICOW -DMAX_GAMEPADS=1 cmake --build build ``` From 13dc57f9a151776faba5312666df30e0bb208d1d Mon Sep 17 00:00:00 2001 From: Gary Date: Sun, 8 Feb 2026 04:48:49 -0500 Subject: [PATCH 09/13] Added Wii U GameCube emulation support for Wii U and Switch --- Firmware/RP2040/CMakeLists.txt | 2 + Firmware/RP2040/src/Board/ogxm_log.cpp | 1 + Firmware/RP2040/src/Descriptors/WiiU.h | 168 ++++++++++++++++++ Firmware/RP2040/src/Gamepad/Gamepad.h | 2 +- .../DeviceDriver/DeviceDriverTypes.h | 1 + .../USBDevice/DeviceDriver/Switch/Switch.cpp | 4 +- .../src/USBDevice/DeviceDriver/WiiU/WiiU.cpp | 165 +++++++++++++++++ .../src/USBDevice/DeviceDriver/WiiU/WiiU.h | 28 +++ .../RP2040/src/USBDevice/DeviceManager.cpp | 17 ++ .../RP2040/src/USBDevice/tud_callbacks.cpp | 3 +- Firmware/RP2040/src/USBHost/HardwareIDs.h | 35 +--- .../HostDriver/SwitchWired/SwitchWired.cpp | 7 +- Firmware/RP2040/src/USBHost/HostManager.h | 15 ++ .../RP2040/src/UserSettings/UserSettings.cpp | 7 +- Firmware/RP2040/src/main.cpp | 11 ++ 15 files changed, 425 insertions(+), 41 deletions(-) create mode 100644 Firmware/RP2040/src/Descriptors/WiiU.h create mode 100644 Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp create mode 100644 Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h diff --git a/Firmware/RP2040/CMakeLists.txt b/Firmware/RP2040/CMakeLists.txt index 291dbf50..36d45e1a 100644 --- a/Firmware/RP2040/CMakeLists.txt +++ b/Firmware/RP2040/CMakeLists.txt @@ -63,6 +63,7 @@ set(SOURCES_BOARD ${SRC}/USBDevice/DeviceDriver/PSClassic/PSClassic.cpp ${SRC}/USBDevice/DeviceDriver/PS3/PS3.cpp ${SRC}/USBDevice/DeviceDriver/Switch/Switch.cpp + ${SRC}/USBDevice/DeviceDriver/WiiU/WiiU.cpp ${SRC}/USBDevice/DeviceDriver/XInput/XInput.cpp ${SRC}/USBDevice/DeviceDriver/XboxOG/XboxOG_GP.cpp ${SRC}/USBDevice/DeviceDriver/XboxOG/XboxOG_SB.cpp @@ -325,6 +326,7 @@ if(CMAKE_BUILD_TYPE STREQUAL "Debug") message(STATUS "UART port: ${UART_PORT}, TX: ${TX_PIN}, RX: ${RX_PIN}") + pico_enable_stdio_usb(${FW_NAME} 0) pico_enable_stdio_uart(${FW_NAME} 1) target_compile_definitions(${FW_NAME} PRIVATE PICO_DEFAULT_UART=${UART_PORT} diff --git a/Firmware/RP2040/src/Board/ogxm_log.cpp b/Firmware/RP2040/src/Board/ogxm_log.cpp index 6bb55c5f..bd2d6b18 100644 --- a/Firmware/RP2040/src/Board/ogxm_log.cpp +++ b/Firmware/RP2040/src/Board/ogxm_log.cpp @@ -24,6 +24,7 @@ std::ostream& operator<<(std::ostream& os, DeviceDriverType type) { case DeviceDriverType::DINPUT: os << "DINPUT"; break; case DeviceDriverType::PSCLASSIC: os << "PSCLASSIC"; break; case DeviceDriverType::SWITCH: os << "SWITCH"; break; + case DeviceDriverType::WIIU: os << "WIIU"; break; case DeviceDriverType::WEBAPP: os << "WEBAPP"; break; case DeviceDriverType::UART_BRIDGE: os << "UART_BRIDGE"; break; default: os << "UNKNOWN"; break; diff --git a/Firmware/RP2040/src/Descriptors/WiiU.h b/Firmware/RP2040/src/Descriptors/WiiU.h new file mode 100644 index 00000000..5c97fd18 --- /dev/null +++ b/Firmware/RP2040/src/Descriptors/WiiU.h @@ -0,0 +1,168 @@ +#ifndef _WIIU_DESCRIPTORS_H_ +#define _WIIU_DESCRIPTORS_H_ + +#include + +#include "tusb.h" + +namespace WiiU +{ + // Wii U GameCube Adapter protocol (same as Dolphin / gc_adapter_read.py) + static constexpr uint8_t HID_REPORT_ID = 0x21; + static constexpr uint8_t INIT_PAYLOAD = 0x13; + static constexpr uint8_t CONTROLLER_PAYLOAD_SIZE = 37; // report_id + 4 * 9 bytes per port + + // Controller type in first byte of each 9-byte port block + static constexpr uint8_t PORT_WIRED = (1 << 4); + static constexpr uint8_t PORT_WIRELESS = (1 << 5); + + // GameCube button bits (bytes 1 and 2 of each port block) + namespace GCButtons + { + // Byte 1 + static constexpr uint8_t A = (1 << 0); + static constexpr uint8_t B = (1 << 1); + static constexpr uint8_t X = (1 << 2); + static constexpr uint8_t Y = (1 << 3); + static constexpr uint8_t LEFT = (1 << 4); + static constexpr uint8_t RIGHT = (1 << 5); + static constexpr uint8_t DOWN = (1 << 6); + static constexpr uint8_t UP = (1 << 7); + // Byte 2 + static constexpr uint8_t START = (1 << 0); + static constexpr uint8_t Z = (1 << 1); + static constexpr uint8_t R = (1 << 2); + static constexpr uint8_t L = (1 << 3); + } + + static constexpr uint8_t STICK_CENTER = 128; + + #pragma pack(push, 1) + // Full adapter input report: report ID 0x21 + 4 ports × 9 bytes + struct InReport + { + uint8_t report_id{ HID_REPORT_ID }; + uint8_t port_data[4][9]; // [port][type, b1, b2, stickX, stickY, substickX, substickY, triggerL, triggerR] + }; + static_assert(sizeof(InReport) == CONTROLLER_PAYLOAD_SIZE, "WiiU::InReport must be 37 bytes"); + #pragma pack(pop) + + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const uint8_t STRING_MANUFACTURER[] = "Nintendo"; + static const uint8_t STRING_PRODUCT[] = "GameCube For Switch"; + static const uint8_t STRING_SERIAL[] = "GH-GC-001 T8"; + static const uint8_t STRING_VERSION[] = "1.0"; + + static const uint8_t *STRING_DESCRIPTORS[] __attribute__((unused)) = + { + STRING_LANGUAGE, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_SERIAL, + STRING_VERSION + }; + + // Nintendo Wii U GameCube Adapter + static const uint8_t DEVICE_DESCRIPTORS[] = + { + 0x12, // bLength + 0x01, // bDescriptorType (Device) + 0x00, 0x02, // bcdUSB 2.00 + 0x00, // bDeviceClass (Use class information in the Interface Descriptors) + 0x00, // bDeviceSubClass + 0x00, // bDeviceProtocol + 0x40, // bMaxPacketSize0 64 + 0x7E, 0x05, // idVendor 0x057E (Nintendo) + 0x37, 0x03, // idProduct 0x0337 (Wii U GameCube Adapter) + 0x00, 0x01, // bcdDevice 1.00 + 0x01, // iManufacturer (String Index) + 0x02, // iProduct (String Index) + 0x03, // iSerialNumber (String Index) + 0x01, // bNumConfigurations 1 + }; + + // HID report descriptor (exact dump from real Nintendo Wii U GameCube Adapter 057e:0337) + static const uint8_t REPORT_DESCRIPTORS[] = + { + 0x05, 0x05, 0x09, 0x00, 0xA1, 0x01, 0x85, 0x11, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, + 0xFF, 0x00, 0x75, 0x08, 0x95, 0x05, 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x21, 0x19, 0x00, 0x2A, + 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x25, 0x81, 0x00, 0xC0, 0xA1, 0x01, + 0x85, 0x12, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, + 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x22, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, + 0x00, 0x75, 0x08, 0x95, 0x19, 0x81, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x13, 0x19, 0x00, 0x2A, 0xFF, + 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, + 0x23, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, + 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x14, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, + 0x75, 0x08, 0x95, 0x01, 0x91, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x24, 0x19, 0x00, 0x2A, 0xFF, 0x00, + 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x02, 0x81, 0x00, 0xC0, 0xA1, 0x01, 0x85, 0x15, + 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, 0x08, 0x95, 0x01, 0x91, 0x00, + 0xC0, 0xA1, 0x01, 0x85, 0x25, 0x19, 0x00, 0x2A, 0xFF, 0x00, 0x15, 0x00, 0x26, 0xFF, 0x00, 0x75, + 0x08, 0x95, 0x02, 0x81, 0x00, 0xC0, + }; + static_assert(sizeof(REPORT_DESCRIPTORS) == 214, "WiiU report descriptor must be 214 bytes"); + + // HID class descriptor (matches real adapter: bcdHID 1.10, report len 214) + static const uint8_t HID_DESCRIPTORS[] = + { + 0x09, // bLength + 0x21, // bDescriptorType (HID) + 0x10, 0x01, // bcdHID 1.10 + 0x00, // bCountryCode + 0x01, // bNumDescriptors + 0x22, // bDescriptorType[0] (Report) + 0xD6, 0x00, // wDescriptorLength 214 + }; + + // Raw configuration descriptor (matches real adapter: IN 37 bytes, OUT 5 bytes, bInterval 8, bmAttributes 0xe0) + static const uint8_t CONFIGURATION_DESCRIPTORS[] = + { + // Configuration descriptor + 0x09, // bLength + 0x02, // bDescriptorType (Configuration) + 0x29, 0x00, // wTotalLength 41 + 0x01, // bNumInterfaces 1 + 0x01, // bConfigurationValue 1 + 0x00, // iConfiguration (String Index) + 0xE0, // bmAttributes (Self Powered | Remote Wakeup) + 0xFA, // bMaxPower 500mA (0xFA * 2mA) + + // Interface descriptor (HID) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x00, // bInterfaceNumber 0 + 0x00, // bAlternateSetting 0 + 0x02, // bNumEndpoints 2 + 0x03, // bInterfaceClass (HID) + 0x00, // bInterfaceSubClass + 0x00, // bInterfaceProtocol + 0x00, // iInterface (String Index) + + // HID descriptor + 0x09, // bLength + 0x21, // bDescriptorType (HID) + 0x10, 0x01, // bcdHID 1.10 + 0x00, // bCountryCode + 0x01, // bNumDescriptors + 0x22, // bDescriptorType[0] (Report) + 0xD6, 0x00, // wDescriptorLength 214 + + // Endpoint descriptor (OUT 0x02, 5 bytes, interval 8) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x02, // bEndpointAddress (OUT) + 0x03, // bmAttributes (Interrupt) + 0x05, 0x00, // wMaxPacketSize 5 + 0x08, // bInterval 8 + + // Endpoint descriptor (IN 0x81, 37 bytes, interval 8) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x81, // bEndpointAddress (IN) + 0x03, // bmAttributes (Interrupt) + 0x25, 0x00, // wMaxPacketSize 37 + 0x08, // bInterval 8 + }; + +}; // namespace WiiU + +#endif // _WIIU_DESCRIPTORS_H_ diff --git a/Firmware/RP2040/src/Gamepad/Gamepad.h b/Firmware/RP2040/src/Gamepad/Gamepad.h index 9f8cca5e..e6348cb5 100644 --- a/Firmware/RP2040/src/Gamepad/Gamepad.h +++ b/Firmware/RP2040/src/Gamepad/Gamepad.h @@ -21,8 +21,8 @@ class Gamepad { public: + //Defaults used by device to get buttons - static constexpr uint8_t DPAD_UP = 0x01; static constexpr uint8_t DPAD_DOWN = 0x02; static constexpr uint8_t DPAD_LEFT = 0x04; diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h index 1fae44ec..5cfd7443 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/DeviceDriverTypes.h @@ -14,6 +14,7 @@ enum class DeviceDriverType : uint8_t DINPUT, PSCLASSIC, SWITCH, + WIIU, WEBAPP = 100, UART_BRIDGE }; diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp index df1dddde..2604e347 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/Switch/Switch.cpp @@ -72,10 +72,10 @@ void SwitchDevice::process(const uint8_t idx, Gamepad& gamepad) if (gp_in.buttons & Gamepad::BUTTON_R3) in_report.buttons |= SwitchWired::Buttons::R3; if (gp_in.buttons & Gamepad::BUTTON_SYS) in_report.buttons |= SwitchWired::Buttons::HOME; if (gp_in.buttons & Gamepad::BUTTON_MISC) in_report.buttons |= SwitchWired::Buttons::CAPTURE; - + if (gp_in.trigger_l) in_report.buttons |= SwitchWired::Buttons::ZL; if (gp_in.trigger_r) in_report.buttons |= SwitchWired::Buttons::ZR; - + in_report.joystick_lx = Scale::int16_to_uint8(gp_in.joystick_lx); in_report.joystick_ly = Scale::int16_to_uint8(gp_in.joystick_ly); in_report.joystick_rx = Scale::int16_to_uint8(gp_in.joystick_rx); diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp new file mode 100644 index 00000000..6f5a8c20 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp @@ -0,0 +1,165 @@ +#include + +#include "Board/ogxm_log.h" +#include "USBDevice/DeviceDriver/WiiU/WiiU.h" + +// Set to 1 to bypass init gate: always send reports for testing (Dolphin/Linux may not send 0x13). +#ifndef WIIU_SKIP_INIT_GATE +#define WIIU_SKIP_INIT_GATE 1 +#endif + +void WiiUDevice::initialize() +{ + class_driver_ = + { + .name = TUD_DRV_NAME("WIIU"), + .init = hidd_init, + .deinit = hidd_deinit, + .reset = hidd_reset, + .open = hidd_open, + .control_xfer_cb = hidd_control_xfer_cb, + .xfer_cb = hidd_xfer_cb, + .sof = NULL + }; + + in_report_.report_id = WiiU::HID_REPORT_ID; + std::memset(in_report_.port_data, 0, sizeof(in_report_.port_data)); + init_received_ = false; +} + +// GC stick deadzone: values near center map to 128 to avoid spurious LEFT/RIGHT/UP/DOWN +static constexpr int16_t STICK_DEADZONE = 4096; // ~12.5% of -32768..32767 + +static uint8_t stick_to_gc(int16_t value) +{ + if (value > -STICK_DEADZONE && value < STICK_DEADZONE) + return WiiU::STICK_CENTER; + return Scale::int16_to_uint8(value); +} + +static void fill_port_block(uint8_t port[9], const Gamepad::PadIn& gp_in) +{ + // Byte 0 (type): Bit 4 = Wired. Dolphin: (type & 0x10) != 0 → connected. + port[0] = WiiU::PORT_WIRED; + + // Byte 1 (b1): A, B, X, Y, D-pad Left, Right, Down, Up — direct mapping (A→A, B→B, etc.) + uint8_t b1 = 0; + if (gp_in.buttons & Gamepad::BUTTON_B) b1 |= WiiU::GCButtons::A; + if (gp_in.buttons & Gamepad::BUTTON_A) b1 |= WiiU::GCButtons::B; + if (gp_in.buttons & Gamepad::BUTTON_Y) b1 |= WiiU::GCButtons::X; + if (gp_in.buttons & Gamepad::BUTTON_X) b1 |= WiiU::GCButtons::Y; + if (gp_in.dpad & Gamepad::DPAD_LEFT) b1 |= WiiU::GCButtons::LEFT; + if (gp_in.dpad & Gamepad::DPAD_RIGHT) b1 |= WiiU::GCButtons::RIGHT; + if (gp_in.dpad & Gamepad::DPAD_DOWN) b1 |= WiiU::GCButtons::DOWN; + if (gp_in.dpad & Gamepad::DPAD_UP) b1 |= WiiU::GCButtons::UP; + port[1] = b1; + + // Byte 2 (b2): Start, Z, R (digital), L (digital) + // L/R come from triggers at 255; Z from RB; LB maps to nothing + uint8_t b2 = 0; + if (gp_in.buttons & Gamepad::BUTTON_START) b2 |= WiiU::GCButtons::START; + if (gp_in.buttons & Gamepad::BUTTON_RB) b2 |= WiiU::GCButtons::Z; + if (gp_in.trigger_l >= 255) b2 |= WiiU::GCButtons::L; + if (gp_in.trigger_r >= 255) b2 |= WiiU::GCButtons::R; + port[2] = b2; + + // Bytes 3–4: main stick X, Y (0–255, 128 = center) — Y axis inverted + port[3] = stick_to_gc(gp_in.joystick_lx); + port[4] = stick_to_gc(-gp_in.joystick_ly); + // Bytes 5–6: C-stick (substick) X, Y — Y axis inverted + port[5] = stick_to_gc(gp_in.joystick_rx); + port[6] = stick_to_gc(-gp_in.joystick_ry); + // Bytes 7–8: L/R trigger analog (0–255) + port[7] = gp_in.trigger_l; + port[8] = gp_in.trigger_r; +} + +void WiiUDevice::process(const uint8_t idx, Gamepad& gamepad) +{ + if (idx >= 4) + return; + + // Always update from current gamepad state so late controller connections are reflected immediately. + // Dolphin may stop re-checking after seeing initial zeros; filling every frame avoids that. + // Combo check (check_for_driver_change) also uses get_pad_in(); both see the same current state. + Gamepad::PadIn gp_in = gamepad.get_pad_in(); + fill_port_block(in_report_.port_data[idx], gp_in); + + if (tud_suspended()) + tud_remote_wakeup(); + + // Send once per frame after the last port, only when init received (or WIIU_SKIP_INIT_GATE) and endpoint ready. + // Real adapter does not send reports until host sends Start Polling (0x13). + constexpr uint8_t last_port = (MAX_GAMEPADS >= 4) ? 3 : (MAX_GAMEPADS - 1); + if (idx == last_port && (WIIU_SKIP_INIT_GATE || init_received_) && tud_hid_n_ready(0)) + { + // TinyUSB prepends report_id; pass only port_data (36 bytes) to avoid double report ID + tud_hid_n_report(0, WiiU::HID_REPORT_ID, in_report_.port_data, sizeof(in_report_.port_data)); +#if defined(CONFIG_OGXM_DEBUG) + static uint32_t send_count = 0; + if (++send_count % 500 == 0) + OGXM_LOG("WiiU: IN report sent (count " + std::to_string(send_count) + ")\n"); +#endif + } +} + +uint16_t WiiUDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) +{ + if (report_type != HID_REPORT_TYPE_INPUT || report_id != WiiU::HID_REPORT_ID || itf != 0) + return 0; + if (reqlen < sizeof(WiiU::InReport)) + return 0; + std::memcpy(buffer, &in_report_, sizeof(WiiU::InReport)); + return sizeof(WiiU::InReport); +} + +void WiiUDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) +{ + // Host sends Start Polling (0x13) via Output report. May be [0x13] or [0x21, 0x13] if report ID prepended. + bool is_init = (bufsize >= 1 && buffer[0] == WiiU::INIT_PAYLOAD) || + (bufsize >= 2 && buffer[1] == WiiU::INIT_PAYLOAD); + if (is_init) + { + init_received_ = true; +#if defined(CONFIG_OGXM_DEBUG) + OGXM_LOG("WiiU: init 0x13 received from host\n"); +#endif + } + (void)itf; + (void)report_id; + (void)report_type; +} + +bool WiiUDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +{ + (void)rhport; + (void)stage; + (void)request; + return false; +} + +const uint16_t* WiiUDevice::get_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + const char *value = reinterpret_cast(WiiU::STRING_DESCRIPTORS[index]); + return get_string_descriptor(value, index); +} + +const uint8_t* WiiUDevice::get_descriptor_device_cb() +{ + return WiiU::DEVICE_DESCRIPTORS; +} + +const uint8_t* WiiUDevice::get_hid_descriptor_report_cb(uint8_t itf) +{ + return WiiU::REPORT_DESCRIPTORS; +} + +const uint8_t* WiiUDevice::get_descriptor_configuration_cb(uint8_t index) +{ + return WiiU::CONFIGURATION_DESCRIPTORS; +} + +const uint8_t* WiiUDevice::get_descriptor_device_qualifier_cb() +{ + return nullptr; +} diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h new file mode 100644 index 00000000..537a12e3 --- /dev/null +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.h @@ -0,0 +1,28 @@ +#ifndef _WIIU_DEVICE_H_ +#define _WIIU_DEVICE_H_ + +#include + +#include "USBDevice/DeviceDriver/DeviceDriver.h" +#include "Descriptors/WiiU.h" + +class WiiUDevice : public DeviceDriver +{ +public: + void initialize() override; + void process(const uint8_t idx, Gamepad& gamepad) override; + uint16_t get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer, uint16_t reqlen) override; + void set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) override; + bool vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) override; + const uint16_t* get_descriptor_string_cb(uint8_t index, uint16_t langid) override; + const uint8_t* get_descriptor_device_cb() override; + const uint8_t* get_hid_descriptor_report_cb(uint8_t itf) override; + const uint8_t* get_descriptor_configuration_cb(uint8_t index) override; + const uint8_t* get_descriptor_device_qualifier_cb() override; + +private: + WiiU::InReport in_report_{}; + bool init_received_{ false }; // true after host sends Start Polling (0x13) +}; + +#endif // _WIIU_DEVICE_H_ diff --git a/Firmware/RP2040/src/USBDevice/DeviceManager.cpp b/Firmware/RP2040/src/USBDevice/DeviceManager.cpp index ef660535..76e8e6c4 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceManager.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceManager.cpp @@ -4,6 +4,7 @@ #include "USBDevice/DeviceDriver/PSClassic/PSClassic.h" #include "USBDevice/DeviceDriver/XInput/XInput.h" #include "USBDevice/DeviceDriver/Switch/Switch.h" +#include "USBDevice/DeviceDriver/WiiU/WiiU.h" #include "USBDevice/DeviceDriver/DInput/DInput.h" #include "USBDevice/DeviceDriver/PS3/PS3.h" #include "USBDevice/DeviceDriver/XboxOG/XboxOG_GP.h" @@ -20,40 +21,56 @@ void DeviceManager::initialize_driver( DeviceDriverType driver_type, Gamepad(&gamepads)[MAX_GAMEPADS]) { //TODO: Put gamepad setup in the drivers themselves bool has_analog = false; + + printf("Attempting to allocate driver\n"); switch (driver_type) { case DeviceDriverType::DINPUT: + printf("DINPUT Loaded\n"); has_analog = true; device_driver_ = std::make_unique(); break; case DeviceDriverType::PS3: + printf("PS3 Loaded\n"); has_analog = true; device_driver_ = std::make_unique(); break; case DeviceDriverType::PSCLASSIC: + printf("PSCLASSIC Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::SWITCH: + printf("SWITCH Loaded\n"); device_driver_ = std::make_unique(); break; + case DeviceDriverType::WIIU: + printf("WIIU Loaded\n"); + device_driver_ = std::make_unique(); + break; case DeviceDriverType::XINPUT: + printf("XINPUT Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::XBOXOG: + printf("XBOXOG Loaded\n"); has_analog = true; device_driver_ = std::make_unique(); break; case DeviceDriverType::XBOXOG_SB: + printf("XBOXOG SB Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::XBOXOG_XR: + printf("XBOXOG XR Loaded\n"); device_driver_ = std::make_unique(); break; case DeviceDriverType::WEBAPP: + printf("WEBAPP Loaded\n"); device_driver_ = std::make_unique(); break; #if defined(CONFIG_EN_UART_BRIDGE) case DeviceDriverType::UART_BRIDGE: + printf("UART Loaded\n"); device_driver_ = std::make_unique(); break; #endif //defined(CONFIG_EN_UART_BRIDGE) diff --git a/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp b/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp index c463ff98..b6ee0d4d 100644 --- a/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp +++ b/Firmware/RP2040/src/USBDevice/tud_callbacks.cpp @@ -20,7 +20,8 @@ uint16_t tud_hid_get_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) { DeviceManager::get_instance().get_driver()->set_report_cb(itf, report_id, report_type, buffer, bufsize); - tud_hid_report(report_id, buffer, bufsize); + // Do not echo received output back as input report (e.g. Wii U GC adapter init 0x13 must only be received; + // TinyUSB re-arms the OUT endpoint after this returns so the host can keep sending 0x13). } bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) diff --git a/Firmware/RP2040/src/USBHost/HardwareIDs.h b/Firmware/RP2040/src/USBHost/HardwareIDs.h index ccb924b9..faff4776 100644 --- a/Firmware/RP2040/src/USBHost/HardwareIDs.h +++ b/Firmware/RP2040/src/USBHost/HardwareIDs.h @@ -18,7 +18,7 @@ static const HardwareID DINPUT_IDS[] = {0x0810, 0x0003}, // Personal Communication Systems, Inc. Generic {0x146B, 0x0902}, // BigBen Interactive Wired Mini PS3 Game Controller {0x2563, 0x0575}, // SHANWAN 2In1 USB Joystick - {0x046D, 0xC218} // Logitech RumblePad 2 + {0x046D, 0xC218}, // Logitech RumblePad 2 }; static const HardwareID PS3_IDS[] = @@ -51,6 +51,7 @@ static const HardwareID PSCLASSIC_IDS[] = static const HardwareID SWITCH_PRO_IDS[] = { {0x057E, 0x2009}, // Switch Pro + {0x057E, 0x0330} // Wii U Pro // {0x20D6, 0xA711}, // OpenSteamController, emulated pro controller }; @@ -67,34 +68,6 @@ static const HardwareID N64_IDS[] = {0x0079, 0x0006} // Retrolink N64 USB gamepad }; -static const HardwareID WIIU_PRO_IDS[] = -{ - {0x057E, 0x0330} // Wii U Pro Wireless Controller -}; - -static const HardwareID OUYA_IDS[] = -{ - {0x2836, 0x0001} // OUYA Controller -}; - -static const HardwareID Bit_Ult_IDS[] = -{ - {0x057E, 0x2009} // 8Bit Ultimate Bluetooth Controller -}; - -static const HardwareID Wii_IDS[] = -{ - {0x057e, 0x0306} // Wii Motion Plus Controller - -}; - -static const HardwareID Switch_Joycon_IDS[] = -{ - {0x057e, 0x2007} // Switch Joycon (R) - {0x057e, 0x2006} // Switch Joycon (L) - -}; - struct HostTypeMap { const HardwareID* ids; @@ -112,10 +85,6 @@ static const HostTypeMap HOST_TYPE_MAP[] = { SWITCH_PRO_IDS, sizeof(SWITCH_PRO_IDS) / sizeof(HardwareID), HostDriverType::SWITCH_PRO }, { PSCLASSIC_IDS, sizeof(PSCLASSIC_IDS) / sizeof(HardwareID), HostDriverType::PSCLASSIC }, { N64_IDS, sizeof(N64_IDS) / sizeof(HardwareID), HostDriverType::N64 }, - { WIIU_PRO_IDS, sizeof(WIIU_PRO_IDS) / sizeof(HardwareID), HostDriverType::WIIUPRO }, - { Bit_Ult_IDS, sizeof(Bit_Ult_IDS) / sizeof(HardwareID), HostDriverType::Bit_Ult }, - { Wii_IDS, sizeof(Wii_IDS) / sizeof(HardwareID), HostDriverType::Wii }, - { OUYA_IDS, sizeof(OUYA_IDS) / sizeof(HardwareID), HostDriverType::OUYA }, }; #endif // _HW_ID_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp b/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp index b5c6210a..5f821c92 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/SwitchWired/SwitchWired.cpp @@ -7,6 +7,7 @@ void SwitchWiredHost::initialize(Gamepad& gamepad, uint8_t address, uint8_t instance, const uint8_t* report_desc, uint16_t desc_len) { + tuh_hid_receive_report(address, instance); } @@ -64,9 +65,9 @@ void SwitchWiredHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t if (in_report->buttons & SwitchWired::Buttons::L3) gp_in.buttons |= gamepad.MAP_BUTTON_L3; if (in_report->buttons & SwitchWired::Buttons::R3) gp_in.buttons |= gamepad.MAP_BUTTON_R3; - gp_in.trigger_l = (in_report->buttons & SwitchWired::Buttons::ZL) ? Range::MAX : Range::MIN; - gp_in.trigger_r = (in_report->buttons & SwitchWired::Buttons::ZR) ? Range::MAX : Range::MIN; - + if (in_report->buttons & SwitchWired::Buttons::ZR) gp_in.trigger_r = Range::MAX; + if (in_report->buttons & SwitchWired::Buttons::ZL) gp_in.trigger_l = Range::MAX; + std::tie(gp_in.joystick_lx, gp_in.joystick_ly) = gamepad.scale_joystick_l(in_report->joystick_lx, in_report->joystick_ly); std::tie(gp_in.joystick_rx, gp_in.joystick_ry) = gamepad.scale_joystick_r(in_report->joystick_rx, in_report->joystick_ry); diff --git a/Firmware/RP2040/src/USBHost/HostManager.h b/Firmware/RP2040/src/USBHost/HostManager.h index e8638fb2..eaf05c39 100644 --- a/Firmware/RP2040/src/USBHost/HostManager.h +++ b/Firmware/RP2040/src/USBHost/HostManager.h @@ -70,48 +70,63 @@ class HostManager Device& device_slot = device_slots_[dev_idx]; Interface& interface = device_slot.interfaces[instance]; + debug_printf("Attempting to allocate driver for index %d\n", gp_idx); + switch (driver_type) { case HostDriverType::PS5: + debug_printf("PS5 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::PS4: + debug_printf("PS4 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::PS3: + debug_printf("PS3 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::DINPUT: + debug_printf("DINPUT Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::SWITCH: + debug_printf("SWITCH Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::SWITCH_PRO: + debug_printf("SWITCH PRO Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::N64: + debug_printf("N64 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::PSCLASSIC: + debug_printf("PSCLASSIC Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOXOG: + debug_printf("XBOXOG Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOXONE: + debug_printf("XBOXONE Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOX360: + debug_printf("XBOX360 Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; case HostDriverType::XBOX360W: //Composite device, takes up all 4 gamepads when mounted + debug_printf("XBOX360W Loaded\n"); fflush(stdout); interface.driver = std::make_unique(gp_idx); break; default: if (is_hid_gamepad(report_desc, desc_len)) { interface.driver = std::make_unique(gp_idx); + debug_printf("HIDHOST Loaded\n"); fflush(stdout); } else { diff --git a/Firmware/RP2040/src/UserSettings/UserSettings.cpp b/Firmware/RP2040/src/UserSettings/UserSettings.cpp index b0cd1805..ec75d608 100644 --- a/Firmware/RP2040/src/UserSettings/UserSettings.cpp +++ b/Firmware/RP2040/src/UserSettings/UserSettings.cpp @@ -18,6 +18,7 @@ namespace ButtonCombo { static constexpr uint32_t DINPUT = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_RB, Gamepad::DPAD_LEFT); static constexpr uint32_t XINPUT = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_UP); static constexpr uint32_t SWITCH = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_DOWN); + static constexpr uint32_t WIIU = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB, Gamepad::DPAD_DOWN); static constexpr uint32_t XBOXOG = BUTTON_COMBO(Gamepad::BUTTON_START, Gamepad::DPAD_RIGHT); static constexpr uint32_t XBOXOG_SB = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_RB, Gamepad::DPAD_RIGHT); static constexpr uint32_t XBOXOG_XR = BUTTON_COMBO(Gamepad::BUTTON_START | Gamepad::BUTTON_LB, Gamepad::DPAD_RIGHT); @@ -32,6 +33,7 @@ static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { DeviceDriverType::XINPUT, DeviceDriverType::PS3, DeviceDriverType::PSCLASSIC, + DeviceDriverType::WIIU, DeviceDriverType::WEBAPP, #if defined(XREMOTE_ROM_AVAILABLE) DeviceDriverType::XBOXOG_XR, @@ -40,6 +42,7 @@ static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { #elif MAX_GAMEPADS > 1 DeviceDriverType::DINPUT, DeviceDriverType::SWITCH, + DeviceDriverType::WIIU, DeviceDriverType::WEBAPP, #else // MAX_GAMEPADS == 1 @@ -47,6 +50,7 @@ static constexpr DeviceDriverType VALID_DRIVER_TYPES[] = { DeviceDriverType::XBOXOG_SB, DeviceDriverType::DINPUT, DeviceDriverType::SWITCH, + DeviceDriverType::WIIU, DeviceDriverType::WEBAPP, DeviceDriverType::PS3, DeviceDriverType::PSCLASSIC, @@ -63,13 +67,14 @@ struct ComboMap { DeviceDriverType driver; }; -static constexpr std::array BUTTON_COMBO_MAP = {{ +static constexpr std::array BUTTON_COMBO_MAP = {{ { ButtonCombo::XBOXOG, DeviceDriverType::XBOXOG }, { ButtonCombo::XBOXOG_SB, DeviceDriverType::XBOXOG_SB }, { ButtonCombo::XBOXOG_XR, DeviceDriverType::XBOXOG_XR }, { ButtonCombo::WEBAPP, DeviceDriverType::WEBAPP }, { ButtonCombo::DINPUT, DeviceDriverType::DINPUT }, { ButtonCombo::SWITCH, DeviceDriverType::SWITCH }, + { ButtonCombo::WIIU, DeviceDriverType::WIIU }, { ButtonCombo::XINPUT, DeviceDriverType::XINPUT }, { ButtonCombo::PS3, DeviceDriverType::PS3 }, { ButtonCombo::PSCLASSIC, DeviceDriverType::PSCLASSIC } diff --git a/Firmware/RP2040/src/main.cpp b/Firmware/RP2040/src/main.cpp index afdb49d4..51f2254e 100644 --- a/Firmware/RP2040/src/main.cpp +++ b/Firmware/RP2040/src/main.cpp @@ -1,8 +1,19 @@ #include #include "OGXMini/OGXMini.h" +#include "pico/stdlib.h" +#include "pico/flash.h" +#include int main() { + stdio_init_all(); + uart_init(uart0, 115200); // Initialize UART0 at 115200 baud + gpio_set_function(0, GPIO_FUNC_UART); // TX + gpio_set_function(1, GPIO_FUNC_UART); // RX + printf("Debug ready\n"); + + flash_safe_execute_core_init(); + OGXMini::initialize(); OGXMini::run(); return 0; From 8a3d938fd35498432789436668013af64d04f19c Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Sun, 8 Feb 2026 05:24:42 -0500 Subject: [PATCH 10/13] Revise README with platform support and features Updated supported platforms and added new features for Wii U and Switch. --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a36b0db7..ca738846 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ Firmware for the RP2040, capable of emulating gamepads for several game consoles ## Supported platforms - Original Xbox - Playstation 3 -- Nintendo Switch (docked) +- Nintendo Switch 1 & 2 (docked) - XInput (use [**UsbdSecPatch**](https://github.com/InvoxiPlayGames/UsbdSecPatch) for Xbox 360, or select the patch in J-Runner while flashing your NAND) - Playstation Classic - DInput +- Wii U (GameCube Adapter) ## Changing platforms By default the OGX-Mini will emulate an OG Xbox controller, you must hold a button combo for 3 seconds to change which platform you want to play on. Your chosen mode will persist after powering off the device. @@ -32,9 +33,12 @@ Start = Plus (Switch) = Options (Dualsense/DS4) - Start + Dpad Left - PlayStation Classic - Start + A (Cross for PlayStation and B for Switch gamepads) +- Wii U (GameCube Adapter) + - Start + Left Bumper + D-Pad Down - Web Application Mode - Start + Left Bumper + Right Bumper + After a new mode is stored, the RP2040 will reset itself so you don't need to unplug it. ## Disconnecting Controllers @@ -83,6 +87,7 @@ Please visit [**this page**](https://bluepad32.readthedocs.io/en/latest/supporte # Features new to this fork: Note: These features have been added to the Pico W/ Pico 2 W firmware support, I do not have the other boards to test and implement the same fixes at this time. +- Ability to emulate the GameCube controller adapter for Wii U. (Also tested on Switch 1/ 2) - Wii U & Switch Pro controllers are supported fully with working LT and RT - Wii Remotes are supported along with the following controllers connected: Nunchuck/ GameCube/ Wii Gamepad. - Disconnection Combo has been added: Start+Select for most controllers, L3+R3 for OUYA controllers. @@ -116,7 +121,6 @@ Note: These features have been added to the Pico W/ Pico 2 W firmware support, I ## Planned additions for this fork - Output to the following consoles: - GameCube - - Wii U (GameCube controller adapter) - PS2 - DreamCast - NES From e222300c9c4cfb5d5dc70bbb95bd084acfd48c09 Mon Sep 17 00:00:00 2001 From: Gary Date: Thu, 12 Feb 2026 07:10:09 -0500 Subject: [PATCH 11/13] Fixed a bug with Wii U mode that caused pushing Left stick up to input as down --- .../docs/XINPUT_JOYPAD_OS_COMPARISON.md | 160 ++++++++++ Firmware/RP2040/src/Bluepad32/Bluepad32.cpp | 6 +- Firmware/RP2040/src/Bluepad32/Bluepad32.h | 1 + Firmware/RP2040/src/Descriptors/XInput.h | 78 ++--- Firmware/RP2040/src/Descriptors/XboxOne.h | 300 +++++++++++++++++- Firmware/RP2040/src/Gamepad/Gamepad.h | 6 + .../src/USBDevice/DeviceDriver/WiiU/WiiU.cpp | 33 +- .../USBDevice/DeviceDriver/XInput/XInput.cpp | 7 +- .../XInput/tud_xinput/tud_xinput.cpp | 115 ++++--- .../XInput/tud_xinput/tud_xinput.h | 5 +- .../src/USBHost/HostDriver/XInput/XboxOne.cpp | 35 +- Firmware/RP2040/src/USBHost/HostManager.h | 11 + 12 files changed, 602 insertions(+), 155 deletions(-) create mode 100644 Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md diff --git a/Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md b/Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md new file mode 100644 index 00000000..dadbbac2 --- /dev/null +++ b/Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md @@ -0,0 +1,160 @@ +# XInput (Xbox 360) Report & Descriptor Comparison: OGX-Mini vs joypad-os + +This document compares the XInput device implementation in this project (OGX-Mini) with [joypad-os](https://github.com/joypad-ai/joypad-os) so we can align behavior and add **real Xbox 360 console** support. + +--- + +## 1. Input report (20 bytes) — byte-by-byte + +Both projects use the **same 20-byte input report layout**. Field names differ; byte positions and semantics match. + +| Byte | OGX-Mini (`XInput::InReport`) | joypad-os (`xinput_in_report_t`) | Notes | +|------|------------------------------|-----------------------------------|--------| +| 0 | `report_id` | `report_id` | Always `0x00` | +| 1 | `report_size` | `report_size` | Always `0x14` (20) | +| 2 | `buttons[0]` | `buttons0` | D-pad (0–3), Start (4), Back (5), L3 (6), R3 (7) | +| 3 | `buttons[1]` | `buttons1` | LB (0), RB (1), Guide (2), — (3), A (4), B (5), X (6), Y (7) | +| 4 | `trigger_l` | `trigger_l` | 0–255 | +| 5 | `trigger_r` | `trigger_r` | 0–255 | +| 6–7 | `joystick_lx` | `stick_lx` | int16 LE (-32768..32767) | +| 8–9 | `joystick_ly` | `stick_ly` | int16 LE (Y inverted: positive = up) | +| 10–11 | `joystick_rx` | `stick_rx` | int16 LE | +| 12–13 | `joystick_ry` | `stick_ry` | int16 LE (Y inverted) | +| 14–19 | `reserved[6]` | `reserved[6]` | Zero padding | + +**Conclusion:** No change needed for the **input report format**. OGX-Mini’s mapping in `XInput.cpp` already matches joypad-os’s `xinput_mode.c` (same button bits and axis handling, including Y inversion). + +--- + +## 2. Button bit definitions — identical + +| Button | Byte | Bit | OGX-Mini | joypad-os | +|--------|------|-----|----------|-----------| +| D-pad U/D/L/R | 2 | 0–3 | `Buttons0::DPAD_*` | `XINPUT_BTN_DPAD_*` | +| Start | 2 | 4 | `Buttons0::START` | `XINPUT_BTN_START` | +| Back | 2 | 5 | `Buttons0::BACK` | `XINPUT_BTN_BACK` | +| L3 / R3 | 2 | 6–7 | `Buttons0::L3/R3` | `XINPUT_BTN_L3/R3` | +| LB / RB | 3 | 0–1 | `Buttons1::LB/RB` | `XINPUT_BTN_LB/RB` | +| Guide | 3 | 2 | `Buttons1::HOME` | `XINPUT_BTN_GUIDE` | +| A / B / X / Y | 3 | 4–7 | `Buttons1::A/B/X/Y` | `XINPUT_BTN_A/B/X/Y` | + +No code changes required for button mapping. + +--- + +## 3. Output report (8 bytes) — identical + +| Byte | Content | OGX-Mini | joypad-os | +|------|---------|----------|-----------| +| 0 | report_id | RUMBLE=0x00, LED=0x01 | Same | +| 1 | report_size | 0x08 | 0x08 | +| 2 | led | LED pattern | led | +| 3 | rumble_l | rumble_l | rumble_l | +| 4 | rumble_r | rumble_r | rumble_r | +| 5–7 | reserved | 0 | reserved[3] | + +No change needed for output report layout. + +--- + +## 4. Device descriptor — same VID/PID, minor bmAttributes + +| Field | OGX-Mini | joypad-os | +|-------|----------|-----------| +| idVendor | 0x045E | XINPUT_VID 0x045E | +| idProduct | 0x028E | XINPUT_PID 0x028E | +| bcdDevice | 0x0114 (2.14) | XINPUT_BCD_DEVICE 0x0114 | +| bmAttributes | 0x80 (no remote wakeup) | 0xA0 (remote wakeup) | + +Optional: set bmAttributes to `0xA0` in `DESC_DEVICE[]` if you want remote wakeup to match joypad-os and some 360 behavior. + +--- + +## 5. Configuration descriptor — main difference (PC vs 360 console) + +### OGX-Mini (current): PC-only, 1 interface + +- **wTotalLength:** 48 (0x30) +- **bNumInterfaces:** 1 +- **Interface 0:** Gamepad (0xFF / 0x5D / 0x01), 2 endpoints + - EP 0x81 IN (interrupt, 32 bytes, **bInterval 1**) + - EP 0x01 OUT (interrupt, 32 bytes, bInterval 8) +- Followed by a 16-byte vendor/HID-style descriptor (0x21 with bNumDescriptors, etc.). + +This is enough for **PC (XInput)** and many games. It is **not** enough for a **real Xbox 360 console**, which expects a multi-interface device and XSM3 security. + +### joypad-os: PC + Xbox 360 console, 4 interfaces + +- **wTotalLength:** 153 +- **bNumInterfaces:** 4 + +| Iface | Class / SubClass / Protocol | Purpose | Endpoints | +|-------|-----------------------------|---------|-----------| +| 0 | 0xFF / 0x5D / 0x01 | Gamepad | EP 0x81 IN (4 ms), EP 0x02 OUT (8 ms) | +| 1 | 0xFF / 0x5D / 0x03 | Audio (stub) | 4 EPs (not opened by driver) | +| 2 | 0xFF / 0x5D / 0x02 | Plugin (stub) | 1 EP (not opened) | +| 3 | 0xFF / **0xFD** / **0x13** | **Security (XSM3)** | 0 EPs (control only) | + +- Gamepad uses a **17-byte vendor descriptor** (0x21) after the interface, not the longer HID-style block. +- **Interface 3** is the security interface; the console uses **vendor control requests** on the device to run the XSM3 challenge/response. Without this interface and the XSM3 handler, the 360 console will not accept the device. + +--- + +## 6. What to add for real Xbox 360 console support + +To have OGX-Mini work on a **real Xbox 360** (like joypad-os), you need the following. + +### 6.1 Four-interface configuration descriptor + +- Replace the single-interface configuration in `Descriptors/XInput.h` with a **153-byte** configuration that includes: + - Interface 0: Gamepad (0xFF/0x5D/0x01) + 17-byte vendor descriptor + EP 0x81 IN (bInterval **4**), EP 0x02 OUT (8). + - Interface 1: Audio (0xFF/0x5D/0x03), stub only (descriptors present, no need to open EPs). + - Interface 2: Plugin (0xFF/0x5D/0x02), stub only. + - Interface 3: Security (0xFF/0xFD/0x13), no endpoints, security descriptor (0x41) only. + +You can copy the exact bytes from joypad-os’s `xinput_descriptors.h` (`xinput_config_descriptor[]`) and adapt to OGX-Mini’s naming (e.g. `DESC_CONFIGURATION[]` / `XINPUT_CONFIG_TOTAL_LEN`). + +### 6.2 XSM3 authentication + +- The 360 console sends **vendor control** requests (bRequest): + - **0x81** GET_SERIAL — device returns 29-byte ID (from libxsm3). + - **0x82** INIT_AUTH — console sends 34-byte challenge; device must respond with 46-byte response. + - **0x83** RESPOND — device returns challenge response (46 or 22 bytes). + - **0x84** KEEPALIVE — device responds with 0 bytes. + - **0x86** STATE — device returns 2-byte state (e.g. 2 = response ready). + - **0x87** VERIFY — console sends 22-byte verify; device responds with 22-byte response. + +- joypad-os uses **[libxsm3](https://github.com/InvoxiPlayGames/libxsm3)** (LGPL-2.1) to compute the challenge/response. You would need to: + - Add libxsm3 (or a port) to the OGX-Mini firmware. + - In the USB device stack, route **vendor control** transfers to an XInput/XSM3 handler (joypad-os does this in `tud_xinput_vendor_control_xfer_cb`). + - Implement the same state machine: on 0x82 store the challenge and call libxsm3 init; on 0x83 return the response; on 0x87 run verify and return the verify response; etc. + +### 6.3 Class driver `open()` for multiple interfaces + +- joypad-os’s `tud_xinput.c` `xinput_open()` is called for **each** interface. It: + - **Opens** only Interface 0 (gamepad): claims the interface and opens EP IN and EP OUT. + - **Skips** Interfaces 1 and 2 (audio, plugin): advances the descriptor pointer without opening endpoints. + - **Records** Interface 3 (security) index (e.g. `_sec_itf_num`) for XSM3; no endpoints. + +OGX-Mini’s `tud_xinput` currently assumes a single interface. It needs to be extended so that when the device presents the 4-interface configuration, the driver parses and skips the stub interfaces and only opens the gamepad endpoints, and recognizes the security interface (for routing vendor requests in usbd, not necessarily by interface number in the driver). + +### 6.4 Optional: IN endpoint interval + +- joypad-os uses **bInterval 4** (4 ms) for the gamepad IN endpoint; OGX-Mini uses **1** (1 ms). For 360 compatibility, changing the gamepad IN endpoint to **4** in the new configuration descriptor is a good idea. + +--- + +## 7. Summary table + +| Item | OGX-Mini | joypad-os | Action for 360 | +|------|----------|-----------|----------------| +| Input report (20 bytes) | Same layout | Same | None | +| Button bits | Same | Same | None | +| Output report (8 bytes) | Same | Same | None | +| VID/PID | 045E:028E | 045E:028E | None | +| Config: num interfaces | 1 | 4 | Add 4-interface, 153-byte config | +| Config: gamepad EP IN interval | 1 | 4 | Use 4 in new config | +| Vendor control (XSM3) | Not implemented | 0x81/82/83/84/86/87 | Add libxsm3 + vendor handler | +| Class driver multi-interface | Single iface | Opens if 0, skips 1–2, records 3 | Extend open() for 4-iface config | + +Implementing the 4-interface descriptor and XSM3 (with libxsm3) will align OGX-Mini with joypad-os’s XInput mode for **real Xbox 360 console** output while keeping the existing report code unchanged. diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp index 83df3b64..02cdc520 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.cpp @@ -381,7 +381,7 @@ uni_platform* get_driver() //Public API -void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) +void init(Gamepad(&gamepads)[MAX_GAMEPADS]) { for (uint8_t i = 0; i < MAX_GAMEPADS; ++i) { @@ -396,7 +396,11 @@ void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) led_timer_.context = nullptr; btstack_run_loop_set_timer(&led_timer_, LED_CHECK_TIME_MS); btstack_run_loop_add_timer(&led_timer_); +} +void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]) +{ + init(gamepads); btstack_run_loop_execute(); } diff --git a/Firmware/RP2040/src/Bluepad32/Bluepad32.h b/Firmware/RP2040/src/Bluepad32/Bluepad32.h index 19b13a21..c823ed92 100644 --- a/Firmware/RP2040/src/Bluepad32/Bluepad32.h +++ b/Firmware/RP2040/src/Bluepad32/Bluepad32.h @@ -11,4 +11,5 @@ namespace bluepad32 { void run_task(Gamepad(&gamepads)[MAX_GAMEPADS]); + void init(Gamepad(&gamepads)[MAX_GAMEPADS]); } \ No newline at end of file diff --git a/Firmware/RP2040/src/Descriptors/XInput.h b/Firmware/RP2040/src/Descriptors/XInput.h index 4f72b306..c625d889 100644 --- a/Firmware/RP2040/src/Descriptors/XInput.h +++ b/Firmware/RP2040/src/Descriptors/XInput.h @@ -172,7 +172,7 @@ namespace XInput static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; static const uint8_t STRING_MANUFACTURER[] = "Microsoft"; - static const uint8_t STRING_PRODUCT[] = "XInput STANDARD GAMEPAD"; + static const uint8_t STRING_PRODUCT[] = "Xbox 360 Controller"; static const uint8_t STRING_VERSION[] = "1.0"; static const uint8_t *DESC_STRING[] __attribute__((unused)) = @@ -180,7 +180,7 @@ namespace XInput STRING_LANGUAGE, STRING_MANUFACTURER, STRING_PRODUCT, - STRING_VERSION + STRING_VERSION, }; static const uint8_t DESC_DEVICE[] = @@ -188,68 +188,44 @@ namespace XInput 0x12, // bLength 0x01, // bDescriptorType (Device) 0x00, 0x02, // bcdUSB 2.00 - 0xFF, // bDeviceClass - 0xFF, // bDeviceSubClass - 0xFF, // bDeviceProtocol - 0x40, // bMaxPacketSize0 64 - 0x5E, 0x04, // idVendor 0x045E - 0x8E, 0x02, // idProduct 0x028E - 0x14, 0x01, // bcdDevice 2.14 + 0xFF, // bDeviceClass + 0xFF, // bDeviceSubClass + 0xFF, // bDeviceProtocol + 0x40, // bMaxPacketSize0 64 + 0x5E, 0x04, // idVendor 0x045E (Microsoft) + 0x8E, 0x02, // idProduct 0x028E (Xbox 360 Controller) + 0x14, 0x01, // bcdDevice 1.14 0x01, // iManufacturer (String Index) 0x02, // iProduct (String Index) 0x03, // iSerialNumber (String Index) 0x01, // bNumConfigurations 1 }; + // Single-interface XInput config (48 bytes) for PC + static constexpr uint16_t CONFIG_TOTAL_LENGTH = 48; + static const uint8_t DESC_CONFIGURATION[] = { - 0x09, // bLength - 0x02, // bDescriptorType (Configuration) - 0x30, 0x00, // wTotalLength 48 + // Configuration descriptor (9 bytes) + 0x09, 0x02, + 0x30, 0x00, // wTotalLength 48 (LE) 0x01, // bNumInterfaces 1 0x01, // bConfigurationValue - 0x00, // iConfiguration (String Index) - 0x80, // bmAttributes + 0x00, // iConfiguration + 0x80, // bmAttributes (bus powered) 0xFA, // bMaxPower 500mA - 0x09, // bLength - 0x04, // bDescriptorType (Interface) - 0x00, // bInterfaceNumber 0 - 0x00, // bAlternateSetting - 0x02, // bNumEndpoints 2 - 0xFF, // bInterfaceClass - 0x5D, // bInterfaceSubClass - 0x01, // bInterfaceProtocol - 0x00, // iInterface (String Index) - - 0x10, // bLength - 0x21, // bDescriptorType (HID) - // 0x10, 0x01, // bcdHID 1.10 - 0x00, 0x01, // bcdHID 1.00 - 0x01, // bCountryCode - 0x24, // bNumDescriptors - 0x81, // bDescriptorType[0] (Unknown 0x81) - 0x14, 0x03, // wDescriptorLength[0] 788 - 0x00, // bDescriptorType[1] (Unknown 0x00) - 0x03, 0x13, // wDescriptorLength[1] 4867 - 0x01, // bDescriptorType[2] (Unknown 0x02) - 0x00, 0x03, // wDescriptorLength[2] 768 - 0x00, // bDescriptorType[3] (Unknown 0x00) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x81, // bEndpointAddress (IN/D2H) - 0x03, // bmAttributes (Interrupt) - 0x20, 0x00, // wMaxPacketSize 32 - 0x01, // bInterval 1 (unit depends on device speed) - - 0x07, // bLength - 0x05, // bDescriptorType (Endpoint) - 0x01, // bEndpointAddress (OUT/H2D) - 0x03, // bmAttributes (Interrupt) - 0x20, 0x00, // wMaxPacketSize 32 - 0x08, // bInterval 8 (unit depends on device speed) + // Interface 0: Gamepad (0xFF/0x5D/0x01) + 0x09, 0x04, 0x00, 0x00, 0x02, 0xFF, 0x5D, 0x01, 0x00, + // Vendor descriptor (type 0x21, 16 bytes) + 0x10, 0x21, 0x00, 0x01, 0x01, 0x25, 0x81, 0x14, + 0x00, 0x00, 0x00, 0x00, 0x13, 0x02, 0x08, 0x00, + // EP 0x81 IN - Interrupt, 32 bytes, 1ms + 0x07, 0x05, 0x81, 0x03, 0x20, 0x00, 0x01, + // EP 0x01 OUT - Interrupt, 32 bytes, 8ms + 0x07, 0x05, 0x01, 0x03, 0x20, 0x00, 0x08, }; + static_assert(sizeof(DESC_CONFIGURATION) == CONFIG_TOTAL_LENGTH, "XInput config descriptor size"); }; #endif // _XINPUT_DESCRIPTORS_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Descriptors/XboxOne.h b/Firmware/RP2040/src/Descriptors/XboxOne.h index cda84eb7..2517b1ef 100644 --- a/Firmware/RP2040/src/Descriptors/XboxOne.h +++ b/Firmware/RP2040/src/Descriptors/XboxOne.h @@ -28,36 +28,302 @@ namespace XboxOne static constexpr uint8_t R3 = (1 << 7); }; + // GIP Command IDs + static constexpr uint8_t GIP_CMD_DEVICE_ARRIVAL = 0x02; + static constexpr uint8_t GIP_CMD_DEVICE_STATUS = 0x03; + static constexpr uint8_t GIP_CMD_DEVICE_DESCRIPTOR = 0x04; + static constexpr uint8_t GIP_CMD_VIRTUAL_KEY = 0x07; // Guide/Home; payload byte 4: 01=pressed, 00=released + static constexpr uint8_t GIP_CMD_SET_DEVICE_STATE = 0x09; + static constexpr uint8_t GIP_CMD_INPUT = 0x20; + static constexpr uint8_t GIP_CMD_SYSTEM_FOCUS_CHANGE = 0xE0; + + // GIP Header Flags + namespace GipFlags + { + static constexpr uint8_t NONE = 0x00; + static constexpr uint8_t INTERNAL = 0x20; // Core protocol message (request from host) + static constexpr uint8_t RESPONSE_FIRST = 0xF0; // First chunk of response (from device) + static constexpr uint8_t RESPONSE_CONTINUE = 0xA0; // Continuation chunk of response (from device) + } + + // GIP Battery Types + namespace BatteryType + { + static constexpr uint8_t WIRED = 0; + static constexpr uint8_t STANDARD = 1; + static constexpr uint8_t CHARGE_KIT = 2; + } + + // GIP Battery Levels + namespace BatteryLevel + { + static constexpr uint8_t LOW = 0; + static constexpr uint8_t MEDIUM = 1; + static constexpr uint8_t HIGH = 2; + static constexpr uint8_t FULL = 3; + } + + // GIP Interface GUIDs (for descriptor) + static constexpr uint8_t GUID_GAMEPAD[16] = { + 0x08, 0x2E, 0x40, 0x2C, 0x07, 0xDF, 0x45, 0xE1, + 0xA5, 0xAB, 0xA3, 0x12, 0x7A, 0xF1, 0x97, 0xB5 + }; + #pragma pack(push, 1) - struct InReport + + // GIP Message Header (8 bytes for USB GIP) + struct GipMessageHeader { - struct GipHeader - { - uint8_t command; - uint8_t client : 4; - uint8_t needsAck : 1; - uint8_t internal : 1; - uint8_t chunkStart : 1; - uint8_t chunked : 1; - uint8_t sequence; - uint8_t length; - } header; + uint8_t command; + uint8_t client : 4; + uint8_t needsAck : 1; + uint8_t internal : 1; + uint8_t chunkStart : 1; + uint8_t chunked : 1; + uint8_t sequence; + uint8_t length; + }; + static_assert(sizeof(GipMessageHeader) == 4, "GipMessageHeader must be 4 bytes"); - uint8_t buttons[2]; + // Device Arrival Message (GIP CMD 0x02) + struct DeviceArrivalMessage + { + GipMessageHeader header; + uint64_t deviceId; // Unique device identifier + uint16_t vendorId; // 0x045E + uint16_t productId; // 0x02D1 + struct { + uint16_t major; + uint16_t minor; + uint16_t build; + uint16_t revision; + } firmwareVersion; + uint8_t unknown[8]; // Padding to make total 32 bytes + }; + static_assert(sizeof(DeviceArrivalMessage) == 32, "DeviceArrivalMessage must be 32 bytes"); + + // Device Status Message (GIP CMD 0x03) + struct DeviceStatusMessage + { + GipMessageHeader header; + uint8_t batteryLevel : 2; + uint8_t batteryType : 2; + uint8_t unknown : 3; + uint8_t connected : 1; + uint8_t unknown2[3]; + }; + static_assert(sizeof(DeviceStatusMessage) == 8, "DeviceStatusMessage must be 8 bytes"); - uint16_t trigger_l; - uint16_t trigger_r; + // Device Descriptor Message (GIP CMD 0x04) + // Windows requests this to identify the device type (gamepad, chatpad, etc.) + // According to GIP spec, this is a variable-size message (typically 200-500 bytes) + // It contains a descriptor header with offsets to various metadata blocks + // NOTE: Vendor/product IDs are NOT in the descriptor header - they're only in Device Arrival message + struct DeviceDescriptorHeader + { + uint16_t unknown1; // 0x0293 (real controller value) + uint16_t unknown2; // 0x0010 (real controller value) + uint16_t unknown3; // 0x0001 (real controller value) + uint8_t padding[8]; // 8 bytes of zeros + uint16_t offset_externalCommands; + uint16_t offset_firmwareVersions; + uint16_t offset_audioFormats; + uint16_t offset_inputCommands; + uint16_t offset_outputCommands; + uint16_t offset_classNames; + uint16_t offset_interfaceGuids; + uint16_t offset_hidDescriptor; + uint16_t unknown4; // 0x004C (real controller value) + }; + // Note: Size may be 32 or 36 bytes depending on compiler alignment + // We'll use sizeof() in the code rather than hardcoding + + // Minimal Device Descriptor Message structure + // We'll build this dynamically to include at least the interface GUID + struct DeviceDescriptorMessage + { + GipMessageHeader header; // 4 bytes + DeviceDescriptorHeader descHeader; // 32 bytes + // Following buffer will contain metadata blocks + // For a minimal gamepad descriptor, we need at least: + // - Interface GUID block (16 bytes + 1 byte count = 17 bytes minimum) + uint8_t buffer[64]; // Buffer for metadata (can be expanded) + }; + + // 48-byte input report (4-byte header + 44-byte payload). Header: 0x20, 0x00, sequence, 0x2C. + // We put gamepad data at payload offset 0 (report bytes 4-17) for Linux xpad/GamepadTester. + // Real controller uses payload offset 6 for buttons; both formats are 48 bytes total. + static constexpr uint16_t GIP_INPUT_REPORT_SIZE = 48; + static constexpr uint8_t GIP_INPUT_PAYLOAD_SIZE = 44; // 0x2C + + // Input Report (GIP CMD 0x20) - logical gamepad state (we build 48-byte wire format from this) + struct InReport + { + GipMessageHeader header; + uint16_t buttons; // Bitfield: Sync, Menu, View, A, B, X, Y, Dpad, Shoulders, Thumbs + uint16_t trigger_l; // 0x000 to 0x3FF (0-1023) + uint16_t trigger_r; // 0x000 to 0x3FF (0-1023) int16_t joystick_lx; int16_t joystick_ly; int16_t joystick_rx; int16_t joystick_ry; + uint8_t guide_pressed; // Home/Guide: when set, report bytes 12-13 = 0xF5FE (real controller encoding) + }; + static_assert(sizeof(InReport) == 19, "XboxOne::InReport must be 19 bytes"); + + // Button bitfield for InReport.buttons (uint16_t) + // Order matches Linux evdev / GamepadTester / official Xbox One controller: + // B0=A, B1=B, B2=X, B3=Y, B4=LB, B5=RB, B6=LT, B7=RT, B8=Select, B9=Start, + // B10=L3, B11=R3, B12=DPAD_UP, B13=DPAD_DOWN, B14=DPAD_LEFT, B15=DPAD_RIGHT. (B16=Home separate.) + namespace GamepadButtons + { + static constexpr uint16_t A = 0x0001; // B0 + static constexpr uint16_t B = 0x0002; // B1 + static constexpr uint16_t X = 0x0004; // B2 + static constexpr uint16_t Y = 0x0008; // B3 + static constexpr uint16_t LEFT_SHOULDER = 0x0010; // B4 LB + static constexpr uint16_t RIGHT_SHOULDER = 0x0020; // B5 RB + static constexpr uint16_t LEFT_TRIGGER_BTN = 0x0040; // B6 LT (from trigger value) + static constexpr uint16_t RIGHT_TRIGGER_BTN = 0x0080; // B7 RT + static constexpr uint16_t VIEW = 0x0100; // B8 Select/Back + static constexpr uint16_t MENU = 0x0200; // B9 Start + static constexpr uint16_t LEFT_THUMB = 0x0400; // B10 L3 + static constexpr uint16_t RIGHT_THUMB = 0x0800; // B11 R3 + static constexpr uint16_t DPAD_UP = 0x1000; // B12 + static constexpr uint16_t DPAD_DOWN = 0x2000; // B13 + static constexpr uint16_t DPAD_LEFT = 0x4000; // B14 + static constexpr uint16_t DPAD_RIGHT = 0x8000; // B15 + // B16 = Home/Guide (evdev often as separate key; we can't fit in 16 bits here) + static constexpr uint16_t SYNC = 0x0000; // unused in 16-bit; map to KEY_PROG1 etc. if needed + } - uint8_t reserved[18]; // 18-byte padding at the end + // Set Device State / Force Feedback (GIP CMD 0x09) + struct OutReport + { + GipMessageHeader header; + uint8_t unknown1; + uint8_t flags; // Force feedback flags + uint8_t leftTrigger; // Left trigger vibration (0-255) + uint8_t rightTrigger; // Right trigger vibration (0-255) + uint8_t leftMotor; // Left motor (0-255) + uint8_t rightMotor; // Right motor (0-255) + uint8_t duration; // Duration in 10ms units + uint8_t delay; // Delay before starting + uint8_t repeat; // Repeat count }; - static_assert(sizeof(InReport) == 36, "XboxOne::InReport is not the correct size"); + static_assert(sizeof(OutReport) == 13, "XboxOne::OutReport must be 13 bytes"); + + // Force Feedback Flags + namespace ForceFeedbackFlags + { + static constexpr uint8_t RIGHT_MOTOR = 0x01; + static constexpr uint8_t LEFT_MOTOR = 0x02; + static constexpr uint8_t RIGHT_TRIGGER = 0x04; + static constexpr uint8_t LEFT_TRIGGER = 0x08; + } #pragma pack(pop) + // USB Descriptors + static const uint8_t STRING_LANGUAGE[] = { 0x09, 0x04 }; + static const uint8_t STRING_MANUFACTURER[] = "Microsoft"; + static const uint8_t STRING_PRODUCT[] = "Xbox One Controller"; + static const uint8_t STRING_SERIAL[] = "000000000000"; + static const uint8_t STRING_VERSION[] = "1.0"; + + static const uint8_t *STRING_DESCRIPTORS[] __attribute__((unused)) = + { + STRING_LANGUAGE, + STRING_MANUFACTURER, + STRING_PRODUCT, + STRING_SERIAL, + STRING_VERSION + }; + + // Device descriptor - Windows loads xb1usb for VID_045E&PID_02D1 (Xbox One Controller) + static const uint8_t DEVICE_DESCRIPTORS[] = + { + 0x12, // bLength + 0x01, // bDescriptorType (Device) + 0x00, 0x02, // bcdUSB 2.00 + 0xFF, // bDeviceClass (Vendor Specific) + 0x47, // bDeviceSubClass (71) + 0xD0, // bDeviceProtocol (208) + 0x40, // bMaxPacketSize0 64 + 0x5E, 0x04, // idVendor 0x045E (Microsoft) + 0xD1, 0x02, // idProduct 0x02D1 (Xbox One Controller - Windows compatible) + 0x16, 0x05, // bcdDevice 5.16 (match capture) + 0x01, // iManufacturer (String Index) + 0x02, // iProduct (String Index) + 0x03, // iSerialNumber (String Index) + 0x01, // bNumConfigurations 1 + }; + + // Configuration descriptor - match real controller: Interface 0 has BOTH GIP endpoints (0x02 OUT, 0x82 IN) + static const uint8_t CONFIGURATION_DESCRIPTORS[] = + { + // Configuration descriptor + 0x09, // bLength + 0x02, // bDescriptorType (Configuration) + 0x32, 0x00, // wTotalLength 50 bytes + 0x03, // bNumInterfaces 3 + 0x01, // bConfigurationValue 1 + 0x00, // iConfiguration (String Index) + 0xA0, // bmAttributes (Bus Powered, Remote Wakeup supported) + 0xFA, // bMaxPower 500mA + + // Interface 0 - GIP data (both IN and OUT on this interface, like real controller) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x00, // bInterfaceNumber 0 + 0x00, // bAlternateSetting 0 + 0x02, // bNumEndpoints 2 + 0xFF, // bInterfaceClass (Vendor Specific) + 0x47, // bInterfaceSubClass (71) + 0xD0, // bInterfaceProtocol (208) + 0x00, // iInterface (String Index) + + // Endpoint 0x02 OUT (first EP in descriptor = 0x02) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x02, // bEndpointAddress (OUT) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x04, // bInterval 4 + + // Endpoint 0x82 IN (second EP = 0x82) + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x82, // bEndpointAddress (IN) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x04, // bInterval 4 + + // Interface 1 - no endpoints (placeholder for 3-interface layout) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x01, // bInterfaceNumber 1 + 0x00, // bAlternateSetting 0 + 0x00, // bNumEndpoints 0 + 0xFF, // bInterfaceClass (Vendor Specific) + 0x47, // bInterfaceSubClass (71) + 0xD0, // bInterfaceProtocol (208) + 0x00, // iInterface (String Index) + + // Interface 2 - no endpoints (placeholder for 3-interface layout) + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x02, // bInterfaceNumber 2 + 0x00, // bAlternateSetting 0 + 0x00, // bNumEndpoints 0 + 0xFF, // bInterfaceClass (Vendor Specific) + 0x47, // bInterfaceSubClass (71) + 0xD0, // bInterfaceProtocol (208) + 0x00, // iInterface (String Index) + }; + + static constexpr uint16_t DRIVER_LEN = sizeof(CONFIGURATION_DESCRIPTORS); + }; // namespace XboxOne #endif // _XBOX_ONE_DESCRIPTORS_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/Gamepad/Gamepad.h b/Firmware/RP2040/src/Gamepad/Gamepad.h index e6348cb5..d223aad9 100644 --- a/Firmware/RP2040/src/Gamepad/Gamepad.h +++ b/Firmware/RP2040/src/Gamepad/Gamepad.h @@ -224,6 +224,10 @@ class Gamepad mutex_exit(&chatpad_in_mutex_); } + // Wii U GC adapter: set by host when controller uses positive Y for physical up (e.g. Xbox One/360) + void set_stick_y_positive_is_up(bool v) { stick_y_positive_is_up_ = v; } + bool stick_y_positive_is_up() const { return stick_y_positive_is_up_; } + inline void reset_pad_in() { mutex_enter_blocking(&pad_in_mutex_); @@ -359,6 +363,8 @@ class Gamepad bool profile_analog_enabled_{false}; + bool stick_y_positive_is_up_{false}; // true for Xbox (positive Y = up), false for Wii U/Nintendo + JoystickSettings joy_settings_l_; JoystickSettings joy_settings_r_; TriggerSettings trig_settings_l_; diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp index 6f5a8c20..6ae32692 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/WiiU/WiiU.cpp @@ -30,6 +30,9 @@ void WiiUDevice::initialize() // GC stick deadzone: values near center map to 128 to avoid spurious LEFT/RIGHT/UP/DOWN static constexpr int16_t STICK_DEADZONE = 4096; // ~12.5% of -32768..32767 +// Dolphin GameCube test reported max stick range: Left ±111, C-stick ~±110..112 (center 0). +// Our bytes 0–255 map to that via host; Y uses 1..254 to avoid full-up→down misread. + static uint8_t stick_to_gc(int16_t value) { if (value > -STICK_DEADZONE && value < STICK_DEADZONE) @@ -37,7 +40,18 @@ static uint8_t stick_to_gc(int16_t value) return Scale::int16_to_uint8(value); } -static void fill_port_block(uint8_t port[9], const Gamepad::PadIn& gp_in) +// Y axis only: map to 1..254 so 0/255 are never sent (some hosts treat extremes as wrong direction). +static uint8_t stick_y_to_gc(int16_t value) +{ + if (value > -STICK_DEADZONE && value < STICK_DEADZONE) + return WiiU::STICK_CENTER; + uint8_t u = Scale::int16_to_uint8(value); + if (u <= 1) return 1u; + if (u >= 254) return 254u; + return u; +} + +static void fill_port_block(uint8_t port[9], const Gamepad::PadIn& gp_in, bool stick_y_positive_is_up) { // Byte 0 (type): Bit 4 = Wired. Dolphin: (type & 0x10) != 0 → connected. port[0] = WiiU::PORT_WIRED; @@ -63,12 +77,19 @@ static void fill_port_block(uint8_t port[9], const Gamepad::PadIn& gp_in) if (gp_in.trigger_r >= 255) b2 |= WiiU::GCButtons::R; port[2] = b2; - // Bytes 3–4: main stick X, Y (0–255, 128 = center) — Y axis inverted + // Bytes 3–4: main stick X, Y (0–255, 128 = center). + // Y: set by host — Xbox uses positive=up (send value), Wii U/Nintendo use negative=up (send 255-value). port[3] = stick_to_gc(gp_in.joystick_lx); - port[4] = stick_to_gc(-gp_in.joystick_ly); - // Bytes 5–6: C-stick (substick) X, Y — Y axis inverted + { + uint8_t ly = stick_y_to_gc(gp_in.joystick_ly); + port[4] = stick_y_positive_is_up ? ly : static_cast(255 - ly); + } + // Bytes 5–6: C-stick (substick) X, Y port[5] = stick_to_gc(gp_in.joystick_rx); - port[6] = stick_to_gc(-gp_in.joystick_ry); + { + uint8_t ry = stick_y_to_gc(gp_in.joystick_ry); + port[6] = stick_y_positive_is_up ? ry : static_cast(255 - ry); + } // Bytes 7–8: L/R trigger analog (0–255) port[7] = gp_in.trigger_l; port[8] = gp_in.trigger_r; @@ -83,7 +104,7 @@ void WiiUDevice::process(const uint8_t idx, Gamepad& gamepad) // Dolphin may stop re-checking after seeing initial zeros; filling every frame avoids that. // Combo check (check_for_driver_change) also uses get_pad_in(); both see the same current state. Gamepad::PadIn gp_in = gamepad.get_pad_in(); - fill_port_block(in_report_.port_data[idx], gp_in); + fill_port_block(in_report_.port_data[idx], gp_in, gamepad.stick_y_positive_is_up()); if (tud_suspended()) tud_remote_wakeup(); diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp index 43d1465d..89e47d89 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/XInput.cpp @@ -3,7 +3,7 @@ #include "USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h" #include "USBDevice/DeviceDriver/XInput/XInput.h" -void XInputDevice::initialize() +void XInputDevice::initialize() { class_driver_ = *tud_xinput::class_driver(); } @@ -94,8 +94,11 @@ uint16_t XInputDevice::get_report_cb(uint8_t itf, uint8_t report_id, hid_report_ void XInputDevice::set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t report_type, uint8_t const *buffer, uint16_t bufsize) {} -bool XInputDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) +bool XInputDevice::vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + (void)rhport; + (void)stage; + (void)request; return false; } diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp index 26f38f1d..0f6b07e6 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.cpp @@ -13,14 +13,13 @@ namespace tud_xinput { static constexpr uint16_t ENDPOINT_SIZE = 32; +static constexpr uint8_t DESC_TYPE_VENDOR = 0x21; uint8_t endpoint_in_ = 0xFF; uint8_t endpoint_out_ = 0xFF; uint8_t ep_in_buffer_[ENDPOINT_SIZE]; uint8_t ep_out_buffer_[ENDPOINT_SIZE]; -//Class Driver - static void init(void) { endpoint_in_ = 0xFF; @@ -37,66 +36,75 @@ static bool deinit(void) static void reset(uint8_t rhport) { + (void)rhport; init(); } +static inline uint16_t tu_desc_len(uint8_t const* desc) { return desc[0]; } + static uint16_t open(uint8_t rhport, tusb_desc_interface_t const *itf_descriptor, uint16_t max_length) { - uint16_t driver_length = sizeof(tusb_desc_interface_t) + (itf_descriptor->bNumEndpoints * sizeof(tusb_desc_endpoint_t)) + 16; - - TU_VERIFY(max_length >= driver_length, 0); - - uint8_t const *current_descriptor = tu_desc_next(itf_descriptor); - uint8_t found_endpoints = 0; - while ((found_endpoints < itf_descriptor->bNumEndpoints) && (driver_length <= max_length)) - { - tusb_desc_endpoint_t const *endpoint_descriptor = (tusb_desc_endpoint_t const *)current_descriptor; - if (TUSB_DESC_ENDPOINT == tu_desc_type(endpoint_descriptor)) - { - TU_ASSERT(usbd_edpt_open(rhport, endpoint_descriptor)); - - if (tu_edpt_dir(endpoint_descriptor->bEndpointAddress) == TUSB_DIR_IN) - { - endpoint_in_ = endpoint_descriptor->bEndpointAddress; - } - else - { - endpoint_out_ = endpoint_descriptor->bEndpointAddress; - } - - ++found_endpoints; - } - - current_descriptor = tu_desc_next(current_descriptor); - } - return driver_length; + uint16_t drv_len = sizeof(tusb_desc_interface_t); + uint8_t const *p_desc = tu_desc_next((uint8_t const*)itf_descriptor); + + if (itf_descriptor->bInterfaceClass != 0xFF || + itf_descriptor->bInterfaceSubClass != 0x5D || + itf_descriptor->bInterfaceProtocol != 0x01) + { + return 0; + } + + if (p_desc[1] == DESC_TYPE_VENDOR) { + drv_len += tu_desc_len(p_desc); + p_desc = tu_desc_next(p_desc); + } + + for (uint8_t i = 0; i < itf_descriptor->bNumEndpoints; i++) { + tusb_desc_endpoint_t const *ep_desc = (tusb_desc_endpoint_t const *)p_desc; + if (TUSB_DESC_ENDPOINT != tu_desc_type(ep_desc)) break; + TU_VERIFY(usbd_edpt_open(rhport, ep_desc), 0); + if (tu_edpt_dir(ep_desc->bEndpointAddress) == TUSB_DIR_IN) + endpoint_in_ = ep_desc->bEndpointAddress; + else + endpoint_out_ = ep_desc->bEndpointAddress; + drv_len += sizeof(tusb_desc_endpoint_t); + p_desc = tu_desc_next(p_desc); + } + + if (endpoint_out_ != 0xFF) + usbd_edpt_xfer(rhport, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); + + TU_VERIFY(max_length >= drv_len, 0); + return drv_len; } static bool control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { - return true; + (void)rhport; + (void)stage; + (void)request; + return true; } static bool xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - if (ep_addr == endpoint_out_) - { - usbd_edpt_xfer(BOARD_TUD_RHPORT, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); + (void)result; + (void)xferred_bytes; + if (ep_addr == endpoint_out_) { + usbd_edpt_xfer(rhport, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); } - return true; + return true; } -// Public API - const usbd_class_driver_t* class_driver() { static const usbd_class_driver_t tud_class_driver_ = { - #if CFG_TUSB_DEBUG >= 2 +#if CFG_TUSB_DEBUG >= 2 .name = "XINPUT", - #else +#else .name = NULL, - #endif +#endif .init = init, .deinit = deinit, .reset = reset, @@ -110,31 +118,23 @@ const usbd_class_driver_t* class_driver() bool send_report_ready() { - if (tud_ready() && - (endpoint_in_ != 0xFF) && - (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_in_))) - { - return true; - } - return false; + return tud_ready() && + (endpoint_in_ != 0xFF) && + (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_in_)); } bool receive_report_ready() { - if (tud_ready() && - (endpoint_out_ != 0xFF) && - (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_out_))) - { - return true; - } - return false; + return tud_ready() && + (endpoint_out_ != 0xFF) && + (!usbd_edpt_busy(BOARD_TUD_RHPORT, endpoint_out_)); } bool send_report(const uint8_t *report, uint16_t len) { if (send_report_ready()) { - std::memcpy(ep_in_buffer_, report, std::min(len, ENDPOINT_SIZE)); + std::memcpy(ep_in_buffer_, report, std::min(len, (uint16_t)ENDPOINT_SIZE)); usbd_edpt_claim(BOARD_TUD_RHPORT, endpoint_in_); usbd_edpt_xfer(BOARD_TUD_RHPORT, endpoint_in_, ep_in_buffer_, sizeof(XInput::InReport)); usbd_edpt_release(BOARD_TUD_RHPORT, endpoint_in_); @@ -151,11 +151,10 @@ bool receive_report(uint8_t *report, uint16_t len) usbd_edpt_xfer(BOARD_TUD_RHPORT, endpoint_out_, ep_out_buffer_, ENDPOINT_SIZE); usbd_edpt_release(BOARD_TUD_RHPORT, endpoint_out_); } - - std::memcpy(report, ep_out_buffer_, std::min(len, ENDPOINT_SIZE)); + std::memcpy(report, ep_out_buffer_, std::min(len, (uint16_t)ENDPOINT_SIZE)); return true; } } // namespace tud_xinput -#endif // (TUSB_OPT_DEVICE_ENABLED && CFG_TUD_XINPUT) \ No newline at end of file +#endif // (TUSB_OPT_DEVICE_ENABLED && CFG_TUD_XINPUT) diff --git a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h index 74a87473..9da42428 100644 --- a/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h +++ b/Firmware/RP2040/src/USBDevice/DeviceDriver/XInput/tud_xinput/tud_xinput.h @@ -6,13 +6,12 @@ #include "tusb.h" #include "device/usbd_pvt.h" -namespace tud_xinput +namespace tud_xinput { bool send_report_ready(); bool send_report(const uint8_t *report, uint16_t len); bool receive_report(uint8_t *report, uint16_t len); const usbd_class_driver_t* class_driver(); - -} // namespace tud_xinput +} #endif // _TUD_XINPUT_H_ \ No newline at end of file diff --git a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp index 6dab8113..14ff9024 100644 --- a/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp +++ b/Firmware/RP2040/src/USBHost/HostDriver/XInput/XboxOne.cpp @@ -21,23 +21,24 @@ void XboxOneHost::process_report(Gamepad& gamepad, uint8_t address, uint8_t inst Gamepad::PadIn gp_in; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_UP) gp_in.dpad |= gamepad.MAP_DPAD_UP; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_DOWN) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_LEFT) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; - if (in_report->buttons[1] & XboxOne::Buttons1::DPAD_RIGHT) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; - - if (in_report->buttons[1] & XboxOne::Buttons1::L3) gp_in.buttons |= gamepad.MAP_BUTTON_L3; - if (in_report->buttons[1] & XboxOne::Buttons1::R3) gp_in.buttons |= gamepad.MAP_BUTTON_R3; - if (in_report->buttons[1] & XboxOne::Buttons1::LB) gp_in.buttons |= gamepad.MAP_BUTTON_LB; - if (in_report->buttons[1] & XboxOne::Buttons1::RB) gp_in.buttons |= gamepad.MAP_BUTTON_RB; - if (in_report->buttons[0] & XboxOne::Buttons0::BACK) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; - if (in_report->buttons[0] & XboxOne::Buttons0::START) gp_in.buttons |= gamepad.MAP_BUTTON_START; - if (in_report->buttons[0] & XboxOne::Buttons0::SYNC) gp_in.buttons |= gamepad.MAP_BUTTON_MISC; - if (in_report->buttons[0] & XboxOne::Buttons0::GUIDE) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; - if (in_report->buttons[0] & XboxOne::Buttons0::A) gp_in.buttons |= gamepad.MAP_BUTTON_A; - if (in_report->buttons[0] & XboxOne::Buttons0::B) gp_in.buttons |= gamepad.MAP_BUTTON_B; - if (in_report->buttons[0] & XboxOne::Buttons0::X) gp_in.buttons |= gamepad.MAP_BUTTON_X; - if (in_report->buttons[0] & XboxOne::Buttons0::Y) gp_in.buttons |= gamepad.MAP_BUTTON_Y; + const uint16_t b = in_report->buttons; + if (b & XboxOne::GamepadButtons::DPAD_UP) gp_in.dpad |= gamepad.MAP_DPAD_UP; + if (b & XboxOne::GamepadButtons::DPAD_DOWN) gp_in.dpad |= gamepad.MAP_DPAD_DOWN; + if (b & XboxOne::GamepadButtons::DPAD_LEFT) gp_in.dpad |= gamepad.MAP_DPAD_LEFT; + if (b & XboxOne::GamepadButtons::DPAD_RIGHT) gp_in.dpad |= gamepad.MAP_DPAD_RIGHT; + + if (b & XboxOne::GamepadButtons::LEFT_THUMB) gp_in.buttons |= gamepad.MAP_BUTTON_L3; + if (b & XboxOne::GamepadButtons::RIGHT_THUMB) gp_in.buttons |= gamepad.MAP_BUTTON_R3; + if (b & XboxOne::GamepadButtons::LEFT_SHOULDER) gp_in.buttons |= gamepad.MAP_BUTTON_LB; + if (b & XboxOne::GamepadButtons::RIGHT_SHOULDER) gp_in.buttons |= gamepad.MAP_BUTTON_RB; + if (b & XboxOne::GamepadButtons::VIEW) gp_in.buttons |= gamepad.MAP_BUTTON_BACK; + if (b & XboxOne::GamepadButtons::MENU) gp_in.buttons |= gamepad.MAP_BUTTON_START; + if (b & XboxOne::GamepadButtons::SYNC) gp_in.buttons |= gamepad.MAP_BUTTON_MISC; + if (in_report->guide_pressed) gp_in.buttons |= gamepad.MAP_BUTTON_SYS; + if (b & XboxOne::GamepadButtons::A) gp_in.buttons |= gamepad.MAP_BUTTON_A; + if (b & XboxOne::GamepadButtons::B) gp_in.buttons |= gamepad.MAP_BUTTON_B; + if (b & XboxOne::GamepadButtons::X) gp_in.buttons |= gamepad.MAP_BUTTON_X; + if (b & XboxOne::GamepadButtons::Y) gp_in.buttons |= gamepad.MAP_BUTTON_Y; gp_in.trigger_l = gamepad.scale_trigger_l(static_cast(in_report->trigger_l >> 2)); gp_in.trigger_r = gamepad.scale_trigger_r(static_cast(in_report->trigger_r >> 2)); diff --git a/Firmware/RP2040/src/USBHost/HostManager.h b/Firmware/RP2040/src/USBHost/HostManager.h index eaf05c39..18b8ca99 100644 --- a/Firmware/RP2040/src/USBHost/HostManager.h +++ b/Firmware/RP2040/src/USBHost/HostManager.h @@ -9,7 +9,14 @@ #include #include "Board/Config.h" +#include "Board/ogxm_log.h" #include "USBHost/HardwareIDs.h" + +#if defined(CONFIG_OGXM_DEBUG) +#define debug_printf OGXM_LOG +#else +#define debug_printf(...) ((void)0) +#endif #include "USBHost/HostDriver/XInput/tuh_xinput/tuh_xinput.h" #include "USBHost/HostDriver/HostDriver.h" #include "USBHost/HostDriver/PS5/PS5.h" @@ -138,6 +145,10 @@ class HostManager device_slot.address = address; interface.gamepad_idx = gp_idx; interface.gamepad = gamepads_[gp_idx]; + // Wii U GC adapter: Xbox controllers report positive Y for up; Nintendo use negative + const bool xbox_stick_y = (driver_type == HostDriverType::XBOXONE || driver_type == HostDriverType::XBOX360 + || driver_type == HostDriverType::XBOX360W || driver_type == HostDriverType::XBOXOG); + interface.gamepad->set_stick_y_positive_is_up(xbox_stick_y); interface.driver->initialize(*interface.gamepad, device_slot.address, instance, report_desc, desc_len); return true; From 8f18ecfc6313db08071b62448786260c7665b6f8 Mon Sep 17 00:00:00 2001 From: Gary Date: Thu, 12 Feb 2026 07:10:55 -0500 Subject: [PATCH 12/13] Fixed a bug with Wii U mode that caused pushing Left stick up to input as down --- .../docs/XINPUT_JOYPAD_OS_COMPARISON.md | 160 ------------------ 1 file changed, 160 deletions(-) delete mode 100644 Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md diff --git a/Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md b/Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md deleted file mode 100644 index dadbbac2..00000000 --- a/Firmware/RP2040/docs/XINPUT_JOYPAD_OS_COMPARISON.md +++ /dev/null @@ -1,160 +0,0 @@ -# XInput (Xbox 360) Report & Descriptor Comparison: OGX-Mini vs joypad-os - -This document compares the XInput device implementation in this project (OGX-Mini) with [joypad-os](https://github.com/joypad-ai/joypad-os) so we can align behavior and add **real Xbox 360 console** support. - ---- - -## 1. Input report (20 bytes) — byte-by-byte - -Both projects use the **same 20-byte input report layout**. Field names differ; byte positions and semantics match. - -| Byte | OGX-Mini (`XInput::InReport`) | joypad-os (`xinput_in_report_t`) | Notes | -|------|------------------------------|-----------------------------------|--------| -| 0 | `report_id` | `report_id` | Always `0x00` | -| 1 | `report_size` | `report_size` | Always `0x14` (20) | -| 2 | `buttons[0]` | `buttons0` | D-pad (0–3), Start (4), Back (5), L3 (6), R3 (7) | -| 3 | `buttons[1]` | `buttons1` | LB (0), RB (1), Guide (2), — (3), A (4), B (5), X (6), Y (7) | -| 4 | `trigger_l` | `trigger_l` | 0–255 | -| 5 | `trigger_r` | `trigger_r` | 0–255 | -| 6–7 | `joystick_lx` | `stick_lx` | int16 LE (-32768..32767) | -| 8–9 | `joystick_ly` | `stick_ly` | int16 LE (Y inverted: positive = up) | -| 10–11 | `joystick_rx` | `stick_rx` | int16 LE | -| 12–13 | `joystick_ry` | `stick_ry` | int16 LE (Y inverted) | -| 14–19 | `reserved[6]` | `reserved[6]` | Zero padding | - -**Conclusion:** No change needed for the **input report format**. OGX-Mini’s mapping in `XInput.cpp` already matches joypad-os’s `xinput_mode.c` (same button bits and axis handling, including Y inversion). - ---- - -## 2. Button bit definitions — identical - -| Button | Byte | Bit | OGX-Mini | joypad-os | -|--------|------|-----|----------|-----------| -| D-pad U/D/L/R | 2 | 0–3 | `Buttons0::DPAD_*` | `XINPUT_BTN_DPAD_*` | -| Start | 2 | 4 | `Buttons0::START` | `XINPUT_BTN_START` | -| Back | 2 | 5 | `Buttons0::BACK` | `XINPUT_BTN_BACK` | -| L3 / R3 | 2 | 6–7 | `Buttons0::L3/R3` | `XINPUT_BTN_L3/R3` | -| LB / RB | 3 | 0–1 | `Buttons1::LB/RB` | `XINPUT_BTN_LB/RB` | -| Guide | 3 | 2 | `Buttons1::HOME` | `XINPUT_BTN_GUIDE` | -| A / B / X / Y | 3 | 4–7 | `Buttons1::A/B/X/Y` | `XINPUT_BTN_A/B/X/Y` | - -No code changes required for button mapping. - ---- - -## 3. Output report (8 bytes) — identical - -| Byte | Content | OGX-Mini | joypad-os | -|------|---------|----------|-----------| -| 0 | report_id | RUMBLE=0x00, LED=0x01 | Same | -| 1 | report_size | 0x08 | 0x08 | -| 2 | led | LED pattern | led | -| 3 | rumble_l | rumble_l | rumble_l | -| 4 | rumble_r | rumble_r | rumble_r | -| 5–7 | reserved | 0 | reserved[3] | - -No change needed for output report layout. - ---- - -## 4. Device descriptor — same VID/PID, minor bmAttributes - -| Field | OGX-Mini | joypad-os | -|-------|----------|-----------| -| idVendor | 0x045E | XINPUT_VID 0x045E | -| idProduct | 0x028E | XINPUT_PID 0x028E | -| bcdDevice | 0x0114 (2.14) | XINPUT_BCD_DEVICE 0x0114 | -| bmAttributes | 0x80 (no remote wakeup) | 0xA0 (remote wakeup) | - -Optional: set bmAttributes to `0xA0` in `DESC_DEVICE[]` if you want remote wakeup to match joypad-os and some 360 behavior. - ---- - -## 5. Configuration descriptor — main difference (PC vs 360 console) - -### OGX-Mini (current): PC-only, 1 interface - -- **wTotalLength:** 48 (0x30) -- **bNumInterfaces:** 1 -- **Interface 0:** Gamepad (0xFF / 0x5D / 0x01), 2 endpoints - - EP 0x81 IN (interrupt, 32 bytes, **bInterval 1**) - - EP 0x01 OUT (interrupt, 32 bytes, bInterval 8) -- Followed by a 16-byte vendor/HID-style descriptor (0x21 with bNumDescriptors, etc.). - -This is enough for **PC (XInput)** and many games. It is **not** enough for a **real Xbox 360 console**, which expects a multi-interface device and XSM3 security. - -### joypad-os: PC + Xbox 360 console, 4 interfaces - -- **wTotalLength:** 153 -- **bNumInterfaces:** 4 - -| Iface | Class / SubClass / Protocol | Purpose | Endpoints | -|-------|-----------------------------|---------|-----------| -| 0 | 0xFF / 0x5D / 0x01 | Gamepad | EP 0x81 IN (4 ms), EP 0x02 OUT (8 ms) | -| 1 | 0xFF / 0x5D / 0x03 | Audio (stub) | 4 EPs (not opened by driver) | -| 2 | 0xFF / 0x5D / 0x02 | Plugin (stub) | 1 EP (not opened) | -| 3 | 0xFF / **0xFD** / **0x13** | **Security (XSM3)** | 0 EPs (control only) | - -- Gamepad uses a **17-byte vendor descriptor** (0x21) after the interface, not the longer HID-style block. -- **Interface 3** is the security interface; the console uses **vendor control requests** on the device to run the XSM3 challenge/response. Without this interface and the XSM3 handler, the 360 console will not accept the device. - ---- - -## 6. What to add for real Xbox 360 console support - -To have OGX-Mini work on a **real Xbox 360** (like joypad-os), you need the following. - -### 6.1 Four-interface configuration descriptor - -- Replace the single-interface configuration in `Descriptors/XInput.h` with a **153-byte** configuration that includes: - - Interface 0: Gamepad (0xFF/0x5D/0x01) + 17-byte vendor descriptor + EP 0x81 IN (bInterval **4**), EP 0x02 OUT (8). - - Interface 1: Audio (0xFF/0x5D/0x03), stub only (descriptors present, no need to open EPs). - - Interface 2: Plugin (0xFF/0x5D/0x02), stub only. - - Interface 3: Security (0xFF/0xFD/0x13), no endpoints, security descriptor (0x41) only. - -You can copy the exact bytes from joypad-os’s `xinput_descriptors.h` (`xinput_config_descriptor[]`) and adapt to OGX-Mini’s naming (e.g. `DESC_CONFIGURATION[]` / `XINPUT_CONFIG_TOTAL_LEN`). - -### 6.2 XSM3 authentication - -- The 360 console sends **vendor control** requests (bRequest): - - **0x81** GET_SERIAL — device returns 29-byte ID (from libxsm3). - - **0x82** INIT_AUTH — console sends 34-byte challenge; device must respond with 46-byte response. - - **0x83** RESPOND — device returns challenge response (46 or 22 bytes). - - **0x84** KEEPALIVE — device responds with 0 bytes. - - **0x86** STATE — device returns 2-byte state (e.g. 2 = response ready). - - **0x87** VERIFY — console sends 22-byte verify; device responds with 22-byte response. - -- joypad-os uses **[libxsm3](https://github.com/InvoxiPlayGames/libxsm3)** (LGPL-2.1) to compute the challenge/response. You would need to: - - Add libxsm3 (or a port) to the OGX-Mini firmware. - - In the USB device stack, route **vendor control** transfers to an XInput/XSM3 handler (joypad-os does this in `tud_xinput_vendor_control_xfer_cb`). - - Implement the same state machine: on 0x82 store the challenge and call libxsm3 init; on 0x83 return the response; on 0x87 run verify and return the verify response; etc. - -### 6.3 Class driver `open()` for multiple interfaces - -- joypad-os’s `tud_xinput.c` `xinput_open()` is called for **each** interface. It: - - **Opens** only Interface 0 (gamepad): claims the interface and opens EP IN and EP OUT. - - **Skips** Interfaces 1 and 2 (audio, plugin): advances the descriptor pointer without opening endpoints. - - **Records** Interface 3 (security) index (e.g. `_sec_itf_num`) for XSM3; no endpoints. - -OGX-Mini’s `tud_xinput` currently assumes a single interface. It needs to be extended so that when the device presents the 4-interface configuration, the driver parses and skips the stub interfaces and only opens the gamepad endpoints, and recognizes the security interface (for routing vendor requests in usbd, not necessarily by interface number in the driver). - -### 6.4 Optional: IN endpoint interval - -- joypad-os uses **bInterval 4** (4 ms) for the gamepad IN endpoint; OGX-Mini uses **1** (1 ms). For 360 compatibility, changing the gamepad IN endpoint to **4** in the new configuration descriptor is a good idea. - ---- - -## 7. Summary table - -| Item | OGX-Mini | joypad-os | Action for 360 | -|------|----------|-----------|----------------| -| Input report (20 bytes) | Same layout | Same | None | -| Button bits | Same | Same | None | -| Output report (8 bytes) | Same | Same | None | -| VID/PID | 045E:028E | 045E:028E | None | -| Config: num interfaces | 1 | 4 | Add 4-interface, 153-byte config | -| Config: gamepad EP IN interval | 1 | 4 | Use 4 in new config | -| Vendor control (XSM3) | Not implemented | 0x81/82/83/84/86/87 | Add libxsm3 + vendor handler | -| Class driver multi-interface | Single iface | Opens if 0, skips 1–2, records 3 | Extend open() for 4-iface config | - -Implementing the 4-interface descriptor and XSM3 (with libxsm3) will align OGX-Mini with joypad-os’s XInput mode for **real Xbox 360 console** output while keeping the existing report code unchanged. From 04836d0afc12a676e38971e252103f70356bda9c Mon Sep 17 00:00:00 2001 From: Gary L <81448614+MegaCadeDev@users.noreply.github.com> Date: Thu, 12 Feb 2026 07:25:14 -0500 Subject: [PATCH 13/13] Change project name to OGX-Mini 2026 Updated project name and year in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca738846..74421805 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OGX-Mini Fork +# OGX-Mini 2026 ![OGX-Mini Boards](images/OGX-Mini-github.jpg "OGX-Mini Boards") Firmware for the RP2040, capable of emulating gamepads for several game consoles. The firmware comes in many flavors, supported on the [Adafruit Feather USB Host board](https://www.adafruit.com/product/5723), Pi Pico, Pi Pico 2, Pi Pico W, Pi Pico 2 W, Waveshare RP2040-Zero, Pico/ESP32 hybrid, and a 4-Channel RP2040-Zero setup.