From 7cf833e59fdd7ef6345e449f1069f3b978654999 Mon Sep 17 00:00:00 2001 From: moparisthebest Date: Fri, 4 Dec 2020 20:34:40 -0500 Subject: [PATCH] Initial commit --- .clang-format | 182 +++++++++++++ .gitignore | 3 + CMakeLists.txt | 3 + code-format.sh | 4 + include/README | 39 +++ lib/README | 46 ++++ platformio.ini | 151 +++++++++++ sdkconfig.defaults | 13 + src/Debug.cpp | 28 ++ src/RadioReceiver.cpp | 31 +++ src/SnesNes.cpp | 225 ++++++++++++++++ src/gamepad/Debug-Gamepad/DebugGamepad.h | 31 +++ .../ESP32-BLE-Gamepad/BleConnectionStatus.cpp | 20 ++ .../ESP32-BLE-Gamepad/BleConnectionStatus.h | 20 ++ src/gamepad/ESP32-BLE-Gamepad/BleGamepad.h | 115 ++++++++ src/gamepad/Gamepad.h | 26 ++ src/gamepad/HIDTypes.h | 101 ++++++++ src/gamepad/Radio-Gamepad/RadioGamepad.h | 37 +++ src/gamepad/USB-Gamepad/UsbGamepad.h | 131 ++++++++++ src/gamepad/common.h | 245 ++++++++++++++++++ test/README | 11 + 21 files changed, 1462 insertions(+) create mode 100644 .clang-format create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100755 code-format.sh create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 sdkconfig.defaults create mode 100644 src/Debug.cpp create mode 100644 src/RadioReceiver.cpp create mode 100644 src/SnesNes.cpp create mode 100644 src/gamepad/Debug-Gamepad/DebugGamepad.h create mode 100644 src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.cpp create mode 100644 src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.h create mode 100644 src/gamepad/ESP32-BLE-Gamepad/BleGamepad.h create mode 100644 src/gamepad/Gamepad.h create mode 100644 src/gamepad/HIDTypes.h create mode 100644 src/gamepad/Radio-Gamepad/RadioGamepad.h create mode 100644 src/gamepad/USB-Gamepad/UsbGamepad.h create mode 100644 src/gamepad/common.h create mode 100644 test/README diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..e231664 --- /dev/null +++ b/.clang-format @@ -0,0 +1,182 @@ +--- +Language: Cpp +# BasedOnStyle: Google +AccessModifierOffset: -1 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: false +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: Align +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortEnumsOnASingleLine: false +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 200 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentCaseBlocks: false +IndentGotoLabels: true +IndentPPDirectives: None +IndentExternBlock: AfterExternBlock +IndentWidth: 4 +IndentWrappedFunctionNames: false +InsertTrailingCommas: None +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCBreakBeforeNestedBlockParam: true +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: false +SortIncludes: false +SortUsingDeclarations: false +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +UseCRLF: false +UseTab: Always +WhitespaceSensitiveMacros: + - STRINGIZE + - PP_STRINGIZE + - BOOST_PP_STRINGIZE +... + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa961c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pio +ignore/ +*.kate-swp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b8b784c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16.0) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(OpenRetroPad) diff --git a/code-format.sh b/code-format.sh new file mode 100755 index 0000000..5d44230 --- /dev/null +++ b/code-format.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +find src -type f \( -iname '*.h' -o -iname '*.c' -o -iname '*.cpp' \) -print0 | xargs -0 clang-format -style=file -i -fallback-style=none + diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..af912ef --- /dev/null +++ b/platformio.ini @@ -0,0 +1,151 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +# env's are laid out like: +# $board-$input-$output +# supported values: +# $board: micro, esp32 +# $input: radio, snes, debug +# $output: radio, usb, bt, debug +# please note not all boards are compatible with all inputs/outputs +# for example esp32 can only do bt +# micro can only do radio or usb + +[common] +# change GAMEPAD_COUNT if you want +build_flags = -DGAMEPAD_COUNT=2 + +[env] +monitor_speed = 115200 +framework = arduino + +# boards + +[esp32] +platform = espressif32 +board = esp32dev + +[micro] +platform = atmelavr +board = micro + +# outputs + +[out-debug] +build_flags = -DGAMEPAD_OUTPUT=0 +src_filter = + + +[out-radio] +build_flags = -DGAMEPAD_OUTPUT=1 +src_filter = + +lib_deps = tmrh20/RF24@^1.3.9 + +[out-usb] +build_flags = -DGAMEPAD_OUTPUT=2 +src_filter = + +framework = arduino +platform = atmelavr + +[out-bt] +build_flags = -DGAMEPAD_OUTPUT=3 +src_filter = + + +# radio input + +[in-radio] +extends = out-radio +src_filter = -<*> + +build_flags = ${common.build_flags} -DGAMEPAD_INPUT=1 + +# this one is a bit crazy, just use esp32-$input-bt directly +[env:esp32-radio-bt] +extends = esp32, in-radio, out-bt +src_filter = ${in-radio.src_filter} ${out-bt.src_filter} +build_flags = ${in-radio.build_flags} ${out-bt.build_flags} + +[env:esp32-radio-debug] +extends = esp32, in-radio, out-debug +src_filter = ${in-radio.src_filter} ${out-debug.src_filter} +build_flags = ${in-radio.build_flags} ${out-debug.build_flags} + +[env:micro-radio-usb] +extends = micro, in-radio, out-usb +src_filter = ${in-radio.src_filter} ${out-usb.src_filter} +build_flags = ${in-radio.build_flags} ${out-usb.build_flags} + +[env:micro-radio-debug] +extends = micro, in-radio, out-debug +src_filter = ${in-radio.src_filter} ${out-debug.src_filter} +build_flags = ${in-radio.build_flags} ${out-debug.build_flags} + +# not enough pins for env:micro-radio-radio , plus it'd be crazy + +# snes input + +[in-snes] +src_filter = -<*> + +build_flags = ${common.build_flags} -DGAMEPAD_INPUT=2 + +[env:esp32-snes-bt] +extends = esp32, in-snes, out-bt +src_filter = ${in-snes.src_filter} ${out-bt.src_filter} +build_flags = ${in-snes.build_flags} ${out-bt.build_flags} + +[env:esp32-snes-debug] +extends = esp32, in-snes, out-debug +src_filter = ${in-snes.src_filter} ${out-debug.src_filter} +build_flags = ${in-snes.build_flags} ${out-debug.build_flags} + +[env:micro-snes-usb] +extends = micro, in-snes, out-usb +src_filter = ${in-snes.src_filter} ${out-usb.src_filter} +build_flags = ${in-snes.build_flags} ${out-usb.build_flags} + +[env:micro-snes-debug] +extends = micro, in-snes, out-debug +src_filter = ${in-snes.src_filter} ${out-debug.src_filter} +build_flags = ${in-snes.build_flags} ${out-debug.build_flags} + +[env:micro-snes-radio] +extends = micro, in-snes, out-radio +src_filter = ${in-snes.src_filter} ${out-radio.src_filter} +build_flags = ${in-snes.build_flags} ${out-radio.build_flags} + +# debug input + +[in-debug] +src_filter = -<*> + +build_flags = ${common.build_flags} -DGAMEPAD_INPUT=2 + +[env:esp32-debug-bt] +extends = esp32, in-debug, out-bt +src_filter = ${in-debug.src_filter} ${out-bt.src_filter} +build_flags = ${in-debug.build_flags} ${out-bt.build_flags} + +[env:esp32-debug-debug] +extends = esp32, in-debug, out-debug +src_filter = ${in-debug.src_filter} ${out-debug.src_filter} +build_flags = ${in-debug.build_flags} ${out-debug.build_flags} + +[env:micro-debug-usb] +extends = micro, in-debug, out-usb +src_filter = ${in-debug.src_filter} ${out-usb.src_filter} +build_flags = ${in-debug.build_flags} ${out-usb.build_flags} + +[env:micro-debug-debug] +extends = micro, in-debug, out-debug +src_filter = ${in-debug.src_filter} ${out-debug.src_filter} +build_flags = ${in-debug.build_flags} ${out-debug.build_flags} + +[env:micro-debug-radio] +extends = micro, in-debug, out-radio +src_filter = ${in-debug.src_filter} ${out-radio.src_filter} +build_flags = ${in-debug.build_flags} ${out-radio.build_flags} + diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..a7095ff --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,13 @@ +# Override some defaults so BT stack is enabled +# and WiFi disabled by default in this example +CONFIG_BT_ENABLED=y +CONFIG_ESP32_ENABLE_STACK_BT=y + +CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y + +CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n +CONFIG_BTDM_CTRL_MODE_BTDM=n + +CONFIG_WIFI_ENABLED=n +CONFIG_ESP32_ENABLE_STACK_WIFI=n +CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=n diff --git a/src/Debug.cpp b/src/Debug.cpp new file mode 100644 index 0000000..50c7c28 --- /dev/null +++ b/src/Debug.cpp @@ -0,0 +1,28 @@ + +#include "Arduino.h" + +#include "gamepad/Gamepad.h" + +Gamepad gamepad; + +void setup() { + Serial.begin(115200); + Serial.println("Starting debug work!"); + gamepad.begin(); +} + +void loop() { + if (gamepad.isConnected()) { + // test code to automate sending to test RadioReceiver (or any gamepad impl) + uint8_t c = 0; + Serial.println("Press buttons 1 and 32. Move all axes to center. Set DPAD to down right."); + gamepad.press(c, BUTTON_1); + gamepad.press(c, BUTTON_32); + gamepad.setAxes(c, 0, 0, 0, 0, 0, 0, DPAD_DOWN_RIGHT); + delay(500); + Serial.println("Release all buttons. Move all axes to center. Set DPAD to center."); + gamepad.buttons(c, 0); + gamepad.setAxes(c, 0, 0, 0, 0, 0, 0, DPAD_CENTER); + delay(500); + } +} diff --git a/src/RadioReceiver.cpp b/src/RadioReceiver.cpp new file mode 100644 index 0000000..a112402 --- /dev/null +++ b/src/RadioReceiver.cpp @@ -0,0 +1,31 @@ + +#include "Arduino.h" + +#include +#include +#include + +#define GAMEPAD_REPORT_ARRAY_ADD 1 + +#include "gamepad/Gamepad.h" + +RF24 radio(7, 8); // CE, CSN + +const byte address[13] = "OpenRetroPad"; + +Gamepad gamepad; + +void setup() { + gamepad.begin(); + radio.begin(); + radio.openReadingPipe(0, address); + radio.setPALevel(RF24_PA_MIN); + radio.startListening(); +} + +void loop() { + if (radio.available() && gamepad.isConnected()) { + radio.read(&gamepad.gamepadReport, 16); + gamepad.sync(gamepad.gamepadReport[15]); + } +} diff --git a/src/SnesNes.cpp b/src/SnesNes.cpp new file mode 100644 index 0000000..2e0e3cf --- /dev/null +++ b/src/SnesNes.cpp @@ -0,0 +1,225 @@ + +#include "Arduino.h" + +#define GAMEPAD_COUNT 2 +#define BUTTON_COUNT 12 // SNES has 12, NES only has 8 + +//individual data pin for each controller +static const int DATA_PIN[GAMEPAD_COUNT] = {18, 19}; // grey, 20, 21 are the next logical ones + +//shared pins between all controllers +#if defined(CONFIG_BT_ENABLED) +// ESP32 +static const int LATCH_PIN = 16; // brown +static const int CLOCK_PIN = 17; // white +#else +// micro +static const int LATCH_PIN = 2; // brown +static const int CLOCK_PIN = 3; // white +#endif +// power red, ground black + +#include "gamepad/Gamepad.h" + +static const int translateToNES[12] = {1, 8, 2, 3, 4, 5, 6, 7, 0, 8, 8, 8}; + +class GameControllers { + public: + enum Type + { + NES = 0, + SNES = 1, + }; + + enum Button + { + B = 0, + Y = 1, + SELECT = 2, + START = 3, + UP = 4, + DOWN = 5, + LEFT = 6, + RIGHT = 7, + A = 8, + X = 9, + L = 10, + R = 11, + }; + + Type types[GAMEPAD_COUNT]; + int latchPin; + int clockPin; + int dataPins[GAMEPAD_COUNT]; + long buttons[GAMEPAD_COUNT][12]; + + int changedControllers[GAMEPAD_COUNT]; + + ///This has to be initialized once for the shared pins latch and clock + void init(int latch, int clock) { + latchPin = latch; + clockPin = clock; + pinMode(latchPin, OUTPUT); + digitalWrite(latchPin, LOW); + pinMode(clockPin, OUTPUT); + digitalWrite(clockPin, HIGH); + for (int c = 0; c < GAMEPAD_COUNT; c++) { + for (int i = 0; i < 12; i++) { + buttons[c][i] = -1; + } + types[c] = NES; // todo: if SNES button is ever pressed, change type to SNES, maybe buttons to manually toggle? + dataPins[c] = -1; + } + } + + ///This sets the controller type and initializes its individual data pin + void setController(int controller, Type type, int dataPin) { + types[controller] = type; + dataPins[controller] = dataPin; + pinMode(dataPins[controller], INPUT_PULLUP); + } + + void poll(void (*controllerChanged)(const int controller)) { + memset(changedControllers, 0, GAMEPAD_COUNT); + + digitalWrite(latchPin, HIGH); + delayMicroseconds(12); + digitalWrite(latchPin, LOW); + delayMicroseconds(6); + for (int i = 0; i < 12; i++) { + for (int c = 0; c < GAMEPAD_COUNT; c++) + if (dataPins[c] > -1) { + if (digitalRead(dataPins[c])) { + if (-1 != buttons[c][i]) { + buttons[c][i] = -1; + changedControllers[c] = 1; + } + } else { + //++buttons[c][i]; + //changedControllers[c] = 1; + if (0 != buttons[c][i]) { + buttons[c][i] = 0; + changedControllers[c] = 1; + } + } + } + digitalWrite(clockPin, LOW); + delayMicroseconds(6); + digitalWrite(clockPin, HIGH); + delayMicroseconds(6); + } + + for (int c = 0; c < GAMEPAD_COUNT; c++) { + // have any buttons changed state? + if (1 == changedControllers[c]) { + controllerChanged(c); + } + } + } + + int translate(int controller, Button b) const { + if (types[controller] == SNES) + return b; + return translateToNES[b]; + } + + ///returns if button is currently down + bool down(int controller, Button b) const { + return buttons[controller][translate(controller, b)] >= 0; + } +}; + +static const int translateToHid[12] = { + BUTTON_B, + BUTTON_Y, + BUTTON_SELECT, + BUTTON_START, + DPAD_UP, + DPAD_DOWN, + DPAD_LEFT, + DPAD_RIGHT, + BUTTON_A, + BUTTON_X, + BUTTON_L, + BUTTON_R, +}; + +GameControllers controllers; + +Gamepad gamepad; + +void setup() { + gamepad.begin(); + + //initialize shared pins + controllers.init(LATCH_PIN, CLOCK_PIN); + + //activate first controller ans set the type to SNES + for (int c = 0; c < GAMEPAD_COUNT; c++) { + controllers.setController(c, GameControllers::NES, DATA_PIN[c]); + } +} + +/* +void controllerChangedDebug(const int c) { + Serial.print("controllerChanged!!!!: "); + Serial.println(c); + Serial.print("c: "); + Serial.print(c); + for (uint8_t btn = 0; btn < 12; btn++) { + Serial.print("; "); + Serial.print(btn); + Serial.print(", "); + Serial.print(controllers.buttons[c][controllers.translate(c, static_cast(btn))]); + } + Serial.println(""); +} +*/ + +void controllerChanged(const int c) { + //controllerChangedDebug(c); + + gamepad.buttons(c, 0); + // if start and select are held at the same time, send menu and only menu + if (controllers.down(c, GameControllers::START) && controllers.down(c, GameControllers::SELECT)) { + gamepad.press(c, BUTTON_MENU); + } else { + // actually send buttons held + for (uint8_t btn = 0; btn < 12; btn++) { + if (btn > 3 && btn < 8) + continue; // skip dpad + if (controllers.down(c, static_cast(btn))) { + gamepad.press(c, translateToHid[btn]); + } + } + } + if (controllers.down(c, GameControllers::DOWN)) { + if (controllers.down(c, GameControllers::RIGHT)) { + gamepad.setHatSync(c, DPAD_DOWN_RIGHT); + } else if (controllers.down(c, GameControllers::LEFT)) { + gamepad.setHatSync(c, DPAD_DOWN_LEFT); + } else { + gamepad.setHatSync(c, DPAD_DOWN); + } + } else if (controllers.down(c, GameControllers::UP)) { + if (controllers.down(c, GameControllers::RIGHT)) { + gamepad.setHatSync(c, DPAD_UP_RIGHT); + } else if (controllers.down(c, GameControllers::LEFT)) { + gamepad.setHatSync(c, DPAD_UP_LEFT); + } else { + gamepad.setHatSync(c, DPAD_UP); + } + } else if (controllers.down(c, GameControllers::RIGHT)) { + gamepad.setHatSync(c, DPAD_RIGHT); + } else if (controllers.down(c, GameControllers::LEFT)) { + gamepad.setHatSync(c, DPAD_LEFT); + } else { + gamepad.setHatSync(c, DPAD_CENTERED); + } +} + +void loop() { + if (gamepad.isConnected()) { + controllers.poll(controllerChanged); //read all controllers at once + } +} diff --git a/src/gamepad/Debug-Gamepad/DebugGamepad.h b/src/gamepad/Debug-Gamepad/DebugGamepad.h new file mode 100644 index 0000000..63921cb --- /dev/null +++ b/src/gamepad/Debug-Gamepad/DebugGamepad.h @@ -0,0 +1,31 @@ +#ifndef USB_GAMEPAD_H +#define USB_GAMEPAD_H + +#include + +#include "../common.h" + +class DebugGamepad : public AbstractGamepad { + public: + DebugGamepad() : AbstractGamepad() { + } + + void begin(void) { + Serial.begin(115200); + Serial.println("DebugGamepad.begin"); + } + + void setAxes(const uint8_t cIdx, int16_t x, int16_t y, int16_t z, int16_t rZ, char rX, char rY, signed char hat) { + Serial.println("DebugGamepad.setAxes"); + AbstractGamepad::setAxes(cIdx, x, y, z, rZ, rX, rY, hat); + } + + void sync(const uint8_t cIdx) { + Serial.print("DebugGamepad.sync: "); + Serial.println(cIdx); + } +}; + +typedef DebugGamepad Gamepad; + +#endif // USB_GAMEPAD_H diff --git a/src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.cpp b/src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.cpp new file mode 100644 index 0000000..088675e --- /dev/null +++ b/src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.cpp @@ -0,0 +1,20 @@ +#include "BleConnectionStatus.h" + +BleConnectionStatus::BleConnectionStatus(void) { +} + +void BleConnectionStatus::onConnect(BLEServer *pServer) { + this->connected = true; + for (int c = 0; c < GAMEPAD_COUNT; c++) { + BLE2902 *desc = (BLE2902 *)this->inputGamepad[c]->getDescriptorByUUID(BLEUUID((uint16_t)0x2902)); + desc->setNotifications(true); + } +} + +void BleConnectionStatus::onDisconnect(BLEServer *pServer) { + this->connected = false; + for (int c = 0; c < GAMEPAD_COUNT; c++) { + BLE2902 *desc = (BLE2902 *)this->inputGamepad[c]->getDescriptorByUUID(BLEUUID((uint16_t)0x2902)); + desc->setNotifications(false); + } +} diff --git a/src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.h b/src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.h new file mode 100644 index 0000000..06984cf --- /dev/null +++ b/src/gamepad/ESP32-BLE-Gamepad/BleConnectionStatus.h @@ -0,0 +1,20 @@ +#ifndef ESP32_BLE_CONNECTION_STATUS_H +#define ESP32_BLE_CONNECTION_STATUS_H +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) + +#include "BLE2902.h" +#include "BLECharacteristic.h" +#include + +class BleConnectionStatus : public BLEServerCallbacks { + public: + BleConnectionStatus(void); + bool connected = false; + void onConnect(BLEServer *pServer); + void onDisconnect(BLEServer *pServer); + BLECharacteristic *inputGamepad[GAMEPAD_COUNT]; +}; + +#endif // CONFIG_BT_ENABLED +#endif // ESP32_BLE_CONNECTION_STATUS_H diff --git a/src/gamepad/ESP32-BLE-Gamepad/BleGamepad.h b/src/gamepad/ESP32-BLE-Gamepad/BleGamepad.h new file mode 100644 index 0000000..480c4d5 --- /dev/null +++ b/src/gamepad/ESP32-BLE-Gamepad/BleGamepad.h @@ -0,0 +1,115 @@ +#ifndef ESP32_BLE_GAMEPAD_H +#define ESP32_BLE_GAMEPAD_H +#include "sdkconfig.h" +#if defined(CONFIG_BT_ENABLED) + +#include "../common.h" + +#include "BLE2902.h" +#include "BLEHIDDevice.h" +#include "HIDKeyboardTypes.h" +#include "HIDTypes.h" +#include +#include +#include +#include + +#include "BLECharacteristic.h" +#include "BLEHIDDevice.h" +#include "BleConnectionStatus.h" + +#if defined(CONFIG_ARDUHAL_ESP_LOG) +#include "esp32-hal-log.h" +#define LOG_TAG "" +#else +#include "esp_log.h" +static const char *LOG_TAG = "BLEDevice"; +#endif + +class BleGamepad : public AbstractGamepad { + private: + BleConnectionStatus *connectionStatus; + BLEHIDDevice *hid; + BLECharacteristic *inputGamepad[GAMEPAD_COUNT]; + void rawAction(uint8_t msg[], char msgSize); + static void taskServer(void *pvParameter) { + BleGamepad *BleGamepadInstance = (BleGamepad *)pvParameter; // static_cast(pvParameter); + BLEDevice::init(BleGamepadInstance->deviceName); + BLEServer *pServer = BLEDevice::createServer(); + pServer->setCallbacks(BleGamepadInstance->connectionStatus); + + BleGamepadInstance->hid = new BLEHIDDevice(pServer); + + for (int c = 0; c < GAMEPAD_COUNT; c++) { + BleGamepadInstance->inputGamepad[c] = BleGamepadInstance->hid->inputReport(c); // <-- input REPORTID from report map + BleGamepadInstance->connectionStatus->inputGamepad[c] = BleGamepadInstance->inputGamepad[c]; + } + + BleGamepadInstance->hid->manufacturer()->setValue(BleGamepadInstance->deviceManufacturer); + + // todo: is this change required for 2 inputs??? + // BleGamepadInstance->hid->pnp(0x01,0x02e5,0xabcc,0x0110); + BleGamepadInstance->hid->pnp(0x02, 0x8282, 0x0132, 0x0106); + + BleGamepadInstance->hid->hidInfo(0x00, 0x01); + + BLESecurity *pSecurity = new BLESecurity(); + + pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND); + + BleGamepadInstance->hid->reportMap((uint8_t *)_hidReportDescriptor, sizeof(_hidReportDescriptor)); + BleGamepadInstance->hid->startServices(); + + BleGamepadInstance->onStarted(pServer); + + BLEAdvertising *pAdvertising = pServer->getAdvertising(); + pAdvertising->setAppearance(HID_GAMEPAD); + pAdvertising->addServiceUUID(BleGamepadInstance->hid->hidService()->getUUID()); + pAdvertising->start(); + BleGamepadInstance->hid->setBatteryLevel(BleGamepadInstance->batteryLevel); + + ESP_LOGD(LOG_TAG, "Advertising started!"); + vTaskDelay(portMAX_DELAY); // delay(portMAX_DELAY); + } + + public: + uint8_t batteryLevel; + std::string deviceManufacturer; + std::string deviceName; + + BleGamepad(std::string deviceName = "8BitDo SN30 Pro+", std::string deviceManufacturer = "8Bitdo SF30 Pro", uint8_t batteryLevel = 100) : AbstractGamepad(), hid(0) { + this->deviceName = deviceName; + this->deviceManufacturer = deviceManufacturer; + this->batteryLevel = batteryLevel; + this->connectionStatus = new BleConnectionStatus(); + } + + void begin(void) { + xTaskCreate(this->taskServer, "server", 20000, (void *)this, 5, NULL); + } + + void sync(const uint8_t cIdx) { + if (this->isConnected()) { + this->inputGamepad[cIdx]->setValue(gamepadReport, GAMEPAD_REPORT_LEN); + this->inputGamepad[cIdx]->notify(); + } + } + + bool isConnected(void) { + return this->connectionStatus->connected; + } + + void setBatteryLevel(uint8_t level) { + this->batteryLevel = level; + if (hid != 0) + this->hid->setBatteryLevel(this->batteryLevel); + } + + protected: + virtual void onStarted(BLEServer *pServer){}; +}; + +typedef BleGamepad Gamepad; + +#endif // CONFIG_BT_ENABLED +#endif // ESP32_BLE_GAMEPAD_H diff --git a/src/gamepad/Gamepad.h b/src/gamepad/Gamepad.h new file mode 100644 index 0000000..3719b38 --- /dev/null +++ b/src/gamepad/Gamepad.h @@ -0,0 +1,26 @@ + +#if GAMEPAD_OUTPUT == 0 + +#include "Debug-Gamepad/DebugGamepad.h" + +#elif GAMEPAD_OUTPUT == 1 + +#include "Radio-Gamepad/RadioGamepad.h" + +#elif GAMEPAD_OUTPUT == 2 + +#include "USB-Gamepad/UsbGamepad.h" + +#elif GAMEPAD_OUTPUT == 3 + +#ifndef CONFIG_BT_ENABLED +#error BT output unsupported if CONFIG_BT_ENABLED is not set +#endif // not CONFIG_BT_ENABLED + +#include "ESP32-BLE-Gamepad/BleGamepad.h" + +#else + +#error Unsupported value for GAMEPAD_OUTPUT, must be 0-3 + +#endif diff --git a/src/gamepad/HIDTypes.h b/src/gamepad/HIDTypes.h new file mode 100644 index 0000000..c5a99de --- /dev/null +++ b/src/gamepad/HIDTypes.h @@ -0,0 +1,101 @@ +/* Copyright (c) 2010-2011 mbed.org, MIT License +* +* 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. +*/ + +// adapted from HIDTypes.h from ESP32 lib, search for modified: in this file + +#ifndef USBCLASS_HID_TYPES +#define USBCLASS_HID_TYPES + +#include + +/* */ +#define HID_VERSION_1_11 (0x0111) + +/* HID Class */ +#define HID_CLASS (3) + +// modified: redefined from arduino HID.h, about the same, so let's go with their version +//#define HID_SUBCLASS_NONE (0) +//#define HID_PROTOCOL_NONE (0) +#define HID_SUBCLASS_NONE 0 +#define HID_PROTOCOL_NONE 0 + +/* Descriptors */ +#define HID_DESCRIPTOR (33) +#define HID_DESCRIPTOR_LENGTH (0x09) +#define REPORT_DESCRIPTOR (34) + +/* Class requests */ +#define GET_REPORT (0x1) +#define GET_IDLE (0x2) +#define SET_REPORT (0x9) +#define SET_IDLE (0xa) + +/* HID Class Report Descriptor */ +/* Short items: size is 0, 1, 2 or 3 specifying 0, 1, 2 or 4 (four) bytes */ +/* of data as per HID Class standard */ + +/* Main items */ +#define HIDINPUT(size) (0x80 | size) +#define HIDOUTPUT(size) (0x90 | size) + +// modified: was FEATURE but conflicted with nRF24L01.h FEATURE +//#define FEATURE(size) (0xb0 | size) +#define HIDFEATURE(size) (0xb0 | size) + +#define COLLECTION(size) (0xa0 | size) +#define END_COLLECTION(size) (0xc0 | size) + +/* Global items */ +#define USAGE_PAGE(size) (0x04 | size) +#define LOGICAL_MINIMUM(size) (0x14 | size) +#define LOGICAL_MAXIMUM(size) (0x24 | size) +#define PHYSICAL_MINIMUM(size) (0x34 | size) +#define PHYSICAL_MAXIMUM(size) (0x44 | size) +#define UNIT_EXPONENT(size) (0x54 | size) +#define UNIT(size) (0x64 | size) +#define REPORT_SIZE(size) (0x74 | size) //bits +#define REPORT_ID(size) (0x84 | size) +#define REPORT_COUNT(size) (0x94 | size) //bytes +#define PUSH(size) (0xa4 | size) +#define POP(size) (0xb4 | size) + +/* Local items */ +#define USAGE(size) (0x08 | size) +#define USAGE_MINIMUM(size) (0x18 | size) +#define USAGE_MAXIMUM(size) (0x28 | size) +#define DESIGNATOR_INDEX(size) (0x38 | size) +#define DESIGNATOR_MINIMUM(size) (0x48 | size) +#define DESIGNATOR_MAXIMUM(size) (0x58 | size) +#define STRING_INDEX(size) (0x78 | size) +#define STRING_MINIMUM(size) (0x88 | size) +#define STRING_MAXIMUM(size) (0x98 | size) +#define DELIMITER(size) (0xa8 | size) + +/* HID Report */ +/* Where report IDs are used the first byte of 'data' will be the */ +/* report ID and 'length' will include this report ID byte. */ + +#define MAX_HID_REPORT_SIZE (64) + +typedef struct { + uint32_t length; + uint8_t data[MAX_HID_REPORT_SIZE]; +} HID_REPORT; + +#endif diff --git a/src/gamepad/Radio-Gamepad/RadioGamepad.h b/src/gamepad/Radio-Gamepad/RadioGamepad.h new file mode 100644 index 0000000..aa7cc11 --- /dev/null +++ b/src/gamepad/Radio-Gamepad/RadioGamepad.h @@ -0,0 +1,37 @@ +#ifndef RADIO_GAMEPAD_H +#define RADIO_GAMEPAD_H + +#include +#include +#include + +#define GAMEPAD_REPORT_ARRAY_ADD 1 + +#include "../common.h" + +RF24 radio(7, 8); // CE, CSN +const byte address[13] = "OpenRetroPad"; + +class RadioGamepad : public AbstractGamepad { + public: + RadioGamepad() : AbstractGamepad() { + } + + void begin(void) { + Serial.println("RadioGamepad.begin"); + radio.begin(); + radio.openWritingPipe(address); + radio.setPALevel(RF24_PA_MIN); + radio.stopListening(); + } + + void sync(const uint8_t cIdx) { + Serial.println("RadioGamepad.sync"); + gamepadReport[15] = cIdx; + radio.write(&gamepadReport, 16); + } +}; + +typedef RadioGamepad Gamepad; + +#endif // RADIO_GAMEPAD_H diff --git a/src/gamepad/USB-Gamepad/UsbGamepad.h b/src/gamepad/USB-Gamepad/UsbGamepad.h new file mode 100644 index 0000000..ba2ef2a --- /dev/null +++ b/src/gamepad/USB-Gamepad/UsbGamepad.h @@ -0,0 +1,131 @@ +#ifndef USB_GAMEPAD_H +#define USB_GAMEPAD_H + +#include + +#include "../common.h" + +#include "HID.h" + +// ATT: 20 chars max (including NULL at the end) according to Arduino source code. +// Additionally serial number is used to differentiate arduino projects to have different button maps! +const char* gp_serial = "SF30 Pro"; + +class Gamepad_ : public PluggableUSBModule { + private: + uint8_t reportId; + + protected: + uint8_t epType[1]; + uint8_t protocol; + uint8_t idle; + + /* + int getInterface(uint8_t* interfaceCount); + int getDescriptor(USBSetup& setup); + uint8_t getShortName(char *name); + bool setup(USBSetup& setup); + */ + + int getInterface(uint8_t* interfaceCount) { + *interfaceCount += 1; // uses 1 + HIDDescriptor hidInterface = {D_INTERFACE(pluggedInterface, 1, USB_DEVICE_CLASS_HUMAN_INTERFACE, HID_SUBCLASS_NONE, HID_PROTOCOL_NONE), + D_HIDREPORT(sizeof(_hidReportDescriptor)), + D_ENDPOINT(USB_ENDPOINT_IN(pluggedEndpoint), USB_ENDPOINT_TYPE_INTERRUPT, USB_EP_SIZE, 0x01)}; + return USB_SendControl(0, &hidInterface, sizeof(hidInterface)); + } + + int getDescriptor(USBSetup& setup) { + // Check if this is a HID Class Descriptor request + if (setup.bmRequestType != REQUEST_DEVICETOHOST_STANDARD_INTERFACE) { + return 0; + } + if (setup.wValueH != HID_REPORT_DESCRIPTOR_TYPE) { + return 0; + } + + // In a HID Class Descriptor wIndex cointains the interface number + if (setup.wIndex != pluggedInterface) { + return 0; + } + + // Reset the protocol on reenumeration. Normally the host should not assume the state of the protocol + // due to the USB specs, but Windows and Linux just assumes its in report mode. + protocol = HID_REPORT_PROTOCOL; + + return USB_SendControl(TRANSFER_PGM, _hidReportDescriptor, sizeof(_hidReportDescriptor)); + } + + bool setup(USBSetup& setup) { + if (pluggedInterface != setup.wIndex) { + return false; + } + + uint8_t request = setup.bRequest; + uint8_t requestType = setup.bmRequestType; + + if (requestType == REQUEST_DEVICETOHOST_CLASS_INTERFACE) { + if (request == HID_GET_REPORT) { + // TODO: HID_GetReport(); + return true; + } + if (request == HID_GET_PROTOCOL) { + // TODO: Send8(protocol); + return true; + } + } + + if (requestType == REQUEST_HOSTTODEVICE_CLASS_INTERFACE) { + if (request == HID_SET_PROTOCOL) { + protocol = setup.wValueL; + return true; + } + if (request == HID_SET_IDLE) { + idle = setup.wValueL; + return true; + } + if (request == HID_SET_REPORT) { + } + } + + return false; + } + + uint8_t getShortName(char* name) { + if (!next) { + strcpy(name, gp_serial); + return strlen(name); + } + return 0; + } + + public: + Gamepad_(void) : PluggableUSBModule(1, 1, epType), protocol(HID_REPORT_PROTOCOL), idle(1) { + epType[0] = EP_TYPE_INTERRUPT_IN; + PluggableUSB().plug(this); + } + + void send(const void* d, int len) { + USB_Send(pluggedEndpoint | TRANSFER_RELEASE, d, len); + } +}; + +class UsbGamepad : public AbstractGamepad { + public: + String deviceManufacturer; + String deviceName; + Gamepad_ gamepad[GAMEPAD_COUNT]; + + UsbGamepad(String deviceName = "8BitDo SN30 Pro+", String deviceManufacturer = "8Bitdo SF30 Pro") : AbstractGamepad() { + this->deviceName = deviceName; + this->deviceManufacturer = deviceManufacturer; + } + + void sync(const uint8_t cIdx) { + gamepad[cIdx].send(&gamepadReport, GAMEPAD_REPORT_LEN); + } +}; + +typedef UsbGamepad Gamepad; + +#endif // USB_GAMEPAD_H diff --git a/src/gamepad/common.h b/src/gamepad/common.h new file mode 100644 index 0000000..43eadf3 --- /dev/null +++ b/src/gamepad/common.h @@ -0,0 +1,245 @@ + +#include + +#define ARDUINO_ARCH_ESP32 1 +#include "HIDTypes.h" + +#define BUTTON_A 1 +#define BUTTON_B 2 +#define BUTTON_MENU 4 +#define BUTTON_X 8 +#define BUTTON_Y 16 +#define BUTTON_TL 64 +#define BUTTON_L 64 +#define BUTTON_TR 128 +#define BUTTON_R 128 +#define BUTTON_TL2 256 +#define BUTTON_TR2 512 +#define BUTTON_SELECT 1024 +#define BUTTON_START 2048 +#define BUTTON_THUMBL 8192 +#define BUTTON_THUMBR 16384 + +#define BUTTON_1 1 +#define BUTTON_2 2 +#define BUTTON_3 4 +#define BUTTON_4 8 +#define BUTTON_5 16 +#define BUTTON_6 32 +#define BUTTON_7 64 +#define BUTTON_8 128 +#define BUTTON_9 256 +#define BUTTON_10 512 +#define BUTTON_11 1024 +#define BUTTON_12 2048 +#define BUTTON_13 4096 +#define BUTTON_14 8192 +#define BUTTON_15 16384 +#define BUTTON_16 32768 +#define BUTTON_17 65536 +#define BUTTON_18 131072 +#define BUTTON_19 262144 +#define BUTTON_20 524288 +#define BUTTON_21 1048576 +#define BUTTON_22 2097152 +#define BUTTON_23 4194304 +#define BUTTON_24 8388608 +#define BUTTON_25 16777216 +#define BUTTON_26 33554432 +#define BUTTON_27 67108864 +#define BUTTON_28 134217728 +#define BUTTON_29 268435456 +#define BUTTON_30 536870912 +#define BUTTON_31 1073741824 +#define BUTTON_32 2147483648 + +#define DPAD_CENTER 0 +#define DPAD_CENTERED 0 +#define DPAD_UP 1 +#define DPAD_UP_RIGHT 2 +#define DPAD_RIGHT 3 +#define DPAD_DOWN_RIGHT 4 +#define DPAD_DOWN 5 +#define DPAD_DOWN_LEFT 6 +#define DPAD_LEFT 7 +#define DPAD_UP_LEFT 8 + +#ifndef GAMEPAD_REPORT_ARRAY_ADD +// this is used by radio gamepad to send additional info +#define GAMEPAD_REPORT_ARRAY_ADD 0 +#endif + +// this must match _hidReportDescriptor, length to actually send +#define GAMEPAD_REPORT_LEN 15 + +static const uint8_t _hidReportDescriptor[] PROGMEM = { + USAGE_PAGE(1), + 0x01, // USAGE_PAGE (Generic Desktop) + USAGE(1), + 0x05, // USAGE (Gamepad) + COLLECTION(1), + 0x01, // COLLECTION (Application) + USAGE(1), + 0x01, // USAGE (Pointer) + COLLECTION(1), + 0x00, // COLLECTION (Physical) + + // ------------------------------------------------- Buttons (1 to 32) + USAGE_PAGE(1), + 0x09, // USAGE_PAGE (Button) + USAGE_MINIMUM(1), + 0x01, // USAGE_MINIMUM (Button 1) + USAGE_MAXIMUM(1), + 0x20, // USAGE_MAXIMUM (Button 32) + LOGICAL_MINIMUM(1), + 0x00, // LOGICAL_MINIMUM (0) + LOGICAL_MAXIMUM(1), + 0x01, // LOGICAL_MAXIMUM (1) + REPORT_SIZE(1), + 0x01, // REPORT_SIZE (1) + REPORT_COUNT(1), + 0x20, // REPORT_COUNT (32) + HIDINPUT(1), + 0x02, // INPUT (Data, Variable, Absolute) ;32 button bits + // ------------------------------------------------- X/Y position, Z/rZ position + USAGE_PAGE(1), + 0x01, // USAGE_PAGE (Generic Desktop) + COLLECTION(1), + 0x00, // COLLECTION (Physical) + USAGE(1), + 0x30, // USAGE (X) + USAGE(1), + 0x31, // USAGE (Y) + USAGE(1), + 0x32, // USAGE (Z) + USAGE(1), + 0x35, // USAGE (rZ) + 0x16, + 0x01, + 0x80, //LOGICAL_MINIMUM (-32767) + 0x26, + 0xFF, + 0x7F, //LOGICAL_MAXIMUM (32767) + REPORT_SIZE(1), + 0x10, // REPORT_SIZE (16) + REPORT_COUNT(1), + 0x04, // REPORT_COUNT (4) + HIDINPUT(1), + 0x02, // INPUT (Data,Var,Abs) + // ------------------------------------------------- Triggers + USAGE(1), + 0x33, // USAGE (rX) Left Trigger + USAGE(1), + 0x34, // USAGE (rY) Right Trigger + LOGICAL_MINIMUM(1), + 0x81, // LOGICAL_MINIMUM (-127) + LOGICAL_MAXIMUM(1), + 0x7f, // LOGICAL_MAXIMUM (127) + REPORT_SIZE(1), + 0x08, // REPORT_SIZE (8) + REPORT_COUNT(1), + 0x02, // REPORT_COUNT (2) + HIDINPUT(1), + 0x02, // INPUT (Data, Variable, Absolute) ;4 bytes (X,Y,Z,rZ) + END_COLLECTION(0), // END_COLLECTION + + USAGE_PAGE(1), + 0x01, // USAGE_PAGE (Generic Desktop) + USAGE(1), + 0x39, // USAGE (Hat switch) + USAGE(1), + 0x39, // USAGE (Hat switch) + LOGICAL_MINIMUM(1), + 0x01, // LOGICAL_MINIMUM (1) + LOGICAL_MAXIMUM(1), + 0x08, // LOGICAL_MAXIMUM (8) + REPORT_SIZE(1), + 0x04, // REPORT_SIZE (4) + REPORT_COUNT(1), + 0x02, // REPORT_COUNT (2) + HIDINPUT(1), + 0x02, // INPUT (Data, Variable, Absolute) ;1 byte Hat1, Hat2 + + END_COLLECTION(0), // END_COLLECTION + END_COLLECTION(0) // END_COLLECTION +}; + +class AbstractGamepad { + public: + uint32_t _buttons[GAMEPAD_COUNT]; + uint8_t gamepadReport[GAMEPAD_REPORT_LEN + GAMEPAD_REPORT_ARRAY_ADD]; + + AbstractGamepad() { + } + + virtual void begin(void) { + } + + virtual void end(void) { + } + + virtual bool isConnected(void) { + return true; + } + + virtual void setAxes(const uint8_t cIdx, int16_t x, int16_t y, int16_t z, int16_t rZ, char rX, char rY, signed char hat) { + if (x == -32768) { + x = -32767; + } + if (y == -32768) { + y = -32767; + } + if (z == -32768) { + z = -32767; + } + if (rZ == -32768) { + rZ = -32767; + } + + gamepadReport[0] = _buttons[cIdx]; + gamepadReport[1] = (_buttons[cIdx] >> 8); + gamepadReport[2] = (_buttons[cIdx] >> 16); + gamepadReport[3] = (_buttons[cIdx] >> 24); + gamepadReport[4] = x; + gamepadReport[5] = (x >> 8); + gamepadReport[6] = y; + gamepadReport[7] = (y >> 8); + gamepadReport[8] = z; + gamepadReport[9] = (z >> 8); + gamepadReport[10] = rZ; + gamepadReport[11] = (rZ >> 8); + gamepadReport[12] = (signed char)(rX - 128); + gamepadReport[13] = (signed char)(rY - 128); + gamepadReport[14] = hat; + if (gamepadReport[12] == -128) { + gamepadReport[12] = -127; + } + if (gamepadReport[13] == -128) { + gamepadReport[13] = -127; + } + + this->sync(cIdx); + } + + virtual void setHatSync(const uint8_t cIdx, signed char hat) { + setAxes(cIdx, 0, 0, 0, 0, 0, 0, hat); + } + + virtual void buttons(const uint8_t cIdx, uint32_t b) { + _buttons[cIdx] = b; + } + + virtual void press(const uint8_t cIdx, uint32_t b) { + buttons(cIdx, _buttons[cIdx] | b); + } + + virtual void release(const uint8_t cIdx, uint32_t b) { + buttons(cIdx, _buttons[cIdx] & ~b); + } + + virtual bool isPressed(const uint8_t cIdx, uint32_t b) { + return ((b & _buttons[cIdx]) > 0); + } + + virtual void sync(const uint8_t cIdx); // actually sends report +}; diff --git a/test/README b/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html