From feae683ebb34615f9710958928f3a317044c3880 Mon Sep 17 00:00:00 2001 From: Luke Arntson Date: Wed, 20 Dec 2023 10:55:18 -0500 Subject: [PATCH] Xbox One Support (Passthrough Dongle Required) (#671) * [ImgBot] Optimize images *Total -- 17,269.00kb -> 14,501.79kb (16.02%) /site/docs/assets/images/gpc-add-ons-input-history.png -- 6.80kb -> 3.09kb (54.57%) /configs/OpenCore0WASD/assets/Open_Core0_WASD_pinout.png -- 61.94kb -> 29.30kb (52.69%) /configs/OpenCore0/assets/Open_Core0_WASD_pinout.png -- 61.94kb -> 29.30kb (52.69%) /configs/RanaTadpole/assets/RanaTadpole_buttons.png -- 173.20kb -> 109.41kb (36.83%) /configs/ReflexEncodeV2.0/assets/ReflexBoard_V2.png -- 3,498.35kb -> 2,412.30kb (31.04%) /site/docs/assets/images/gpc-add-ons-keyboard-host-configuration.png -- 62.86kb -> 43.65kb (30.57%) /site/docs/assets/images/gpc-macros-input-line.png -- 25.08kb -> 17.43kb (30.5%) /site/docs/assets/images/gpc-keyboard-mapping.png -- 49.17kb -> 34.21kb (30.43%) /site/docs/assets/images/gpc-backup-and-restore.png -- 13.17kb -> 9.25kb (29.75%) /site/docs/assets/images/gpc-macros.png -- 88.21kb -> 62.03kb (29.68%) /site/docs/assets/images/gpc-hotkey-settings.png -- 86.91kb -> 61.12kb (29.67%) /site/docs/assets/images/gpc-add-ons-focus-mode.png -- 15.88kb -> 11.31kb (28.81%) /site/docs/assets/images/gpc-restore.png -- 25.15kb -> 17.93kb (28.7%) /site/docs/assets/images/gpc-home.png -- 58.23kb -> 41.75kb (28.3%) /site/docs/assets/images/gpc-backup.png -- 24.80kb -> 17.79kb (28.26%) /site/docs/assets/images/gpc-add-ons-tilt.png -- 49.57kb -> 35.71kb (27.96%) /site/docs/assets/images/gpc-add-ons-snespad-input-pinout.svg -- 11.18kb -> 8.18kb (26.81%) /site/docs/assets/images/gpc-profile-settings.png -- 64.89kb -> 47.94kb (26.12%) /site/docs/assets/images/gpc-add-ons-dual-directional.png -- 16.54kb -> 12.26kb (25.89%) /site/docs/assets/images/gpc-add-ons-player-number.png -- 22.67kb -> 16.83kb (25.77%) /site/docs/assets/images/gpc-add-ons-ps4-mode.png -- 44.27kb -> 33.09kb (25.25%) /site/docs/assets/images/gpc-add-ons-ps-passthrough.png -- 24.37kb -> 18.38kb (24.57%) /configs/Liatris/assets/Liatris.png -- 759.92kb -> 581.83kb (23.44%) /site/docs/assets/images/gpc-documentation-current-version.png -- 4.06kb -> 3.11kb (23.36%) /site/docs/assets/images/gpc-add-ons-joystick-slider.png -- 20.35kb -> 15.63kb (23.23%) /site/docs/assets/images/wii-extension-controllers/turntable.svg -- 19.46kb -> 15.65kb (19.59%) /site/docs/assets/images/gpc-documentation-next-version.png -- 4.68kb -> 3.78kb (19.34%) /site/docs/assets/gp2040-ce-placeholder.png -- 36.97kb -> 30.24kb (18.22%) /site/docs/assets/images/wii-extension-controllers/taiko.svg -- 12.77kb -> 10.73kb (15.93%) /site/docs/assets/boards/PicoW.jpg -- 71.43kb -> 60.90kb (14.74%) /site/docs/assets/images/wii-extension-controllers/classic.svg -- 30.16kb -> 25.95kb (13.94%) /site/static/img/gp2040-ce-logo.svg -- 1,228.80kb -> 1,059.02kb (13.82%) /site/docs/assets/images/wii-extension-controllers/guitar.svg -- 25.92kb -> 22.51kb (13.15%) /site/docs/assets/images/wii-extension-controllers/nunchuck.svg -- 25.25kb -> 22.32kb (11.64%) /site/docs/assets/images/gpc-add-ons-wii-extensions.png -- 85.62kb -> 76.22kb (10.98%) /configs/RP2040AdvancedBreakoutBoardUSBPassthrough/assets/RP2040 Advanced Breakout Board - Passthrough.jpg -- 3,150.54kb -> 2,827.79kb (10.24%) /configs/OpenCore0/assets/Open_Core0_WASD.jpg -- 3,533.08kb -> 3,219.62kb (8.87%) /configs/OpenCore0WASD/assets/Open_Core0_WASD.jpg -- 3,533.08kb -> 3,219.62kb (8.87%) /site/docs/assets/boards/Liatris.jpg -- 10.05kb -> 9.22kb (8.31%) /site/docs/assets/images/wii-extension-controllers/drums.svg -- 46.31kb -> 43.46kb (6.16%) /site/docs/assets/boards/ReflexCtrlSNES.jpg -- 14.82kb -> 14.03kb (5.37%) /site/docs/assets/boards/OpenCore0.jpg -- 22.83kb -> 21.86kb (4.25%) /site/docs/assets/boards/FlatboxRev5Southpaw.jpg -- 20.13kb -> 19.39kb (3.71%) /site/docs/assets/boards/KeyboardConverter.jpg -- 18.39kb -> 17.99kb (2.2%) /site/docs/assets/boards/SGFDevices.jpg -- 14.18kb -> 13.87kb (2.2%) /site/docs/assets/boards/OpenCore0WASD.jpg -- 11.63kb -> 11.45kb (1.59%) /site/docs/assets/boards/ReflexEncode_v2.0.jpg -- 37.80kb -> 37.80kb (0.01%) /site/docs/assets/boards/RP2040AdvancedBreakoutBoardUSBPassthrough.jpg -- 45.57kb -> 45.57kb (0.01%) Signed-off-by: ImgBotApp * Initial Xbox One wip * Fixing conflicts * Lots of WIP, lots of not working code, do not use * Got a lot of clean-up but this will cause Windows to attempt auth * Removed a ton of print messages to narrow down the auth issues. Added a hack to give direct access to the xbox auth passthrough which will need to be cleaned. Some other hacks and what not, getting closer but VERY much a PoC do not use. * Moved sends to a queue in-case we try to send too fast. This is still lots of hacks but we are auth'ing in Windows * First steps of adding our XGIP transceiver protocol. Descriptor is working, auth is next * Moved xbox pass through over to the XGIP protocol transceiver * Blue light! checking in while its working * Its delicate as this requires printfs to work for timing. But this should get us to reports * Working without printfs! * Xbox One auth working * Randomize the serial based on time * [ImgBot] Optimize images *Total -- 7,719.41kb -> 5,978.77kb (22.55%) /configs/Haute42/assets/Haute42_logo.png -- 11.96kb -> 3.55kb (70.33%) /configs/Haute42/assets/Haute42_T16.png -- 50.72kb -> 19.89kb (60.78%) /configs/SGFBridget/assets/SGF_Logo.png -- 14.39kb -> 6.05kb (57.92%) /configs/SGFFaust/assets/SGF_Logo.png -- 14.39kb -> 6.05kb (57.92%) /configs/SGFFaust/assets/SGF_Faust_Layout.png -- 154.32kb -> 70.79kb (54.13%) /configs/Haute42/assets/Haute42_Mini.png -- 29.51kb -> 14.34kb (51.41%) /configs/Haute42/assets/Haute42_G16.png -- 44.83kb -> 28.07kb (37.39%) /configs/Haute42/assets/Haute42_G13.png -- 39.38kb -> 24.97kb (36.58%) /configs/Haute42/assets/Haute42_G12.png -- 36.26kb -> 23.24kb (35.91%) /configs/Haute42/assets/Haute42_T13.png -- 44.24kb -> 29.54kb (33.24%) /configs/SGFBridget/assets/SGF_Bridget.png -- 2,455.65kb -> 1,787.95kb (27.19%) /configs/SGFFaust/assets/SGF_Faust.png -- 3,513.55kb -> 2,742.10kb (21.96%) /configs/Haute42/assets/Haute42_Mini_series.png -- 512.67kb -> 436.98kb (14.76%) /configs/Haute42/assets/Haute42_G_series.png -- 356.78kb -> 350.79kb (1.68%) /configs/Haute42/assets/Haute42_T_series.png -- 440.77kb -> 434.46kb (1.43%) Signed-off-by: ImgBotApp * Fully working on non-test setups * Updating MIT licenses before I do the merge * Lots of code clean-ups, looking much closer to PR * Add guide button support, working unique serial from pico ID, and code clean-ups * Guide button is now fully working, fixed Left-Right analog (should be fixed) * Cleaning up and getting ready * More cleaning * Revising tinyusb * Changed tinyusb to point to hathach version * Last bits of clean-up * Fix for latest TinyUSB * Thanks to Santroller and GIMX added to README.md * Small code clean-ups * Missed Xbox One pass through enabled in the webconfig save. * Missed this when doing the migrate * Capitalization gotchas * Fix for Magic-X dongle (assume dongle is still ready on unmount/remount), fix for dev server * Moving xbox one input mode USB/xbone passthrough from optional to required. * Fix for guide button * Fix to copyright on new Xbox One gamepad * Quick fix for idle-comparison --------- Signed-off-by: ImgBotApp Co-authored-by: ImgBotApp --- .gitmodules | 3 + CMakeLists.txt | 4 + LICENSE | 1 + README.md | 1 + headers/addons/keyboard_host.h | 2 + headers/addons/pspassthrough.h | 2 + headers/addons/xbonepassthrough.h | 42 ++ headers/gamepad.h | 8 + headers/gamepad/GamepadDescriptors.h | 219 +--------- headers/gamepad/GamepadState.h | 3 + .../gamepad/descriptors/XBOneDescriptors.h | 203 +++++++++ headers/gp2040.h | 1 + headers/tusb_config.h | 3 + headers/usbaddon.h | 2 + headers/usbhostmanager.h | 10 + lib/TinyUSB_Gamepad/CMakeLists.txt | 3 + lib/TinyUSB_Gamepad/src/tusb_driver.cpp | 36 +- lib/TinyUSB_Gamepad/src/usb_descriptors.cpp | 18 +- lib/TinyUSB_Gamepad/src/usb_driver.h | 1 + lib/TinyUSB_Gamepad/src/xbone_driver.cpp | 402 ++++++++++++++++++ lib/TinyUSB_Gamepad/src/xbone_driver.h | 114 +++++ lib/TinyUSB_Gamepad/src/xgip_protocol.cpp | 384 +++++++++++++++++ lib/TinyUSB_Gamepad/src/xgip_protocol.h | 98 +++++ lib/TinyUSB_Gamepad/src/xinput_host.cpp | 276 ++++++++++++ lib/TinyUSB_Gamepad/src/xinput_host.h | 102 +++++ lib/rndis/rndis.c | 2 +- lib/tinyusb | 1 + proto/config.proto | 10 +- proto/enums.proto | 1 + src/addons/i2cdisplay.cpp | 1 + src/addons/inputhistory.cpp | 24 +- src/addons/xbonepassthrough.cpp | 134 ++++++ src/config_utils.cpp | 6 +- src/configs/webconfig.cpp | 11 +- src/gamepad.cpp | 175 +++++++- src/gamepad/GamepadDescriptors.cpp | 6 + src/gp2040.cpp | 15 +- src/gp2040aux.cpp | 2 + src/usbhostmanager.cpp | 78 +++- www/server/app.js | 3 +- .../{Passthrough.tsx => PSPassthrough.tsx} | 0 www/src/Addons/XBOnePassthrough.tsx | 51 +++ www/src/Locales/en/AddonsConfig.jsx | 3 +- www/src/Locales/en/SettingsPage.jsx | 1 + www/src/Locales/pt-BR/SettingsPage.jsx | 1 + www/src/Locales/zh-CN/SettingsPage.jsx | 1 + www/src/Pages/AddonsConfigPage.jsx | 9 +- www/src/Pages/SettingsPage.jsx | 2 + 48 files changed, 2226 insertions(+), 249 deletions(-) create mode 100644 headers/addons/xbonepassthrough.h create mode 100644 headers/gamepad/descriptors/XBOneDescriptors.h create mode 100644 lib/TinyUSB_Gamepad/src/xbone_driver.cpp create mode 100644 lib/TinyUSB_Gamepad/src/xbone_driver.h create mode 100644 lib/TinyUSB_Gamepad/src/xgip_protocol.cpp create mode 100644 lib/TinyUSB_Gamepad/src/xgip_protocol.h create mode 100644 lib/TinyUSB_Gamepad/src/xinput_host.cpp create mode 100644 lib/TinyUSB_Gamepad/src/xinput_host.h create mode 160000 lib/tinyusb create mode 100644 src/addons/xbonepassthrough.cpp rename www/src/Addons/{Passthrough.tsx => PSPassthrough.tsx} (100%) create mode 100644 www/src/Addons/XBOnePassthrough.tsx diff --git a/.gitmodules b/.gitmodules index 31e2a79e5..2e77590a5 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/pico_pio_usb"] path = lib/pico_pio_usb url = https://github.com/sekigon-gonnoc/Pico-PIO-USB +[submodule "lib/tinyusb"] + path = lib/tinyusb + url = https://github.com/hathach/tinyusb/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f6b6de397..0062d6944 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,9 @@ add_compile_options(-Wall include(compile_proto.cmake) compile_proto() +#pull in tinyUSB +set(PICO_TINYUSB_PATH "${CMAKE_CURRENT_LIST_DIR}/lib/tinyusb") + # initialize the Raspberry Pi Pico SDK pico_sdk_init() @@ -174,6 +177,7 @@ src/addons/inputhistory.cpp src/gamepad/GamepadDebouncer.cpp src/gamepad/GamepadDescriptors.cpp src/addons/tilt.cpp +src/addons/xbonepassthrough.cpp ${PROTO_OUTPUT_DIR}/enums.pb.c ${PROTO_OUTPUT_DIR}/config.pb.c ) diff --git a/LICENSE b/LICENSE index b41072839..2ef0d3808 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ MIT License +Copyright (c) 2023 OpenStickCommunity (gp2040-ce.info) Copyright (c) 2021 Jason Skuby (mytechtoybox.com) Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/README.md b/README.md index af2d97773..c8b7f2086 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,4 @@ Please respect the coding style of the file(s) you are working in, and enforce t * [TheTrain](https://github.com/TheTrainGoes/GP2040-Projects) and [Fortinbra](https://github.com/Fortinbra) for helping keep our community chugging along * [PassingLink](https://github.com/passinglink/passinglink) for the technical details and code for PS4 implementation * [Youssef Habchi](https://youssef-habchi.com/) for allowing us to purchase a license to use Road Rage font for the project +* [Santroller](https://github.com/Santroller/Santroller) and [GIMX](https://github.com/matlo/GIMX) for technical examples of Xbox One authentication using pass-through diff --git a/headers/addons/keyboard_host.h b/headers/addons/keyboard_host.h index af9c61b04..87512e152 100644 --- a/headers/addons/keyboard_host.h +++ b/headers/addons/keyboard_host.h @@ -45,8 +45,10 @@ class KeyboardHostAddon : public USBAddon { virtual std::string name() { return KeyboardHostName; } // USB Add-on Features virtual void mount(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len); + virtual void xmount(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype) {} virtual void unmount(uint8_t dev_addr); virtual void report_received(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + virtual void report_sent(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) {} virtual void set_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len) {} virtual void get_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len) {} private: diff --git a/headers/addons/pspassthrough.h b/headers/addons/pspassthrough.h index af5db466d..a950bcdf6 100644 --- a/headers/addons/pspassthrough.h +++ b/headers/addons/pspassthrough.h @@ -29,10 +29,12 @@ class PSPassthroughAddon : public USBAddon { virtual std::string name() { return PSPassthroughName; } // USB Add-on Features virtual void mount(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len); + virtual void xmount(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype) {} virtual void unmount(uint8_t dev_addr); virtual void set_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len); virtual void get_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len); virtual void report_received(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) {} + virtual void report_sent(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) {} private: uint8_t ps_dev_addr; uint8_t ps_instance; diff --git a/headers/addons/xbonepassthrough.h b/headers/addons/xbonepassthrough.h new file mode 100644 index 000000000..419834d2b --- /dev/null +++ b/headers/addons/xbonepassthrough.h @@ -0,0 +1,42 @@ +#ifndef _XBOnePassthrough_H +#define _XBOnePassthrough_H + +#include "usbaddon.h" + +#include "xgip_protocol.h" + +#ifndef XBONEPASSTHROUGH_ENABLED +#define XBONEPASSTHROUGH_ENABLED 0 +#endif + +// KeyboardHost Module Name +#define XBOnePassthroughName "XBOnePassthrough" + +class XBOnePassthroughAddon : public USBAddon { +public: + virtual bool available(); + virtual void setup(); // XBOnePassthrough Setup + virtual void process(); // XBOnePassthrough Process + virtual void preprocess() {} + virtual std::string name() { return XBOnePassthroughName; } +// USB Add-on Features + virtual void mount(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) {} + virtual void xmount(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype); + virtual void unmount(uint8_t dev_addr); + virtual void set_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len) {} + virtual void get_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len) {} + virtual void report_received(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + virtual void report_sent(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) {} + + void queue_host_report(void* report, uint16_t len); +private: + uint8_t xbone_dev_addr; + uint8_t xbone_instance; + + bool dongle_ready; + + XGIPProtocol incomingXGIP; + XGIPProtocol outgoingXGIP; +}; + +#endif // _PSPassthrough_H_ \ No newline at end of file diff --git a/headers/gamepad.h b/headers/gamepad.h index 6393f1c6d..44a420333 100644 --- a/headers/gamepad.h +++ b/headers/gamepad.h @@ -20,6 +20,7 @@ #include "gamepad/descriptors/AstroDescriptors.h" #include "gamepad/descriptors/PSClassicDescriptors.h" #include "gamepad/descriptors/XboxOriginalDescriptors.h" +#include "gamepad/descriptors/XBOneDescriptors.h" #include "pico/stdlib.h" @@ -72,11 +73,13 @@ class Gamepad { */ bool hasRightAnalogStick {false}; + void sendReportSuccess(); void *getReport(); uint16_t getReportSize(); HIDReport *getHIDReport(); SwitchReport *getSwitchReport(); XInputReport *getXInputReport(); + XboxOneGamepad_Data_t *getXBOneReport(); KeyboardReport *getKeyboardReport(); PS4Report *getPS4Report(); NeogeoReport *getNeogeoReport(); @@ -193,6 +196,11 @@ class Gamepad { const HotkeyOptions& hotkeyOptions; GamepadHotkey lastAction = HOTKEY_NONE; + + uint32_t keep_alive_timer; + uint8_t keep_alive_sequence; + uint8_t virtual_keycode_sequence; + bool xb1_guide_pressed; }; #endif diff --git a/headers/gamepad/GamepadDescriptors.h b/headers/gamepad/GamepadDescriptors.h index 454f6559a..b4e86384a 100644 --- a/headers/gamepad/GamepadDescriptors.h +++ b/headers/gamepad/GamepadDescriptors.h @@ -19,211 +19,15 @@ #include "descriptors/AstroDescriptors.h" #include "descriptors/PSClassicDescriptors.h" #include "descriptors/XboxOriginalDescriptors.h" +#include "descriptors/XBOneDescriptors.h" #include "enums.pb.h" // Default value used for networking, override if necessary static uint8_t macAddress[6] = { 0x02, 0x02, 0x84, 0x6A, 0x96, 0x00 }; -static const uint8_t *getConfigurationDescriptor(uint16_t *size, InputMode mode) -{ - switch (mode) - { - case INPUT_MODE_XINPUT: - *size = sizeof(xinput_configuration_descriptor); - return xinput_configuration_descriptor; - - case INPUT_MODE_SWITCH: - *size = sizeof(switch_configuration_descriptor); - return switch_configuration_descriptor; - - case INPUT_MODE_KEYBOARD: - *size = sizeof(keyboard_configuration_descriptor); - return keyboard_configuration_descriptor; - - case INPUT_MODE_PS4: - *size = sizeof(ps4_configuration_descriptor); - return ps4_configuration_descriptor; - - case INPUT_MODE_NEOGEO: - *size = sizeof(neogeo_configuration_descriptor); - return neogeo_configuration_descriptor; - - case INPUT_MODE_MDMINI: - *size = sizeof(mdmini_configuration_descriptor); - return mdmini_configuration_descriptor; - - case INPUT_MODE_PCEMINI: - *size = sizeof(pcengine_configuration_descriptor); - return pcengine_configuration_descriptor; - - case INPUT_MODE_EGRET: - *size = sizeof(egret_configuration_descriptor); - return egret_configuration_descriptor; - - case INPUT_MODE_ASTRO: - *size = sizeof(astro_configuration_descriptor); - return astro_configuration_descriptor; - - case INPUT_MODE_PSCLASSIC: - *size = sizeof(psclassic_configuration_descriptor); - return psclassic_configuration_descriptor; - - case INPUT_MODE_XBOXORIGINAL: - *size = sizeof(xboxoriginal_configuration_descriptor); - return xboxoriginal_configuration_descriptor; - - default: - *size = sizeof(hid_configuration_descriptor); - return hid_configuration_descriptor; - } -} - -static const uint8_t *getDeviceDescriptor(uint16_t *size, InputMode mode) -{ - switch (mode) - { - case INPUT_MODE_XINPUT: - *size = sizeof(xinput_device_descriptor); - return xinput_device_descriptor; - - case INPUT_MODE_SWITCH: - *size = sizeof(switch_device_descriptor); - return switch_device_descriptor; - - case INPUT_MODE_KEYBOARD: - *size = sizeof(keyboard_device_descriptor); - return keyboard_device_descriptor; - - case INPUT_MODE_PS4: - *size = sizeof(ps4_device_descriptor); - return ps4_device_descriptor; - - case INPUT_MODE_NEOGEO: - *size = sizeof(neogeo_device_descriptor); - return neogeo_device_descriptor; - - case INPUT_MODE_MDMINI: - *size = sizeof(mdmini_device_descriptor); - return mdmini_device_descriptor; - - case INPUT_MODE_PCEMINI: - *size = sizeof(pcengine_device_descriptor); - return pcengine_device_descriptor; - - case INPUT_MODE_EGRET: - *size = sizeof(egret_device_descriptor); - return egret_device_descriptor; - - case INPUT_MODE_ASTRO: - *size = sizeof(astro_device_descriptor); - return astro_device_descriptor; - - case INPUT_MODE_PSCLASSIC: - *size = sizeof(psclassic_device_descriptor); - return psclassic_device_descriptor; - - case INPUT_MODE_XBOXORIGINAL: - *size = sizeof(xboxoriginal_device_descriptor); - return xboxoriginal_device_descriptor; - - default: - *size = sizeof(hid_device_descriptor); - return hid_device_descriptor; - } -} - -static const uint8_t *getHIDDescriptor(uint16_t *size, InputMode mode) -{ - switch (mode) - { - case INPUT_MODE_SWITCH: - *size = sizeof(switch_hid_descriptor); - return switch_hid_descriptor; - - case INPUT_MODE_KEYBOARD: - *size = sizeof(keyboard_hid_descriptor); - return keyboard_hid_descriptor; - - case INPUT_MODE_PS4: - *size = sizeof(ps4_hid_descriptor); - return ps4_hid_descriptor; - - case INPUT_MODE_NEOGEO: - *size = sizeof(neogeo_hid_descriptor); - return neogeo_hid_descriptor; - - case INPUT_MODE_MDMINI: - *size = sizeof(mdmini_hid_descriptor); - return mdmini_hid_descriptor; - - case INPUT_MODE_PCEMINI: - *size = sizeof(pcengine_hid_descriptor); - return pcengine_hid_descriptor; - - case INPUT_MODE_EGRET: - *size = sizeof(egret_hid_descriptor); - return egret_hid_descriptor; - - case INPUT_MODE_ASTRO: - *size = sizeof(astro_hid_descriptor); - return astro_hid_descriptor; - - case INPUT_MODE_PSCLASSIC: - *size = sizeof(psclassic_hid_descriptor); - return psclassic_hid_descriptor; - - default: - *size = sizeof(hid_hid_descriptor); - return hid_hid_descriptor; - } -} - -static const uint8_t *getHIDReport(uint16_t *size, InputMode mode) -{ - switch (mode) - { - case INPUT_MODE_SWITCH: - *size = sizeof(switch_report_descriptor); - return switch_report_descriptor; - - case INPUT_MODE_KEYBOARD: - *size = sizeof(keyboard_report_descriptor); - return keyboard_report_descriptor; - - case INPUT_MODE_PS4: - *size = sizeof(ps4_report_descriptor); - return ps4_report_descriptor; - - case INPUT_MODE_NEOGEO: - *size = sizeof(neogeo_report_descriptor); - return neogeo_report_descriptor; - - case INPUT_MODE_MDMINI: - *size = sizeof(mdmini_report_descriptor); - return mdmini_report_descriptor; - - case INPUT_MODE_PCEMINI: - *size = sizeof(pcengine_report_descriptor); - return pcengine_report_descriptor; - - case INPUT_MODE_EGRET: - *size = sizeof(egret_report_descriptor); - return egret_report_descriptor; - - case INPUT_MODE_ASTRO: - *size = sizeof(astro_report_descriptor); - return astro_report_descriptor; - - case INPUT_MODE_PSCLASSIC: - *size = sizeof(psclassic_report_descriptor); - return psclassic_report_descriptor; - - default: - *size = sizeof(hid_report_descriptor); - return hid_report_descriptor; - } -} +static const uint8_t *getHIDDescriptor(uint16_t *size, InputMode mode); +static const uint8_t *getHIDReport(uint16_t *size, InputMode mode); // Convert ASCII string into UTF-16 static const uint16_t *convertStringDescriptor(uint16_t *payloadSize, const char *str, int charCount) @@ -248,12 +52,7 @@ static const uint16_t *getStringDescriptor(uint16_t *size, InputMode mode, uint8 uint8_t charCount = 0; char *str = 0; - if (index == 0) - { - str = (char *)xinput_string_descriptors[0]; - charCount = 1; - } - else if (index == 5) + if (index == 5) { // Convert MAC address into UTF-16 for (int i = 0; i < 6; i++) @@ -270,6 +69,10 @@ static const uint16_t *getStringDescriptor(uint16_t *size, InputMode mode, uint8 str = (char *)xinput_string_descriptors[index]; break; + case INPUT_MODE_XBONE: + str = (char*)xbone_get_string_descriptor(index); + break; + case INPUT_MODE_SWITCH: str = (char *)switch_string_descriptors[index]; break; @@ -314,8 +117,10 @@ static const uint16_t *getStringDescriptor(uint16_t *size, InputMode mode, uint8 str = (char *)hid_string_descriptors[index]; break; } - - charCount = strlen(str); + if ( index == 0 ) // language always has a character count of 1 + charCount = 1; + else + charCount = strlen(str); } return convertStringDescriptor(size, str, charCount); diff --git a/headers/gamepad/GamepadState.h b/headers/gamepad/GamepadState.h index bfd0a56ae..66be6e32c 100644 --- a/headers/gamepad/GamepadState.h +++ b/headers/gamepad/GamepadState.h @@ -143,6 +143,9 @@ inline uint16_t GetJoystickMidValue(uint8_t mode) { case INPUT_MODE_XINPUT: return GAMEPAD_JOYSTICK_MID; + case INPUT_MODE_XBONE: + return GAMEPAD_JOYSTICK_MID; + case INPUT_MODE_SWITCH: return SWITCH_JOYSTICK_MID << 8; diff --git a/headers/gamepad/descriptors/XBOneDescriptors.h b/headers/gamepad/descriptors/XBOneDescriptors.h new file mode 100644 index 000000000..99d71c33d --- /dev/null +++ b/headers/gamepad/descriptors/XBOneDescriptors.h @@ -0,0 +1,203 @@ +/* + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2023 OpenStickCommunity (gp2040-ce.info) + */ + +#pragma once + +#include +#include +#include + +#define XBONE_ENDPOINT_SIZE 64 + +// 0x80 = std. device +// + +static const uint8_t xbone_string_language[] = { 0x09, 0x04 }; +static const uint8_t xbone_string_manufacturer[] = "Open Stick Community"; +static const uint8_t xbone_string_product[] = "GP2040-CE (Xbox One)"; +static const uint8_t xbone_string_version[] = "1.0"; + +static const uint8_t *xbone_string_descriptors[] __attribute__((unused)) = +{ + xbone_string_language, + xbone_string_manufacturer, + xbone_string_product, + xbone_string_version +}; + +static uint8_t uniqueSerial[] = "012345678ABCDEFGH"; +static const uint8_t xboxSecurityMethod[] = "Xbox Security Method 3, Version 1.00, \xa9 2005 Microsoft Corporation. All rights reserved."; +static const uint8_t xboxOSDescriptor[] = "MSFT100\x20\x00"; + +static const uint8_t * xbone_get_string_descriptor(int index) { + if ( index == 3 ) { + // Generate a serial number from the pico's unique ID + pico_unique_board_id_t id; + pico_get_unique_board_id(&id); + memcpy(uniqueSerial, (uint8_t*)&id, PICO_UNIQUE_BOARD_ID_SIZE_BYTES); + return uniqueSerial; + } else if ( index == 4 ) { // security method used + return xboxSecurityMethod; + } else if ( index == 0xEE ) { // ONLY WINDOWS DOES THIS?? + return xboxOSDescriptor; + } + + return xbone_string_descriptors[index]; +} + +// MOVE THIS TO XBOX ONE DRIVER +typedef enum +{ + GIP_ACK_RESPONSE = 0x01, // Xbox One ACK + GIP_ANNOUNCE = 0x02, // Xbox One Announce + GIP_KEEPALIVE = 0x03, // Xbox One Keep-Alive + GIP_DEVICE_DESCRIPTOR = 0x04, // Xbox One Definition + GIP_POWER_MODE_DEVICE_CONFIG = 0x05, // Xbox One Power Mode Config + GIP_AUTH = 0x06, // Xbox One Authentication + GIP_VIRTUAL_KEYCODE = 0x07, // XBox One Guide button pressed + GIP_CMD_RUMBLE = 0x09, // Xbox One Rumble Command + GIP_CMD_WAKEUP = 0x0A, // Xbox One (Wake-up Maybe?) + GIP_FINAL_AUTH = 0x1E, // Xbox One (Final auth?) + GIP_INPUT_REPORT = 0x20, // Xbox One Input Report + GIP_HID_REPORT = 0x21, // Xbox One HID Report +} XboxOneReport; + +typedef struct +{ + 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; +} __attribute__((packed)) GipHeader_t; + +#define GIP_HEADER(packet, cmd, isInternal, seq) \ + packet->Header.command = cmd; \ + packet->Header.internal = isInternal; \ + packet->Header.sequence = seq; \ + packet->Header.client = 0; \ + packet->Header.needsAck = 0; \ + packet->Header.chunkStart = 0; \ + packet->Header.chunked = 0; \ + packet->Header.length = sizeof(*packet) - sizeof(GipHeader_t); + +typedef struct +{ + GipHeader_t Header; + + uint8_t sync : 1; + uint8_t guide : 1; + uint8_t start : 1; // menu + uint8_t back : 1; // view + + uint8_t a : 1; + uint8_t b : 1; + uint8_t x : 1; + uint8_t y : 1; + + uint8_t dpadUp : 1; + uint8_t dpadDown : 1; + uint8_t dpadLeft : 1; + uint8_t dpadRight : 1; + + uint8_t leftShoulder : 1; + uint8_t rightShoulder : 1; + uint8_t leftThumbClick : 1; + uint8_t rightThumbClick : 1; + + uint16_t leftTrigger; + uint16_t rightTrigger; + + int16_t leftStickX; + int16_t leftStickY; + int16_t rightStickX; + int16_t rightStickY; + + uint8_t reserved[18]; // 18-byte padding at the end +} __attribute__((packed)) XboxOneGamepad_Data_t; + +typedef struct { + GipHeader_t Header; + uint8_t sync : 1; + uint8_t guide : 1; + uint8_t start : 1; // menu + uint8_t back : 1; // view +} __attribute__((packed)) XboxOneInputHeader_Data_t; + +static const uint8_t xbone_device_qualifier[] = +{ + 0x0A, // bLength + 0x06, // bDescriptorType (Qualifier Type) + 0x00, 0x02, // bcdUSB 2.00 + 0xFF, // bDeviceClass + 0xFF, // bDeviceSubClass + 0xFF, // bDeviceProtocol + 0x40, // bMaxPacketSize0 64 + 0x01, // bNumConfigurations + 0x00 // bReserved +}; + +static const uint8_t xbone_device_descriptor[] = +{ + 0x12, // bLength + 0x01, // bDescriptorType (Device) + 0x00, 0x02, // bcdUSB 2.00 + 0xFF, // bDeviceClass + 0xFF, // bDeviceSubClass + 0xFF, // bDeviceProtocol + 0x40, // bMaxPacketSize0 64 + 0x6F, 0x0E, // idVendor 0x045E = Xbox One 0x0E6F = SuperPDP 0x0079 = MagicBootS + 0xA4, 0x02, // idProduct 0x02A4 = SuperPDP Gamepad 0x02EA = Xbox One S 0x02D1 = Xbox One 0x2DD = Xbox One v2 0x1894 = MagicBootS + 0x01, 0x01, // bcdDevice 1.01? + 0x01, // iManufacturer (String Index) + 0x02, // iProduct (String Index) + 0x03, // iSerialNumber (String Index) + 0x01, // bNumConfigurations 1 +}; + + +static const uint8_t xbone_configuration_descriptor[] = +{ + 0x09, // bLength + 0x02, // bDescriptorType (Configuration) + 0x20, 0x00, // wTotalLength 32 + 0x01, // bNumInterfaces 1 + 0x01, // bConfigurationValue + 0x00, // iConfiguration (String Index) + 0xA0, // bmAttributes (USB_CONFIG_ATTRIBUTE_RESERVED | USB_CONFIG_ATTRIBUTE_REMOTEWAKEUP) + 0xFA, // bMaxPower 500mA + + 0x09, // bLength + 0x04, // bDescriptorType (Interface) + 0x00, // bInterfaceNumber 0 + 0x00, // bAlternateSetting + 0x02, // bNumEndpoints 2 + 0xFF, // bInterfaceClass + 0x47, // bInterfaceSubClass + 0xD0, // bInterfaceProtocol + 0x00, // iInterface (String Index) + + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x81, // bEndpointAddress (IN/D2H) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x01, // bInterval 1 (unit depends on device speed) + + 0x07, // bLength + 0x05, // bDescriptorType (Endpoint) + 0x02, // bEndpointAddress (OUT/H2D) + 0x03, // bmAttributes (Interrupt) + 0x40, 0x00, // wMaxPacketSize 64 + 0x01, // bInterval 1 (unit depends on device speed) +}; + +static uint8_t const * xbone_configuration_descriptor_cb(uint8_t index) +{ + return xbone_configuration_descriptor; +} diff --git a/headers/gp2040.h b/headers/gp2040.h index 8661fc92c..6e0b3e12d 100644 --- a/headers/gp2040.h +++ b/headers/gp2040.h @@ -48,6 +48,7 @@ class GP2040 { SET_INPUT_MODE_XINPUT, SET_INPUT_MODE_KEYBOARD, SET_INPUT_MODE_PS4, + SET_INPUT_MODE_XBONE, SET_INPUT_MODE_NEOGEO, SET_INPUT_MODE_MDMINI, SET_INPUT_MODE_PCEMINI, diff --git a/headers/tusb_config.h b/headers/tusb_config.h index b419bb539..ae4260b3c 100644 --- a/headers/tusb_config.h +++ b/headers/tusb_config.h @@ -90,6 +90,9 @@ #define CFG_TUH_ENABLED 1 #define CFG_TUH_RPI_PIO_USB 1 +// Enable X-Input host config +#define CFG_TUH_XINPUT 1 + # define TUH_OPT_RHPORT 1 // CFG_TUSB_DEBUG is defined by compiler in DEBUG build // #define CFG_TUSB_DEBUG 0 diff --git a/headers/usbaddon.h b/headers/usbaddon.h index e5f4a9121..82864678a 100644 --- a/headers/usbaddon.h +++ b/headers/usbaddon.h @@ -15,8 +15,10 @@ class USBAddon : public GPAddon virtual void preprocess() = 0; virtual std::string name() = 0; virtual void mount(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_report, uint16_t desc_len) = 0; + virtual void xmount(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype) = 0; virtual void unmount(uint8_t dev_addr) = 0; virtual void report_received(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) = 0; + virtual void report_sent(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) = 0; virtual void set_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len) = 0; virtual void get_report_complete(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len) = 0; }; diff --git a/headers/usbhostmanager.h b/headers/usbhostmanager.h index 43b9e9ac9..82f069fa6 100644 --- a/headers/usbhostmanager.h +++ b/headers/usbhostmanager.h @@ -6,6 +6,11 @@ #include "pio_usb.h" +#include "host/usbh_pvt.h" + +// USB Host manager decides on TinyUSB Host driver +usbh_class_driver_t const* usbh_app_driver_get_cb(uint8_t *driver_count); + // Missing TinyUSB call bool tuh_hid_get_report(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, void* report, uint16_t len); @@ -26,6 +31,11 @@ class USBHostManager { void hid_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); void hid_set_report_complete_cb(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len); void hid_get_report_complete_cb(uint8_t dev_addr, uint8_t instance, uint8_t report_id, uint8_t report_type, uint16_t len); + void xinput_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype); + void xinput_umount_cb(uint8_t dev_addr); + void xinput_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + void xinput_report_sent_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + private: USBHostManager() : tuh_ready(false), core0Ready(false), core1Ready(false) { diff --git a/lib/TinyUSB_Gamepad/CMakeLists.txt b/lib/TinyUSB_Gamepad/CMakeLists.txt index 2f2d81fdd..11add49c6 100644 --- a/lib/TinyUSB_Gamepad/CMakeLists.txt +++ b/lib/TinyUSB_Gamepad/CMakeLists.txt @@ -13,6 +13,9 @@ src/xid_driver/xid.c src/xid_driver/xid_gamepad.c src/xid_driver/xid_remote.c src/xid_driver/xid_steelbattalion.c +src/xgip_protocol.cpp +src/xinput_host.cpp +src/xbone_driver.cpp ${PROTO_OUTPUT_DIR}/enums.pb.h ) target_include_directories(TinyUSB_Gamepad PUBLIC diff --git a/lib/TinyUSB_Gamepad/src/tusb_driver.cpp b/lib/TinyUSB_Gamepad/src/tusb_driver.cpp index e16ed4966..bad4c7714 100644 --- a/lib/TinyUSB_Gamepad/src/tusb_driver.cpp +++ b/lib/TinyUSB_Gamepad/src/tusb_driver.cpp @@ -18,6 +18,7 @@ #include "xinput_driver.h" #include "ps4_driver.h" #include "xid_driver/xid_driver.h" +#include "xbone_driver.h" UsbMode usb_mode = USB_MODE_HID; InputMode input_mode = INPUT_MODE_XINPUT; @@ -81,6 +82,9 @@ bool send_report(void *report, uint16_t report_size) case INPUT_MODE_XINPUT: sent = send_xinput_report(report, report_size); break; + case INPUT_MODE_XBONE: + sent = send_xbone_report(report, report_size); + break; case INPUT_MODE_KEYBOARD: sent = send_keyboard_report(report); break; @@ -92,10 +96,22 @@ bool send_report(void *report, uint16_t report_size) if (sent) memcpy(previous_report, report, report_size); } - + return sent; } +// Some input drivers need their own process/update logic +void update_input_driver() { + switch (input_mode) + { + case INPUT_MODE_XBONE: + xbone_driver_update(); + break; + default: + break; + }; +} + /* USB Driver Callback (Required for XInput) */ const usbd_class_driver_t *usbd_app_driver_get_cb(uint8_t *driver_count) @@ -116,8 +132,11 @@ const usbd_class_driver_t *usbd_app_driver_get_cb(uint8_t *driver_count) case INPUT_MODE_PS4: return &ps4_driver; - case INPUT_MODE_XBOXORIGINAL: - return xid_get_driver(); + case INPUT_MODE_XBOXORIGINAL: + return xid_get_driver(); + + case INPUT_MODE_XBONE: + return &xbone_driver; default: return &hid_driver; @@ -211,6 +230,8 @@ void tud_hid_set_report_cb(uint8_t itf, uint8_t report_id, hid_report_type_t rep set_ps4_report(report_id, buffer, bufsize); } break; + default: + break; } // echo back anything we received from host @@ -254,9 +275,12 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, bool ret = false; switch (input_mode) { - case INPUT_MODE_XBOXORIGINAL: - ret |= xid_get_driver()->control_xfer_cb(rhport, stage, request); - break; + case INPUT_MODE_XBOXORIGINAL: + ret |= xid_get_driver()->control_xfer_cb(rhport, stage, request); + break; + case INPUT_MODE_XBONE: + ret = xbone_vendor_control_xfer_cb(rhport, stage, request); + break; default: break; } diff --git a/lib/TinyUSB_Gamepad/src/usb_descriptors.cpp b/lib/TinyUSB_Gamepad/src/usb_descriptors.cpp index ed254be64..1678c97be 100644 --- a/lib/TinyUSB_Gamepad/src/usb_descriptors.cpp +++ b/lib/TinyUSB_Gamepad/src/usb_descriptors.cpp @@ -29,7 +29,7 @@ uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) // Invoked when received GET DEVICE DESCRIPTOR // Application return pointer to descriptor -uint8_t const *tud_descriptor_device_cb(void) +uint8_t const *tud_descriptor_device_cb() { switch (get_input_mode()) { @@ -39,6 +39,9 @@ uint8_t const *tud_descriptor_device_cb(void) case INPUT_MODE_XINPUT: return xinput_device_descriptor; + case INPUT_MODE_XBONE: + return xbone_device_descriptor; + case INPUT_MODE_PS4: return ps4_device_descriptor; @@ -127,6 +130,9 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) case INPUT_MODE_XINPUT: return xinput_configuration_descriptor; + case INPUT_MODE_XBONE: + return xbone_configuration_descriptor_cb(index); + case INPUT_MODE_PS4: return ps4_configuration_descriptor; @@ -160,4 +166,14 @@ uint8_t const *tud_descriptor_configuration_cb(uint8_t index) default: return hid_configuration_descriptor; } +} + +uint8_t const* tud_descriptor_device_qualifier_cb(void) { + switch (get_input_mode()) + { + case INPUT_MODE_XBONE: + return xbone_device_qualifier; + default: + return nullptr; + } } \ No newline at end of file diff --git a/lib/TinyUSB_Gamepad/src/usb_driver.h b/lib/TinyUSB_Gamepad/src/usb_driver.h index accbd0819..f60b3da4a 100644 --- a/lib/TinyUSB_Gamepad/src/usb_driver.h +++ b/lib/TinyUSB_Gamepad/src/usb_driver.h @@ -20,4 +20,5 @@ bool get_usb_suspended(void); void initialize_driver(InputMode mode); void receive_report(uint8_t *buffer); bool send_report(void *report, uint16_t report_size); +void update_input_driver(); diff --git a/lib/TinyUSB_Gamepad/src/xbone_driver.cpp b/lib/TinyUSB_Gamepad/src/xbone_driver.cpp new file mode 100644 index 000000000..da1bf7bf5 --- /dev/null +++ b/lib/TinyUSB_Gamepad/src/xbone_driver.cpp @@ -0,0 +1,402 @@ +/* + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2021 Jason Skuby (mytechtoybox.com) + */ + +#include "xbone_driver.h" +#include "gamepad/descriptors/XBOneDescriptors.h" + +#include "system.h" + +#define ENDPOINT_SIZE 64 + +#define CFG_TUD_XBONE 8 +#define CFG_TUD_XINPUT_TX_BUFSIZE 64 +#define CFG_TUD_XINPUT_RX_BUFSIZE 64 + +#define USB_SETUP_DEVICE_TO_HOST 0x80 +#define USB_SETUP_HOST_TO_DEVICE 0x00 +#define USB_SETUP_TYPE_VENDOR 0x40 +#define USB_SETUP_TYPE_CLASS 0x20 +#define USB_SETUP_TYPE_STANDARD 0x00 +#define USB_SETUP_RECIPIENT_INTERFACE 0x01 +#define USB_SETUP_RECIPIENT_DEVICE 0x00 +#define USB_SETUP_RECIPIENT_ENDPOINT 0x02 +#define USB_SETUP_RECIPIENT_OTHER 0x03 + +#define REQ_GET_OS_FEATURE_DESCRIPTOR 0x20 +#define DESC_EXTENDED_COMPATIBLE_ID_DESCRIPTOR 0x0004 +#define DESC_EXTENDED_PROPERTIES_DESCRIPTOR 0x0005 +#define REQ_GET_XGIP_HEADER 0x90 + +static bool waiting_ack=false; +static uint32_t waiting_ack_timeout=0; +uint8_t xbone_out_buffer[XBONE_OUT_SIZE] = {}; +uint32_t timer_wait_for_announce = 0; +uint32_t xbox_one_powered_on = false; +uint32_t keep_alive_timer = 0; + +// Sent report queue every 15 milliseconds +static uint32_t lastReportQueueSent = 0; +#define REPORT_QUEUE_INTERVAL 15 + +// Report Queue for big report sizes from dongle +#include +typedef struct { + uint8_t report[XBONE_ENDPOINT_SIZE]; + uint16_t len; +} report_queue_t; + +static std::queue report_queue; + +#define XGIP_ACK_WAIT_TIMEOUT 2000 + +typedef struct { + uint8_t itf_num; + uint8_t ep_in; + uint8_t ep_out; // optional Out endpoint + CFG_TUSB_MEM_ALIGN uint8_t epin_buf[CFG_TUD_XINPUT_TX_BUFSIZE]; + CFG_TUSB_MEM_ALIGN uint8_t epout_buf[CFG_TUD_XINPUT_RX_BUFSIZE]; +} xboned_interface_t; + +CFG_TUSB_MEM_SECTION static xboned_interface_t _xboned_itf[CFG_TUD_XBONE]; +static inline uint8_t get_index_by_itfnum(uint8_t itf_num) { + for (uint8_t i = 0; i < CFG_TUD_XBONE; i++) { + if (itf_num == _xboned_itf[i].itf_num) return i; + } + + return 0xFF; +} + +typedef enum { + IDLE_STATE = 0, + READY_ANNOUNCE, + WAIT_DESCRIPTOR_REQUEST, + SEND_DESCRIPTOR, + SETUP_AUTH +} XboxOneDriverState; + +static XboxOneDriverState xboneDriverState; + +static XGIPProtocol outgoingXGIP; +static XGIPProtocol incomingXGIP; + +// Check if Auth is completed (start is 0x01, 0x01, and invalid is 0x01, 0x07) +const uint8_t authReady[] = {0x01, 0x00}; + +// Xbox One Announce +static uint8_t announcePacket[] = { + 0x00, 0x2a, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, + 0xdf, 0x33, 0x14, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x17, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x01, 0x00}; + +// Xbox One Descriptor +const uint8_t xboxOneDescriptor[] = { + 0x10, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCA, 0x00, + 0x8B, 0x00, 0x16, 0x00, 0x1F, 0x00, 0x20, 0x00, + 0x27, 0x00, 0x2D, 0x00, 0x4A, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x06, 0x01, 0x02, 0x03, 0x04, 0x06, 0x07, 0x05, + 0x01, 0x04, 0x05, 0x06, 0x0A, 0x01, 0x1A, 0x00, + 0x57, 0x69, 0x6E, 0x64, 0x6F, 0x77, 0x73, 0x2E, + 0x58, 0x62, 0x6F, 0x78, 0x2E, 0x49, 0x6E, 0x70, + 0x75, 0x74, 0x2E, 0x47, 0x61, 0x6D, 0x65, 0x70, + 0x61, 0x64, 0x04, 0x56, 0xFF, 0x76, 0x97, 0xFD, + 0x9B, 0x81, 0x45, 0xAD, 0x45, 0xB6, 0x45, 0xBB, + 0xA5, 0x26, 0xD6, 0x2C, 0x40, 0x2E, 0x08, 0xDF, + 0x07, 0xE1, 0x45, 0xA5, 0xAB, 0xA3, 0x12, 0x7A, + 0xF1, 0x97, 0xB5, 0xE7, 0x1F, 0xF3, 0xB8, 0x86, + 0x73, 0xE9, 0x40, 0xA9, 0xF8, 0x2F, 0x21, 0x26, + 0x3A, 0xCF, 0xB7, 0xFE, 0xD2, 0xDD, 0xEC, 0x87, + 0xD3, 0x94, 0x42, 0xBD, 0x96, 0x1A, 0x71, 0x2E, + 0x3D, 0xC7, 0x7D, 0x02, 0x17, 0x00, 0x20, 0x20, + 0x00, 0x01, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x17, 0x00, 0x09, 0x3C, 0x00, + 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00}; + +// Windows requires a Descriptor Single for Xbox One +typedef struct { + uint32_t TotalLength; + uint16_t Version; + uint16_t Index; + uint8_t TotalSections; + uint8_t Reserved[7]; + uint8_t FirstInterfaceNumber; + uint8_t Reserved2; + uint8_t CompatibleID[8]; + uint8_t SubCompatibleID[8]; + uint8_t Reserved3[6]; +} __attribute__((packed)) OS_COMPATIBLE_ID_DESCRIPTOR_SINGLE; + +const OS_COMPATIBLE_ID_DESCRIPTOR_SINGLE DevCompatIDsOne = { + TotalLength : sizeof(OS_COMPATIBLE_ID_DESCRIPTOR_SINGLE), + Version : 0x0100, + Index : DESC_EXTENDED_COMPATIBLE_ID_DESCRIPTOR, + TotalSections : 1, + Reserved : {0}, + Reserved2 : 0x01, + CompatibleID : {'X','G','I','P','1','0',0,0}, + SubCompatibleID : {0}, + Reserved3 : {0} +}; + +void queue_xbone_report(void *report, uint16_t report_size) { + report_queue_t item; + memcpy(item.report, report, report_size); + item.len = report_size; + report_queue.push(item); +} + +void set_ack_wait() { + waiting_ack = true; + waiting_ack_timeout = to_ms_since_boot(get_absolute_time()); // 2 second time-out +} + +static void xbone_reset(uint8_t rhport) { + (void)rhport; + timer_wait_for_announce = to_ms_since_boot(get_absolute_time()); + xbox_one_powered_on = false; + while(!report_queue.empty()) + report_queue.pop(); + + xboneDriverState = XboxOneDriverState::READY_ANNOUNCE; + + // close any endpoints that are open + tu_memclr(&_xboned_itf, sizeof(_xboned_itf)); +} + +static void xbone_init(void) { + xbone_reset(TUD_OPT_RHPORT); +} + +static uint16_t xbone_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16_t max_len) { + uint16_t drv_len = 0; + if (TUSB_CLASS_VENDOR_SPECIFIC == itf_desc->bInterfaceClass) { + TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == itf_desc->bInterfaceClass, 0); + + drv_len = sizeof(tusb_desc_interface_t) + + (itf_desc->bNumEndpoints * sizeof(tusb_desc_endpoint_t)); + TU_VERIFY(max_len >= drv_len, 0); + + // Find available interface + xboned_interface_t *p_xbone = NULL; + for (uint8_t i = 0; i < CFG_TUD_XBONE; i++) { + if (_xboned_itf[i].ep_in == 0 && _xboned_itf[i].ep_out == 0) { + p_xbone = &_xboned_itf[i]; + break; + } + } + + TU_VERIFY(p_xbone, 0); + uint8_t const *p_desc = (uint8_t const *)itf_desc; + + // Xbox One interface (subclass = 0x47, protocol = 0xD0) + if (itf_desc->bInterfaceSubClass == 0x47 && + itf_desc->bInterfaceProtocol == 0xD0) { + p_desc = tu_desc_next(p_desc); + TU_ASSERT(usbd_open_edpt_pair(rhport, p_desc, itf_desc->bNumEndpoints, TUSB_XFER_INTERRUPT, &p_xbone->ep_out, &p_xbone->ep_in), 0); + + p_xbone->itf_num = itf_desc->bInterfaceNumber; + + // Prepare for output endpoint + if (p_xbone->ep_out) { + if (!usbd_edpt_xfer(rhport, p_xbone->ep_out, p_xbone->epout_buf, sizeof(p_xbone->epout_buf))) { + TU_LOG_FAILED(); + TU_BREAKPOINT(); + } + } + } + } + + return drv_len; +} + +static bool xbone_device_control_request(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { + return true; +} + +static bool xbone_control_complete(uint8_t rhport, tusb_control_request_t const *request) { + (void)rhport; + (void)request; + return true; +} + +bool xbone_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, + uint32_t xferred_bytes) { + (void)result; + uint8_t itf = 0; + xboned_interface_t *p_xbone = _xboned_itf; + + for (;; itf++, p_xbone++) { + if (itf >= TU_ARRAY_SIZE(_xboned_itf)) return false; + if (ep_addr == p_xbone->ep_out || ep_addr == p_xbone->ep_in) break; + } + + if (ep_addr == p_xbone->ep_out) { + // Parse incoming packet and verify its valid + incomingXGIP.parse(p_xbone->epout_buf, xferred_bytes); + + // Setup an ack before we change anything about the incoming packet + if ( incomingXGIP.ackRequired() == true ) { + queue_xbone_report((uint8_t*)incomingXGIP.generateAckPacket(), incomingXGIP.getPacketLength()); + } + + uint8_t command = incomingXGIP.getCommand(); + if ( command == GIP_ACK_RESPONSE ) { + waiting_ack = false; + } else if ( command == GIP_DEVICE_DESCRIPTOR ) { + // setup descriptor packet + outgoingXGIP.reset(); // reset if anything was in there + outgoingXGIP.setAttributes(GIP_DEVICE_DESCRIPTOR, incomingXGIP.getSequence(), 1, 1, 0); + outgoingXGIP.setData(xboxOneDescriptor, sizeof(xboxOneDescriptor)); + xboneDriverState = XboxOneDriverState::SEND_DESCRIPTOR; + } else if ( command == GIP_POWER_MODE_DEVICE_CONFIG || command == GIP_CMD_WAKEUP || command == GIP_CMD_RUMBLE ) { + xbox_one_powered_on = true; + } else if ( command == GIP_AUTH || command == GIP_FINAL_AUTH) { + if (incomingXGIP.getDataLength() == 2 && memcmp(incomingXGIP.getData(), authReady, sizeof(authReady))==0 ) + XboxOneData::getInstance().setAuthCompleted(true); + if ( (incomingXGIP.getChunked() == true && incomingXGIP.endOfChunk() == true) || + (incomingXGIP.getChunked() == false )) { + XboxOneData::getInstance().setAuthData(incomingXGIP.getData(), incomingXGIP.getDataLength(), incomingXGIP.getSequence(), + incomingXGIP.getCommand(), XboxOneState::send_auth_console_to_dongle); + incomingXGIP.reset(); + } + } + + TU_ASSERT(usbd_edpt_xfer(rhport, p_xbone->ep_out, p_xbone->epout_buf, + sizeof(p_xbone->epout_buf))); + } else if (ep_addr == p_xbone->ep_in) { + // Nothing needed + } + return true; +} + +// DevCompatIDsOne sends back XGIP10 data when requested by Windows +bool xbone_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, + tusb_control_request_t const *request) { + uint8_t buf[255]; + + // nothing to with DATA & ACK stage + if (stage != CONTROL_STAGE_SETUP) + return true; + + if (request->bmRequestType_bit.direction == TUSB_DIR_IN) { // This is where we should be + uint16_t len = request->wLength; + if ( request->bmRequestType == (USB_SETUP_DEVICE_TO_HOST | USB_SETUP_RECIPIENT_DEVICE | USB_SETUP_TYPE_VENDOR) && request->bRequest == REQ_GET_OS_FEATURE_DESCRIPTOR && + request->wIndex == DESC_EXTENDED_COMPATIBLE_ID_DESCRIPTOR) { + memcpy(buf, &DevCompatIDsOne, len); + } + tud_control_xfer(rhport, request, (void*)buf, len); + } else { + tud_control_xfer(rhport, request, (void*)buf, request->wLength); + } + return true; +} + +// Send a packet to our Xbox One driver end-point +bool send_xbone_report(void *report, uint16_t report_size) { + uint8_t itf = 0; + xboned_interface_t *p_xbone = _xboned_itf; + bool ret = false; + for (;; itf++, p_xbone++) { + if (itf >= TU_ARRAY_SIZE(_xboned_itf)) { + return false; + } + if (p_xbone->ep_in) + break; + } + if ( tud_ready() && // Is the device ready? + (p_xbone->ep_in != 0) && (!usbd_edpt_busy(TUD_OPT_RHPORT, p_xbone->ep_in))) // Is the IN endpoint available? + { + usbd_edpt_claim(0, p_xbone->ep_in); // Take control of IN endpoint + ret = usbd_edpt_xfer(0, p_xbone->ep_in, (uint8_t *)report, report_size); // Send report buffer + usbd_edpt_release(0, p_xbone->ep_in); // Release control of IN endpoint + } + + return ret; +} + +const usbd_class_driver_t xbone_driver = + { +#if CFG_TUSB_DEBUG >= 2 + .name = "XBONE", +#endif + .init = xbone_init, + .reset = xbone_reset, + .open = xbone_open, + .control_xfer_cb = tud_vendor_control_xfer_cb, + .xfer_cb = xbone_xfer_cb, + .sof = NULL}; + +// Update our Xbox One driver as things need to happen under-the-hood +void xbone_driver_update() { + uint32_t now = to_ms_since_boot(get_absolute_time()); + + if ( !report_queue.empty() ) { + if ( (now - lastReportQueueSent) > REPORT_QUEUE_INTERVAL ) { + if ( send_xbone_report(report_queue.front().report, report_queue.front().len) ) { + report_queue.pop(); + lastReportQueueSent = now; + } else { + sleep_ms(REPORT_QUEUE_INTERVAL); + } + } + } + + // Do not add logic until our ACK returns + if ( waiting_ack == true ) { + if ((now - waiting_ack_timeout) < XGIP_ACK_WAIT_TIMEOUT) { + return; + } else { // ACK wait time out + waiting_ack = false; + } + } + + switch(xboneDriverState) { + case READY_ANNOUNCE: + // Xbox One announce must wait around 0.5s before sending + if ( now - timer_wait_for_announce > 500 ) { + memcpy((void*)&announcePacket[3], &now, 3); + outgoingXGIP.setAttributes(GIP_ANNOUNCE, 1, 1, 0, 0); + outgoingXGIP.setData(announcePacket, sizeof(announcePacket)); + queue_xbone_report(outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + xboneDriverState = WAIT_DESCRIPTOR_REQUEST; + } + break; + case SEND_DESCRIPTOR: + queue_xbone_report(outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + if ( outgoingXGIP.endOfChunk() == true ) { + xboneDriverState = SETUP_AUTH; + } + if ( outgoingXGIP.getPacketAck() == 1 ) { // ACK can happen at different chunks + set_ack_wait(); + } + break; + case SETUP_AUTH: + if ( XboxOneData::getInstance().getState() == XboxOneState::send_auth_dongle_to_console ) { + bool isChunked = (XboxOneData::getInstance().getAuthLen() > GIP_MAX_CHUNK_SIZE); + outgoingXGIP.reset(); + outgoingXGIP.setAttributes(XboxOneData::getInstance().getAuthType(), XboxOneData::getInstance().getSequence(), 1, isChunked, 1); + outgoingXGIP.setData(XboxOneData::getInstance().getAuthBuffer(), XboxOneData::getInstance().getAuthLen()); + XboxOneData::getInstance().setState(wait_auth_dongle_to_console); + } else if ( XboxOneData::getInstance().getState() == XboxOneState::wait_auth_dongle_to_console ) { + queue_xbone_report(outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + if ( outgoingXGIP.getChunked() == false || outgoingXGIP.endOfChunk() == true ) { + XboxOneData::getInstance().setState(XboxOneState::auth_idle_state); + } + if ( outgoingXGIP.getPacketAck() == 1 ) { // ACK can happen at different chunks + set_ack_wait(); + } + } + break; + case IDLE_STATE: + default: + break; + }; +} diff --git a/lib/TinyUSB_Gamepad/src/xbone_driver.h b/lib/TinyUSB_Gamepad/src/xbone_driver.h new file mode 100644 index 000000000..453f3d122 --- /dev/null +++ b/lib/TinyUSB_Gamepad/src/xbone_driver.h @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: MIT + * SPDX-FileCopyrightText: Copyright (c) 2021 Jason Skuby (mytechtoybox.com) + */ + +#pragma once + +#include +#include "tusb.h" +#include "device/usbd_pvt.h" + +#include "gamepad/descriptors/XBOneDescriptors.h" + +#include "xgip_protocol.h" + +#define XBONE_OUT_SIZE 64 + +// USB endpoint state vars +extern uint8_t xbone_out_buffer[XBONE_OUT_SIZE]; +extern const usbd_class_driver_t xbone_driver; + +//extern void send_xbhost_report(void *report, uint16_t report_size); + +extern bool send_xbone_report(void *report, uint16_t report_size); +extern bool xbone_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, + tusb_control_request_t const *request); + +extern uint32_t timer_wait_for_auth; + +extern void xbone_driver_update(); + +typedef enum { + auth_idle_state = 0, + send_auth_console_to_dongle = 1, + send_auth_dongle_to_console = 2, + wait_auth_console_to_dongle = 3, + wait_auth_dongle_to_console = 4, +} XboxOneState; + +// Storage manager for board, LED options, and thread-safe settings +class XboxOneData { +public: + XboxOneData(XboxOneData const&) = delete; + void operator=(XboxOneData const&) = delete; + static XboxOneData& getInstance() // Thread-safe storage ensures cross-thread talk + { + static XboxOneData instance; + return instance; + } + + void setState(XboxOneState newState) { + xboneState = newState; + } + + uint8_t * getAuthBuffer() { + return authBuffer; + } + + uint8_t getSequence() { + return authSequence; + } + + uint16_t getAuthLen() { + return authLen; + } + + uint8_t getAuthType() { + return authType; + } + + XboxOneState getState() { + return xboneState; + } + + void setAuthData(uint8_t * buf, uint16_t bufLen, uint8_t seq, uint8_t type, XboxOneState newState) { + // Cannot copy larger than our buffer + if ( bufLen > 1024) { + return; + } + memcpy(authBuffer, buf, bufLen); + authLen = bufLen; + authType = type; + authSequence = seq; + xboneState = newState; + } + + bool getAuthCompleted() { + return authCompleted; + } + + void setAuthCompleted(bool completed) { + authCompleted = completed; + } + +private: + XboxOneData() { + xboneState = auth_idle_state; + authLen = 0; + authSequence = 0; + authCompleted = false; + } + + XboxOneState xboneState; + + // Console-to-Host e.g. Xbox One to MagicBoots + // Note: the Xbox One Passthrough can call send_xbone_report() directly but not the other way around + bool authCompleted; + + // Auth Buffer + uint8_t authBuffer[1024]; + uint8_t authSequence; + uint16_t authLen; + uint8_t authType; +}; diff --git a/lib/TinyUSB_Gamepad/src/xgip_protocol.cpp b/lib/TinyUSB_Gamepad/src/xgip_protocol.cpp new file mode 100644 index 000000000..deaff05cb --- /dev/null +++ b/lib/TinyUSB_Gamepad/src/xgip_protocol.cpp @@ -0,0 +1,384 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 OpenStickCommunity (gp2040-ce.info) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "xgip_protocol.h" +#include "gamepad/descriptors/XBOneDescriptors.h" + +// Default Constructor +XGIPProtocol::XGIPProtocol() { + reset(); +} + +// Default Destructor +XGIPProtocol::~XGIPProtocol() { + if ( data != nullptr ) { + delete data; + } +} + +// Reset packet information +void XGIPProtocol::reset() { + memset((void*)&header, 0, sizeof(GipHeader_t)); + totalChunkLength = 0; // How big is the chunk? + actualDataReceived = 0; // How much actual data have we received? + totalChunkReceived = 0; // How much have we received in chunk mode length? (length | 0x80) + totalChunkSent = 0; // How much chunk-data (not real total) have we sent? + totalDataSent = 0; // How much actual data have we sent? + numberOfChunksSent = 0; // How many actual chunks have we sent? + chunkEnded = false; // Are we at the end of the chunk? + isValidPacket = false; // Is this a valid packet? + if ( data != nullptr ) { // Delete our data if its not null + delete [] data; + } + data = nullptr; + dataLength = 0; // Set data length to 0 + memset(packet, 0, sizeof(packet)); // Set our packet to 0 + packetLength = 0; // Set packet length to 0 +} + +// Parse incoming packet +bool XGIPProtocol::parse(const uint8_t * buffer, uint16_t len) { + // Do we have enough room for a header? No, this isn't valid + if ( len < 4 ) { + reset(); + isValidPacket = false; + return false; + } + + // Set packet length + packetLength = len; + + // Use buffer as a raw packet without copying to our internal structure + GipHeader_t * newPacket = (GipHeader_t*)buffer; + if ( newPacket->command == GIP_ACK_RESPONSE ) { + if ( len != 13 || newPacket->internal != 0x01 || newPacket->length != 0x09 ) { + reset(); + isValidPacket = false; + return false; // something malformed in this packet + } + memcpy((void*)&header, buffer, sizeof(GipHeader_t)); + isValidPacket = true; // don't do anything with ack packets for now + return true; + } else { // Non-ACK + // Continue parsing chunked data + if ( newPacket->chunked == true ) { + memcpy((void*)&header, buffer, sizeof(GipHeader_t)); // Always copy to header buffer + if ( header.length == 0 ) { // END OF CHUNK + uint16_t endChunkSize = (buffer[4] | buffer[5] << 8); + // Verify chunk is good + if ( totalChunkLength != endChunkSize) { + isValidPacket = false; + return false; + } + chunkEnded = true; + isValidPacket = true; + return true; // we're good! + } + if ( header.chunkStart == 1 ) { // START OF CHUNK + reset(); + memcpy((void*)&header, buffer, sizeof(GipHeader_t)); + + // Get total chunk length in uint16 + if ( header.length > GIP_MAX_CHUNK_SIZE && buffer[4] == 0x00 ) { // if we see 0xBA and buf[4] == 0, single-byte mode + totalChunkLength = (uint16_t)buffer[5]; // byte is correct + } else { + // we need to calculate the actual buffer length as this number is not right + totalChunkLength = ((uint16_t)buffer[4] | ((uint16_t)buffer[5] << 8)); // not the actual length but the chunked length (length | 0x80) + } + + // Real data length = chunk length > 0x100? (chunk length - 0x100) - ((chunk length / 0x100)*0x80) + dataLength = totalChunkLength; + if ( totalChunkLength > 0x100 ) { + dataLength = dataLength - 0x100; + dataLength = dataLength - ((dataLength / 0x100)*0x80); + } + + // Ensure we clear data if its set to something else + if ( data != nullptr ) + delete [] data; + data = new uint8_t[dataLength]; + actualDataReceived = 0; // haven't received anything yet + totalChunkReceived = header.length; // + } else { + totalChunkReceived += header.length; // not actual data length, but chunk value + } + uint16_t copyLength = header.length; + if ( header.length > GIP_MAX_CHUNK_SIZE ) { // if length is greater than 0x3A (bigger than 64 bytes), we know it is | 0x80 so we can ^ 0x80 and get the real length + copyLength ^= 0x80; // packet length is set to length | 0x80 (0xBA instead of 0x3A) + } + memcpy(&data[actualDataReceived], &buffer[6], copyLength); // + actualDataReceived += copyLength; + numberOfChunksSent++; // count our chunks for the ACK + isValidPacket = true; + } else { + reset(); + memcpy((void*)&header, buffer, sizeof(GipHeader_t)); + if ( header.length > 0 ) { + if (data != nullptr) + delete [] data; + data = new uint8_t[header.length]; + memcpy(data, &buffer[4], header.length); // copy incoming data + } + actualDataReceived = header.length; + dataLength = actualDataReceived; + isValidPacket = true; + } + } + + return false; +} + +bool XGIPProtocol::endOfChunk() { + return chunkEnded; +} + +bool XGIPProtocol::validate() { // is valid packet? + return isValidPacket; +} + +void XGIPProtocol::incrementSequence() { + header.sequence++; + if ( header.sequence == 0 ) + header.sequence = 1; +} + +void XGIPProtocol::setAttributes(uint8_t cmd, uint8_t seq, uint8_t internal, uint8_t isChunked, uint8_t needsAck) { // Set attributes for next output packet + header.command = cmd; + header.sequence = seq; + header.internal = internal; + header.chunked = isChunked; + header.needsAck = needsAck; +} + +bool XGIPProtocol::setData(const uint8_t * buffer, uint16_t len) { + if ( len > 0x3000) { // arbitrary but this should cover us if something bad happens + return false; + } + if ( data != nullptr ) + delete [] data; + data = new uint8_t[len]; + memcpy(data, buffer, len); + dataLength = len; + return true; +} + +// Generate XGIP Packet for output +uint8_t * XGIPProtocol::generatePacket() { + if ( header.chunked == 0 ) { // Simple data packet does not require chunk logic + header.length = (uint8_t)dataLength; + memcpy(packet, &header, sizeof(GipHeader_t)); + memcpy((void*)&packet[4], data, dataLength); + packetLength = sizeof(GipHeader_t) + dataLength; + } else { // Are we a chunk? + if ( numberOfChunksSent > 0 && totalDataSent == dataLength ) { // General Final Chunk Packet (End-Packet) + header.needsAck = 0; + header.length = 0; + memcpy(packet, &header, sizeof(GipHeader_t)); + packet[4] = totalChunkLength & 0x00FF; + packet[5] = (totalChunkLength & 0xFF00) >> 8; + packetLength = sizeof(GipHeader_t) + 2; + chunkEnded = true; + } else { + if ( numberOfChunksSent == 0 ) { + if ( dataLength < GIP_MAX_CHUNK_SIZE ) { + // In the rare case the chunked packet is < max chunk size + // we set the chunk flags to 0, set our actual data length + // BUT we still require an ACK and have to reply to it + totalChunkLength = dataLength; + header.chunkStart = 0; + header.chunked = 0; + } else { + header.chunkStart = 1; + // Calculate our chunk length by replicating the output of our chunks in 0x3A size + uint16_t i = dataLength; + uint16_t j = 0; + do { + if ( i < GIP_MAX_CHUNK_SIZE ) { + if ( (j / 0x100) != ((j + i) / 0x100)) { // if we go 0x100 to 0x200, or 0x200 to 0x300, | 0x80 + j = j + (i | 0x80); + } else { + j = j + i; + } + i = 0; + } else { + if ( (j + GIP_MAX_CHUNK_SIZE > 0x80) && (j + GIP_MAX_CHUNK_SIZE < 0x100) ) { + j = j + GIP_MAX_CHUNK_SIZE + 0x100; // first 0x80 bytes, move up 0x100 if we get this far + } else { + if ( (j / 0x100) != ((j + GIP_MAX_CHUNK_SIZE) / 0x100)) { + j = j + (GIP_MAX_CHUNK_SIZE | 0x80); + } else { + j = j + GIP_MAX_CHUNK_SIZE; + } + } + i = i - GIP_MAX_CHUNK_SIZE; + } + } while( i != 0 ); + totalChunkLength = j; + } + } else { + header.chunkStart = 0; // set chunk start to 0 in all other cases + } + + // Ack on 1st and every 5th interval + // Note: this will send on (0 chunks sent) 1st, (4 chunks sent) 5th, (5 chunks sent) 10th, (5 chunks sent) 15th. this is correct + if ( numberOfChunksSent == 0 || (numberOfChunksSent+1)%5 == 0 ) { + header.needsAck = 1; + } else { + header.needsAck = 0; + } + + // Assume we're sending the maximum chunk size + uint16_t dataToSend = GIP_MAX_CHUNK_SIZE; + + // If we're at the end, reduce data to send and set the ack flag + if ( (dataLength - totalDataSent) < dataToSend ) { + dataToSend = dataLength - totalDataSent; + header.needsAck = 1; + } + + // If we've sent our first chunk already and total chunks sent is < 0x100, | 0x80 + if ( numberOfChunksSent > 0 && totalChunkSent < 0x100 ) { + header.length = dataToSend | 0x80; + // we haven't sent any chunks and data length total < 0x80 + } else if ( numberOfChunksSent == 0 && dataLength > GIP_MAX_CHUNK_SIZE && dataLength < 0x80 ) { + header.length = dataToSend | 0x80; // data < 0x80 and first chunk, we |0x80 + } else { + header.length = dataToSend; // length is actual data to send + } + + // Copy our header and data to the packet + memcpy(packet, &header, sizeof(GipHeader_t)); + memcpy((void*)&packet[6], &data[totalDataSent], dataToSend); + + // Set our packet length + packetLength = sizeof(GipHeader_t) + 2 + dataToSend; + + // If first packet, chunk value in [4][5] is total chunk length + uint16_t chunkValue; + if ( numberOfChunksSent == 0 ) { + chunkValue = totalChunkLength; + // else, chunk value is total chunk sent + } else { + chunkValue = totalChunkSent; + } + + // Place value in right-byte if our chunk value is < 0x100 + if ( chunkValue < 0x100 ) { + packet[4] = 0x00; + packet[5] = (uint8_t) chunkValue; + // Split appropriately + } else { + packet[4] = chunkValue & 0x00FF; + packet[5] = (chunkValue & 0xFF00) >> 8; + } + + // XGIP Hashing: If we're sending over 0x80, + ( data to send + 0x100 ) + if ( totalChunkSent < 0x100 && (totalChunkSent + dataToSend) > 0x80) { + totalChunkSent = totalChunkSent + dataToSend + 0x100; + // else if our next chunk sent will roll over the 3rd digit e.g. 0x200 to 0x300, + ( data to send | 0x80 ) + } else if ( ((totalChunkSent + dataToSend)/0x100) > (totalChunkSent/0x100)) { + totalChunkSent = totalChunkSent + (dataToSend | 0x80); + // else + ( data to send ) + } else { + totalChunkSent = totalChunkSent + dataToSend; + } + totalDataSent += dataToSend; // Total Data Sent in bytes + numberOfChunksSent++; // Number of Chunks sent so far + } + } + return packet; +} + +uint8_t * XGIPProtocol::generateAckPacket() { // Generate output packet + packet[0] = 0x01; + packet[1] = 0x20; + packet[2] = header.sequence; + packet[3] = 0x09; + packet[4] = 0x00; + packet[5] = header.command; + packet[6] = 0x20; + + // we have to keep track of # of chunks because data received for ACK is +2 for size of chunk + uint16_t dataReceived = actualDataReceived; + packet[7] = dataReceived & 0x00FF; + packet[8] = (dataReceived & 0xFF00) >> 8; + packet[9] = 0x00; + packet[10] = 0x00; + if ( header.chunked == true ) { // Are we a chunk? + uint16_t left = dataLength - dataReceived; + packet[11] = left & 0x00FF; + packet[12] = (left & 0xFF00) >> 8; + } else { + packet[11] = 0; + packet[12] = 0; + } + packetLength = 13; + return packet; +} + +// Get last generated output packet length +uint8_t XGIPProtocol::getPacketLength() { + return packetLength; +} + +// Get the header information if the packet needs an ACK +uint8_t XGIPProtocol::getPacketAck() { + return header.needsAck; +} + +// Get command of a parsed packet +uint8_t XGIPProtocol::getCommand() { + return header.command; +} + +// Is this packet chunked? +uint8_t XGIPProtocol::getChunked(){ + return header.chunked; +} + +// Get seqeuence in the header +uint8_t XGIPProtocol::getSequence() { + return header.sequence; +} + +// Get data from a packet or packet-chunk +uint8_t * XGIPProtocol::getData() { + return data; +} + +// Get length of a packet or packet-chunk +uint16_t XGIPProtocol::getDataLength() { + return dataLength; +} + +// Get chunk data from incoming packet +bool XGIPProtocol::getChunkData(XGIPProtocol & packet) { + return false; +} + +// Last packet parsed needs an ACK +bool XGIPProtocol::ackRequired() { + return header.needsAck; +} diff --git a/lib/TinyUSB_Gamepad/src/xgip_protocol.h b/lib/TinyUSB_Gamepad/src/xgip_protocol.h new file mode 100644 index 000000000..740049562 --- /dev/null +++ b/lib/TinyUSB_Gamepad/src/xgip_protocol.h @@ -0,0 +1,98 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 OpenStickCommunity (gp2040-ce.info) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _XGIP_PROTOCOL_ +#define _XGIP_PROTOCOL_ + +// +// XGIP Protocol +// XGIP10 or Xbox Game Input Protocol (1.0?) +// Used in Xbox 360 and Xbox One controller +// communication and reporting. +// +// Documentation comes from various sources +// including Santroller (https://github.com/Santroller/Santroller), +// GIMX (https://github.com/matlo/GIMX), and other +// open-source Github projects. +// +// This is a free, open-source, guess-work +// based interpretation of the protocol for +// the GP2040-CE controller platform and in +// no way reflects any commercial software or +// protocols. This implementation is provided +// as-is and we are not responsible for +// this interpretation corrupting data or sending +// invalid packets. +// +// !!!USE AT YOUR OWN RISK!!! +// + +#include + +#include "gamepad/descriptors/XBOneDescriptors.h" + +// All max chunks are this size +#define GIP_MAX_CHUNK_SIZE 0x3A + +class XGIPProtocol { +public: + XGIPProtocol(); + ~XGIPProtocol(); + void reset(); // Reset packet information + bool parse(const uint8_t * buffer, uint16_t len); // Parse incoming packet + bool validate(); // is valid packet? + bool endOfChunk(); // Is this the end of the chunk? + void setAttributes(uint8_t cmd, uint8_t seq, uint8_t internal, uint8_t isChunked, uint8_t needsAck); // Set attributes for next output packet + void incrementSequence(); // Add 1 to sequence + bool setData(const uint8_t* data, uint16_t len); // Set data (buf and length) + uint8_t * generatePacket(); // Generate output packet (chunk will generate on-going packet) + uint8_t * generateAckPacket(); // Generate an ack for the last received packet + bool validateAck(XGIPProtocol & ackPacket); // Validate an incoming ack packet against + uint8_t getCommand(); // Get command of a parsed packet + uint8_t getSequence(); // Get sequence of a parsed packet + uint8_t getChunked(); // Is this packet chunked? + uint8_t getPacketAck(); // Did the packet require an ACK? + uint8_t getPacketLength(); // Get packet length of our last output + uint8_t * getData(); // Get data from a packet or packet-chunk + uint16_t getDataLength(); // Get length of a packet or packet-chunk + bool getChunkData(XGIPProtocol & packet); // Get chunk data from incoming packet + bool ackRequired(); // Did our last parsed packet require an ack? +private: + GipHeader_t header; // On-going GIP header + uint16_t totalChunkLength; // How big is the chunk? + uint16_t actualDataReceived; // How much actual data have we received? + uint16_t totalChunkReceived; // How much have we received in chunk mode length? (length | 0x80) + uint16_t totalChunkSent; // How much have we sent? + uint16_t totalDataSent; // How much actual data have we sent? + uint16_t numberOfChunksSent; // How many actual chunks have we sent? + bool chunkEnded; // did we hit the end of the chunk successfully? + uint8_t packet[64]; // for output packets + uint16_t packetLength; // LAST SENT packet length + uint8_t * data; // Total data in this packet + uint16_t dataLength; // actual length of data + bool isValidPacket; // is this a valid packet or did we get an error? +}; + +#endif \ No newline at end of file diff --git a/lib/TinyUSB_Gamepad/src/xinput_host.cpp b/lib/TinyUSB_Gamepad/src/xinput_host.cpp new file mode 100644 index 000000000..8dc2e7049 --- /dev/null +++ b/lib/TinyUSB_Gamepad/src/xinput_host.cpp @@ -0,0 +1,276 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013 OpenStickCommunity (gp2040-ce.info) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "tusb_option.h" + +#if (CFG_TUH_ENABLED && CFG_TUH_XINPUT) + +#include "hardware/structs/usb.h" + +#include "host/usbh.h" +#include "host/usbh_pvt.h" +#include "xinput_host.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ + +typedef struct +{ + uint8_t itf_num; + uint8_t ep_in; + uint8_t ep_out; + uint8_t type; + uint8_t subtype; + + uint16_t epin_size; + uint16_t epout_size; + + uint8_t epin_buf[CFG_TUH_XINPUT_EPIN_BUFSIZE]; + uint8_t epout_buf[CFG_TUH_XINPUT_EPOUT_BUFSIZE]; +} xinputh_interface_t; + +typedef struct +{ + uint8_t inst_count; + xinputh_interface_t instances[CFG_TUH_XINPUT]; +} xinputh_device_t; +static xinputh_device_t _xinputh_dev[CFG_TUH_DEVICE_MAX]; + +//------------- Internal prototypes -------------// + +// Get HID device & interface +TU_ATTR_ALWAYS_INLINE static inline xinputh_device_t *get_dev(uint8_t dev_addr); +TU_ATTR_ALWAYS_INLINE static inline xinputh_interface_t *get_instance(uint8_t dev_addr, uint8_t instance); +static uint8_t get_instance_id_by_itfnum(uint8_t dev_addr, uint8_t itf); +static uint8_t get_instance_id_by_epaddr(uint8_t dev_addr, uint8_t ep_addr); + +//--------------------------------------------------------------------+ +// Interface API +//--------------------------------------------------------------------+ + +uint8_t tuh_xinput_instance_count(uint8_t dev_addr) { + return get_dev(dev_addr)->inst_count; +} + +bool tuh_xinput_mounted(uint8_t dev_addr, uint8_t instance) { + if (get_dev(dev_addr)->inst_count < instance) return false; + xinputh_interface_t *hid_itf = get_instance(dev_addr, instance); + return (hid_itf->ep_in != 0) || (hid_itf->ep_out != 0); +} + +//--------------------------------------------------------------------+ +// Interrupt Endpoint API +//--------------------------------------------------------------------+ + +bool tuh_xinput_receive_report(uint8_t dev_addr, uint8_t instance) { + xinputh_interface_t *xid_itf = get_instance(dev_addr, instance); + + // claim endpoint + TU_VERIFY(usbh_edpt_claim(dev_addr, xid_itf->ep_in)); + + if (!usbh_edpt_xfer(dev_addr, xid_itf->ep_in, xid_itf->epin_buf, xid_itf->epin_size)) { + usbh_edpt_release(dev_addr, xid_itf->ep_in); + return false; + } + + return true; +} + +bool tuh_xinput_send_report(uint8_t dev_addr, uint8_t instance, uint8_t const *report, uint16_t len) { + xinputh_interface_t *xid_itf = get_instance(dev_addr, instance); + + bool ret = false; + + // claim endpoint + TU_ASSERT(len <= xid_itf->epout_size); + bool tuh_rdy = tuh_ready(dev_addr); + bool edpt_busy = usbh_edpt_busy(dev_addr, xid_itf->ep_out); + if (tuh_rdy && + (xid_itf->ep_out != 0) && (!edpt_busy)) { + TU_VERIFY(usbh_edpt_claim(dev_addr, xid_itf->ep_out)); + memcpy(xid_itf->epout_buf, report, len); + if (!usbh_edpt_xfer(dev_addr, xid_itf->ep_out, xid_itf->epout_buf, len)) { + usbh_edpt_release(dev_addr, xid_itf->ep_out); + ret = false; + } + ret = true; + + } + + return ret; +} + +bool tuh_xinput_ready(uint8_t dev_addr, uint8_t instance) { + TU_VERIFY(tuh_xinput_mounted(dev_addr, instance)); + + xinputh_interface_t *hid_itf = get_instance(dev_addr, instance); + return !usbh_edpt_busy(dev_addr, hid_itf->ep_in); +} + +//--------------------------------------------------------------------+ +// USBH API +//--------------------------------------------------------------------+ +void xinputh_init(void) { + tu_memclr(_xinputh_dev, sizeof(_xinputh_dev)); +} + +bool xinputh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void)result; + + uint8_t const dir = tu_edpt_dir(ep_addr); + uint8_t const instance = get_instance_id_by_epaddr(dev_addr, ep_addr); + xinputh_interface_t *xinput_itf = get_instance(dev_addr, instance); + + if (dir == TUSB_DIR_IN) { + tuh_xinput_report_received_cb(dev_addr, instance, xinput_itf->epin_buf, (uint16_t)xferred_bytes); + + // Is this double sending? + usbh_edpt_xfer(dev_addr, xinput_itf->ep_in, xinput_itf->epin_buf, xinput_itf->epin_size); + } else { + if (tuh_xinput_report_sent_cb) + tuh_xinput_report_sent_cb(dev_addr, instance, xinput_itf->epout_buf, xferred_bytes); + } + + return true; +} + +void xinputh_close(uint8_t dev_addr) { + TU_VERIFY(dev_addr <= CFG_TUH_DEVICE_MAX, ); + xinputh_device_t *hid_dev = get_dev(dev_addr); + if (tuh_xinput_umount_cb) { + for (uint8_t inst = 0; inst < hid_dev->inst_count; inst++) tuh_xinput_umount_cb(dev_addr, inst); + } + + tu_memclr(hid_dev, sizeof(xinputh_device_t)); +} + +//--------------------------------------------------------------------+ +// Enumeration +//--------------------------------------------------------------------+ +typedef enum +{ + UNKNOWN = 0, + XBOX360, + XBOXONE, +} xinput_type_t; + +bool xinputh_open(uint8_t rhport, uint8_t dev_addr, tusb_desc_interface_t const *desc_itf, uint16_t max_len) { + (void)rhport; + (void)max_len; + TU_VERIFY(TUSB_CLASS_VENDOR_SPECIFIC == desc_itf->bInterfaceClass || TUSB_CLASS_HID == desc_itf->bInterfaceClass, 0); + xinputh_interface_t *p_xinput = NULL; + for (uint8_t i = 0; i < CFG_TUH_XINPUT; i++) { + xinputh_interface_t *xid_itf = get_instance(dev_addr, i); + if (xid_itf->ep_in == 0 && xid_itf->ep_out == 0) { + p_xinput = xid_itf; + break; + } + } + TU_VERIFY(p_xinput, 0); + uint8_t const *p_desc = (uint8_t const *)desc_itf; + if (desc_itf->bInterfaceSubClass == 0x47 && + desc_itf->bInterfaceProtocol == 0xD0 && desc_itf->bNumEndpoints) { + uint8_t endpoints = desc_itf->bNumEndpoints; + while (endpoints--) { + p_desc = tu_desc_next(p_desc); + tusb_desc_endpoint_t const *desc_ep = + (tusb_desc_endpoint_t const *)p_desc; + TU_ASSERT(TUSB_DESC_ENDPOINT == desc_ep->bDescriptorType); + if (desc_ep->bEndpointAddress & 0x80) { + p_xinput->ep_in = desc_ep->bEndpointAddress; + p_xinput->epin_size = tu_edpt_packet_size(desc_ep); + TU_ASSERT(tuh_edpt_open(dev_addr, desc_ep)); + } else { + p_xinput->ep_out = desc_ep->bEndpointAddress; + p_xinput->epout_size = tu_edpt_packet_size(desc_ep); + TU_ASSERT(tuh_edpt_open(dev_addr, desc_ep)); + } + } + p_xinput->itf_num = desc_itf->bInterfaceNumber; + p_xinput->type = XBOXONE; + + _xinputh_dev->inst_count++; + usbh_edpt_xfer(dev_addr, p_xinput->ep_in, p_xinput->epin_buf, p_xinput->epin_size); + return true; + } + return false; +} + +//--------------------------------------------------------------------+ +// Set Configure +//--------------------------------------------------------------------+ +static void config_driver_mount_complete(uint8_t dev_addr, uint8_t instance); +static void process_set_config(tuh_xfer_t *xfer); + +bool xinputh_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t instance = get_instance_id_by_itfnum(dev_addr, itf_num); + config_driver_mount_complete(dev_addr, instance); + return true; +} + +static void config_driver_mount_complete(uint8_t dev_addr, uint8_t instance) { + xinputh_interface_t *xid_itf = get_instance(dev_addr, instance); + + // enumeration is complete + tuh_xinput_mount_cb(dev_addr, instance, xid_itf->type, xid_itf->subtype); + + // notify usbh that driver enumeration is complete + usbh_driver_set_config_complete(dev_addr, xid_itf->itf_num); +} + +//--------------------------------------------------------------------+ +// Helper +//--------------------------------------------------------------------+ + +// Get Device by address +TU_ATTR_ALWAYS_INLINE static inline xinputh_device_t *get_dev(uint8_t dev_addr) { + return &_xinputh_dev[dev_addr - 1]; +} + +// Get Interface by instance number +TU_ATTR_ALWAYS_INLINE static inline xinputh_interface_t *get_instance(uint8_t dev_addr, uint8_t instance) { + return &_xinputh_dev[dev_addr - 1].instances[instance]; +} + +// Get instance ID by interface number +static uint8_t get_instance_id_by_itfnum(uint8_t dev_addr, uint8_t itf) { + for (uint8_t inst = 0; inst < CFG_TUH_XINPUT; inst++) { + xinputh_interface_t *hid = get_instance(dev_addr, inst); + if ((hid->itf_num == itf)) return inst; + } + return 0xff; +} + +// Get instance ID by endpoint address +static uint8_t get_instance_id_by_epaddr(uint8_t dev_addr, uint8_t ep_addr) { + for (uint8_t inst = 0; inst < CFG_TUH_XINPUT; inst++) { + xinputh_interface_t *hid = get_instance(dev_addr, inst); + if ((ep_addr == hid->ep_in) || (ep_addr == hid->ep_out)) return inst; + } + return 0xff; +} + +#endif diff --git a/lib/TinyUSB_Gamepad/src/xinput_host.h b/lib/TinyUSB_Gamepad/src/xinput_host.h new file mode 100644 index 000000000..8227e3273 --- /dev/null +++ b/lib/TinyUSB_Gamepad/src/xinput_host.h @@ -0,0 +1,102 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 OpenStickCommunity (gp2040-ce.info) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _TUSB_XINPUT_HOST_H_ +#define _TUSB_XINPUT_HOST_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +// TODO Highspeed interrupt can be up to 512 bytes +#ifndef CFG_TUH_XINPUT_EPIN_BUFSIZE +#define CFG_TUH_XINPUT_EPIN_BUFSIZE 64 +#endif + +#ifndef CFG_TUH_XINPUT_EPOUT_BUFSIZE +#define CFG_TUH_XINPUT_EPOUT_BUFSIZE 64 +#endif + +//--------------------------------------------------------------------+ +// Interface API +//--------------------------------------------------------------------+ + +// Get the number of XINPUT instances +uint8_t tuh_xinput_instance_count(uint8_t dev_addr); + +// Check if XINPUT instance is mounted +bool tuh_xinput_mounted(uint8_t dev_addr, uint8_t instance); + +//--------------------------------------------------------------------+ +// Interrupt Endpoint API +//--------------------------------------------------------------------+ + +// Try to receive next report on Interrupt Endpoint. Immediately return +// - true If succeeded, tuh_xinput_report_received_cb() callback will be invoked when report is available +// - false if failed to queue the transfer e.g endpoint is busy +bool tuh_xinput_receive_report(uint8_t dev_addr, uint8_t instance); + +//--------------------------------------------------------------------+ +// Callbacks (Weak is optional) +//--------------------------------------------------------------------+ + +// Invoked when device with XINPUT interface is mounted +// Report descriptor is also available for use. tuh_xinput_parse_report_descriptor() +// can be used to parse common/simple enough descriptor. +// Note: if report descriptor length > CFG_TUH_ENUMERATION_BUFSIZE, it will be skipped +// therefore report_desc = NULL, desc_len = 0 +void tuh_xinput_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t type, uint8_t subtype); +bool tuh_xinput_ready(uint8_t dev_addr, uint8_t instance); +bool tuh_xinput_send_report(uint8_t dev_addr, uint8_t instance, uint8_t const *report, uint16_t len); + +// Invoked when device with XINPUT interface is un-mounted +TU_ATTR_WEAK void tuh_xinput_umount_cb(uint8_t dev_addr, uint8_t instance); + +// Invoked when received report from device via interrupt endpoint +// Note: if there is report ID (composite), it is 1st byte of report +void tuh_xinput_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + +// Invoked when sent report to device successfully via interrupt endpoint +TU_ATTR_WEAK void tuh_xinput_report_sent_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +void xinputh_init(void); +bool xinputh_open(uint8_t rhport, uint8_t dev_addr, tusb_desc_interface_t const* desc_itf, uint16_t max_len); +bool xinputh_set_config(uint8_t dev_addr, uint8_t itf_num); +bool xinputh_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +void xinputh_close(uint8_t dev_addr); +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_XINPUT_HOST_H_ */ diff --git a/lib/rndis/rndis.c b/lib/rndis/rndis.c index 38368786d..d5f756244 100644 --- a/lib/rndis/rndis.c +++ b/lib/rndis/rndis.c @@ -61,7 +61,7 @@ static struct pbuf *received_frame; /* this is used by this code, ./class/net/net_driver.c, and usb_descriptors.c */ /* ideally speaking, this should be generated from the hardware's unique ID (if available) */ /* it is suggested that the first byte is 0x02 to indicate a link-local address */ -const uint8_t tud_network_mac_address[6] = {0x02, 0x02, 0x84, 0x6A, 0x96, 0x00}; +uint8_t tud_network_mac_address[6] = {0x02, 0x02, 0x84, 0x6A, 0x96, 0x00}; /* network parameters of this MCU */ static const ip4_addr_t ipaddr = INIT_IP4(192, 168, 7, 1); diff --git a/lib/tinyusb b/lib/tinyusb new file mode 160000 index 000000000..9474db8b0 --- /dev/null +++ b/lib/tinyusb @@ -0,0 +1 @@ +Subproject commit 9474db8b0fc3e96d654ed49529b82bbd329f2f56 diff --git a/proto/config.proto b/proto/config.proto index f02a033ca..1ce55d00b 100644 --- a/proto/config.proto +++ b/proto/config.proto @@ -494,8 +494,13 @@ message PS4Options message PSPassthroughOptions { optional bool enabled = 1; - optional int32 pinDplus = 2; - optional int32 pin5V = 3; + optional int32 pinDplus = 2 [deprecated = true]; + optional int32 pin5V = 3 [deprecated = true]; +} + +message XBOnePassthroughOptions +{ + optional bool enabled = 1; } message WiiOptions @@ -704,6 +709,7 @@ message AddonOptions optional PSPassthroughOptions psPassthroughOptions = 19; optional MacroOptions macroOptions = 20; optional InputHistoryOptions inputHistoryOptions = 21; + optional XBOnePassthroughOptions xbonePassthroughOptions = 22; } message MigrationHistory diff --git a/proto/enums.proto b/proto/enums.proto index 6d383ce9a..1c4efbf35 100644 --- a/proto/enums.proto +++ b/proto/enums.proto @@ -88,6 +88,7 @@ enum InputMode INPUT_MODE_HID = 2; INPUT_MODE_KEYBOARD = 3; INPUT_MODE_PS4 = 4; + INPUT_MODE_XBONE = 5; INPUT_MODE_MDMINI = 6; INPUT_MODE_NEOGEO = 7; INPUT_MODE_PCEMINI = 8; diff --git a/src/addons/i2cdisplay.cpp b/src/addons/i2cdisplay.cpp index b426ffeea..c132f6d49 100644 --- a/src/addons/i2cdisplay.cpp +++ b/src/addons/i2cdisplay.cpp @@ -1037,6 +1037,7 @@ void I2CDisplayAddon::drawStatusBar(Gamepad * gamepad) statusBar += "PS5 "; } break; + case INPUT_MODE_XBONE: statusBar += "XBONE"; break; case INPUT_MODE_KEYBOARD: statusBar += "HID-KB"; break; case INPUT_MODE_CONFIG: statusBar += "CONFIG"; break; } diff --git a/src/addons/inputhistory.cpp b/src/addons/inputhistory.cpp index 82be91010..7261a5215 100644 --- a/src/addons/inputhistory.cpp +++ b/src/addons/inputhistory.cpp @@ -10,16 +10,17 @@ const map displayModeLookup = { {INPUT_MODE_HID, 0}, {INPUT_MODE_SWITCH, 1}, {INPUT_MODE_XINPUT, 2}, + {INPUT_MODE_XBONE, 2}, {INPUT_MODE_KEYBOARD, 3}, + {INPUT_MODE_CONFIG, 3}, {INPUT_MODE_PS4, 4}, {INPUT_MODE_PSCLASSIC, 4}, - {INPUT_MODE_CONFIG, 5}, - {INPUT_MODE_MDMINI, 6}, - {INPUT_MODE_NEOGEO, 7}, - {INPUT_MODE_PCEMINI, 8}, - {INPUT_MODE_EGRET, 9}, - {INPUT_MODE_ASTRO, 10}, - {INPUT_MODE_XBOXORIGINAL, 11}, + {INPUT_MODE_MDMINI, 5}, + {INPUT_MODE_NEOGEO, 6}, + {INPUT_MODE_PCEMINI, 7}, + {INPUT_MODE_EGRET, 8}, + {INPUT_MODE_ASTRO, 9}, + {INPUT_MODE_XBOXORIGINAL, 10}, }; static const std::string displayNames[][INPUT_HISTORY_MAX_INPUTS] = { @@ -58,13 +59,6 @@ static const std::string displayNames[][INPUT_HISTORY_MAX_INPUTS] = { "L1", "R1", "L2", "R2", CHAR_SHARE_P, "OP", "L3", "R3", CHAR_HOME_P, CHAR_TPAD_P }, - { // Config - CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, - CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, - "B1", "B2", "B3", "B4", - "L1", "R1", "L2", "R2", - "S1", "S2", "L3", "R3", "A1", "A2" - }, { // GEN/MD Mini CHAR_UP, CHAR_DOWN, CHAR_LEFT, CHAR_RIGHT, CHAR_UL, CHAR_UR, CHAR_DL, CHAR_DR, @@ -106,7 +100,7 @@ static const std::string displayNames[][INPUT_HISTORY_MAX_INPUTS] = { "A", "B", "X", "Y", "BL", "WH", "L", "R", "BK", "ST", "LS", "RS", "", "" - }, + } }; bool InputHistoryAddon::available() { diff --git a/src/addons/xbonepassthrough.cpp b/src/addons/xbonepassthrough.cpp new file mode 100644 index 000000000..7a2e34f09 --- /dev/null +++ b/src/addons/xbonepassthrough.cpp @@ -0,0 +1,134 @@ +#include "addons/xbonepassthrough.h" +#include "storagemanager.h" +#include "usbhostmanager.h" +#include "peripheralmanager.h" + +#include "xbone_driver.h" +#include "xgip_protocol.h" +#include "xinput_host.h" + +#define XBONE_EXTENSION_DEBUG true + +// power-on states and rumble-on with everything disabled +static uint8_t xb1_power_on[] = {0x06, 0x62, 0x45, 0xb8, 0x77, 0x26, 0x2c, 0x55, + 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f}; +static uint8_t xb1_power_on_single[] = {0x00}; +static uint8_t xb1_rumble_on[] = {0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xeb}; + +bool XBOnePassthroughAddon::available() { + const XBOnePassthroughOptions& xboneOptions = Storage::getInstance().getAddonOptions().xbonePassthroughOptions; + return xboneOptions.enabled && PeripheralManager::getInstance().isUSBEnabled(0); +} + +void XBOnePassthroughAddon::setup() { + dongle_ready = false; +} + +// Report Queue for big report sizes from dongle +#include +typedef struct { + uint8_t report[XBONE_ENDPOINT_SIZE]; + uint16_t len; +} report_queue_t; + +static std::queue report_queue; +static uint32_t lastReportQueueSent = 0; +#define REPORT_QUEUE_INTERVAL 15 + +void XBOnePassthroughAddon::process() { + // Do not begin processing console auth unless we have the dongle ready + if ( dongle_ready == true ) { + if ( XboxOneData::getInstance().getState() == XboxOneState::send_auth_console_to_dongle ) { + uint8_t isChunked = ( XboxOneData::getInstance().getAuthLen() > GIP_MAX_CHUNK_SIZE ); + uint8_t needsAck = (XboxOneData::getInstance().getAuthLen() > 2); + outgoingXGIP.reset(); + outgoingXGIP.setAttributes(XboxOneData::getInstance().getAuthType(), XboxOneData::getInstance().getSequence(), 1, isChunked, needsAck); + outgoingXGIP.setData(XboxOneData::getInstance().getAuthBuffer(), XboxOneData::getInstance().getAuthLen()); + XboxOneData::getInstance().setState(XboxOneState::wait_auth_console_to_dongle); + } else if ( XboxOneData::getInstance().getState() == XboxOneState::wait_auth_console_to_dongle) { + queue_host_report(outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + if ( outgoingXGIP.getChunked() == false || outgoingXGIP.endOfChunk() == true ) { + XboxOneData::getInstance().setState(XboxOneState::auth_idle_state); + } + } + } + + uint32_t now = to_ms_since_boot(get_absolute_time()); + if ( !report_queue.empty() && (now - lastReportQueueSent) > REPORT_QUEUE_INTERVAL ) { + if ( tuh_xinput_send_report(xbone_dev_addr, xbone_instance, report_queue.front().report, report_queue.front().len) ) { + report_queue.pop(); + lastReportQueueSent = now; + } else { // FAILED: Keeping it on the queue to send again + sleep_ms(REPORT_QUEUE_INTERVAL); + } + } +} + +void XBOnePassthroughAddon::xmount(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype) { + xbone_dev_addr = dev_addr; + xbone_instance = instance; + incomingXGIP.reset(); + outgoingXGIP.reset(); +} + +void XBOnePassthroughAddon::unmount(uint8_t dev_addr) { + // Do not reset dongle_ready on unmount (Magic-X will remount but still be ready) +} + +void XBOnePassthroughAddon::report_received(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { + incomingXGIP.parse(report, len); + if ( incomingXGIP.validate() == false ) { + sleep_ms(50); // First packet is invalid, drop and wait for dongle to boot + incomingXGIP.reset(); + return; + } + + // Setup an ack before we change anything about the incoming packet + if ( incomingXGIP.ackRequired() == true ) { + queue_host_report((uint8_t*)incomingXGIP.generateAckPacket(), incomingXGIP.getPacketLength()); + } + + switch ( incomingXGIP.getCommand() ) { + case GIP_ANNOUNCE: + outgoingXGIP.reset(); + outgoingXGIP.setAttributes(GIP_DEVICE_DESCRIPTOR, 1, 1, false, 0); + queue_host_report((uint8_t*)outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + break; + case GIP_DEVICE_DESCRIPTOR: + if ( incomingXGIP.endOfChunk() == true ) { + outgoingXGIP.reset(); // Power-on full string + outgoingXGIP.setAttributes(GIP_POWER_MODE_DEVICE_CONFIG, 2, 1, false, 0); + outgoingXGIP.setData(xb1_power_on, sizeof(xb1_power_on)); + queue_host_report((uint8_t*)outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + outgoingXGIP.reset(); // Power-on with 0x00 + outgoingXGIP.setAttributes(GIP_POWER_MODE_DEVICE_CONFIG, 3, 1, false, 0); + outgoingXGIP.setData(xb1_power_on_single, sizeof(xb1_power_on_single)); + queue_host_report((uint8_t*)outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + outgoingXGIP.reset(); // Rumble Support to enable dongle + outgoingXGIP.setAttributes(GIP_CMD_RUMBLE, 1, 0, false, 0); // not internal function + outgoingXGIP.setData(xb1_rumble_on, sizeof(xb1_rumble_on)); + queue_host_report((uint8_t*)outgoingXGIP.generatePacket(), outgoingXGIP.getPacketLength()); + dongle_ready = true; + } + break; + case GIP_AUTH: + case GIP_FINAL_AUTH: + if ( incomingXGIP.getChunked() == false || + (incomingXGIP.getChunked() == true && incomingXGIP.endOfChunk() == true )) { + XboxOneData::getInstance().setAuthData(incomingXGIP.getData(), incomingXGIP.getDataLength(), incomingXGIP.getSequence(), + incomingXGIP.getCommand(), XboxOneState::send_auth_dongle_to_console); + incomingXGIP.reset(); + } + break; + case GIP_ACK_RESPONSE: + default: + break; + }; +} + +void XBOnePassthroughAddon::queue_host_report(void* report, uint16_t len) { + report_queue_t new_queue; + memcpy(new_queue.report, report, len); + new_queue.len = len; + report_queue.push(new_queue); +} diff --git a/src/config_utils.cpp b/src/config_utils.cpp index acb4a4714..797b24f36 100644 --- a/src/config_utils.cpp +++ b/src/config_utils.cpp @@ -32,6 +32,7 @@ #include "addons/wiiext.h" #include "addons/snes_input.h" #include "addons/input_macro.h" +#include "addons/xbonepassthrough.h" #include "CRC32.h" #include "FlashPROM.h" @@ -552,8 +553,9 @@ void ConfigUtils::initUnsetPropertiesWithDefaults(Config& config) // PS Passthrough INIT_UNSET_PROPERTY(config.addonOptions.psPassthroughOptions, enabled, PSPASSTHROUGH_ENABLED); - INIT_UNSET_PROPERTY(config.addonOptions.psPassthroughOptions, pinDplus, PSPASSTHROUGH_PIN_DPLUS); - INIT_UNSET_PROPERTY(config.addonOptions.psPassthroughOptions, pin5V, PSPASSTHROUGH_PIN_5V); + + // Xbox One Passthrough + INIT_UNSET_PROPERTY(config.addonOptions.xbonePassthroughOptions, enabled, XBONEPASSTHROUGH_ENABLED); INIT_UNSET_PROPERTY(config.addonOptions.macroOptions, enabled, !!INPUT_MACRO_ENABLED); INIT_UNSET_PROPERTY(config.addonOptions.macroOptions, pin, INPUT_MACRO_PIN); diff --git a/src/configs/webconfig.cpp b/src/configs/webconfig.cpp index e6af822c7..8dcf764f9 100644 --- a/src/configs/webconfig.cpp +++ b/src/configs/webconfig.cpp @@ -1168,7 +1168,7 @@ std::string setAddonOptions() docToValue(dualDirectionalOptions.fourWayMode, doc, "dualDirFourWayMode"); docToValue(dualDirectionalOptions.enabled, doc, "DualDirectionalInputEnabled"); - TiltOptions& tiltOptions = Storage::getInstance().getAddonOptions().tiltOptions; + TiltOptions& tiltOptions = Storage::getInstance().getAddonOptions().tiltOptions; docToPin(tiltOptions.tilt1Pin, doc, "tilt1Pin"); docToValue(tiltOptions.factorTilt1LeftX, doc, "factorTilt1LeftX"); docToValue(tiltOptions.factorTilt1LeftY, doc, "factorTilt1LeftY"); @@ -1315,6 +1315,10 @@ std::string setAddonOptions() gpioMappings[oldPsPinDplus+1].action = GpioAction::NONE; docToPin(psPassthroughOptions.pin5V, doc, "psPassthroughPin5V"); + + XBOnePassthroughOptions& xbonePassthroughOptions = Storage::getInstance().getAddonOptions().xbonePassthroughOptions; + docToValue(xbonePassthroughOptions.enabled, doc, "XBOnePassthroughAddonEnabled"); + Storage::getInstance().save(); return serialize_json(doc); @@ -1713,8 +1717,9 @@ std::string getAddonOptions() PSPassthroughOptions& psPassthroughOptions = Storage::getInstance().getAddonOptions().psPassthroughOptions; writeDoc(doc, "PSPassthroughAddonEnabled", psPassthroughOptions.enabled); - writeDoc(doc, "psPassthroughPinDplus", psPassthroughOptions.pinDplus); - writeDoc(doc, "psPassthroughPin5V", psPassthroughOptions.pin5V); + + XBOnePassthroughOptions& xbonePassthroughOptions = Storage::getInstance().getAddonOptions().xbonePassthroughOptions; + writeDoc(doc, "XBOnePassthroughAddonEnabled", xbonePassthroughOptions.enabled); const FocusModeOptions& focusModeOptions = Storage::getInstance().getAddonOptions().focusModeOptions; writeDoc(doc, "focusModePin", cleanPin(focusModeOptions.pin)); diff --git a/src/gamepad.cpp b/src/gamepad.cpp index 303927058..814a8641a 100644 --- a/src/gamepad.cpp +++ b/src/gamepad.cpp @@ -18,6 +18,9 @@ // PS5 compatibility #include "ps4_driver.h" +// Xbox One compatibility +#include "xbone_driver.h" + // MUST BE DEFINED for mpgs uint32_t getMillis() { return to_ms_since_boot(get_absolute_time()); @@ -172,6 +175,35 @@ static XboxOriginalReport xboxOriginalReport .rightStickY = 0, }; +static XboxOneGamepad_Data_t xboneReport +{ + .sync = 0, + .guide = 0, + .start = 0, + .back = 0, + .a = 0, + .b = 0, + .x = 0, + .y = 0, + .dpadUp = 0, + .dpadDown = 0, + .dpadLeft = 0, + .dpadRight = 0, + .leftShoulder = 0, + .rightShoulder = 0, + .leftThumbClick = 0, + .rightThumbClick = 0, + .leftTrigger = 0, + .rightTrigger = 0, + .leftStickX = GAMEPAD_JOYSTICK_MID, + .leftStickY = GAMEPAD_JOYSTICK_MID, + .rightStickX = GAMEPAD_JOYSTICK_MID, + .rightStickY = GAMEPAD_JOYSTICK_MID, + .reserved = {} +}; + +static uint16_t xboneReportSize; + static TouchpadData touchpadData; static uint8_t last_report_counter = 0; @@ -247,6 +279,13 @@ void Gamepad::setup() // setup PS5 compatibility PS4Data::getInstance().ps4ControllerType = gamepadOptions.ps4ControllerType; + + // Xbox One Keep-Alive + keep_alive_timer = 0; + keep_alive_sequence = 0; + virtual_keycode_sequence = 0; + xb1_guide_pressed = false; + xboneReportSize = sizeof(XboxOneGamepad_Data_t); } /** @@ -535,6 +574,9 @@ void * Gamepad::getReport() case INPUT_MODE_PS4: return getPS4Report(); + case INPUT_MODE_XBONE: + return getXBOneReport(); + case INPUT_MODE_KEYBOARD: return getKeyboardReport(); @@ -578,6 +620,9 @@ uint16_t Gamepad::getReportSize() case INPUT_MODE_PS4: return sizeof(PS4Report); + case INPUT_MODE_XBONE: + return xboneReportSize; + case INPUT_MODE_KEYBOARD: return sizeof(KeyboardReport); @@ -729,6 +774,135 @@ XInputReport *Gamepad::getXInputReport() return &xinputReport; } +static uint8_t xb1_guide_on[] = { 0x01, 0x5b }; +static uint8_t xb1_guide_off[] = { 0x00, 0x5b }; +static uint8_t xboneIdle[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +// On Send report Success +void Gamepad::sendReportSuccess() { + switch( (options.inputMode) ) { + case INPUT_MODE_PS4: + last_report_counter = (last_report_counter+1) & 63; + break; + case INPUT_MODE_XBONE: + if ( xboneReport.Header.command == GIP_KEEPALIVE) { + keep_alive_sequence++; // will rollover + if ( keep_alive_sequence == 0 ) + keep_alive_sequence = 1; + } else if ( xboneReport.Header.command == GIP_INPUT_REPORT ) { + if ( memcmp((void*)&((uint8_t*)&xboneReport)[4], xboneIdle, sizeof(xboneIdle)) != 0 ) { + last_report_counter++; + if ( last_report_counter == 0 ) + last_report_counter = 1; + } + } + break; + default: + break; + }; +} + +XboxOneGamepad_Data_t *Gamepad::getXBOneReport() +{ + // No input until auth is ready + if ( XboxOneData::getInstance().getAuthCompleted() == false ) { + GIP_HEADER((&xboneReport), GIP_INPUT_REPORT, false, 0); + return &xboneReport; + } + + uint32_t now = to_ms_since_boot(get_absolute_time()); + // Send Keep-Alive every 15 seconds + if ( (now - keep_alive_timer) > 15000) { + memset(&xboneReport.Header, 0, sizeof(GipHeader_t)); + xboneReport.Header.command = GIP_KEEPALIVE; + xboneReport.Header.internal = 1; + xboneReport.Header.sequence = keep_alive_sequence; + xboneReport.Header.length = 4; + static uint8_t keepAlive[] = { 0x80, 0x00, 0x00, 0x00 }; + memcpy(&((uint8_t*)&xboneReport)[4], &keepAlive, sizeof(keepAlive)); + keep_alive_timer = now; + + xboneReportSize = sizeof(GipHeader_t) + sizeof(keepAlive); + + return &xboneReport; + } + + // Guide button toggles on/off on our virtual keycode sequence + if ( pressedA1() ) { + if (xb1_guide_pressed == false ) { // toggle on if we haven't sent before + virtual_keycode_sequence++; // will rollover + if ( virtual_keycode_sequence == 0 ) + virtual_keycode_sequence = 1; + xb1_guide_pressed = true; + memset(&xboneReport.Header, 0, sizeof(GipHeader_t)); + xboneReport.Header.command = GIP_VIRTUAL_KEYCODE; + xboneReport.Header.internal = 1; + xboneReport.Header.sequence = virtual_keycode_sequence; + xboneReport.Header.length = sizeof(xb1_guide_on); + memcpy(&((uint8_t*)&xboneReport)[4], &xb1_guide_on, sizeof(xb1_guide_on)); + xboneReportSize = sizeof(GipHeader_t) + sizeof(xb1_guide_on); + return &xboneReport; + } else { + return &xboneReport; // do not change the report otherwise and prevent other buttons + } + } else if ( !pressedA1() && xb1_guide_pressed == true ) { // toggle off + virtual_keycode_sequence++; // will rollover + if ( virtual_keycode_sequence == 0 ) + virtual_keycode_sequence = 1; + xb1_guide_pressed = false; + memset(&xboneReport.Header, 0, sizeof(GipHeader_t)); + xboneReport.Header.command = GIP_VIRTUAL_KEYCODE; + xboneReport.Header.internal = 1; + xboneReport.Header.sequence = virtual_keycode_sequence; + xboneReport.Header.length = sizeof(xb1_guide_off); + memcpy(&((uint8_t*)&xboneReport)[4], &xb1_guide_off, sizeof(xb1_guide_off)); + xboneReportSize = sizeof(GipHeader_t) + sizeof(xb1_guide_off); + return &xboneReport; + } + + GIP_HEADER((&xboneReport), GIP_INPUT_REPORT, false, last_report_counter); + + xboneReport.a = pressedB1(); + xboneReport.b = pressedB2(); + xboneReport.x = pressedB3(); + xboneReport.y = pressedB4(); + xboneReport.leftShoulder = pressedL1(); + xboneReport.rightShoulder = pressedR1(); + xboneReport.leftThumbClick = pressedL3(); + xboneReport.rightThumbClick = pressedR3(); + xboneReport.start = pressedS2(); + xboneReport.back = pressedS1(); + xboneReport.guide = 0; // always 0 + xboneReport.sync = 0; + xboneReport.dpadUp = pressedUp(); + xboneReport.dpadDown = pressedDown(); + xboneReport.dpadLeft = pressedLeft(); + xboneReport.dpadRight = pressedRight(); + + xboneReport.leftStickX = static_cast(state.lx) + INT16_MIN; + xboneReport.leftStickY = static_cast(~state.ly) + INT16_MIN; + xboneReport.rightStickX = static_cast(state.rx) + INT16_MIN; + xboneReport.rightStickY = static_cast(~state.ry) + INT16_MIN; + + if (hasAnalogTriggers) + { + xboneReport.leftTrigger = pressedL2() ? 0x03FF : state.lt; + xboneReport.rightTrigger = pressedR2() ? 0x03FF : state.rt; + } + else + { + xboneReport.leftTrigger = pressedL2() ? 0x03FF : 0; + xboneReport.rightTrigger = pressedR2() ? 0x03FF : 0; + } + + xboneReportSize = sizeof(XboxOneGamepad_Data_t); + + return &xboneReport; +} + PS4Report *Gamepad::getPS4Report() { @@ -761,7 +935,6 @@ PS4Report *Gamepad::getPS4Report() ps4Report.button_touchpad = options.switchTpShareForDs4 ? pressedS1() : pressedA2(); // report counter is 6 bits - last_report_counter = (last_report_counter+1) & 63; ps4Report.report_counter = last_report_counter; ps4Report.left_stick_x = static_cast(state.lx >> 8); diff --git a/src/gamepad/GamepadDescriptors.cpp b/src/gamepad/GamepadDescriptors.cpp index 34491f3ac..0b4120a76 100644 --- a/src/gamepad/GamepadDescriptors.cpp +++ b/src/gamepad/GamepadDescriptors.cpp @@ -1,5 +1,6 @@ #include "GamepadDescriptors.h" +// This is never used static uint16_t getConfigurationDescriptor(const uint8_t *buffer, InputMode mode) { switch (mode) @@ -48,6 +49,11 @@ static uint16_t getConfigurationDescriptor(const uint8_t *buffer, InputMode mode buffer = xboxoriginal_configuration_descriptor; return sizeof(xboxoriginal_configuration_descriptor); + case INPUT_MODE_XBONE: + buffer = xbone_configuration_descriptor; + return sizeof(xbone_configuration_descriptor); + + default: buffer = hid_configuration_descriptor; return sizeof(hid_configuration_descriptor); diff --git a/src/gp2040.cpp b/src/gp2040.cpp index c8ecad59a..c2f367ac1 100644 --- a/src/gp2040.cpp +++ b/src/gp2040.cpp @@ -51,7 +51,8 @@ void GP2040::setup() { // Reduce CPU if any USB host add-on is enabled const AddonOptions & addonOptions = Storage::getInstance().getAddonOptions(); if ( addonOptions.keyboardHostOptions.enabled || - addonOptions.psPassthroughOptions.enabled ){ + addonOptions.psPassthroughOptions.enabled || + addonOptions.xbonePassthroughOptions.enabled ){ set_sys_clock_khz(120000, true); // Set Clock to 120MHz to avoid potential USB timing issues } @@ -115,6 +116,7 @@ void GP2040::setup() { case BootAction::SET_INPUT_MODE_SWITCH: case BootAction::SET_INPUT_MODE_XINPUT: case BootAction::SET_INPUT_MODE_PS4: + case BootAction::SET_INPUT_MODE_XBONE: case BootAction::SET_INPUT_MODE_KEYBOARD: case BootAction::SET_INPUT_MODE_NEOGEO: case BootAction::SET_INPUT_MODE_MDMINI: @@ -134,6 +136,8 @@ void GP2040::setup() { inputMode = INPUT_MODE_XINPUT; } else if (bootAction == BootAction::SET_INPUT_MODE_PS4) { inputMode = INPUT_MODE_PS4; + } else if (bootAction == BootAction::SET_INPUT_MODE_XBONE) { + inputMode = INPUT_MODE_XBONE; } else if (bootAction == BootAction::SET_INPUT_MODE_KEYBOARD) { inputMode = INPUT_MODE_KEYBOARD; } else if (bootAction == BootAction::SET_INPUT_MODE_NEOGEO) { @@ -261,8 +265,13 @@ void GP2040::run() { // Copy Processed Gamepad for Core1 (race condition otherwise) memcpy(&processedGamepad->state, &gamepad->state, sizeof(GamepadState)); + // Update input driver + update_input_driver(); + // USB FEATURES : Send/Get USB Features (including Player LEDs on X-Input) - send_report(gamepad->getReport(), gamepad->getReportSize()); + if ( send_report(gamepad->getReport(), gamepad->getReportSize()) ) { + gamepad->sendReportSuccess(); + } // GET USB REPORT (If Endpoint Available) receive_report(featureData); @@ -337,6 +346,8 @@ GP2040::BootAction GP2040::getBootAction() { return BootAction::SET_INPUT_MODE_PSCLASSIC; case INPUT_MODE_XBOXORIGINAL: return BootAction::SET_INPUT_MODE_XBOXORIGINAL; + case INPUT_MODE_XBONE: + return BootAction::SET_INPUT_MODE_XBONE; default: return BootAction::NONE; } diff --git a/src/gp2040aux.cpp b/src/gp2040aux.cpp index c425e2089..52817a667 100644 --- a/src/gp2040aux.cpp +++ b/src/gp2040aux.cpp @@ -14,6 +14,7 @@ #include "addons/pspassthrough.h" #include "addons/neopicoleds.h" #include "addons/inputhistory.h" +#include "addons/xbonepassthrough.h" #include @@ -32,6 +33,7 @@ void GP2040Aux::setup() { // Setup Add-ons addons.LoadUSBAddon(new PSPassthroughAddon(), CORE1_LOOP); + addons.LoadUSBAddon(new XBOnePassthroughAddon(), CORE1_LOOP); addons.LoadAddon(inputHistoryAddon, CORE1_LOOP); addons.LoadAddon(i2CDisplayAddon, CORE1_LOOP); addons.LoadAddon(new NeoPicoLEDAddon(), CORE1_LOOP); diff --git a/src/usbhostmanager.cpp b/src/usbhostmanager.cpp index 4cf894857..784862a81 100644 --- a/src/usbhostmanager.cpp +++ b/src/usbhostmanager.cpp @@ -4,13 +4,18 @@ #include "pio_usb.h" #include "tusb.h" -#include "host/usbh_classdriver.h" + +#include "host/usbh.h" +#include "host/usbh_pvt.h" + +#include "xinput_host.h" void USBHostManager::setDataPin(uint8_t inPin) { dataPin = inPin; } void USBHostManager::start() { + // This will happen after Gamepad has initialized if ( !addons.empty() ) { if (PeripheralManager::getInstance().isUSBEnabled(0)) { pio_usb_configuration_t* pio_cfg = PeripheralManager::getInstance().getUSB(0)->getController(); @@ -63,6 +68,31 @@ void USBHostManager::hid_get_report_complete_cb(uint8_t dev_addr, uint8_t instan } } +void USBHostManager::xinput_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype) { + for( std::vector::iterator it = addons.begin(); it != addons.end(); it++ ){ + (*it)->xmount(dev_addr, instance, controllerType, subtype); + } +} + +void USBHostManager::xinput_umount_cb(uint8_t dev_addr) { + for( std::vector::iterator it = addons.begin(); it != addons.end(); it++ ){ + (*it)->unmount(dev_addr); + } +} + +void USBHostManager::xinput_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { + for( std::vector::iterator it = addons.begin(); it != addons.end(); it++ ){ + (*it)->report_received(dev_addr, instance, report, len); + } +} + +void USBHostManager::xinput_report_sent_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* report, uint16_t len) { + for( std::vector::iterator it = addons.begin(); it != addons.end(); it++ ){ + (*it)->report_sent(dev_addr, instance, report, len); + } +} + +// HID: USB Host static uint8_t _intf_num = 0; // Required helper class for HID_REQ_CONTROL_GET_REPORT addition @@ -124,13 +154,13 @@ void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t const* desc_re if( TUSB_DESC_INTERFACE != tu_desc_type(p_desc) ) return; tusb_desc_interface_t const* desc_itf = (tusb_desc_interface_t const*) p_desc; - // only open and listen to HID endpoint IN + // only open and listen to HID endpoint IN (PS4) if (desc_itf->bInterfaceClass == TUSB_CLASS_HID) { _intf_num = desc_itf->bInterfaceNumber; break; // we got the interface number - } - + } + // next Interface or IAD descriptor uint16_t const drv_len = count_interface_total_len(desc_itf, assoc_itf_count, (uint16_t) (desc_end-p_desc)); p_desc += drv_len; @@ -172,6 +202,44 @@ void tuh_hid_get_report_complete_cb(uint8_t dev_addr, uint8_t instance, uint8_t USBHostManager::getInstance().hid_get_report_complete_cb(dev_addr, instance, report_id, report_type, len); } +// USB Host: X-Input +// Add X-Input Driver +void tuh_xinput_mount_cb(uint8_t dev_addr, uint8_t instance, uint8_t controllerType, uint8_t subtype) { + USBHostManager::getInstance().xinput_mount_cb(dev_addr, instance, controllerType, subtype); +} + +void tuh_xinput_umount_cb(uint8_t dev_addr, uint8_t instance) { + // send to xinput_unmount_cb in usb host manager + USBHostManager::getInstance().xinput_umount_cb(dev_addr); +} + +void tuh_xinput_report_received_cb(uint8_t dev_addr, uint8_t instance, uint8_t const *report, uint16_t len) { + // report received from xinput device + USBHostManager::getInstance().xinput_report_received_cb(dev_addr, instance, report, len); +} + +void tuh_xinput_report_sent_cb(uint8_t dev_addr, uint8_t instance, uint8_t const *report, uint16_t len) { + // report sent to xinput device + USBHostManager::getInstance().xinput_report_sent_cb(dev_addr, instance, report, len); +} + +usbh_class_driver_t driver_host[] = { + { +#if CFG_TUSB_DEBUG >= 2 + .name = "XInput_Host_HID", +#endif + .init = xinputh_init, + .open = xinputh_open, + .set_config = xinputh_set_config, + .xfer_cb = xinputh_xfer_cb, + .close = xinputh_close} +}; + +usbh_class_driver_t const *usbh_app_driver_get_cb(uint8_t *driver_count) { + *driver_count = 1; + return driver_host; +} + // Request for HID_REQ_CONTROL_GET_REPORT missing from TinyUSB static void get_report_complete(tuh_xfer_t* xfer) { @@ -215,4 +283,4 @@ bool tuh_hid_get_report(uint8_t dev_addr, uint8_t instance, uint8_t report_id, u TU_ASSERT( tuh_control_xfer(&xfer) ); return true; -} +} \ No newline at end of file diff --git a/www/server/app.js b/www/server/app.js index 49a7191ab..b6b55bf63 100644 --- a/www/server/app.js +++ b/www/server/app.js @@ -462,8 +462,6 @@ app.get('/api/getAddonsOptions', (req, res) => { keyboardHostPinDplus: 0, keyboardHostPin5V: -1, keyboardHostMap: DEFAULT_KEYBOARD_MAPPING, - psPassthroughPinDplus: 0, - psPassthroughPin5V: -1, AnalogInputEnabled: 1, BoardLedAddonEnabled: 1, FocusModeAddonEnabled: 1, @@ -485,6 +483,7 @@ app.get('/api/getAddonsOptions', (req, res) => { WiiExtensionAddonEnabled: 1, SNESpadAddonEnabled: 1, PSPassthroughAddonEnabled: 1, + XBOnePassthroughAddonEnabled: 1, InputHistoryAddonEnabled: 1, inputHistoryLength: 21, inputHistoryCol: 0, diff --git a/www/src/Addons/Passthrough.tsx b/www/src/Addons/PSPassthrough.tsx similarity index 100% rename from www/src/Addons/Passthrough.tsx rename to www/src/Addons/PSPassthrough.tsx diff --git a/www/src/Addons/XBOnePassthrough.tsx b/www/src/Addons/XBOnePassthrough.tsx new file mode 100644 index 000000000..df3afc6b7 --- /dev/null +++ b/www/src/Addons/XBOnePassthrough.tsx @@ -0,0 +1,51 @@ +import { AppContext } from '../Contexts/AppContext'; +import React, { useContext } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { FormCheck, Row, FormLabel } from 'react-bootstrap'; +import { NavLink } from 'react-router-dom'; +import * as yup from 'yup'; + +import Section from '../Components/Section'; + +export const xbonePassthroughScheme = { + XBOnePassthroughAddonEnabled: yup + .number() + .required() + .label('Xbox One Passthrough Add-On Enabled') +}; + +export const xbonePassthroughState = { + XBOnePassthroughAddonEnabled: -1 +}; + +const XBOnePassthrough = ({ values, errors, handleChange, handleCheckbox }) => { + const { t } = useTranslation(); + const { getAvailablePeripherals } = useContext(AppContext); + return ( +
+ + {getAvailablePeripherals('usb') ? + { + handleCheckbox('XBOnePassthroughAddonEnabled', values); + handleChange(e); + }} + /> + : + {t('PeripheralMapping:header-text')} + } +
+ ); +}; + +export default XBOnePassthrough; diff --git a/www/src/Locales/en/AddonsConfig.jsx b/www/src/Locales/en/AddonsConfig.jsx index 5c748efda..039a5cbb5 100644 --- a/www/src/Locales/en/AddonsConfig.jsx +++ b/www/src/Locales/en/AddonsConfig.jsx @@ -147,5 +147,6 @@ export default { 'input-history-header-text': 'Input History', 'input-history-length-label': 'History length (characters)', 'input-history-col-label': 'Column', - 'input-history-row-label': 'Row' + 'input-history-row-label': 'Row', + 'xbonepassthrough-header-text': 'Xbox One Passthrough', }; diff --git a/www/src/Locales/en/SettingsPage.jsx b/www/src/Locales/en/SettingsPage.jsx index 269e45c7a..f51a03e1b 100644 --- a/www/src/Locales/en/SettingsPage.jsx +++ b/www/src/Locales/en/SettingsPage.jsx @@ -16,6 +16,7 @@ export default { astro: 'ASTROCITY Mini', psclassic: 'Playstation Classic', xboxoriginal: 'Original Xbox', + xbone: 'Xbox One' }, 'input-mode-group': { primary: 'Primary Input Modes', diff --git a/www/src/Locales/pt-BR/SettingsPage.jsx b/www/src/Locales/pt-BR/SettingsPage.jsx index a8bd89c8c..8add59868 100644 --- a/www/src/Locales/pt-BR/SettingsPage.jsx +++ b/www/src/Locales/pt-BR/SettingsPage.jsx @@ -8,6 +8,7 @@ export default { ps3: 'PS3/DirectInput', keyboard: 'Teclado', ps4: 'PS4', + xbone: 'Xbox One', }, 'ps4-mode-options': { controller: 'Controle', diff --git a/www/src/Locales/zh-CN/SettingsPage.jsx b/www/src/Locales/zh-CN/SettingsPage.jsx index 0fce6fa65..9eedc8873 100644 --- a/www/src/Locales/zh-CN/SettingsPage.jsx +++ b/www/src/Locales/zh-CN/SettingsPage.jsx @@ -8,6 +8,7 @@ export default { ps3: 'PS3/DirectInput', keyboard: '键盘', ps4: 'PS4', + xbone: 'Xbox One', }, 'ps4-mode-options': { controller: '游戏控制器', diff --git a/www/src/Pages/AddonsConfigPage.jsx b/www/src/Pages/AddonsConfigPage.jsx index 83ad1ee46..ddf2db824 100644 --- a/www/src/Pages/AddonsConfigPage.jsx +++ b/www/src/Pages/AddonsConfigPage.jsx @@ -35,7 +35,7 @@ import Ps4, { ps4Scheme, ps4State } from '../Addons/Ps4'; import PSPassthrough, { psPassthroughScheme, psPassthroughState, -} from '../Addons/Passthrough'; +} from '../Addons/PSPassthrough'; import Wii, { wiiScheme, wiiState } from '../Addons/Wii'; import SNES, { snesState } from '../Addons/SNES'; import FocusMode, { @@ -44,6 +44,10 @@ import FocusMode, { } from '../Addons/FocusMode'; import Keyboard, { keyboardScheme, keyboardState } from '../Addons/Keyboard'; import InputHistory, { inputHistoryScheme, inputHistoryState } from '../Addons/InputHistory'; +import XBOnePassthrough, { + xbonePassthroughScheme, + xbonePassthroughState, +} from '../Addons/XBOnePassthrough'; const schema = yup.object().shape({ ...analogScheme, @@ -60,6 +64,7 @@ const schema = yup.object().shape({ ...socdScheme, ...ps4Scheme, ...psPassthroughScheme, + ...xbonePassthroughScheme, ...wiiScheme, ...focusModeScheme, ...keyboardScheme, @@ -81,6 +86,7 @@ const defaultValues = { ...socdState, ...ps4State, ...psPassthroughState, + ...xbonePassthroughState, ...wiiState, ...snesState, ...focusModeState, @@ -103,6 +109,7 @@ const ADDONS = [ SOCD, Ps4, PSPassthrough, + XBOnePassthrough, Wii, SNES, FocusMode, diff --git a/www/src/Pages/SettingsPage.jsx b/www/src/Pages/SettingsPage.jsx index d21fc02bd..f9a005a04 100644 --- a/www/src/Pages/SettingsPage.jsx +++ b/www/src/Pages/SettingsPage.jsx @@ -18,6 +18,7 @@ const INPUT_MODES = [ { labelKey: 'input-mode-options.ps3', value: 2, group: 'primary' }, { labelKey: 'input-mode-options.keyboard', value: 3, group: 'primary' }, { labelKey: 'input-mode-options.ps4', value: PS4Mode, group: 'primary', optional: ['usb','ps4auth','ps4mode'] }, + { labelKey: 'input-mode-options.xbone', value: 5, group: 'primary', required: ['usb','xboxone'] }, { labelKey: 'input-mode-options.mdmini', value: 6, group: 'mini' }, { labelKey: 'input-mode-options.neogeo', value: 7, group: 'mini' }, { labelKey: 'input-mode-options.pcemini', value: 8, group: 'mini' }, @@ -34,6 +35,7 @@ const INPUT_BOOT_MODES = [ { labelKey: 'input-mode-options.ps3', value: 2, group: 'primary' }, { labelKey: 'input-mode-options.keyboard', value: 3, group: 'primary' }, { labelKey: 'input-mode-options.ps4', value: PS4Mode, group: 'primary', optional: ['usb','ps4auth','ps4mode'] }, + { labelKey: 'input-mode-options.xbone', value: 5, group: 'primary', required: ['usb','xboxone'] }, { labelKey: 'input-mode-options.mdmini', value: 6, group: 'mini' }, { labelKey: 'input-mode-options.neogeo', value: 7, group: 'mini' }, { labelKey: 'input-mode-options.pcemini', value: 8, group: 'mini' },