Initial commit

master
Travis Burtrum 2020-12-04 20:34:40 -05:00
commit 7cf833e59f
21 changed files with 1462 additions and 0 deletions

182
.clang-format Normal file
View File

@ -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: '^<ext/.*\.h>'
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
...

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.pio
ignore/
*.kate-swp

3
CMakeLists.txt Normal file
View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(OpenRetroPad)

4
code-format.sh Executable file
View File

@ -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

39
include/README Normal file
View File

@ -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

46
lib/README Normal file
View File

@ -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 <Foo.h>
#include <Bar.h>
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

151
platformio.ini Normal file
View File

@ -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 = +<gamepad/Debug-Gamepad>
[out-radio]
build_flags = -DGAMEPAD_OUTPUT=1
src_filter = +<gamepad/Radio-Gamepad>
lib_deps = tmrh20/RF24@^1.3.9
[out-usb]
build_flags = -DGAMEPAD_OUTPUT=2
src_filter = +<gamepad/USB-Gamepad>
framework = arduino
platform = atmelavr
[out-bt]
build_flags = -DGAMEPAD_OUTPUT=3
src_filter = +<gamepad/ESP32-BLE-Gamepad>
# radio input
[in-radio]
extends = out-radio
src_filter = -<*> +<RadioReceiver.cpp>
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 = -<*> +<SnesNes.cpp>
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 = -<*> +<Debug.cpp>
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}

13
sdkconfig.defaults Normal file
View File

@ -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

28
src/Debug.cpp Normal file
View File

@ -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);
}
}

31
src/RadioReceiver.cpp Normal file
View File

@ -0,0 +1,31 @@
#include "Arduino.h"
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#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]);
}
}

225
src/SnesNes.cpp Normal file
View File

@ -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<GameControllers::Button>(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<GameControllers::Button>(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
}
}

View File

@ -0,0 +1,31 @@
#ifndef USB_GAMEPAD_H
#define USB_GAMEPAD_H
#include <WString.h>
#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

View File

@ -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);
}
}

View File

@ -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 <BLEServer.h>
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

View File

@ -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 <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <driver/adc.h>
#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<BleGamepad *>(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

26
src/gamepad/Gamepad.h Normal file
View File

@ -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

101
src/gamepad/HIDTypes.h Normal file
View File

@ -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 <stdint.h>
/* */
#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

View File

@ -0,0 +1,37 @@
#ifndef RADIO_GAMEPAD_H
#define RADIO_GAMEPAD_H
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#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

View File

@ -0,0 +1,131 @@
#ifndef USB_GAMEPAD_H
#define USB_GAMEPAD_H
#include <WString.h>
#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

245
src/gamepad/common.h Normal file
View File

@ -0,0 +1,245 @@
#include <Arduino.h>
#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
};

11
test/README Normal file
View File

@ -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