diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d767e9118..0acd51554e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,6 +121,7 @@ src/addons/playernum.cpp src/addons/playerleds.cpp src/addons/ps4mode.cpp src/addons/reverse.cpp +src/addons/sidewindergp.cpp src/addons/turbo.cpp src/addons/slider_socd.cpp src/addons/wiiext.cpp diff --git a/docs/assets/images/gpc-add-ons-sidewindergp.png b/docs/assets/images/gpc-add-ons-sidewindergp.png new file mode 100644 index 0000000000..2fccc950d6 Binary files /dev/null and b/docs/assets/images/gpc-add-ons-sidewindergp.png differ diff --git a/docs/web-configurator.md b/docs/web-configurator.md index 2a070f22a5..7e825ea846 100644 --- a/docs/web-configurator.md +++ b/docs/web-configurator.md @@ -287,6 +287,37 @@ Classic Controller support includes Classic, Classic Pro, and NES/SNES Mini Cont Original Classic Controller L & R triggers are analog sensitive, where Pro triggers are not. +### Sidewinder Gamepad + +![GP2040 Configurator - Sidewinder Gamepad](assets/images/gpc-add-ons-sidewindergp.png) + +Designed to be mapped on DA-15 gameport of the Sidewinder Gamepad. + +Connections: + + - DA-15 pins 1, 8, 9, 15 to +5V + - DA-15 pins 4, 5, 12 to GND + - DA-15 pin 2 to Clock pin + - DA-15 pin 3 to Trigger pin + - DA-15 pin 7 to Data pin + +Mapping is a follows: + +| Sidewinder GP | GP2040-CE | +|---------------|-----------| +| D-Pad | Analog | +| A | B1 | +| B | B2 | +| C | L3 | +| X | B3 | +| Y | B4 | +| Z | R3 | +| LT | L1 | +| RT | R1 | +| Start | S2 | +| M | S1 | + + ## Data Backup and Restoration ![GP2040-CE Configurator - Add-Ons Backup and Restore](assets/images/gpc-backup-and-restore.png) diff --git a/headers/addons/sidewindergp.h b/headers/addons/sidewindergp.h new file mode 100644 index 0000000000..74f45d0ab3 --- /dev/null +++ b/headers/addons/sidewindergp.h @@ -0,0 +1,43 @@ +#ifndef _SidewinderGP_H +#define _SidewinderGP_H + +#include "gpaddon.h" + +#include "GamepadEnums.h" + +#include "BoardConfig.h" + +#ifndef SIDEWINDERGP_INPUT_ENABLED +#define SIDEWINDERGP_INPUT_ENABLED 0 +#endif + +#ifndef SIDEWINDERGP_PIN_TRIGGER +#define SIDEWINDERGP_PIN_TRIGGER -1 +#endif + +#ifndef SIDEWINDERGP_PIN_CLOCK +#define SIDEWINDERGP_PIN_CLOCK -1 +#endif + +#ifndef SIDEWINDERGP_PIN_DATA +#define SIDEWINDERGP_PIN_DATA -1 +#endif + + +// SidewinderGP Module Name +#define SideWinderGPName "Sidewinder Gamepad" + +class SidewinderGPInput : public GPAddon { +public: + virtual bool available(); + virtual void setup(); // Setup + virtual void process(); // Process + virtual void preprocess() {} + virtual std::string name() { return SideWinderGPName; } +private: + uint8_t sidewinderGPPinTrigger; + uint8_t sidewinderGPPinClock; + uint8_t sidewinderGPPinData; +}; + +#endif // _SidewinderGP_H_ \ No newline at end of file diff --git a/headers/configs/base64.h b/headers/configs/base64.h index 11ccbf7200..15d8fd65ef 100644 --- a/headers/configs/base64.h +++ b/headers/configs/base64.h @@ -26,6 +26,7 @@ */ #include +#include class Base64 { public: diff --git a/headers/storagemanager.h b/headers/storagemanager.h index 015cf76d44..d60ceff87c 100644 --- a/headers/storagemanager.h +++ b/headers/storagemanager.h @@ -144,6 +144,9 @@ struct AddonOptions { uint8_t wiiExtensionSCLPin; int wiiExtensionBlock; uint32_t wiiExtensionSpeed; + uint8_t sidewinderGPPinTrigger; + uint8_t sidewinderGPPinClock; + uint8_t sidewinderGPPinData; uint8_t AnalogInputEnabled; uint8_t BoardLedAddonEnabled; uint8_t BootselButtonAddonEnabled; @@ -161,6 +164,7 @@ struct AddonOptions { uint8_t TurboInputEnabled; uint8_t SliderSOCDInputEnabled; uint8_t WiiExtensionAddonEnabled; + uint8_t SidewinderGPEnabled; uint32_t checksum; }; diff --git a/src/addons/sidewindergp.cpp b/src/addons/sidewindergp.cpp new file mode 100644 index 0000000000..8a6c010902 --- /dev/null +++ b/src/addons/sidewindergp.cpp @@ -0,0 +1,97 @@ +#include "addons/sidewindergp.h" +#include "storagemanager.h" + + +uint32_t SidewinderGPInputMapping[14] = { + GAMEPAD_MASK_DU, + GAMEPAD_MASK_DD, + GAMEPAD_MASK_DR, + GAMEPAD_MASK_DL, + GAMEPAD_MASK_B1, + GAMEPAD_MASK_B2, + GAMEPAD_MASK_L3, + GAMEPAD_MASK_B3, + GAMEPAD_MASK_B4, + GAMEPAD_MASK_R3, + GAMEPAD_MASK_L1, + GAMEPAD_MASK_R1, + GAMEPAD_MASK_S2, + GAMEPAD_MASK_S1 +}; + +bool SidewinderGPInput::available() { + const AddonOptions& options = Storage::getInstance().getAddonOptions(); + sidewinderGPPinTrigger = Storage::getInstance().getAddonOptions().sidewinderGPPinTrigger; + sidewinderGPPinClock = Storage::getInstance().getAddonOptions().sidewinderGPPinClock; + sidewinderGPPinData = Storage::getInstance().getAddonOptions().sidewinderGPPinData; + return options.SidewinderGPEnabled; +} + +void SidewinderGPInput::setup() { + gpio_init(sidewinderGPPinTrigger); + gpio_set_dir(sidewinderGPPinTrigger, GPIO_OUT); + gpio_put(sidewinderGPPinTrigger, 1); + + gpio_init(sidewinderGPPinClock); + gpio_set_dir(sidewinderGPPinClock, GPIO_IN); + gpio_pull_up(sidewinderGPPinClock); + + gpio_init(sidewinderGPPinData); + gpio_set_dir(sidewinderGPPinData, GPIO_IN); + gpio_pull_up(sidewinderGPPinData); +} + +void SidewinderGPInput::process() +{ + Gamepad * gamepad = Storage::getInstance().GetGamepad(); + + gpio_put(sidewinderGPPinTrigger, 0); + sleep_us(225); + gpio_put(sidewinderGPPinTrigger, 1); + + bool btns[14] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + bool checksum = 0; + bool ok = 0; + + for (int i=0; i < 15; i++) { + // Wait for clock falling edge + uint16_t timeout = 10000; + while (timeout-- && !gpio_get(sidewinderGPPinClock)) {} + while (timeout-- && gpio_get(sidewinderGPPinClock)) {} + if (!timeout) + break; + if (i < 14) { + btns[i] = !gpio_get(sidewinderGPPinData); + checksum ^= btns[i]; + } else { + ok = (checksum == gpio_get(sidewinderGPPinData)); + } + } + if (!ok) + return; + + gamepad->state.lx = 0x8000; + gamepad->state.ly = 0x8000; + for (int i=0; i < 14; i++) { + if (!btns[i]) + continue; + uint32_t btnMap = SidewinderGPInputMapping[i]; + if (btnMap > (GAMEPAD_MASK_A2)) { + switch (btnMap) { + case (GAMEPAD_MASK_DU): + gamepad->state.ly = 0x0; + break; + case (GAMEPAD_MASK_DD): + gamepad->state.ly = 0xffff; + break; + case (GAMEPAD_MASK_DL): + gamepad->state.lx = 0x0; + break; + case (GAMEPAD_MASK_DR): + gamepad->state.lx = 0xffff; + break; + } + } + else gamepad->state.buttons |= btnMap; + } +} diff --git a/src/configs/webconfig.cpp b/src/configs/webconfig.cpp index 7cd4aa4186..39287565b5 100644 --- a/src/configs/webconfig.cpp +++ b/src/configs/webconfig.cpp @@ -826,6 +826,9 @@ std::string setAddonOptions() docToPin(addonOptions.wiiExtensionSDAPin, doc, "wiiExtensionSDAPin"); docToPin(addonOptions.wiiExtensionSCLPin, doc, "wiiExtensionSCLPin"); docToValue(addonOptions.wiiExtensionBlock, doc, "wiiExtensionBlock"); + docToPin(addonOptions.sidewinderGPPinTrigger, doc, "sidewinderGPPinTrigger"); + docToPin(addonOptions.sidewinderGPPinClock, doc, "sidewinderGPPinClock"); + docToPin(addonOptions.sidewinderGPPinData, doc, "sidewinderGPPinData"); docToValue(addonOptions.wiiExtensionSpeed, doc, "wiiExtensionSpeed"); docToValue(addonOptions.AnalogInputEnabled, doc, "AnalogInputEnabled"); docToValue(addonOptions.BoardLedAddonEnabled, doc, "BoardLedAddonEnabled"); @@ -841,6 +844,7 @@ std::string setAddonOptions() docToValue(addonOptions.ReverseInputEnabled, doc, "ReverseInputEnabled"); docToValue(addonOptions.TurboInputEnabled, doc, "TurboInputEnabled"); docToValue(addonOptions.WiiExtensionAddonEnabled, doc, "WiiExtensionAddonEnabled"); + docToValue(addonOptions.SidewinderGPEnabled, doc, "SidewinderGPEnabled"); Storage::getInstance().setAddonOptions(addonOptions); @@ -1002,6 +1006,9 @@ std::string getAddonOptions() writeDoc(doc, "wiiExtensionSCLPin", addonOptions.wiiExtensionSCLPin == 0xFF ? -1 : addonOptions.wiiExtensionSCLPin); writeDoc(doc, "wiiExtensionBlock", addonOptions.wiiExtensionBlock); writeDoc(doc, "wiiExtensionSpeed", addonOptions.wiiExtensionSpeed); + writeDoc(doc, "sidewinderGPPinTrigger", addonOptions.sidewinderGPPinTrigger == 0xFF ? -1 : addonOptions.sidewinderGPPinTrigger); + writeDoc(doc, "sidewinderGPPinClock", addonOptions.sidewinderGPPinClock == 0xFF ? -1 : addonOptions.sidewinderGPPinClock); + writeDoc(doc, "sidewinderGPPinData", addonOptions.sidewinderGPPinData == 0xFF ? -1 : addonOptions.sidewinderGPPinData); writeDoc(doc, "AnalogInputEnabled", addonOptions.AnalogInputEnabled); writeDoc(doc, "BoardLedAddonEnabled", addonOptions.BoardLedAddonEnabled); writeDoc(doc, "BuzzerSpeakerAddonEnabled", addonOptions.BuzzerSpeakerAddonEnabled); @@ -1016,6 +1023,7 @@ std::string getAddonOptions() writeDoc(doc, "ReverseInputEnabled", addonOptions.ReverseInputEnabled); writeDoc(doc, "TurboInputEnabled", addonOptions.TurboInputEnabled); writeDoc(doc, "WiiExtensionAddonEnabled", addonOptions.WiiExtensionAddonEnabled); + writeDoc(doc, "SidewinderGPEnabled", addonOptions.SidewinderGPEnabled); return serialize_json(doc); } diff --git a/src/gp2040.cpp b/src/gp2040.cpp index 2e1b8ce3f9..c8e900ba6e 100644 --- a/src/gp2040.cpp +++ b/src/gp2040.cpp @@ -18,6 +18,7 @@ #include "addons/turbo.h" #include "addons/slider_socd.h" #include "addons/wiiext.h" +#include "addons/sidewindergp.h" // Pico includes #include "pico/bootrom.h" @@ -108,6 +109,7 @@ void GP2040::setup() { addons.LoadAddon(new WiiExtensionInput(), CORE0_INPUT); addons.LoadAddon(new PlayerNumAddon(), CORE0_USBREPORT); addons.LoadAddon(new SliderSOCDInput(), CORE0_INPUT); + addons.LoadAddon(new SidewinderGPInput(), CORE0_INPUT); } void GP2040::run() { diff --git a/src/storagemanager.cpp b/src/storagemanager.cpp index f06d6bd74c..ba0b02b9ca 100644 --- a/src/storagemanager.cpp +++ b/src/storagemanager.cpp @@ -29,6 +29,7 @@ #include "addons/pleds.h" #include "addons/reverse.h" #include "addons/turbo.h" +#include "addons/sidewindergp.h" #include "addons/slider_socd.h" #include "addons/wiiext.h" @@ -200,6 +201,9 @@ void Storage::setDefaultAddonOptions() addonOptions.wiiExtensionSCLPin = WII_EXTENSION_I2C_SCL_PIN; addonOptions.wiiExtensionBlock = (WII_EXTENSION_I2C_BLOCK == i2c0) ? 0 : 1; addonOptions.wiiExtensionSpeed = WII_EXTENSION_I2C_SPEED; + addonOptions.sidewinderGPPinTrigger = SIDEWINDERGP_PIN_TRIGGER; + addonOptions.sidewinderGPPinClock = SIDEWINDERGP_PIN_CLOCK; + addonOptions.sidewinderGPPinData = SIDEWINDERGP_PIN_DATA; addonOptions.AnalogInputEnabled = ANALOG_INPUT_ENABLED; addonOptions.BoardLedAddonEnabled = BOARD_LED_ENABLED; addonOptions.BootselButtonAddonEnabled = BOOTSEL_BUTTON_ENABLED; @@ -214,6 +218,7 @@ void Storage::setDefaultAddonOptions() addonOptions.ReverseInputEnabled = REVERSE_ENABLED; addonOptions.TurboInputEnabled = TURBO_ENABLED; addonOptions.WiiExtensionAddonEnabled = WII_EXTENSION_ENABLED; + addonOptions.SidewinderGPEnabled = SIDEWINDERGP_INPUT_ENABLED; setAddonOptions(addonOptions); } diff --git a/www/server/app.js b/www/server/app.js index 40985c20e0..46a837a43e 100644 --- a/www/server/app.js +++ b/www/server/app.js @@ -223,6 +223,9 @@ app.get("/api/getAddonsOptions", (req, res) => { wiiExtensionSCLPin: -1, wiiExtensionBlock: 0, wiiExtensionSpeed: 400000, + sidewinderGPPinTrigger: -1, + sidewinderGPPinClock: -1, + sidewinderGPPinData: -1, AnalogInputEnabled: 1, BoardLedAddonEnabled: 1, BuzzerSpeakerAddonEnabled: 1, @@ -237,6 +240,7 @@ app.get("/api/getAddonsOptions", (req, res) => { SliderSOCDInputEnabled: 1, TurboInputEnabled: 1, WiiExtensionAddonEnabled: 1, + SidewinderGPEnabled: 1, usedPins: Object.values(picoController), }); }); diff --git a/www/src/Pages/AddonsConfigPage.js b/www/src/Pages/AddonsConfigPage.js index f98d20a11f..30c5b4c9dd 100644 --- a/www/src/Pages/AddonsConfigPage.js +++ b/www/src/Pages/AddonsConfigPage.js @@ -270,6 +270,10 @@ const schema = yup.object().shape({ BootselButtonAddonEnabled: yup.number().required().label('Boot Select Button Add-On Enabled'), bootselButtonMap: yup.number().label('BOOTSEL Button Map').validateSelectionWhenValue('BootselButtonAddonEnabled', BUTTON_MASKS), + sidewinderGPPinTrigger : yup.number().required().test('', '${originalValue} is unavailable/already assigned!', (value) => usedPins.indexOf(value) === -1).label('Sidewinder GP Trigger Pin'), + sidewinderGPPinClock : yup.number().required().test('', '${originalValue} is unavailable/already assigned!', (value) => usedPins.indexOf(value) === -1).label('Sidewinder GP Clock Pin'), + sidewinderGPPinData : yup.number().required().test('', '${originalValue} is unavailable/already assigned!', (value) => usedPins.indexOf(value) === -1).label('Sidewinder GP Data Pin'), + BuzzerSpeakerAddonEnabled: yup.number().required().label('Buzzer Speaker Add-On Enabled'), buzzerPin: yup.number().label('Buzzer Pin').validatePinWhenValue('BuzzerSpeakerAddonEnabled'), buzzerVolume: yup.number().label('Buzzer Volume').validateRangeWhenValue('BuzzerSpeakerAddonEnabled', 0, 100), @@ -331,6 +335,7 @@ const schema = yup.object().shape({ wiiExtensionSCLPin: yup.number().required().label('WiiExtension I2C SCL Pin').validatePinWhenValue('WiiExtensionAddonEnabled'), wiiExtensionBlock: yup.number().required().label('WiiExtension I2C Block').validateSelectionWhenValue('WiiExtensionAddonEnabled', I2C_BLOCKS), wiiExtensionSpeed: yup.number().label('WiiExtension I2C Speed').validateNumberWhenValue('WiiExtensionAddonEnabled'), + SidewinderGPEnabled: yup.number().required().label('Sidewinder GP Input Enabled'), }); const defaultValues = { @@ -385,6 +390,9 @@ const defaultValues = { wiiExtensionSCLPin: -1, wiiExtensionBlock: 0, wiiExtensionSpeed: 400000, + sidewinderGPPinTrigger: -1, + sidewinderGPPinClock: -1, + sidewinderGPPinData: -1, AnalogInputEnabled: 0, BoardLedAddonEnabled: 0, BuzzerSpeakerAddonEnabled: 0, @@ -399,6 +407,7 @@ const defaultValues = { ReverseInputEnabled: 0, TurboInputEnabled: 0, WiiExtensionAddonEnabled: 0, + SidewinderGPEnabled: 0, }; const FormContext = ({setStoredData}) => { @@ -530,6 +539,12 @@ const sanitizeData = (values) => { values.wiiExtensionBlock = parseInt(values.wiiExtensionBlock); if (!!values.wiiExtensionSpeed) values.wiiExtensionSpeed = parseInt(values.wiiExtensionSpeed); + if (!!values.sidewinderGPPinTrigger) + values.sidewinderGPPinTrigger = parseInt(values.sidewinderGPPinTrigger); + if (!!values.sidewinderGPPinClock) + values.sidewinderGPPinClock = parseInt(values.sidewinderGPPinClock); + if (!!values.sidewinderGPPinData) + values.sidewinderGPPinData = parseInt(values.sidewinderGPPinData); if (!!values.AnalogInputEnabled) values.AnalogInputEnabled = parseInt(values.AnalogInputEnabled); if (!!values.BoardLedAddonEnabled) @@ -558,6 +573,8 @@ const sanitizeData = (values) => { values.TurboInputEnabled = parseInt(values.TurboInputEnabled); if (!!values.WiiExtensionAddonEnabled) values.WiiExtensionAddonEnabled = parseInt(values.WiiExtensionAddonEnabled); + if (!!values.SidewinderGPEnabled) + values.SidewinderGPEnabled = parseInt(values.SidewinderGPEnabled); } function flattenObject(object) { @@ -1564,6 +1581,59 @@ export default function AddonsConfigPage() { onChange={(e) => {handleCheckbox("WiiExtensionAddonEnabled", values); handleChange(e);}} /> +
+ + {handleCheckbox("SidewinderGPEnabled", values); handleChange(e);}} + /> +
{saveMessage ? {saveMessage} : null}